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
{
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.localPosition = Vector3.zero;
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);
}

View File

@ -114,6 +114,7 @@ namespace net.fushizen.modular_avatar.core.editor
{
case Transform _: break;
case ModularAvatarMergeArmature _: break;
case MAInternalOffsetMarker _: break;
case VRCPhysBone _: case VRCPhysBoneCollider _: hasComponents = true;
break;
default:
@ -146,7 +147,16 @@ namespace net.fushizen.modular_avatar.core.editor
mergedSrcBone.transform.localRotation = src.transform.localRotation;
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);
BoneRemappings[src.transform] = mergedSrcBone.transform;
@ -193,6 +203,8 @@ namespace net.fushizen.modular_avatar.core.editor
}
}
var retainChild = RecursiveMerge(config, childGameObject, childNewParent, shouldZip);
retain = retain || retainChild;
}

View File

@ -7,26 +7,37 @@ namespace net.fushizen.modular_avatar.core.editor
{
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;
public struct MappingEntry
{
public string path;
public string transformPath;
public string Get(bool isTransformMapping)
{
return isTransformMapping ? transformPath : path;
}
}
internal static void Clear()
{
Mappings.Clear();
CachedMappingKeys = null;
}
internal static void Remap(string from, string to)
internal static void Remap(string from, MappingEntry to)
{
Mappings[from] = to;
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);
var bsResult = CachedMappingKeys.BinarySearch(path);
if (bsResult >= 0) return Mappings[path];
if (bsResult >= 0) return Mappings[path].Get(isTransformMapping);
int index = ~bsResult;
if (index == 0) return path;
@ -34,7 +45,7 @@ namespace net.fushizen.modular_avatar.core.editor
var priorKey = CachedMappingKeys[index - 1];
if (path.StartsWith(priorKey + "/"))
{
return Mappings[priorKey] + path.Substring(priorKey.Length);
return Mappings[priorKey].Get(isTransformMapping) + path.Substring(priorKey.Length);
}
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.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.Animations;
using Object = System.Object;
@ -19,7 +18,7 @@ namespace net.fushizen.modular_avatar.core
#if UNITY_EDITOR
void OnValidate()
{
EditorApplication.delayCall += CheckReferences;
UnityEditor.EditorApplication.delayCall += CheckReferences;
}
void CheckReferences() {
@ -48,29 +47,11 @@ namespace net.fushizen.modular_avatar.core
private void CheckConstraint()
{
if (target != null)
if (constraint != null)
{
if (constraint == null)
if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(constraint))
{
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
});
UnityEngine.Object.DestroyImmediate(constraint);
}
}
}

View File

@ -1,18 +1,25 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using Object = UnityEngine.Object;
namespace net.fushizen.modular_avatar.core
{
[ExecuteInEditMode]
public class ModularAvatarMergeArmature : AvatarTagComponent
{
public GameObject mergeTarget;
public string mergeTargetPath;
public string prefix;
public string suffix;
public bool locked;
private bool wasLocked;
#if UNITY_EDITOR
void OnValidate()
{
@ -40,8 +47,55 @@ namespace net.fushizen.modular_avatar.core
suffix = gameObject.name.Substring(insetPos + mergeTarget.name.Length);
}
}
CheckLock();
};
}
#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.Collections.Generic;
using System.Reflection;
using JetBrains.Annotations;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
@ -50,12 +50,58 @@ namespace net.fushizen.modular_avatar.core
public static void MarkDirty(UnityEngine.Object obj)
{
#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
}
#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
webGLWasmStreaming: 0
scriptingDefineSymbols:
1: VRC_SDK_VRCSDK3
1: VRC_SDK_VRCSDK3;MODULAR_AVATAR_DEBUG
platformArchitecture: {}
scriptingBackend: {}
il2cppCompilerConfiguration: {}