Support editing original bone animations in modular components

This adds support for animation mapping for MergeArmature components, as well as supporting locking the position of
MergeArmature-controlled transforms to their original counterparts, to ease animation editing.

With this change it might be best to eliminate the bone proxy component, as it's a bit redundant now.
This commit is contained in:
bd_ 2022-08-29 15:13:26 -07:00
parent 5b52ad45c1
commit b154b01e45
12 changed files with 216 additions and 41 deletions

View File

@ -101,7 +101,7 @@ namespace net.fushizen.modular_avatar.core.editor
} }
else else
{ {
return PathMappings.MapPath(basePath + binding.path); return PathMappings.MapPath(basePath + binding.path, binding.type == typeof(Transform));
} }
} }

View File

@ -21,7 +21,10 @@ namespace net.fushizen.modular_avatar.core.editor
transform.SetParent(proxy.target, false); transform.SetParent(proxy.target, false);
transform.localPosition = Vector3.zero; transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity; transform.localRotation = Quaternion.identity;
PathMappings.Remap(oldPath, RuntimeUtil.AvatarRootPath(proxy.gameObject)); PathMappings.Remap(oldPath, new PathMappings.MappingEntry() {
path = RuntimeUtil.AvatarRootPath(proxy.gameObject),
transformPath = RuntimeUtil.AvatarRootPath(proxy.gameObject)
});
} }
Object.DestroyImmediate(proxy); Object.DestroyImmediate(proxy);
} }

View File

@ -114,6 +114,7 @@ namespace net.fushizen.modular_avatar.core.editor
{ {
case Transform _: break; case Transform _: break;
case ModularAvatarMergeArmature _: break; case ModularAvatarMergeArmature _: break;
case MAInternalOffsetMarker _: break;
case VRCPhysBone _: case VRCPhysBoneCollider _: hasComponents = true; case VRCPhysBone _: case VRCPhysBoneCollider _: hasComponents = true;
break; break;
default: default:
@ -146,7 +147,16 @@ namespace net.fushizen.modular_avatar.core.editor
mergedSrcBone.transform.localRotation = src.transform.localRotation; mergedSrcBone.transform.localRotation = src.transform.localRotation;
mergedSrcBone.transform.localScale = src.transform.localScale; mergedSrcBone.transform.localScale = src.transform.localScale;
if (zipMerge) BoneDatabase.AddMergedBone(mergedSrcBone.transform); if (zipMerge)
{
BoneDatabase.AddMergedBone(mergedSrcBone.transform);
var srcPath = RuntimeUtil.AvatarRootPath(src);
PathMappings.Remap(srcPath, new PathMappings.MappingEntry()
{
transformPath = zipMerge ? RuntimeUtil.AvatarRootPath(newParent) : srcPath,
path = srcPath
});
}
mergedSrcBone.transform.SetParent(newParent.transform, true); mergedSrcBone.transform.SetParent(newParent.transform, true);
BoneRemappings[src.transform] = mergedSrcBone.transform; BoneRemappings[src.transform] = mergedSrcBone.transform;
@ -192,6 +202,8 @@ namespace net.fushizen.modular_avatar.core.editor
shouldZip = false; shouldZip = false;
} }
} }
var retainChild = RecursiveMerge(config, childGameObject, childNewParent, shouldZip); var retainChild = RecursiveMerge(config, childGameObject, childNewParent, shouldZip);
retain = retain || retainChild; retain = retain || retainChild;

View File

@ -7,26 +7,37 @@ namespace net.fushizen.modular_avatar.core.editor
{ {
public static class PathMappings public static class PathMappings
{ {
private static SortedDictionary<string, string> Mappings = new SortedDictionary<string, string>(); private static SortedDictionary<string, MappingEntry> Mappings = new SortedDictionary<string, MappingEntry>();
private static List<string> CachedMappingKeys = null; private static List<string> CachedMappingKeys = null;
public struct MappingEntry
{
public string path;
public string transformPath;
public string Get(bool isTransformMapping)
{
return isTransformMapping ? transformPath : path;
}
}
internal static void Clear() internal static void Clear()
{ {
Mappings.Clear(); Mappings.Clear();
CachedMappingKeys = null; CachedMappingKeys = null;
} }
internal static void Remap(string from, string to) internal static void Remap(string from, MappingEntry to)
{ {
Mappings[from] = to; Mappings[from] = to;
CachedMappingKeys = null; CachedMappingKeys = null;
} }
internal static string MapPath(string path) internal static string MapPath(string path, bool isTransformMapping = false)
{ {
if (CachedMappingKeys == null) CachedMappingKeys = new List<string>(Mappings.Keys); if (CachedMappingKeys == null) CachedMappingKeys = new List<string>(Mappings.Keys);
var bsResult = CachedMappingKeys.BinarySearch(path); var bsResult = CachedMappingKeys.BinarySearch(path);
if (bsResult >= 0) return Mappings[path]; if (bsResult >= 0) return Mappings[path].Get(isTransformMapping);
int index = ~bsResult; int index = ~bsResult;
if (index == 0) return path; if (index == 0) return path;
@ -34,7 +45,7 @@ namespace net.fushizen.modular_avatar.core.editor
var priorKey = CachedMappingKeys[index - 1]; var priorKey = CachedMappingKeys[index - 1];
if (path.StartsWith(priorKey + "/")) if (path.StartsWith(priorKey + "/"))
{ {
return Mappings[priorKey] + path.Substring(priorKey.Length); return Mappings[priorKey].Get(isTransformMapping) + path.Substring(priorKey.Length);
} }
return path; return path;
} }

View File

@ -0,0 +1,4 @@
namespace net.fushizen.modular_avatar.core
{
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c0b936b1b6ec46ea94aa30c4f5f480d3
timeCreated: 1661806899

View File

@ -0,0 +1,58 @@
using Codice.CM.WorkspaceServer.Tree;
using UnityEngine;
namespace net.fushizen.modular_avatar.core
{
[ExecuteInEditMode]
public class MAInternalOffsetMarker : AvatarTagComponent
{
void OnValidate()
{
#if MODULAR_AVATAR_DEBUG
hideFlags = HideFlags.None;
#else
hideFlags = HideFlags.HideInInspector;
#endif
}
private const float POS_EPSILON = 0.01f;
private const float ROT_EPSILON = 0.01f;
private Vector3 lastLocalPos;
private Vector3 lastLocalScale;
private Quaternion lastLocalRot;
public Transform correspondingObject;
public bool lockBasePosition;
public void Update()
{
if (correspondingObject == null) return;
// ReSharper disable once LocalVariableHidesMember
var transform = this.transform;
if ((transform.localPosition - lastLocalPos).sqrMagnitude > POS_EPSILON
|| (transform.localScale - lastLocalScale).sqrMagnitude > POS_EPSILON
|| Quaternion.Angle(lastLocalRot, transform.localRotation) > ROT_EPSILON)
{
if (lockBasePosition) transform.position = correspondingObject.position;
else correspondingObject.localPosition = transform.localPosition;
correspondingObject.localScale = transform.localScale;
correspondingObject.localRotation = transform.localRotation;
}
else
{
if (lockBasePosition) transform.position = correspondingObject.position;
else transform.localPosition = correspondingObject.localPosition;
transform.localScale = correspondingObject.localScale;
transform.localRotation = correspondingObject.localRotation;
}
lastLocalPos = transform.localPosition;
lastLocalScale = transform.localScale;
lastLocalRot = transform.localRotation;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7b59aa936ba8466e85aede8aa3631b84
timeCreated: 1661807782

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityEngine.Animations; using UnityEngine.Animations;
using Object = System.Object; using Object = System.Object;
@ -15,13 +14,13 @@ namespace net.fushizen.modular_avatar.core
public string subPath; public string subPath;
[SerializeField] [HideInInspector] public ParentConstraint constraint; [SerializeField] [HideInInspector] public ParentConstraint constraint;
#if UNITY_EDITOR #if UNITY_EDITOR
void OnValidate() void OnValidate()
{ {
EditorApplication.delayCall += CheckReferences; UnityEditor.EditorApplication.delayCall += CheckReferences;
} }
void CheckReferences() { void CheckReferences() {
if (this == null) return; // post-destroy if (this == null) return; // post-destroy
@ -48,29 +47,11 @@ namespace net.fushizen.modular_avatar.core
private void CheckConstraint() private void CheckConstraint()
{ {
if (target != null) if (constraint != null)
{ {
if (constraint == null) if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(constraint))
{ {
constraint = gameObject.AddComponent<ParentConstraint>(); UnityEngine.Object.DestroyImmediate(constraint);
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
});
} }
} }
} }

View File

@ -1,18 +1,25 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
#if UNITY_EDITOR
using UnityEditor; using UnityEditor;
#endif
using UnityEngine; using UnityEngine;
using Object = UnityEngine.Object;
namespace net.fushizen.modular_avatar.core namespace net.fushizen.modular_avatar.core
{ {
[ExecuteInEditMode]
public class ModularAvatarMergeArmature : AvatarTagComponent public class ModularAvatarMergeArmature : AvatarTagComponent
{ {
public GameObject mergeTarget; public GameObject mergeTarget;
public string mergeTargetPath; public string mergeTargetPath;
public string prefix; public string prefix;
public string suffix; public string suffix;
public bool locked;
private bool wasLocked;
#if UNITY_EDITOR #if UNITY_EDITOR
void OnValidate() void OnValidate()
{ {
@ -40,8 +47,55 @@ namespace net.fushizen.modular_avatar.core
suffix = gameObject.name.Substring(insetPos + mergeTarget.name.Length); suffix = gameObject.name.Substring(insetPos + mergeTarget.name.Length);
} }
} }
CheckLock();
}; };
} }
#endif #endif
void CheckLock()
{
if (RuntimeUtil.isPlaying) return;
if (locked != wasLocked)
{
if (!locked)
{
foreach (var comp in GetComponentsInChildren<MAInternalOffsetMarker>())
{
DestroyImmediate(comp);
}
wasLocked = false;
}
else
{
if (mergeTarget == null) return;
foreach (var xform in GetComponentsInChildren<Transform>(true))
{
Transform baseObject = FindCorresponding(xform);
if (baseObject != null && xform.gameObject.GetComponent<MAInternalOffsetMarker>() == null)
{
var comp = xform.gameObject.AddComponent<MAInternalOffsetMarker>();
comp.correspondingObject = baseObject;
comp.lockBasePosition = baseObject.gameObject == mergeTarget;
}
}
wasLocked = true;
}
}
}
private Transform FindCorresponding(Transform xform)
{
if (xform == null) return null;
if (xform == transform) return mergeTarget.transform;
var correspondingParent = FindCorresponding(xform.parent);
if (correspondingParent == null) return null;
return correspondingParent.Find(prefix + xform.name + suffix);
}
} }
} }

View File

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
using JetBrains.Annotations; using JetBrains.Annotations;
using UnityEditor;
using UnityEngine; using UnityEngine;
using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.Components;
@ -49,13 +49,59 @@ namespace net.fushizen.modular_avatar.core
public static void MarkDirty(UnityEngine.Object obj) public static void MarkDirty(UnityEngine.Object obj)
{ {
#if UNITY_EDITOR #if UNITY_EDITOR
if (PrefabUtility.IsPartOfPrefabInstance(obj)) if (UnityEditor.PrefabUtility.IsPartOfPrefabInstance(obj))
{ {
PrefabUtility.RecordPrefabInstancePropertyModifications(obj); UnityEditor.PrefabUtility.RecordPrefabInstancePropertyModifications(obj);
} }
EditorUtility.SetDirty(obj); UnityEditor.EditorUtility.SetDirty(obj);
#endif #endif
} }
#if UNITY_EDITOR
private static UnityEngine.Object cachedAnimationWindowState;
private static readonly Type animationWindowStateType
= typeof(UnityEditor.Editor).Assembly.GetType("UnityEditorInternal.AnimationWindowState");
private static readonly PropertyInfo recordingProp = animationWindowStateType.GetProperty(
"recording",
BindingFlags.Instance | BindingFlags.Public
);
private static readonly PropertyInfo previewingProp = animationWindowStateType.GetProperty(
"previewing",
BindingFlags.Instance | BindingFlags.Public
);
private static readonly PropertyInfo playingProp = animationWindowStateType.GetProperty(
"playing",
BindingFlags.Instance | BindingFlags.Public
);
#endif
public static bool IsAnimationEditMode()
{
#if !UNITY_EDITOR
return false;
#else
if (cachedAnimationWindowState == null)
{
foreach (var obj in Resources.FindObjectsOfTypeAll(animationWindowStateType))
{
cachedAnimationWindowState = obj;
}
}
if (cachedAnimationWindowState == null) return false;
return (bool) recordingProp.GetValue(cachedAnimationWindowState, null)
|| (bool) previewingProp.GetValue(cachedAnimationWindowState, null)
|| (bool) playingProp.GetValue(cachedAnimationWindowState, null);
#endif
}
#if UNITY_EDITOR
public static bool isPlaying => UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode;
#else
public static bool isPlaying => true;
#endif
} }
} }

View File

@ -619,7 +619,7 @@ PlayerSettings:
webGLThreadsSupport: 0 webGLThreadsSupport: 0
webGLWasmStreaming: 0 webGLWasmStreaming: 0
scriptingDefineSymbols: scriptingDefineSymbols:
1: VRC_SDK_VRCSDK3 1: VRC_SDK_VRCSDK3;MODULAR_AVATAR_DEBUG
platformArchitecture: {} platformArchitecture: {}
scriptingBackend: {} scriptingBackend: {}
il2cppCompilerConfiguration: {} il2cppCompilerConfiguration: {}