Animator merging & bone proxy support

This commit is contained in:
bd_ 2022-08-27 19:04:53 -07:00
parent 625878e698
commit 9376fddc6e
15 changed files with 625 additions and 5 deletions

View File

@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using Object = UnityEngine.Object;
namespace net.fushizen.modular_avatar.core.editor
{
public class AnimatorCombiner
{
private readonly AnimatorController _combined;
private List<AnimatorControllerLayer> _layers = new List<AnimatorControllerLayer>();
private Dictionary<String, AnimatorControllerParameter> _parameters =
new Dictionary<string, AnimatorControllerParameter>();
private Dictionary<KeyValuePair<String, Motion>, Motion> _motions =
new Dictionary<KeyValuePair<string, Motion>, Motion>();
private Dictionary<KeyValuePair<String, AnimatorStateMachine>, AnimatorStateMachine> _stateMachines =
new Dictionary<KeyValuePair<string, AnimatorStateMachine>, AnimatorStateMachine>();
public AnimatorCombiner()
{
_combined = Util.CreateContainer();
}
public AnimatorController Finish()
{
_combined.parameters = _parameters.Values.ToArray();
_combined.layers = _layers.ToArray();
AssetDatabase.SaveAssets();
return _combined;
}
public void AddController(String basePath, AnimatorController controller)
{
foreach (var param in controller.parameters)
{
if (_parameters.TryGetValue(param.name, out var acp))
{
if (acp.type != param.type)
{
throw new Exception($"Parameter {param.name} has different types in {basePath} and {controller.name}");
}
continue;
}
_parameters.Add(param.name, param);
}
bool first = true;
foreach (var layer in controller.layers)
{
insertLayer(basePath, layer, first);
first = false;
}
}
private void insertLayer(string basePath, AnimatorControllerLayer layer, bool first)
{
var newLayer = new AnimatorControllerLayer()
{
name = layer.name,
avatarMask = layer.avatarMask, // TODO map transforms
blendingMode = layer.blendingMode,
defaultWeight = first ? 1 : layer.defaultWeight,
syncedLayerIndex = layer.syncedLayerIndex, // TODO
syncedLayerAffectsTiming = layer.syncedLayerAffectsTiming, // TODO
iKPass = layer.iKPass,
stateMachine = mapStateMachine(basePath, layer.stateMachine),
};
_layers.Add(newLayer);
}
private AnimatorStateMachine mapStateMachine(string basePath, AnimatorStateMachine layerStateMachine)
{
var cacheKey = new KeyValuePair<string, AnimatorStateMachine>(basePath, layerStateMachine);
if (_stateMachines.TryGetValue(cacheKey, out var asm))
{
return asm;
}
asm = deepClone(layerStateMachine, (obj) => customClone(obj, basePath));
_stateMachines[cacheKey] = asm;
return asm;
}
private Object customClone(Object o, string basePath)
{
if (basePath == "") return null;
if (o is AnimationClip clip)
{
AnimationClip newClip = new AnimationClip();
newClip.name = "rebased " + clip.name;
AssetDatabase.AddObjectToAsset(newClip, _combined);
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var newBinding = binding;
newBinding.path = PathMappings.MapPath(basePath + binding.path);
newClip.SetCurve(newBinding.path, newBinding.type, newBinding.propertyName,
AnimationUtility.GetEditorCurve(clip, binding));
}
foreach (var objBinding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
{
var newBinding = objBinding;
newBinding.path = PathMappings.MapPath(basePath + objBinding.path);
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 = null
) where T : Object
{
if (original == null) return null;
if (cloneMap == null) cloneMap = new Dictionary<Object, Object>();
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 || obj is ScriptableObject)
{
obj = Object.Instantiate(original);
}
else
{
obj = (T) ctor.Invoke(Array.Empty<object>());
EditorUtility.CopySerialized(original, obj);
}
cloneMap[original] = obj;
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

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3c76e0db714645f7aa6580f208f98e49
timeCreated: 1661644852

View File

@ -0,0 +1,32 @@
using UnityEngine;
using VRC.SDKBase.Editor.BuildPipeline;
namespace net.fushizen.modular_avatar.core.editor
{
public class BoneProxyHook : IVRCSDKPreprocessAvatarCallback
{
public int callbackOrder => HookSequence.SEQ_BONE_PROXY;
public bool OnPreprocessAvatar(GameObject avatarGameObject)
{
var boneProxies = avatarGameObject.GetComponentsInChildren<ModularAvatarBoneProxy>(true);
foreach (var proxy in boneProxies)
{
if (proxy.constraint != null) UnityEngine.Object.DestroyImmediate(proxy.constraint);
if (proxy.target != null)
{
var oldPath = RuntimeUtil.AvatarRootPath(proxy.gameObject);
Transform transform = proxy.transform;
transform.SetParent(proxy.target, false);
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
PathMappings.Remap(oldPath, RuntimeUtil.AvatarRootPath(proxy.gameObject));
}
Object.DestroyImmediate(proxy);
}
return true;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 21339639c2ce435e97773a969d21f43a
timeCreated: 1661649405

View File

@ -3,7 +3,9 @@
internal static class HookSequence
{
public const int SEQ_RESETTERS = -90000;
public const int SEQ_MERGE_ARMATURE = -80001;
public const int SEQ_RETARGET_MESH = -80000;
public const int SEQ_MERGE_ARMATURE = SEQ_RESETTERS + 1;
public const int SEQ_RETARGET_MESH = SEQ_MERGE_ARMATURE + 1;
public const int SEQ_BONE_PROXY = SEQ_RETARGET_MESH + 1;
public const int SEQ_MERGE_ANIMATORS = SEQ_BONE_PROXY + 1;
}
}

View File

@ -0,0 +1,142 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDKBase.Editor.BuildPipeline;
namespace net.fushizen.modular_avatar.core.editor
{
public class MergeAnimatorHook : IVRCSDKPreprocessAvatarCallback
{
private const string SAMPLE_PATH_PACKAGE = "Packages/com.vrchat.avatars/Samples/AV3 Demo Assets/Animation";
private const string SAMPLE_PATH_LEGACY = "Assets/VRCSDK/Examples3/Animation/Controllers";
public int callbackOrder => HookSequence.SEQ_MERGE_ANIMATORS;
Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner> mergeSessions =
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner>();
public bool OnPreprocessAvatar(GameObject avatarGameObject)
{
var descriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
InitSessions(descriptor.baseAnimationLayers);
InitSessions(descriptor.specialAnimationLayers);
var toMerge = avatarGameObject.transform.GetComponentsInChildren<MergeAnimator>(true);
foreach (var merge in toMerge)
{
if (merge.animator != null && mergeSessions.TryGetValue(merge.layerType, out var session))
{
var relativePath = RuntimeUtil.RelativePath(avatarGameObject, merge.gameObject);
mergeSessions[merge.layerType].AddController(
relativePath != "" ? relativePath + "/" : "",
(AnimatorController) merge.animator
);
}
if (merge.deleteAttachedAnimator)
{
var animator = merge.GetComponent<Animator>();
if (animator != null) Object.DestroyImmediate(animator);
}
}
descriptor.baseAnimationLayers = FinishSessions(descriptor.baseAnimationLayers);
descriptor.specialAnimationLayers = FinishSessions(descriptor.specialAnimationLayers);
return true;
}
private VRCAvatarDescriptor.CustomAnimLayer[] FinishSessions(
VRCAvatarDescriptor.CustomAnimLayer[] layers
)
{
layers = (VRCAvatarDescriptor.CustomAnimLayer[]) layers.Clone();
for (int i = 0; i < layers.Length; i++)
{
layers[i].isDefault = false;
layers[i].animatorController = mergeSessions[layers[i].type].Finish();
}
return layers;
}
private void InitSessions(VRCAvatarDescriptor.CustomAnimLayer[] layers)
{
foreach (var layer in layers)
{
var controller = ResolveLayerController(layer);
if (controller == null) controller = new AnimatorController();
var session = new AnimatorCombiner();
session.AddController("", controller);
mergeSessions[layer.type] = session;
}
}
private static AnimatorController ResolveLayerController(VRCAvatarDescriptor.CustomAnimLayer layer)
{
AnimatorController controller = null;
if (!layer.isDefault && layer.animatorController != null &&
layer.animatorController is AnimatorController c)
{
controller = c;
}
else
{
string name;
switch (layer.type)
{
case VRCAvatarDescriptor.AnimLayerType.Action:
name = "Action";
break;
case VRCAvatarDescriptor.AnimLayerType.Additive:
name = "Idle";
break;
case VRCAvatarDescriptor.AnimLayerType.Base:
name = "Locomotion";
break;
case VRCAvatarDescriptor.AnimLayerType.Gesture:
name = "Hands";
break;
case VRCAvatarDescriptor.AnimLayerType.Sitting:
name = "Sitting";
break;
case VRCAvatarDescriptor.AnimLayerType.FX:
name = "Face";
break;
case VRCAvatarDescriptor.AnimLayerType.TPose:
name = "UtilityTPose";
break;
case VRCAvatarDescriptor.AnimLayerType.IKPose:
name = "UtilityIKPose";
break;
default:
name = null;
break;
}
if (name == null)
{
name = "/vrc_AvatarV3" + name + "Layer";
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(SAMPLE_PATH_PACKAGE + name);
if (controller == null)
{
controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(SAMPLE_PATH_LEGACY + name);
}
}
}
return controller;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 32fddef4bb7c4a5fb1a06b13507bdee8
timeCreated: 1661644932

View File

@ -38,7 +38,7 @@ namespace net.fushizen.modular_avatar.core.editor
foreach (var renderer in avatarGameObject.transform.GetComponentsInChildren<SkinnedMeshRenderer>())
{
var bones = renderer.bones;
for (int i = 0; i < bones.Length; i++) bones[i] = MapBoneReference(bones[i]);
for (int i = 0; i < bones.Length; i++) bones[i] = MapBoneReference(bones[i], false);
renderer.bones = bones;
renderer.rootBone = MapBoneReference(renderer.rootBone);
renderer.probeAnchor = MapBoneReference(renderer.probeAnchor);
@ -93,11 +93,11 @@ namespace net.fushizen.modular_avatar.core.editor
so.ApplyModifiedPropertiesWithoutUndo();
}
private Transform MapBoneReference(Transform bone)
private Transform MapBoneReference(Transform bone, bool markNonRetargetable = true)
{
if (bone != null && BoneRemappings.TryGetValue(bone, out var newBone))
{
BoneDatabase.MarkNonRetargetable(newBone);
if (markNonRetargetable) BoneDatabase.MarkNonRetargetable(newBone);
bone = newBone;
}
return bone;

View File

@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using UnityEngine;
using VRC.SDKBase.Editor.BuildPipeline;
namespace net.fushizen.modular_avatar.core.editor
{
public static class PathMappings
{
private static SortedDictionary<string, string> Mappings = new SortedDictionary<string, string>();
private static List<string> CachedMappingKeys = null;
internal static void Clear()
{
Mappings.Clear();
CachedMappingKeys = null;
}
internal static void Remap(string from, string to)
{
Mappings[from] = to;
CachedMappingKeys = null;
}
internal static string MapPath(string path)
{
if (CachedMappingKeys == null) CachedMappingKeys = new List<string>(Mappings.Keys);
var bsResult = CachedMappingKeys.BinarySearch(path);
if (bsResult >= 0) return Mappings[path];
int index = ~bsResult;
if (index == 0) return path;
var priorKey = CachedMappingKeys[index - 1];
if (path.StartsWith(priorKey + "/"))
{
return Mappings[priorKey] + path.Substring(priorKey.Length);
}
return path;
}
}
internal class ClearPathMappings : IVRCSDKPreprocessAvatarCallback
{
public int callbackOrder => HookSequence.SEQ_RESETTERS;
public bool OnPreprocessAvatar(GameObject avatarGameObject)
{
PathMappings.Clear();
return true;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6a5a2ea7723848d1bfe793debcf298cc
timeCreated: 1661649007

View File

@ -0,0 +1,12 @@
using UnityEngine;
using VRC.SDK3.Avatars.Components;
namespace net.fushizen.modular_avatar.core
{
public class MergeAnimator : AvatarTagComponent
{
public RuntimeAnimatorController animator;
public VRCAvatarDescriptor.AnimLayerType layerType;
public bool deleteAttachedAnimator;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1bb122659f724ebf85fe095ac02dc339
timeCreated: 1661644807

View File

@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.Animations;
using Object = System.Object;
namespace net.fushizen.modular_avatar.core
{
public class ModularAvatarBoneProxy : AvatarTagComponent
{
public Transform target;
public HumanBodyBones boneReference = HumanBodyBones.LastBone;
public string subPath;
[SerializeField] [HideInInspector] public ParentConstraint constraint;
#if UNITY_EDITOR
void OnValidate()
{
EditorApplication.delayCall += CheckReferences;
}
void CheckReferences() {
if (this == null) return; // post-destroy
if (target == null && (boneReference != HumanBodyBones.LastBone || !string.IsNullOrWhiteSpace(subPath)))
{
UpdateDynamicMapping();
if (target != null)
{
RuntimeUtil.MarkDirty(this);
}
} else if (target != null)
{
var origBoneReference = boneReference;
var origSubpath = subPath;
UpdateStaticMapping();
if (origSubpath != subPath || origBoneReference != boneReference)
{
RuntimeUtil.MarkDirty(this);
}
}
CheckConstraint();
}
private void CheckConstraint()
{
if (target != null)
{
if (constraint == null)
{
constraint = gameObject.AddComponent<ParentConstraint>();
constraint.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
constraint.AddSource(new ConstraintSource()
{
weight = 1,
sourceTransform = target
});
constraint.translationOffsets = new Vector3[] {Vector3.zero};
constraint.rotationOffsets = new Vector3[] {Vector3.zero};
constraint.locked = true;
constraint.constraintActive = true;
}
else
{
constraint.SetSource(0, new ConstraintSource()
{
weight = 1,
sourceTransform = target
});
}
}
}
private void OnDestroy()
{
if (constraint != null) DestroyImmediate(constraint);
}
private void UpdateDynamicMapping()
{
var avatar = RuntimeUtil.FindAvatarInParents(transform);
if (avatar == null) return;
if (subPath == "$$AVATAR")
{
target = avatar.transform;
return;
}
if (boneReference == HumanBodyBones.LastBone)
{
target = avatar.transform.Find(subPath);
return;
}
var animator = avatar.GetComponent<Animator>();
if (animator == null) return;
var bone = animator.GetBoneTransform(boneReference);
if (bone == null) return;
if (string.IsNullOrWhiteSpace(subPath)) target = bone;
else target = bone.Find(subPath);
}
private void UpdateStaticMapping()
{
var avatar = RuntimeUtil.FindAvatarInParents(transform);
var humanBones = new Dictionary<Transform, HumanBodyBones>();
var animator = avatar.GetComponent<Animator>();
if (animator == null)
{
return;
}
foreach (var boneTypeObj in Enum.GetValues(typeof(HumanBodyBones)))
{
var boneType = (HumanBodyBones) boneTypeObj;
if (boneType == HumanBodyBones.LastBone) continue;
var bone = animator.GetBoneTransform(boneType);
if (bone != null) humanBones[bone] = boneType;
}
Transform iter = target;
Transform avatarTransform = avatar.transform;
if (target == avatarTransform)
{
boneReference = HumanBodyBones.LastBone;
subPath = "$$AVATAR";
return;
}
while (iter != avatarTransform && !humanBones.ContainsKey(iter))
{
iter = iter.parent;
}
if (iter == avatarTransform)
{
boneReference = HumanBodyBones.LastBone;
}
else
{
boneReference = humanBones[iter];
}
subPath = RuntimeUtil.RelativePath(iter.gameObject, target.gameObject);
}
#endif
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 42581d8044b64899834d3d515ab3a144
timeCreated: 1661648057

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
@ -45,5 +46,14 @@ namespace net.fushizen.modular_avatar.core
return null;
}
public static void MarkDirty(UnityEngine.Object obj)
{
if (PrefabUtility.IsPartOfPrefabInstance(obj))
{
PrefabUtility.RecordPrefabInstancePropertyModifications(obj);
}
EditorUtility.SetDirty(obj);
}
}
}