feat: add merge blend tree component (#572)

* chore: refactor state machine clone logic out as a separate class

* feat: add layer priority and relative path root options to Merge Animator

* feat: add Merge Blend Tree component

* chore: adjust NDMF dependency

* docs: update merge-animator docs

* docs: merge blend tree docs
This commit is contained in:
bd_ 2023-12-22 17:17:40 +09:00 committed by GitHub
parent 63043cb4ec
commit aa698565ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1923 additions and 200 deletions

View File

@ -4,7 +4,7 @@
"version": "3.4.2"
},
"nadena.dev.ndmf": {
"version": "1.3.0-alpha.0"
"version": "1.3.0-alpha.1"
}
},
"locked": {
@ -19,7 +19,7 @@
"dependencies": {}
},
"nadena.dev.ndmf": {
"version": "1.3.0-alpha.0"
"version": "1.3.0-alpha.1"
}
}
}

View File

@ -4,7 +4,7 @@
"version": "3.5.0"
},
"nadena.dev.ndmf": {
"version": "1.3.0-alpha.0"
"version": "1.3.0-alpha.1"
}
},
"locked": {
@ -19,7 +19,7 @@
"dependencies": {}
},
"nadena.dev.ndmf": {
"version": "1.3.0-alpha.0"
"version": "1.3.0-alpha.1"
}
}
}

View File

@ -0,0 +1,210 @@
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using BuildContext = nadena.dev.ndmf.BuildContext;
namespace nadena.dev.modular_avatar.animation
{
using UnityObject = UnityEngine.Object;
internal class DeepClone
{
private bool _isSaved;
private UnityObject _combined;
public AnimatorOverrideController OverrideController { get; set; }
public DeepClone(BuildContext context)
{
_isSaved = context.AssetContainer != null;
_combined = context.AssetContainer;
}
public T DoClone<T>(T original,
string basePath = null,
Dictionary<UnityObject, UnityObject> cloneMap = null
) where T : UnityObject
{
if (original == null) return null;
if (cloneMap == null) cloneMap = new Dictionary<UnityObject, UnityObject>();
Func<UnityObject, UnityObject> visitor = null;
if (basePath != null)
{
visitor = o => CloneWithPathMapping(o, basePath);
}
// We want to avoid trying to copy assets not part of the animation system (eg - textures, meshes,
// MonoScripts...), so check for the types we care about here
switch (original)
{
// Any object referenced by an animator that we intend to mutate needs to be listed here.
case Motion _:
case AnimatorController _:
case AnimatorState _:
case AnimatorStateMachine _:
case AnimatorTransitionBase _:
case StateMachineBehaviour _:
break; // We want to clone these types
// Leave textures, materials, and script definitions alone
case Texture2D _:
case MonoScript _:
case Material _:
return original;
// Also avoid copying unknown scriptable objects.
// This ensures compatibility with e.g. avatar remote, which stores state information in a state
// behaviour referencing a custom ScriptableObject
case ScriptableObject _:
return original;
default:
throw new Exception($"Unknown type referenced from animator: {original.GetType()}");
}
// When using AnimatorOverrideController, replace the original AnimationClip based on AnimatorOverrideController.
if (OverrideController != null && original is AnimationClip srcClip)
{
T overrideClip = OverrideController[srcClip] as T;
if (overrideClip != null)
{
original = overrideClip;
}
}
if (cloneMap.ContainsKey(original))
{
return (T) cloneMap[original];
}
var obj = visitor?.Invoke(original);
if (obj != null)
{
cloneMap[original] = obj;
if (obj != original)
{
ObjectRegistry.RegisterReplacedObject(original, obj);
}
return (T) obj;
}
var ctor = original.GetType().GetConstructor(Type.EmptyTypes);
if (ctor == null || original is ScriptableObject)
{
obj = UnityObject.Instantiate(original);
}
else
{
obj = (T) ctor.Invoke(Array.Empty<object>());
EditorUtility.CopySerialized(original, obj);
}
cloneMap[original] = obj;
ObjectRegistry.RegisterReplacedObject(original, obj);
if (_isSaved)
{
AssetDatabase.AddObjectToAsset(obj, _combined);
}
SerializedObject so = new SerializedObject(obj);
SerializedProperty prop = so.GetIterator();
bool enterChildren = true;
while (prop.Next(enterChildren))
{
enterChildren = true;
switch (prop.propertyType)
{
case SerializedPropertyType.ObjectReference:
{
var newObj = DoClone(prop.objectReferenceValue, basePath, cloneMap);
prop.objectReferenceValue = newObj;
break;
}
// Iterating strings can get super slow...
case SerializedPropertyType.String:
enterChildren = false;
break;
}
}
so.ApplyModifiedPropertiesWithoutUndo();
return (T) obj;
}
private UnityObject CloneWithPathMapping(UnityObject o, string basePath)
{
if (o is AnimationClip clip)
{
// We'll always rebase if the asset is non-persistent, because we can't reference a nonpersistent asset
// from a persistent asset. If the asset is persistent, skip cases where path editing isn't required,
// or where this is one of the special VRC proxy animations.
if (EditorUtility.IsPersistent(o) && (basePath == "" || Util.IsProxyAnimation(clip))) return clip;
AnimationClip newClip = new AnimationClip();
newClip.name = "rebased " + clip.name;
if (_isSaved)
{
AssetDatabase.AddObjectToAsset(newClip, _combined);
}
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var newBinding = binding;
newBinding.path = MapPath(binding, basePath);
newClip.SetCurve(newBinding.path, newBinding.type, newBinding.propertyName,
AnimationUtility.GetEditorCurve(clip, binding));
}
foreach (var objBinding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
{
var newBinding = objBinding;
newBinding.path = MapPath(objBinding, basePath);
AnimationUtility.SetObjectReferenceCurve(newClip, newBinding,
AnimationUtility.GetObjectReferenceCurve(clip, objBinding));
}
newClip.wrapMode = clip.wrapMode;
newClip.legacy = clip.legacy;
newClip.frameRate = clip.frameRate;
newClip.localBounds = clip.localBounds;
AnimationUtility.SetAnimationClipSettings(newClip, AnimationUtility.GetAnimationClipSettings(clip));
return newClip;
}
else if (o is Texture)
{
return o;
}
else
{
return null;
}
}
private static string MapPath(EditorCurveBinding binding, string basePath)
{
if (binding.type == typeof(Animator) && binding.path == "")
{
return "";
}
else
{
var newPath = binding.path == "" ? basePath : basePath + binding.path;
if (newPath.EndsWith("/"))
{
newPath = newPath.Substring(0, newPath.Length - 1);
}
return newPath;
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b33090a3e763464ab05f3efe07e0cbd3
timeCreated: 1703148770

View File

@ -25,6 +25,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEditor.Animations;
@ -43,6 +44,8 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly AnimatorController _combined;
private bool isSaved;
private DeepClone _deepClone;
private AnimatorOverrideController _overrideController;
private List<AnimatorControllerLayer> _layers = new List<AnimatorControllerLayer>();
@ -65,6 +68,8 @@ namespace nadena.dev.modular_avatar.core.editor
_combined = context.CreateAnimator();
isSaved = !string.IsNullOrEmpty(AssetDatabase.GetAssetPath(_combined));
_combined.name = assetName;
_deepClone = new DeepClone(context.PluginBuildContext);
}
public AnimatorController Finish()
@ -74,7 +79,7 @@ namespace nadena.dev.modular_avatar.core.editor
return _combined;
}
public void AddController(string basePath, AnimatorController controller, bool? writeDefaults)
public void AddController(string basePath, AnimatorController controller, bool? writeDefaults, bool forceFirstLayerWeight = false)
{
controllerBaseLayer = _layers.Count;
_cloneMap = new Dictionary<Object, Object>();
@ -103,6 +108,11 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var layer in layers)
{
insertLayer(basePath, layer, first, writeDefaults, layers);
if (first && forceFirstLayerWeight)
{
_layers[_layers.Count - 1].defaultWeight = 1;
}
first = false;
}
}
@ -112,7 +122,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
AnimatorController controller = overrideController.runtimeAnimatorController as AnimatorController;
if (controller == null) return;
_overrideController = overrideController;
_deepClone.OverrideController = overrideController;
try
{
this.AddController(basePath, controller, writeDefaults);
@ -163,8 +173,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
for (int i = 0; i < overrideBehaviors.Length; i++)
{
overrideBehaviors[i] = deepClone(overrideBehaviors[i], x => x,
new Dictionary<Object, Object>());
overrideBehaviors[i] = _deepClone.DoClone(overrideBehaviors[i]);
AdjustBehavior(overrideBehaviors[i]);
}
@ -242,7 +251,7 @@ namespace nadena.dev.modular_avatar.core.editor
return asm;
}
asm = deepClone(layerStateMachine, (obj) => customClone(obj, basePath), _cloneMap);
asm = _deepClone.DoClone(layerStateMachine, basePath, _cloneMap);
foreach (var state in WalkAllStates(asm))
{
@ -271,170 +280,5 @@ namespace nadena.dev.modular_avatar.core.editor
}
#endif
}
private static string MapPath(EditorCurveBinding binding, string basePath)
{
if (binding.type == typeof(Animator) && binding.path == "")
{
return "";
}
else
{
var newPath = binding.path == "" ? basePath : basePath + binding.path;
if (newPath.EndsWith("/"))
{
newPath = newPath.Substring(0, newPath.Length - 1);
}
return newPath;
}
}
private Object customClone(Object o, string basePath)
{
if (o is AnimationClip clip)
{
if (basePath == "" || Util.IsProxyAnimation(clip)) return clip;
AnimationClip newClip = new AnimationClip();
newClip.name = "rebased " + clip.name;
if (isSaved)
{
AssetDatabase.AddObjectToAsset(newClip, _combined);
}
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var newBinding = binding;
newBinding.path = MapPath(binding, basePath);
newClip.SetCurve(newBinding.path, newBinding.type, newBinding.propertyName,
AnimationUtility.GetEditorCurve(clip, binding));
}
foreach (var objBinding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
{
var newBinding = objBinding;
newBinding.path = MapPath(objBinding, basePath);
AnimationUtility.SetObjectReferenceCurve(newClip, newBinding,
AnimationUtility.GetObjectReferenceCurve(clip, objBinding));
}
newClip.wrapMode = clip.wrapMode;
newClip.legacy = clip.legacy;
newClip.frameRate = clip.frameRate;
newClip.localBounds = clip.localBounds;
AnimationUtility.SetAnimationClipSettings(newClip, AnimationUtility.GetAnimationClipSettings(clip));
return newClip;
}
else if (o is Texture)
{
return o;
}
else
{
return null;
}
}
private T deepClone<T>(T original,
Func<Object, Object> visitor,
Dictionary<Object, Object> cloneMap
) where T : Object
{
if (original == null) return null;
// We want to avoid trying to copy assets not part of the animation system (eg - textures, meshes,
// MonoScripts...), so check for the types we care about here
switch (original)
{
// Any object referenced by an animator that we intend to mutate needs to be listed here.
case Motion _:
case AnimatorController _:
case AnimatorState _:
case AnimatorStateMachine _:
case AnimatorTransitionBase _:
case StateMachineBehaviour _:
break; // We want to clone these types
// Leave textures, materials, and script definitions alone
case Texture2D _:
case MonoScript _:
case Material _:
return original;
// Also avoid copying unknown scriptable objects.
// This ensures compatibility with e.g. avatar remote, which stores state information in a state
// behaviour referencing a custom ScriptableObject
case ScriptableObject _:
return original;
default:
throw new Exception($"Unknown type referenced from animator: {original.GetType()}");
}
// When using AnimatorOverrideController, replace the original AnimationClip based on AnimatorOverrideController.
if (_overrideController != null && original is AnimationClip srcClip)
{
T overrideClip = _overrideController[srcClip] as T;
if (overrideClip != null)
{
original = overrideClip;
}
}
if (cloneMap.ContainsKey(original))
{
return (T) cloneMap[original];
}
var obj = visitor(original);
if (obj != null)
{
cloneMap[original] = obj;
return (T) obj;
}
var ctor = original.GetType().GetConstructor(Type.EmptyTypes);
if (ctor == null || original is ScriptableObject)
{
obj = Object.Instantiate(original);
}
else
{
obj = (T) ctor.Invoke(Array.Empty<object>());
EditorUtility.CopySerialized(original, obj);
}
cloneMap[original] = obj;
if (isSaved)
{
AssetDatabase.AddObjectToAsset(obj, _combined);
}
SerializedObject so = new SerializedObject(obj);
SerializedProperty prop = so.GetIterator();
bool enterChildren = true;
while (prop.Next(enterChildren))
{
enterChildren = true;
switch (prop.propertyType)
{
case SerializedPropertyType.ObjectReference:
prop.objectReferenceValue = deepClone(prop.objectReferenceValue, visitor, cloneMap);
break;
// Iterating strings can get super slow...
case SerializedPropertyType.String:
enterChildren = false;
break;
}
}
so.ApplyModifiedPropertiesWithoutUndo();
return (T) obj;
}
}
}

View File

@ -18,7 +18,9 @@ namespace nadena.dev.modular_avatar.core.editor
prop_layerType,
prop_deleteAttachedAnimator,
prop_pathMode,
prop_matchAvatarWriteDefaults;
prop_matchAvatarWriteDefaults,
prop_relativePathRoot,
prop_layerPriority;
private void OnEnable()
{
@ -29,6 +31,9 @@ namespace nadena.dev.modular_avatar.core.editor
prop_pathMode = serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.pathMode));
prop_matchAvatarWriteDefaults =
serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.matchAvatarWriteDefaults));
prop_relativePathRoot =
serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.relativePathRoot));
prop_layerPriority = serializedObject.FindProperty(nameof(ModularAvatarMergeAnimator.layerPriority));
}
protected override void OnInnerInspectorGUI()
@ -39,6 +44,9 @@ namespace nadena.dev.modular_avatar.core.editor
EditorGUILayout.PropertyField(prop_layerType, G("merge_animator.layer_type"));
EditorGUILayout.PropertyField(prop_deleteAttachedAnimator, G("merge_animator.delete_attached_animator"));
EditorGUILayout.PropertyField(prop_pathMode, G("merge_animator.path_mode"));
if (prop_pathMode.enumValueIndex == (int) MergeAnimatorPathMode.Relative)
EditorGUILayout.PropertyField(prop_relativePathRoot, G("merge_animator.relative_path_root"));
EditorGUILayout.PropertyField(prop_layerPriority, G("merge_animator.layer_priority"));
EditorGUILayout.PropertyField(prop_matchAvatarWriteDefaults,
G("merge_animator.match_avatar_write_defaults"));

View File

@ -0,0 +1,37 @@
using UnityEditor;
using UnityEditor.Animations;
using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomEditor(typeof(ModularAvatarMergeBlendTree))]
internal class MergeBlendTreeEditor : MAEditorBase
{
private SerializedProperty _blendTree;
private SerializedProperty _pathMode;
private SerializedProperty _relativePathRoot;
private void OnEnable()
{
_blendTree = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.BlendTree));
_pathMode = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.PathMode));
_relativePathRoot = serializedObject.FindProperty(nameof(ModularAvatarMergeBlendTree.RelativePathRoot));
}
protected override void OnInnerInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.ObjectField(_blendTree, typeof(BlendTree), G("merge_blend_tree.blend_tree"));
EditorGUILayout.PropertyField(_pathMode, G("merge_blend_tree.path_mode"));
if (_pathMode.enumValueIndex == (int) MergeAnimatorPathMode.Relative)
{
EditorGUILayout.PropertyField(_relativePathRoot, G("merge_blend_tree.relative_path_root"));
}
serializedObject.ApplyModifiedProperties();
ShowLanguageUI();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e58c72a0021d43e1a17f551d7d0de677
timeCreated: 1703156816

View File

@ -51,7 +51,11 @@
"merge_animator.path_mode.tooltip": "How to interpret paths in animations. Using relative mode lets you record animations from an animator on this object.",
"merge_animator.match_avatar_write_defaults": "Match Avatar Write Defaults",
"merge_animator.match_avatar_write_defaults.tooltip": "Match the write defaults setting used on the avatar's animator. If the avatar's write defaults settings are inconsistent, the settings on the animator will be left alone.",
"merge_animator.relative_path_root": "Relative Path Root",
"merge_animator.relative_path_root.tooltip": "The root object to use when interpreting relative paths. If not specified, the object this component is attached to will be used.",
"merge_animator.layer_priority": "Layer Priority",
"merge_animator.layer_priority.tooltip": "Controls the order in which layers are merged into the animator - lower to higher. Negative values are merged before the original layer on the avatar descriptor, while zero and positive numbers are merged after.",
"merge_armature.lockmode": "Position sync mode",
"merge_armature.lockmode.not_locked.title": "Not locked",
"merge_armature.lockmode.not_locked.body": "Merged armature does not sync its position with the base avatar.",
@ -67,6 +71,12 @@
"merge_armature.reset_pos.heuristic_scale": "Adjust outfit overall scale to match base avatar",
"merge_armature.reset_pos.heuristic_scale.tooltip": "Will set the overall scale of the outfit as a whole based on armspan measurements. Recommended for setting up outfits.",
"merge_blend_tree.blend_tree": "Blend Tree",
"merge_blend_tree.path_mode": "Path Mode",
"merge_blend_tree.path_mode.tooltip": "How to interpret paths in animations. Using relative mode lets you record animations from an animator on this object.",
"merge_blend_tree.relative_path_root": "Relative Path Root",
"merge_blend_tree.relative_path_root.tooltip": "The root object to use when interpreting relative paths. If not specified, the object this component is attached to will be used.",
"worldfixed.quest": "This component is not compatible with the standalone Oculus Quest and will have no effect.",
"worldfixed.normal": "This object will be fixed to world unless you fixed to avatar with constraint.",
"fpvisible.normal": "This object will be visible in your first person view.",

View File

@ -55,7 +55,12 @@
"merge_armature.reset_pos.heuristic_scale": "衣装の全体的なスケールをアバターに合わせる",
"merge_armature.reset_pos.heuristic_scale.tooltip": "腕の長さを参考に、衣装全体のスケールをアバターに合わせます。非対応衣装を導入するときは推奨です。",
"merge_blend_tree.blend_tree": "ブレンドツリー",
"merge_blend_tree.path_mode": "パスモード",
"merge_blend_tree.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトについているアニメーターでアニメーション編集できます",
"merge_blend_tree.relative_path_root": "相対的パースのルート",
"merge_blend_tree.relative_path_root.tooltip": "相対的パースはこのオブジェクトを基準に解釈されます。指定がない場合は、このコンポーネントがついているオブジェクトを基準とします。",
"path_mode.Relative": "相対的(このオブジェクトからのパスを使用)",
"path_mode.Absolute": "絶対的(アバタールートからのパスを使用)",
"merge_animator.animator": "統合されるアニメーター",
@ -66,6 +71,11 @@
"merge_animator.path_mode.tooltip": "アニメーション内のパスを解釈するモード。相対的にすると、このオブジェクトについているアニメーターでアニメーション編集できます",
"merge_animator.match_avatar_write_defaults": "アバターのWrite Defaults設定に合わせる",
"merge_animator.match_avatar_write_defaults.tooltip": "アバターの該当アニメーターのWrite Defaults設定に合わせます。アバター側の設定が矛盾する場合は、統合されるアニメーターのWD値がそのまま採用されます。",
"merge_animator.relative_path_root": "相対的パスのルート",
"merge_animator.relative_path_root.tooltip": "相対的パスはこのオブジェクトを基準に解釈されます。指定がない場合は、このコンポーネントがついているオブジェクトを基準とします。",
"merge_animator.layer_priority": "レイヤー優先度",
"merge_animator.layer_priority.tooltip": "アニメーターにレイヤーが統合される順番を制御します。低い値から高い値の順に統合されます。マイナスの場合は元々のAvatar Descriptorについているコントローラーより前に統合され、ゼロ以上の場合はそのあとに統合されます。",
"worldfixed.quest": "このコンポーネントはクエスト単体非対応のため無効となっています。",
"worldfixed.normal": "このオブジェクトはConstraint等でアバターに追従させない限りワールドに固定されます。",
"fpvisible.normal": "このオブジェクトは一人視点で表示されます。",

View File

@ -25,6 +25,7 @@
#if MA_VRCSDK3_AVATARS
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEditor.Animations;
@ -67,10 +68,23 @@ namespace nadena.dev.modular_avatar.core.editor
if (descriptor.specialAnimationLayers != null) InitSessions(descriptor.specialAnimationLayers);
var toMerge = avatarGameObject.transform.GetComponentsInChildren<ModularAvatarMergeAnimator>(true);
Dictionary<VRCAvatarDescriptor.AnimLayerType, List<ModularAvatarMergeAnimator>> byLayerType
= new Dictionary<VRCAvatarDescriptor.AnimLayerType, List<ModularAvatarMergeAnimator>>();
foreach (var merge in toMerge)
{
BuildReport.ReportingObject(merge, () => ProcessMergeAnimator(avatarGameObject, context, merge));
if (!byLayerType.TryGetValue(merge.layerType, out var components))
{
components = new List<ModularAvatarMergeAnimator>();
byLayerType[merge.layerType] = components;
}
components.Add(merge);
}
foreach (var entry in byLayerType)
{
ProcessLayerType(context, entry.Key, entry.Value);
}
descriptor.baseAnimationLayers = FinishSessions(descriptor.baseAnimationLayers);
@ -78,35 +92,57 @@ namespace nadena.dev.modular_avatar.core.editor
descriptor.customizeAnimationLayers = true;
}
private void ProcessMergeAnimator(GameObject avatarGameObject, BuildContext context,
ModularAvatarMergeAnimator merge)
private void ProcessLayerType(
BuildContext context,
VRCAvatarDescriptor.AnimLayerType layerType,
List<ModularAvatarMergeAnimator> toMerge
)
{
if (merge.animator == null) return;
// Stable sort
var sorted = toMerge.OrderBy(x => x.layerPriority)
.ToList();
var beforeOriginal = sorted.Where(x => x.layerPriority < 0)
.ToList();
var afterOriginal = sorted.Where(x => x.layerPriority >= 0)
.ToList();
var session = new AnimatorCombiner(context, layerType.ToString() + " (merged)");
mergeSessions[layerType] = session;
foreach (var component in beforeOriginal)
{
MergeSingle(context, session, component);
}
if (defaultControllers_.TryGetValue(layerType, out var defaultController) && defaultController.layers.Length > 0)
{
session.AddController("", defaultController, null, forceFirstLayerWeight: true);
}
foreach (var component in afterOriginal)
{
MergeSingle(context, session, component);
}
}
private void MergeSingle(BuildContext context, AnimatorCombiner session, ModularAvatarMergeAnimator merge)
{
string basePath;
if (merge.pathMode == MergeAnimatorPathMode.Relative)
{
var relativePath = RuntimeUtil.RelativePath(avatarGameObject, merge.gameObject);
var targetObject = merge.relativePathRoot.Get(context.AvatarRootTransform);
if (targetObject == null) targetObject = merge.gameObject;
var relativePath = RuntimeUtil.RelativePath(context.AvatarRootObject, targetObject);
basePath = relativePath != "" ? relativePath + "/" : "";
}
else
{
basePath = "";
}
if (!mergeSessions.TryGetValue(merge.layerType, out var session))
{
session = new AnimatorCombiner(context, merge.layerType.ToString() + " (merged)");
mergeSessions[merge.layerType] = session;
if (defaultControllers_.TryGetValue(merge.layerType, out var defaultController))
{
session.AddController("", defaultController, null);
}
}
bool? writeDefaults = merge.matchAvatarWriteDefaults ? writeDefaults_[merge.layerType] : null;
mergeSessions[merge.layerType]
.AddController(basePath, (AnimatorController) merge.animator, writeDefaults);
session.AddController(basePath, (AnimatorController) merge.animator, writeDefaults);
if (merge.deleteAttachedAnimator)
{

View File

@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using BestHTTP.SecureProtocol.Org.BouncyCastle.Utilities;
using nadena.dev.modular_avatar.animation;
using nadena.dev.ndmf;
using nadena.dev.ndmf.util;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
namespace nadena.dev.modular_avatar.core.editor
{
internal class MergeBlendTreePass : Pass<MergeBlendTreePass>
{
internal const string ALWAYS_ONE = "__ModularAvatarInternal/One";
internal const string BlendTreeLayerName = "ModularAvatar: Merge Blend Tree";
private AnimatorController _controller;
private BlendTree _rootBlendTree;
private HashSet<string> _parameterNames;
protected override void Execute(ndmf.BuildContext context)
{
_rootBlendTree = null;
_parameterNames = new HashSet<string>();
_controller = new AnimatorController();
foreach (var component in
context.AvatarRootObject.GetComponentsInChildren<ModularAvatarMergeBlendTree>(true))
{
ErrorReport.WithContextObject(component, () => ProcessComponent(context, component));
}
List<AnimatorControllerParameter> parameters = new List<AnimatorControllerParameter>(_parameterNames.Count + 1);
if (_controller != null)
{
_parameterNames.Remove(ALWAYS_ONE);
parameters.Add(new AnimatorControllerParameter()
{
defaultFloat = 1,
name = ALWAYS_ONE,
type = AnimatorControllerParameterType.Float
});
foreach (var name in _parameterNames)
{
parameters.Add(new AnimatorControllerParameter()
{
name = name,
type = AnimatorControllerParameterType.Float
});
}
_controller.parameters = parameters.ToArray();
}
}
private void ProcessComponent(ndmf.BuildContext context, ModularAvatarMergeBlendTree component)
{
BlendTree componentBlendTree = component.BlendTree as BlendTree;
if (componentBlendTree == null)
{
ErrorReport.ReportError(Localization.L, ErrorSeverity.NonFatal, "error.merge_blend_tree.missing_tree");
return;
}
string basePath = null;
if (component.PathMode == MergeAnimatorPathMode.Relative)
{
var root = component.RelativePathRoot.Get(context.AvatarRootTransform);
if (root == null) root = component.gameObject;
basePath = RuntimeUtil.AvatarRootPath(root) + "/";
}
var bt = new DeepClone(context).DoClone(componentBlendTree, basePath);
var rootBlend = GetRootBlendTree(context);
rootBlend.AddChild(bt);
var children = rootBlend.children;
children[children.Length - 1].directBlendParameter = ALWAYS_ONE;
rootBlend.children = children;
foreach (var asset in bt.ReferencedAssets(includeScene: false))
{
if (asset is BlendTree bt2)
{
if (!string.IsNullOrEmpty(bt2.blendParameter) && bt2.blendType != BlendTreeType.Direct)
{
_parameterNames.Add(bt2.blendParameter);
}
if (bt2.blendType != BlendTreeType.Direct && bt2.blendType != BlendTreeType.Simple1D)
{
if (!string.IsNullOrEmpty(bt2.blendParameterY))
{
_parameterNames.Add(bt2.blendParameterY);
}
}
if (bt2.blendType == BlendTreeType.Direct)
{
foreach (var childMotion in bt2.children)
{
if (!string.IsNullOrEmpty(childMotion.directBlendParameter))
{
_parameterNames.Add(childMotion.directBlendParameter);
}
}
}
}
}
}
private BlendTree GetRootBlendTree(ndmf.BuildContext context)
{
if (_rootBlendTree != null) return _rootBlendTree;
var newController = new AnimatorController();
var newStateMachine = new AnimatorStateMachine();
var newState = new AnimatorState();
_rootBlendTree = new BlendTree();
_controller = newController;
newController.layers = new[]
{
new AnimatorControllerLayer
{
blendingMode = AnimatorLayerBlendingMode.Override,
defaultWeight = 1,
name = BlendTreeLayerName,
stateMachine = newStateMachine
}
};
newStateMachine.name = "ModularAvatarMergeBlendTree";
newStateMachine.states = new[]
{
new ChildAnimatorState
{
state = newState,
position = Vector3.zero
}
};
newStateMachine.defaultState = newState;
newState.writeDefaultValues = true;
newState.motion = _rootBlendTree;
_rootBlendTree.blendType = BlendTreeType.Direct;
_rootBlendTree.blendParameter = ALWAYS_ONE;
var mergeObject = new GameObject("ModularAvatarMergeBlendTree");
var merger = mergeObject.AddComponent<ModularAvatarMergeAnimator>();
merger.animator = newController;
merger.pathMode = MergeAnimatorPathMode.Absolute;
merger.matchAvatarWriteDefaults = false;
merger.layerType = VRCAvatarDescriptor.AnimLayerType.FX;
merger.deleteAttachedAnimator = false;
merger.layerPriority = Int32.MinValue;
mergeObject.transform.SetParent(context.AvatarRootTransform, false);
mergeObject.transform.SetSiblingIndex(0);
return _rootBlendTree;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 54adb9bad5fa4080bc0a5e4ca2c2c530
timeCreated: 1703148577

View File

@ -38,6 +38,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
seq.Run(MeshSettingsPluginPass.Instance);
#if MA_VRCSDK3_AVATARS
seq.Run(RenameParametersPluginPass.Instance);
seq.Run(MergeBlendTreePass.Instance);
seq.Run(MergeAnimatorPluginPass.Instance);
seq.Run(MenuInstallPluginPass.Instance);
#endif

View File

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEditor.Animations;
@ -171,6 +172,18 @@ namespace nadena.dev.modular_avatar.core.editor
break;
}
case ModularAvatarMergeBlendTree merger:
{
var bt = merger.BlendTree as BlendTree;
if (bt != null)
{
merger.BlendTree = bt = new DeepClone(_context.PluginBuildContext).DoClone(bt);
ProcessBlendtree(bt, remaps);
}
break;
}
case ModularAvatarMenuInstaller installer:
{
if (installer.menuToAppend != null && installer.enabled)

View File

@ -44,6 +44,8 @@ namespace nadena.dev.modular_avatar.core
public bool deleteAttachedAnimator;
public MergeAnimatorPathMode pathMode = MergeAnimatorPathMode.Relative;
public bool matchAvatarWriteDefaults;
public AvatarObjectReference relativePathRoot = new AvatarObjectReference();
public int layerPriority = 0;
public override void ResolveReferences()
{

View File

@ -0,0 +1,14 @@
using UnityEngine;
namespace nadena.dev.modular_avatar.core
{
[AddComponentMenu("Modular Avatar/MA Merge Blend Tree")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/merge-blend-tree?lang=auto")]
public sealed class ModularAvatarMergeBlendTree : AvatarTagComponent
{
// We can't actually reference a BlendTree here because it's not available when building a player build
public UnityEngine.Object BlendTree;
public MergeAnimatorPathMode PathMode = MergeAnimatorPathMode.Relative;
public AvatarObjectReference RelativePathRoot = new AvatarObjectReference();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 229dd561ca024a6588e388160921a70f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: a8edd5bd1a0a64a40aa99cc09fb5f198, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,55 @@
using System.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
namespace modular_avatar_tests
{
public static class AnimationTestUtil
{
public static AnimatorController TestController(string name, Motion motion = null)
{
var controller = new AnimatorController();
var stateMachine = new AnimatorStateMachine();
var state = new AnimatorState();
state.name = name;
controller.layers = new[]
{
new AnimatorControllerLayer
{
blendingMode = AnimatorLayerBlendingMode.Override,
defaultWeight = 1,
name = name,
stateMachine = stateMachine
}
};
stateMachine.name = name;
stateMachine.states = new[]
{
new ChildAnimatorState()
{
state = state
}
};
stateMachine.defaultState = state;
state.motion = motion;
return controller;
}
public static AnimationClip AnimationWithPath(string path)
{
AnimationClip clip = new AnimationClip();
clip.SetCurve(path, typeof(Transform), "localPosition.x", AnimationCurve.Constant(0, 1, 0));
return clip;
}
public static void AssertAnimationHasPath(AnimationClip clip, string path)
{
Assert.IsTrue(AnimationUtility.GetCurveBindings(clip).Any(b => b.path == path));
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8374b5dea0d547fa9a40a5334767bdd0
timeCreated: 1703226767

View File

@ -0,0 +1,128 @@
using System;
using System.Linq;
using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using AvatarProcessor = nadena.dev.ndmf.AvatarProcessor;
namespace modular_avatar_tests
{
public class MergeBlendTreeTest : TestBase
{
[Test]
public void SimpleMergeTest()
{
BlendTree bt1 = new BlendTree();
bt1.blendParameter = "abcd";
bt1.blendParameterY = "defg";
bt1.blendType = BlendTreeType.Simple1D;
bt1.AddChild(AnimationTestUtil.AnimationWithPath("a"));
BlendTree bt2 = new BlendTree();
bt2.blendParameter = "p1";
bt2.blendParameterY = "p2";
bt2.blendType = BlendTreeType.FreeformCartesian2D;
bt2.AddChild(AnimationTestUtil.AnimationWithPath("b"));
var root = CreateRoot("root");
var c1 = CreateChild(root, "child1");
var mergeComponent = c1.AddComponent<ModularAvatarMergeBlendTree>();
mergeComponent.BlendTree = bt1;
mergeComponent.PathMode = MergeAnimatorPathMode.Relative;
CreateChild(c1, "a");
var c2 = CreateChild(root, "child2");
var mergeComponent2 = c2.AddComponent<ModularAvatarMergeBlendTree>();
mergeComponent2.BlendTree = bt2;
mergeComponent2.PathMode = MergeAnimatorPathMode.Absolute;
CreateChild(c2, "b");
AvatarProcessor.ProcessAvatar(root);
var fxController = FindFxController(root).animatorController as AnimatorController;
var fx = findFxLayer(root, MergeBlendTreePass.BlendTreeLayerName);
Assert.AreSame(fxController.layers[0].stateMachine, fx.stateMachine);
Assert.AreEqual(1, fx.stateMachine.states.Length);
var motion = fx.stateMachine.states[0].state.motion as BlendTree;
Assert.AreEqual(BlendTreeType.Direct, motion.blendType);
Assert.AreEqual(2, motion.children.Length);
Assert.AreEqual(MergeBlendTreePass.ALWAYS_ONE, motion.children[0].directBlendParameter);
Assert.AreEqual(MergeBlendTreePass.ALWAYS_ONE, motion.children[1].directBlendParameter);
AnimationTestUtil.AssertAnimationHasPath(((BlendTree)motion.children[0].motion).children[0].motion as AnimationClip, "child1/a");
AnimationTestUtil.AssertAnimationHasPath(((BlendTree)motion.children[1].motion).children[0].motion as AnimationClip, "b");
Assert.IsTrue(fxController.parameters.Any(p =>
p.name == "abcd" && p.type == AnimatorControllerParameterType.Float));
Assert.IsFalse(fxController.parameters.Any(p =>
p.name == "defg" && p.type == AnimatorControllerParameterType.Float));
Assert.IsTrue(fxController.parameters.Any(p =>
p.name == "p1" && p.type == AnimatorControllerParameterType.Float));
Assert.IsTrue(fxController.parameters.Any(p =>
p.name == "p2" && p.type == AnimatorControllerParameterType.Float));
Assert.IsTrue(fxController.parameters.Any(p =>
p.name == MergeBlendTreePass.ALWAYS_ONE && p.type == AnimatorControllerParameterType.Float
&& Math.Abs(p.defaultFloat - 1.0f) < 0.0001f));
}
[Test]
public void AlternateRootTest()
{
BlendTree bt = new BlendTree();
bt.AddChild(AnimationTestUtil.AnimationWithPath("a"));
var root = CreateRoot("root");
var c1 = CreateChild(root, "child1");
var mergeComponent = c1.AddComponent<ModularAvatarMergeBlendTree>();
mergeComponent.BlendTree = bt;
mergeComponent.PathMode = MergeAnimatorPathMode.Relative;
mergeComponent.RelativePathRoot.referencePath = "child2";
CreateChild(c1, "a");
var c2 = CreateChild(root, "child2");
CreateChild(c2, "a");
AvatarProcessor.ProcessAvatar(root);
var fx = findFxLayer(root, MergeBlendTreePass.BlendTreeLayerName);
var motion = fx.stateMachine.states[0].state.motion as BlendTree;
AnimationTestUtil.AssertAnimationHasPath(((BlendTree)motion.children[0].motion).children[0].motion as AnimationClip, "child2/a");
}
[Test]
public void MergeOrderTest()
{
var root = CreateRoot("root");
var bt = new BlendTree();
var c1 = CreateChild(root, "child1");
var mergeComponent = c1.AddComponent<ModularAvatarMergeBlendTree>();
mergeComponent.BlendTree = bt;
TestMerge(root, "m1");
TestMerge(root, "m2").layerPriority = int.MinValue;
TestMerge(root, "m3").layerPriority = int.MaxValue;
AvatarProcessor.ProcessAvatar(root);
var layerNames = (FindFxController(root).animatorController as AnimatorController)
.layers.Select(l => l.name).ToArray();
Assert.AreEqual(new[] {MergeBlendTreePass.BlendTreeLayerName, "m2", "Eyes", "FaceMood", "m1", "m3"}, layerNames);
}
ModularAvatarMergeAnimator TestMerge(GameObject root, string mergeName, Motion motion = null)
{
var obj = CreateChild(root, mergeName);
var merge = obj.AddComponent<ModularAvatarMergeAnimator>();
merge.animator = AnimationTestUtil.TestController(mergeName, motion);
return merge;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a5bd6966db564d38b8b726c57d71ccfb
timeCreated: 1703158381

View File

@ -0,0 +1,28 @@
using System.Linq;
using nadena.dev.ndmf;
using NUnit.Framework;
using UnityEditor.Animations;
namespace modular_avatar_tests
{
public class MergeOrderTest : TestBase
{
[Test]
public void TestMergeOrder()
{
var root = CreatePrefab("MergeOrderTest.prefab");
AvatarProcessor.ProcessAvatar(root);
var fxController = FindFxController(root);
var layerNames = (FindFxController(root).animatorController as AnimatorController)
.layers.Select(l => l.name).ToArray();
Assert.AreEqual(new []
{
"1", "2", "3", "4", "5"
}, layerNames);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4051c70770314cc78c8f1ed584e3b3d6
timeCreated: 1703226547

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 31065186232b77343816d51c1e9560b9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,537 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &328125214001799775
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3308352267436084320}
- component: {fileID: 3884116447948407917}
m_Layer: 0
m_Name: 2
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3308352267436084320
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 328125214001799775}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8977586966452677008}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &3884116447948407917
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 328125214001799775}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1bb122659f724ebf85fe095ac02dc339, type: 3}
m_Name:
m_EditorClassIdentifier:
animator: {fileID: 9100000, guid: eb6145d273e8ef54b9ca2a9e95f92305, type: 2}
layerType: 5
deleteAttachedAnimator: 0
pathMode: 0
matchAvatarWriteDefaults: 0
relativePathRoot:
referencePath:
layerPriority: -100
--- !u!1 &2283261212784644683
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3823446312882549902}
- component: {fileID: 6937154616757373335}
m_Layer: 0
m_Name: 4
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3823446312882549902
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2283261212784644683}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8977586966452677008}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &6937154616757373335
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2283261212784644683}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1bb122659f724ebf85fe095ac02dc339, type: 3}
m_Name:
m_EditorClassIdentifier:
animator: {fileID: 9100000, guid: a8ac270c680173c4799d999c9bac1534, type: 2}
layerType: 5
deleteAttachedAnimator: 0
pathMode: 0
matchAvatarWriteDefaults: 0
relativePathRoot:
referencePath:
layerPriority: 1
--- !u!1 &4823348639853131423
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3390644001987370182}
- component: {fileID: 6865338768055249797}
m_Layer: 0
m_Name: 1
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3390644001987370182
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4823348639853131423}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8977586966452677008}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &6865338768055249797
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4823348639853131423}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1bb122659f724ebf85fe095ac02dc339, type: 3}
m_Name:
m_EditorClassIdentifier:
animator: {fileID: 9100000, guid: 0ff7a3e17c1c03e48a05b1081bf8f820, type: 2}
layerType: 5
deleteAttachedAnimator: 0
pathMode: 0
matchAvatarWriteDefaults: 0
relativePathRoot:
referencePath:
layerPriority: -100
--- !u!1 &5888860052520729914
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8977586966452677008}
- component: {fileID: 4741512682607283207}
- component: {fileID: 203557163609087434}
- component: {fileID: 4929062914605056292}
m_Layer: 0
m_Name: MergeOrderTest
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &8977586966452677008
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5888860052520729914}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -1.2012978, y: 1.1367362, z: 0.22603863}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3823446312882549902}
- {fileID: 3390644001987370182}
- {fileID: 3308352267436084320}
- {fileID: 4333182672044057003}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!95 &4741512682607283207
Animator:
serializedVersion: 5
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5888860052520729914}
m_Enabled: 1
m_Avatar: {fileID: 0}
m_Controller: {fileID: 0}
m_CullingMode: 0
m_UpdateMode: 0
m_ApplyRootMotion: 0
m_LinearVelocityBlending: 0
m_StabilizeFeet: 0
m_WarningMessage:
m_HasTransformHierarchy: 1
m_AllowConstantClipSamplingOptimization: 1
m_KeepAnimatorStateOnDisable: 0
m_WriteDefaultValuesOnDisable: 0
--- !u!114 &203557163609087434
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5888860052520729914}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 542108242, guid: 67cc4cb7839cd3741b63733d5adf0442, type: 3}
m_Name:
m_EditorClassIdentifier:
Name:
ViewPosition: {x: 0, y: 1.6, z: 0.2}
Animations: 0
ScaleIPD: 1
lipSync: 0
lipSyncJawBone: {fileID: 0}
lipSyncJawClosed: {x: 0, y: 0, z: 0, w: 1}
lipSyncJawOpen: {x: 0, y: 0, z: 0, w: 1}
VisemeSkinnedMesh: {fileID: 0}
MouthOpenBlendShapeName: Facial_Blends.Jaw_Down
VisemeBlendShapes: []
unityVersion:
portraitCameraPositionOffset: {x: 0, y: 0, z: 0}
portraitCameraRotationOffset: {x: 0, y: 1, z: 0, w: -0.00000004371139}
networkIDs: []
customExpressions: 0
expressionsMenu: {fileID: 0}
expressionParameters: {fileID: 0}
enableEyeLook: 0
customEyeLookSettings:
eyeMovement:
confidence: 0.5
excitement: 0.5
leftEye: {fileID: 0}
rightEye: {fileID: 0}
eyesLookingStraight:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingUp:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingDown:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingLeft:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyesLookingRight:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidType: 0
upperLeftEyelid: {fileID: 0}
upperRightEyelid: {fileID: 0}
lowerLeftEyelid: {fileID: 0}
lowerRightEyelid: {fileID: 0}
eyelidsDefault:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsClosed:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsLookingUp:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsLookingDown:
upper:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
lower:
linked: 1
left: {x: 0, y: 0, z: 0, w: 0}
right: {x: 0, y: 0, z: 0, w: 0}
eyelidsSkinnedMesh: {fileID: 0}
eyelidsBlendshapes:
customizeAnimationLayers: 1
baseAnimationLayers:
- isEnabled: 0
type: 0
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 4
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 5
animatorController: {fileID: 9100000, guid: d471c3325f786f849b9071ed4caf5e01,
type: 2}
mask: {fileID: 0}
isDefault: 0
specialAnimationLayers:
- isEnabled: 0
type: 6
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 7
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
- isEnabled: 0
type: 8
animatorController: {fileID: 0}
mask: {fileID: 0}
isDefault: 1
AnimationPreset: {fileID: 0}
animationHashSet: []
autoFootsteps: 1
autoLocomotion: 1
collider_head:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_torso:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_footR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_footL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_handR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_handL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerIndexL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerMiddleL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerRingL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerLittleL:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerIndexR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerMiddleR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerRingR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
collider_fingerLittleR:
isMirrored: 1
state: 0
transform: {fileID: 0}
radius: 0
height: 0
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
--- !u!114 &4929062914605056292
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5888860052520729914}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: -1427037861, guid: 4ecd63eff847044b68db9453ce219299, type: 3}
m_Name:
m_EditorClassIdentifier:
launchedFromSDKPipeline: 0
completedSDKPipeline: 0
blueprintId:
contentType: 0
assetBundleUnityVersion:
fallbackStatus: 0
--- !u!1 &8806789658407322211
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4333182672044057003}
- component: {fileID: 2348079790857662602}
m_Layer: 0
m_Name: 3
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &4333182672044057003
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8806789658407322211}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8977586966452677008}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &2348079790857662602
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8806789658407322211}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1bb122659f724ebf85fe095ac02dc339, type: 3}
m_Name:
m_EditorClassIdentifier:
animator: {fileID: 9100000, guid: 8f90ad8b7c986a44d88051d583a20fdf, type: 2}
layerType: 5
deleteAttachedAnimator: 0
pathMode: 0
matchAvatarWriteDefaults: 0
relativePathRoot:
referencePath:
layerPriority: 0

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: db4a93ae91e8fe541b04a3de201eae43
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,72 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1102 &-6723305648047965004
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: a
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: e13eddbf7adb8ec47a8f5af784b14814, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1107 &-6101661261653446652
AnimatorStateMachine:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: 3
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: -6723305648047965004}
m_Position: {x: 467.5824, y: 113.26929, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: -6723305648047965004}
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: FX
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: 3
m_StateMachine: {fileID: -6101661261653446652}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d471c3325f786f849b9071ed4caf5e01
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,72 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1102 &-586138749803539241
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: a
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: e13eddbf7adb8ec47a8f5af784b14814, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: First
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: 1
m_StateMachine: {fileID: 3312057567024330509}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
--- !u!1107 &3312057567024330509
AnimatorStateMachine:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: 1
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: -586138749803539241}
m_Position: {x: 271.8407, y: -366.9505, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: -586138749803539241}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0ff7a3e17c1c03e48a05b1081bf8f820
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,72 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1107 &-6972772901112240568
AnimatorStateMachine:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: 5
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: -6452914645168321189}
m_Position: {x: 277.06042, y: -372.1703, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: -6452914645168321189}
--- !u!1102 &-6452914645168321189
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: a
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: e13eddbf7adb8ec47a8f5af784b14814, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Fourth
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: 5
m_StateMachine: {fileID: -6972772901112240568}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a8ac270c680173c4799d999c9bac1534
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,72 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1102 &-3838991756498900347
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: a
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: e13eddbf7adb8ec47a8f5af784b14814, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1107 &-3521989285324055791
AnimatorStateMachine:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: 2
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: -3838991756498900347}
m_Position: {x: 303.1593, y: -145.10986, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: -3838991756498900347}
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Second
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: 2
m_StateMachine: {fileID: -3521989285324055791}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: eb6145d273e8ef54b9ca2a9e95f92305
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,72 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1102 &-8389599259838401866
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: a
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: e13eddbf7adb8ec47a8f5af784b14814, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1107 &-6972772901112240568
AnimatorStateMachine:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: 4
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: -8389599259838401866}
m_Position: {x: 381.45605, y: -173.81866, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: -8389599259838401866}
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Third
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: 4
m_StateMachine: {fileID: -6972772901112240568}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8f90ad8b7c986a44d88051d583a20fdf
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,53 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!74 &7400000
AnimationClip:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: a
serializedVersion: 7
m_Legacy: 0
m_Compressed: 0
m_UseHighQualityCurve: 1
m_RotationCurves: []
m_CompressedRotationCurves: []
m_EulerCurves: []
m_PositionCurves: []
m_ScaleCurves: []
m_FloatCurves: []
m_PPtrCurves: []
m_SampleRate: 60
m_WrapMode: 0
m_Bounds:
m_Center: {x: 0, y: 0, z: 0}
m_Extent: {x: 0, y: 0, z: 0}
m_ClipBindingConstant:
genericBindings: []
pptrCurveMapping: []
m_AnimationClipSettings:
serializedVersion: 2
m_AdditiveReferencePoseClip: {fileID: 0}
m_AdditiveReferencePoseTime: 0
m_StartTime: 0
m_StopTime: 1
m_OrientationOffsetY: 0
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 0
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0
m_LoopBlendPositionXZ: 0
m_KeepOriginalOrientation: 0
m_KeepOriginalPositionY: 1
m_KeepOriginalPositionXZ: 0
m_HeightFromFeet: 0
m_Mirror: 0
m_EditorCurves: []
m_EulerEditorCurves: []
m_HasGenericRootTransform: 0
m_HasMotionFloatCurves: 0
m_Events: []

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e13eddbf7adb8ec47a8f5af784b14814
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -2,8 +2,10 @@
using nadena.dev.ndmf;
using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.core;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
namespace modular_avatar_tests
{
@ -25,6 +27,44 @@ namespace modular_avatar_tests
Assert.True(new SerializedObject(hq_on.motion).FindProperty("m_UseHighQualityCurve").boolValue);
Assert.False(new SerializedObject(hq_off.motion).FindProperty("m_UseHighQualityCurve").boolValue);
}
[Test]
public void RelativePathTest()
{
var root = CreateRoot("root");
var c1 = CreateChild(root, "child1");
var c2 = CreateChild(root, "child2");
var c3 = CreateChild(root, "child3");
var a1 = CreateChild(c1, "a");
var a2 = CreateChild(c2, "a");
var a3 = CreateChild(c3, "a");
var a0 = CreateChild(root, "a");
var merge_rel_1 = AnimationTestUtil.TestController("T1", AnimationTestUtil.AnimationWithPath("a"));
var merge_rel_2 = AnimationTestUtil.TestController("T2", AnimationTestUtil.AnimationWithPath("a"));
var merge_abs = AnimationTestUtil.TestController("T3", AnimationTestUtil.AnimationWithPath("a"));
var merge_rel_1_comp = c1.AddComponent<ModularAvatarMergeAnimator>();
merge_rel_1_comp.animator = merge_rel_1;
merge_rel_1_comp.pathMode = MergeAnimatorPathMode.Relative;
merge_rel_1_comp.relativePathRoot.referencePath = "child2";
var merge_rel_2_comp = c3.AddComponent<ModularAvatarMergeAnimator>();
merge_rel_2_comp.animator = merge_rel_2;
merge_rel_2_comp.pathMode = MergeAnimatorPathMode.Relative;
var merge_abs_comp = c2.AddComponent<ModularAvatarMergeAnimator>();
merge_abs_comp.animator = merge_abs;
merge_abs_comp.pathMode = MergeAnimatorPathMode.Absolute;
AvatarProcessor.ProcessAvatar(root);
AnimationTestUtil.AssertAnimationHasPath(findFxMotion(root, "T3") as AnimationClip, "a");
AnimationTestUtil.AssertAnimationHasPath(findFxMotion(root, "T1") as AnimationClip, "child2/a");
AnimationTestUtil.AssertAnimationHasPath(findFxMotion(root, "T2") as AnimationClip, "child3/a");
}
}
}

View File

@ -139,8 +139,7 @@ namespace modular_avatar_tests
#if MA_VRCSDK3_AVATARS
protected static AnimatorControllerLayer findFxLayer(GameObject prefab, string layerName)
{
var fx = prefab.GetComponent<VRCAvatarDescriptor>().baseAnimationLayers
.FirstOrDefault(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX);
var fx = FindFxController(prefab);
Assert.NotNull(fx);
var ac = fx.animatorController as AnimatorController;
@ -151,6 +150,12 @@ namespace modular_avatar_tests
Assert.NotNull(layer);
return layer;
}
internal static VRCAvatarDescriptor.CustomAnimLayer FindFxController(GameObject prefab)
{
return prefab.GetComponent<VRCAvatarDescriptor>().baseAnimationLayers
.FirstOrDefault(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX);
}
#endif
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -32,7 +32,13 @@ As a development convenience, you can check the "Delete attached animator" box t
Animations that move humanoid bones ignore the relative path logic, and will always apply to the overall avatar. As such most humanoid animations (e.g. AFK animations) can be used as-is.
### Absolute path mode
### Path mode
The path mode option controls how animation paths are interpreted. In "Relative" mode, all paths are relative to a
specific object, usually the one the Merge Animator component is attached to. This allows you to create gimmicks that
work when they're moved around in the avatar,
and makes it easier to record the animations, by using the Unity animator component (as described above). You can
control which object is used as the root for paths in animations by setting the "Relative Path Root" field.
If you want to animate objects that are already attached to the avatar (that aren't under your object), set the path mode to "Absolute". This will cause the animator to use absolute paths, and will not attempt to interpret paths relative to the Merge Animator component.
This means you will need to record your animations using the avatar's root animator instead.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,32 @@
# Merge Blend Tree
![Merge Blend Tree](merge-blend-tree.png)
The merge blend tree component allows you to merge multiple blend trees into a single FX layer.
This is an advanced component that allows for building lower-overhead animators by merging multiple gimmicks into a
single layer.
## When should I use it?
You should use Merge Blend Tree when you have a blend tree that you want to be always active on the avatar.
## When shouldn't I use it?
You should not use Merge Blend Tree if you need to disable/enable the blend tree, or have control over motion time.
## Setting up Merge Blend Tree
First, create a Blend Tree asset. You can do this by right clicking on the project window and selecting
Create -> BlendTree.
Configure your blend tree as desired, then add a Merge Blend Tree component and specify the Blend Tree in the Blend
Tree field.
You can configure Path Mode and Relative Path Root similarly to Merge Animator; for more details, see the
[Merge Animator documentation](merge-animator.md).
## How blend trees are merged
Modular Avatar will create a new layer at the top of the FX controller. This layer will contain a single state, with
Write Defaults on, and containing a Direct Blend Tree. Each merged blend tree will be attached to this Direct Blend
Tree, with its parameter always set to one.

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -37,7 +37,9 @@ GameObjectにAnimatorコンポーネントも追加して、Animationパネル
ヒューマノイドボーンを操作するアニメーションは上記の相対的なパスで解釈されるのではなく、アバター全体に適用されます。
AFKアニメーションなどほとんどのヒューマイドアニメーションがそのまま使えるというわけです。
### 絶対的パスモード
### パスモード
パスモード設定に応じて、アニメーションのパスの扱いが変わります。「相対的」モードでは、アニメーションのパスが特定のオブジェクトを基準として解釈されます。これで移動されても動作するギミックが作れるし、アニメーションの収録がしやすくなります。デフォルトでは、コンポーネントがついているオブジェクトが基準になりますが、「相対的パスのルート」で変更できます。
自分のプレハブ外の、元々からあったオブジェクトを操作する場合は、「パスモード」を「絶対的」に変えてください。
これでアニメーターの中のパスがアバターを基準に解釈され、Merge Animatorから相対的に解釈されるシステムがはずされます。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,29 @@
# Merge Blend Tree
![Merge Blend Tree](merge-blend-tree.png)
Merge Blend Treeは、複数のブレンドツリーを1つのFXレイヤーにマージすることができます。
複数のギミックを1つのレイヤーにまとめて、負荷を低減するための高度なコンポーネントです。
## いつ使うもの?
ブレンドツリーを常にアバターで稼働させたい場合に使います。
## いつ使わないもの?
ブレンドツリーを無効にしたり、モーションタイムを制御したりする必要がある場合は、Merge Blend Treeを使わないでください。
## セットアップ方法
まず、ブレンドツリーのアセットを作成します。プロジェクトウィンドウで右クリックして、Create -> BlendTreeを選択してください。
ブレンドツリーを設定したら、Merge Blend Treeコンポーネントを追加して、「ブレンドツリー」フィールドに指定します。
パスモードと相対パスルートは、Merge Animatorと同様に設定できます。
詳細は、[Merge Animatorのドキュメント](merge-animator.md)を参照してください。
## ブレンドツリーのマージ方法
Modular Avatarは、FXコントローラーの一番上に新しいレイヤーを作成します。
このレイヤーには、Write Defaultsがオンになっている単一のステートが含まれています。
マージされたブレンドツリーは、パラメーターが常に1に設定されているこのDirect Blend Treeに接続されます。

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -16,6 +16,6 @@
},
"vpmDependencies": {
"com.vrchat.avatars": ">=3.2.0",
"nadena.dev.ndmf": ">=1.3.0-alpha.0 <2.0.0-a"
"nadena.dev.ndmf": ">=1.3.0-alpha.1 <2.0.0-a"
}
}