mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-29 18:55:06 +08:00
Animator merging & bone proxy support
This commit is contained in:
parent
625878e698
commit
9376fddc6e
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c76e0db714645f7aa6580f208f98e49
|
||||
timeCreated: 1661644852
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21339639c2ce435e97773a969d21f43a
|
||||
timeCreated: 1661649405
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32fddef4bb7c4a5fb1a06b13507bdee8
|
||||
timeCreated: 1661644932
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a5a2ea7723848d1bfe793debcf298cc
|
||||
timeCreated: 1661649007
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1bb122659f724ebf85fe095ac02dc339
|
||||
timeCreated: 1661644807
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42581d8044b64899834d3d515ab3a144
|
||||
timeCreated: 1661648057
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user