mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-02-07 06:12:47 +08:00
feat: shape changer can respond to multiple parent objects being animated (#916)
* refactor: generalize the shape changer implementation a bit * feat: shape changer can respond to multiple parent objects being animated Closes: #865
This commit is contained in:
parent
746a72f60b
commit
57fe84548c
@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using nadena.dev.modular_avatar.animation;
|
using nadena.dev.modular_avatar.animation;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using EditorCurveBinding = UnityEditor.EditorCurveBinding;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
|
@ -7,6 +7,7 @@ using nadena.dev.ndmf;
|
|||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.Animations;
|
using UnityEditor.Animations;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using VRC.SDK3.Avatars.Components;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@ -29,6 +30,8 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
private BuildContext _context;
|
private BuildContext _context;
|
||||||
private AnimationDatabase _animationDatabase;
|
private AnimationDatabase _animationDatabase;
|
||||||
private PathMappings _pathMappings;
|
private PathMappings _pathMappings;
|
||||||
|
private ReadableProperty _readableProperty;
|
||||||
|
|
||||||
private Dictionary<GameObject, string> _selfProxies = new();
|
private Dictionary<GameObject, string> _selfProxies = new();
|
||||||
|
|
||||||
public void OnActivate(BuildContext context)
|
public void OnActivate(BuildContext context)
|
||||||
@ -40,6 +43,8 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
|
|
||||||
_pathMappings = new PathMappings();
|
_pathMappings = new PathMappings();
|
||||||
_pathMappings.OnActivate(context, _animationDatabase);
|
_pathMappings.OnActivate(context, _animationDatabase);
|
||||||
|
|
||||||
|
_readableProperty = new ReadableProperty(_context, _animationDatabase, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnDeactivate(BuildContext context)
|
public void OnDeactivate(BuildContext context)
|
||||||
@ -79,6 +84,17 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HACK: This is a temporary crutch until we rework the entire animator services system
|
||||||
|
public void AddPropertyDefinition(AnimatorControllerParameter paramDef)
|
||||||
|
{
|
||||||
|
var fx = (AnimatorController)
|
||||||
|
_context.AvatarDescriptor.baseAnimationLayers
|
||||||
|
.First(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX)
|
||||||
|
.animatorController;
|
||||||
|
|
||||||
|
fx.parameters = fx.parameters.Concat(new[] { paramDef }).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a parameter which proxies the "activeSelf" state of the specified GameObject.
|
/// Returns a parameter which proxies the "activeSelf" state of the specified GameObject.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -98,76 +114,8 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var iid = obj.GetInstanceID();
|
paramName = _readableProperty.ForActiveSelf(_pathMappings.GetObjectIdentifier(obj));
|
||||||
paramName = $"_MA/ActiveSelf/{iid}";
|
return true;
|
||||||
|
|
||||||
var binding = EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive");
|
|
||||||
|
|
||||||
bool hadAnyClip = false;
|
|
||||||
foreach (var clip in clips)
|
|
||||||
{
|
|
||||||
Motion newMotion = ProcessActiveSelf(clip.CurrentClip, paramName, binding);
|
|
||||||
if (newMotion != clip.CurrentClip)
|
|
||||||
{
|
|
||||||
clip.SetCurrentNoInvalidate(newMotion);
|
|
||||||
hadAnyClip = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hadAnyClip)
|
|
||||||
{
|
|
||||||
_selfProxies[obj] = paramName;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_selfProxies[obj] = "";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Motion ProcessActiveSelf(Motion motion, string paramName, EditorCurveBinding binding)
|
|
||||||
{
|
|
||||||
if (motion is AnimationClip clip)
|
|
||||||
{
|
|
||||||
var curve = AnimationUtility.GetEditorCurve(clip, binding);
|
|
||||||
if (curve == null) return motion;
|
|
||||||
|
|
||||||
var newClip = new AnimationClip();
|
|
||||||
EditorUtility.CopySerialized(motion, newClip);
|
|
||||||
|
|
||||||
newClip.SetCurve("", typeof(Animator), paramName, curve);
|
|
||||||
return newClip;
|
|
||||||
}
|
|
||||||
else if (motion is BlendTree bt)
|
|
||||||
{
|
|
||||||
bool anyChanged = false;
|
|
||||||
|
|
||||||
var motions = bt.children.Select(c => // c is struct ChildMotion
|
|
||||||
{
|
|
||||||
var newMotion = ProcessActiveSelf(c.motion, paramName, binding);
|
|
||||||
anyChanged |= newMotion != c.motion;
|
|
||||||
c.motion = newMotion;
|
|
||||||
return c;
|
|
||||||
}).ToArray();
|
|
||||||
|
|
||||||
if (anyChanged)
|
|
||||||
{
|
|
||||||
var newBt = new BlendTree();
|
|
||||||
EditorUtility.CopySerialized(bt, newBt);
|
|
||||||
|
|
||||||
newBt.children = motions;
|
|
||||||
return newBt;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return bt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return motion;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -191,7 +191,7 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string MapPath(EditorCurveBinding binding, string basePath)
|
private static string MapPath(UnityEditor.EditorCurveBinding binding, string basePath)
|
||||||
{
|
{
|
||||||
if (binding.type == typeof(Animator) && binding.path == "")
|
if (binding.type == typeof(Animator) && binding.path == "")
|
||||||
{
|
{
|
||||||
|
18
Editor/Animation/EditorCurveBindingComparer.cs
Normal file
18
Editor/Animation/EditorCurveBindingComparer.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.animation
|
||||||
|
{
|
||||||
|
internal class EditorCurveBindingComparer : IEqualityComparer<EditorCurveBinding>
|
||||||
|
{
|
||||||
|
public bool Equals(UnityEditor.EditorCurveBinding x, UnityEditor.EditorCurveBinding y)
|
||||||
|
{
|
||||||
|
return x.path == y.path && x.type == y.type && x.propertyName == y.propertyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(UnityEditor.EditorCurveBinding obj)
|
||||||
|
{
|
||||||
|
return obj.path.GetHashCode() ^ obj.type.GetHashCode() ^ obj.propertyName.GetHashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/Animation/EditorCurveBindingComparer.cs.meta
Normal file
3
Editor/Animation/EditorCurveBindingComparer.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e751f7889323485bbe202285a47cb0d4
|
||||||
|
timeCreated: 1719196767
|
@ -33,6 +33,7 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
private HashSet<GameObject> _transformLookthroughObjects = new HashSet<GameObject>();
|
private HashSet<GameObject> _transformLookthroughObjects = new HashSet<GameObject>();
|
||||||
private ImmutableDictionary<string, string> _originalPathToMappedPath = null;
|
private ImmutableDictionary<string, string> _originalPathToMappedPath = null;
|
||||||
private ImmutableDictionary<string, string> _transformOriginalPathToMappedPath = null;
|
private ImmutableDictionary<string, string> _transformOriginalPathToMappedPath = null;
|
||||||
|
private ImmutableDictionary<string, GameObject> _pathToObject = null;
|
||||||
|
|
||||||
internal void OnActivate(BuildContext context, AnimationDatabase animationDatabase)
|
internal void OnActivate(BuildContext context, AnimationDatabase animationDatabase)
|
||||||
{
|
{
|
||||||
@ -51,6 +52,7 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
{
|
{
|
||||||
_originalPathToMappedPath = null;
|
_originalPathToMappedPath = null;
|
||||||
_transformOriginalPathToMappedPath = null;
|
_transformOriginalPathToMappedPath = null;
|
||||||
|
_pathToObject = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -194,7 +196,7 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string MapPath(EditorCurveBinding binding)
|
private string MapPath(UnityEditor.EditorCurveBinding binding)
|
||||||
{
|
{
|
||||||
if (binding.type == typeof(Animator) && binding.path == "")
|
if (binding.type == typeof(Animator) && binding.path == "")
|
||||||
{
|
{
|
||||||
@ -298,5 +300,23 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
listener.OnCommitObjectRenames(context, this);
|
listener.OnCommitObjectRenames(context, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GameObject PathToObject(string path)
|
||||||
|
{
|
||||||
|
if (_pathToObject == null)
|
||||||
|
{
|
||||||
|
_pathToObject = _objectToOriginalPaths.SelectMany(kvp => kvp.Value.Select(p => (p, kvp.Key)))
|
||||||
|
.ToImmutableDictionary(t => t.p, t => t.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pathToObject.TryGetValue(path, out var obj))
|
||||||
|
{
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
143
Editor/Animation/ReadableProperty.cs
Normal file
143
Editor/Animation/ReadableProperty.cs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using nadena.dev.ndmf;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.Animations;
|
||||||
|
using UnityEngine;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.animation
|
||||||
|
{
|
||||||
|
internal class ReadableProperty
|
||||||
|
{
|
||||||
|
private readonly BuildContext _context;
|
||||||
|
private readonly AnimationDatabase _animDB;
|
||||||
|
private readonly AnimationServicesContext _asc;
|
||||||
|
private readonly Dictionary<EditorCurveBinding, string> _alreadyBound = new();
|
||||||
|
private long _nextIndex;
|
||||||
|
|
||||||
|
public ReadableProperty(BuildContext context, AnimationDatabase animDB, AnimationServicesContext asc)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_animDB = animDB;
|
||||||
|
_asc = asc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an animator parameter which tracks the effective value of a property on a component. This only
|
||||||
|
/// tracks FX layer properties.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ecb"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string ForBinding(string path, Type componentType, string property)
|
||||||
|
{
|
||||||
|
var ecb = new EditorCurveBinding
|
||||||
|
{
|
||||||
|
path = path,
|
||||||
|
type = componentType,
|
||||||
|
propertyName = property
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_alreadyBound.TryGetValue(ecb, out var reader))
|
||||||
|
{
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastComponent = path.Split("/")[^1];
|
||||||
|
var emuPropName = $"__MA/ReadableProp/{lastComponent}/{componentType}/{property}#{_nextIndex++}";
|
||||||
|
|
||||||
|
float initialValue = 0;
|
||||||
|
var gameObject = _asc.PathMappings.PathToObject(path);
|
||||||
|
Object component = componentType == typeof(GameObject)
|
||||||
|
? gameObject
|
||||||
|
: gameObject?.GetComponent(componentType);
|
||||||
|
if (component != null)
|
||||||
|
{
|
||||||
|
var so = new SerializedObject(component);
|
||||||
|
var prop = so.FindProperty(property);
|
||||||
|
if (prop != null)
|
||||||
|
switch (prop.propertyType)
|
||||||
|
{
|
||||||
|
case SerializedPropertyType.Boolean:
|
||||||
|
initialValue = prop.boolValue ? 1 : 0;
|
||||||
|
break;
|
||||||
|
case SerializedPropertyType.Float:
|
||||||
|
initialValue = prop.floatValue;
|
||||||
|
break;
|
||||||
|
case SerializedPropertyType.Integer:
|
||||||
|
initialValue = prop.intValue;
|
||||||
|
break;
|
||||||
|
default: throw new NotImplementedException($"Property type {prop.type} not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_asc.AddPropertyDefinition(new AnimatorControllerParameter
|
||||||
|
{
|
||||||
|
defaultFloat = initialValue,
|
||||||
|
name = emuPropName,
|
||||||
|
type = AnimatorControllerParameterType.Float
|
||||||
|
});
|
||||||
|
|
||||||
|
BindProperty(ecb, emuPropName);
|
||||||
|
|
||||||
|
_alreadyBound[ecb] = emuPropName;
|
||||||
|
|
||||||
|
return emuPropName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BindProperty(EditorCurveBinding ecb, string propertyName)
|
||||||
|
{
|
||||||
|
var boundProp = new EditorCurveBinding
|
||||||
|
{
|
||||||
|
path = "",
|
||||||
|
type = typeof(Animator),
|
||||||
|
propertyName = propertyName
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var clip in _animDB.ClipsForPath(ecb.path)) ProcessAnyClip(clip);
|
||||||
|
|
||||||
|
void ProcessBlendTree(BlendTree blendTree)
|
||||||
|
{
|
||||||
|
foreach (var child in blendTree.children)
|
||||||
|
switch (child.motion)
|
||||||
|
{
|
||||||
|
case AnimationClip animationClip:
|
||||||
|
ProcessAnimationClip(animationClip);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BlendTree subBlendTree:
|
||||||
|
ProcessBlendTree(subBlendTree);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessAnimationClip(AnimationClip animationClip)
|
||||||
|
{
|
||||||
|
var curve = AnimationUtility.GetEditorCurve(animationClip, ecb);
|
||||||
|
if (curve == null) return;
|
||||||
|
|
||||||
|
AnimationUtility.SetEditorCurve(animationClip, boundProp, curve);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessAnyClip(AnimationDatabase.ClipHolder clip)
|
||||||
|
{
|
||||||
|
switch (clip.CurrentClip)
|
||||||
|
{
|
||||||
|
case AnimationClip animationClip:
|
||||||
|
ProcessAnimationClip(animationClip);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BlendTree blendTree:
|
||||||
|
ProcessBlendTree(blendTree);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ForActiveSelf(string path)
|
||||||
|
{
|
||||||
|
return ForBinding(path, typeof(GameObject), "m_IsActive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/Animation/ReadableProperty.cs.meta
Normal file
3
Editor/Animation/ReadableProperty.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1074339e2a59465ba585cb8cbbc4a88c
|
||||||
|
timeCreated: 1719195449
|
@ -48,7 +48,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
|
|||||||
seq.Run(MeshSettingsPluginPass.Instance);
|
seq.Run(MeshSettingsPluginPass.Instance);
|
||||||
seq.Run(ScaleAdjusterPass.Instance).PreviewingWith(new ScaleAdjusterPreview());
|
seq.Run(ScaleAdjusterPass.Instance).PreviewingWith(new ScaleAdjusterPreview());
|
||||||
#if MA_VRCSDK3_AVATARS
|
#if MA_VRCSDK3_AVATARS
|
||||||
seq.Run(ShapeChangerPrePass.Instance);
|
seq.Run(PropertyOverlayPrePass.Instance);
|
||||||
seq.Run(RenameParametersPluginPass.Instance);
|
seq.Run(RenameParametersPluginPass.Instance);
|
||||||
seq.Run(MergeBlendTreePass.Instance);
|
seq.Run(MergeBlendTreePass.Instance);
|
||||||
seq.Run(MergeAnimatorPluginPass.Instance);
|
seq.Run(MergeAnimatorPluginPass.Instance);
|
||||||
@ -57,7 +57,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
|
|||||||
#endif
|
#endif
|
||||||
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
|
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
|
||||||
{
|
{
|
||||||
seq.Run("Shape Changer", ctx => new ShapeChangerPass(ctx).Execute())
|
seq.Run("Shape Changer", ctx => new PropertyOverlayPass(ctx).Execute())
|
||||||
.PreviewingWith(new ShapeChangerPreview());
|
.PreviewingWith(new ShapeChangerPreview());
|
||||||
seq.Run(MergeArmaturePluginPass.Instance);
|
seq.Run(MergeArmaturePluginPass.Instance);
|
||||||
seq.Run(BoneProxyPluginPass.Instance);
|
seq.Run(BoneProxyPluginPass.Instance);
|
||||||
|
682
Editor/ShapeChanger/PropertyOverlayPass.cs
Normal file
682
Editor/ShapeChanger/PropertyOverlayPass.cs
Normal file
@ -0,0 +1,682 @@
|
|||||||
|
#region
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using nadena.dev.modular_avatar.animation;
|
||||||
|
using nadena.dev.ndmf;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.Animations;
|
||||||
|
using UnityEngine;
|
||||||
|
using VRC.SDK3.Avatars.Components;
|
||||||
|
using EditorCurveBinding = UnityEditor.EditorCurveBinding;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reserve an animator layer for Shape Changer's use. We do this here so that we can take advantage of MergeAnimator's
|
||||||
|
/// layer reference correction logic; this can go away once we have a more unified animation services API.
|
||||||
|
/// </summary>
|
||||||
|
internal class PropertyOverlayPrePass : Pass<PropertyOverlayPrePass>
|
||||||
|
{
|
||||||
|
internal const string TAG_PATH = "__MA/ShapeChanger/PrepassPlaceholder";
|
||||||
|
|
||||||
|
protected override void Execute(ndmf.BuildContext context)
|
||||||
|
{
|
||||||
|
if (context.AvatarRootObject.GetComponentInChildren<ModularAvatarShapeChanger>() != null)
|
||||||
|
{
|
||||||
|
var clip = new AnimationClip();
|
||||||
|
clip.name = "MA Shape Changer Defaults";
|
||||||
|
|
||||||
|
var curve = new AnimationCurve();
|
||||||
|
curve.AddKey(0, 0);
|
||||||
|
clip.SetCurve(TAG_PATH, typeof(Transform), "localPosition.x", curve);
|
||||||
|
|
||||||
|
// Merge using a null blend tree. This also ensures that we initialize the Merge Blend Tree system.
|
||||||
|
var bt = new BlendTree();
|
||||||
|
bt.name = "MA Shape Changer Defaults";
|
||||||
|
bt.blendType = BlendTreeType.Direct;
|
||||||
|
bt.children = new[]
|
||||||
|
{
|
||||||
|
new ChildMotion
|
||||||
|
{
|
||||||
|
motion = clip,
|
||||||
|
timeScale = 1,
|
||||||
|
cycleOffset = 0,
|
||||||
|
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
bt.useAutomaticThresholds = false;
|
||||||
|
|
||||||
|
// This is a hack and a half - put in a dummy path so we can find the cloned clip later on...
|
||||||
|
var obj = new GameObject("MA SC Defaults");
|
||||||
|
obj.transform.SetParent(context.AvatarRootTransform);
|
||||||
|
var mambt = obj.AddComponent<ModularAvatarMergeBlendTree>();
|
||||||
|
mambt.BlendTree = bt;
|
||||||
|
mambt.PathMode = MergeAnimatorPathMode.Absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class PropertyOverlayPass
|
||||||
|
{
|
||||||
|
struct TargetProp
|
||||||
|
{
|
||||||
|
public Object TargetObject;
|
||||||
|
public string PropertyName;
|
||||||
|
|
||||||
|
public bool Equals(TargetProp other)
|
||||||
|
{
|
||||||
|
return Equals(TargetObject, other.TargetObject) && PropertyName == other.PropertyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is TargetProp other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
var hashCode = (TargetObject != null ? TargetObject.GetHashCode() : 0);
|
||||||
|
hashCode = (hashCode * 397) ^ (PropertyName != null ? PropertyName.GetHashCode() : 0);
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyImmediate(float value)
|
||||||
|
{
|
||||||
|
var renderer = (SkinnedMeshRenderer)TargetObject;
|
||||||
|
renderer.SetBlendShapeWeight(renderer.sharedMesh.GetBlendShapeIndex(
|
||||||
|
PropertyName.Substring("blendShape.".Length)
|
||||||
|
), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PropGroup
|
||||||
|
{
|
||||||
|
public TargetProp TargetProp { get; }
|
||||||
|
public string ControlParam { get; set; }
|
||||||
|
|
||||||
|
public bool alwaysDeleted;
|
||||||
|
public float currentState;
|
||||||
|
|
||||||
|
// Objects which trigger deletion of this shape key.
|
||||||
|
public List<ActionGroupKey> actionGroups = new List<ActionGroupKey>();
|
||||||
|
|
||||||
|
public PropGroup(TargetProp key, float currentState)
|
||||||
|
{
|
||||||
|
TargetProp = key;
|
||||||
|
this.currentState = currentState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActionGroupKey
|
||||||
|
{
|
||||||
|
public ActionGroupKey(AnimationServicesContext asc, TargetProp key, GameObject controllingObject, float value)
|
||||||
|
{
|
||||||
|
TargetProp = key;
|
||||||
|
InitiallyActive = controllingObject?.activeInHierarchy == true;
|
||||||
|
|
||||||
|
var origControlling = controllingObject?.name ?? "<null>";
|
||||||
|
while (controllingObject != null && !asc.TryGetActiveSelfProxy(controllingObject, out _))
|
||||||
|
{
|
||||||
|
controllingObject = controllingObject.transform.parent?.gameObject;
|
||||||
|
if (controllingObject != null && RuntimeUtil.IsAvatarRoot(controllingObject.transform))
|
||||||
|
{
|
||||||
|
controllingObject = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newControlling = controllingObject?.name ?? "<null>";
|
||||||
|
Debug.Log("AGK: Controlling object " + origControlling + " => " + newControlling);
|
||||||
|
|
||||||
|
ControllingObject = controllingObject;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TargetProp TargetProp;
|
||||||
|
public float Value;
|
||||||
|
|
||||||
|
public float ConditionKey;
|
||||||
|
// When constructing the 1D blend tree to interpret the sum-of-condition-keys value, we need to ensure that
|
||||||
|
// all valid values are solidly between two control points with the same animation clip, to avoid undesired
|
||||||
|
// interpolation. This is done by constructing a "guard band":
|
||||||
|
// [ valid range ] [ guard band ] [ valid range ]
|
||||||
|
//
|
||||||
|
// The valid range must contain all values that could be created by valid summations. We therefore reserve
|
||||||
|
// a "guard band" in between; by reserving the exponent below each valid stop, we can put our guard bands
|
||||||
|
// in there.
|
||||||
|
// [ valid ] [ guard ] [ valid ]
|
||||||
|
// ^-r0 ^-g0 ^-g1
|
||||||
|
// ^- r1
|
||||||
|
// g0 = r1 / 2 = r0 * 2
|
||||||
|
// g1 = BitDecrement(r1) (we don't actually use this currently as r0-g0 is enough)
|
||||||
|
|
||||||
|
public float Guard => ConditionKey * 2;
|
||||||
|
|
||||||
|
public bool ConditionKeyIsValid => float.IsFinite(ConditionKey)
|
||||||
|
&& float.IsFinite(Guard)
|
||||||
|
&& ConditionKey > 0;
|
||||||
|
|
||||||
|
public GameObject ControllingObject;
|
||||||
|
public bool InitiallyActive;
|
||||||
|
public bool IsDelete;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var obj = ControllingObject?.name ?? "<null>";
|
||||||
|
|
||||||
|
return $"AGK: {TargetProp}={Value} " +
|
||||||
|
$"range={ConditionKey}/{Guard} controlling object={obj}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryMerge(ActionGroupKey other)
|
||||||
|
{
|
||||||
|
if (!TargetProp.Equals(other.TargetProp)) return false;
|
||||||
|
if (Mathf.Abs(Value - other.Value) > 0.001f) return false;
|
||||||
|
if (ControllingObject != other.ControllingObject) return false;
|
||||||
|
if (IsDelete || other.IsDelete) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly ndmf.BuildContext context;
|
||||||
|
private Dictionary<string, float> initialValues = new();
|
||||||
|
|
||||||
|
private AnimationClip _initialStateClip;
|
||||||
|
|
||||||
|
public PropertyOverlayPass(ndmf.BuildContext context)
|
||||||
|
{
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Execute()
|
||||||
|
{
|
||||||
|
Dictionary<TargetProp, PropGroup> shapes = FindShapes(context);
|
||||||
|
PreprocessShapes(shapes, out var initialStates, out var deletedShapes);
|
||||||
|
|
||||||
|
ProcessInitialStates(initialStates);
|
||||||
|
|
||||||
|
foreach (var groups in shapes.Values)
|
||||||
|
{
|
||||||
|
ProcessShapeKey(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessMeshDeletion(deletedShapes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PreprocessShapes(Dictionary<TargetProp, PropGroup> shapes, out Dictionary<TargetProp, float> initialStates, out HashSet<TargetProp> deletedShapes)
|
||||||
|
{
|
||||||
|
// For each shapekey, determine 1) if we can just set an initial state and skip and 2) if we can delete the
|
||||||
|
// corresponding mesh. If we can't, delete ops are merged into the main list of operations.
|
||||||
|
|
||||||
|
initialStates = new Dictionary<TargetProp, float>();
|
||||||
|
deletedShapes = new HashSet<TargetProp>();
|
||||||
|
|
||||||
|
foreach (var (key, info) in shapes.ToList())
|
||||||
|
{
|
||||||
|
var deletions = info.actionGroups.Where(agk => agk.IsDelete).ToList();
|
||||||
|
if (deletions.Any(d => d.ControllingObject == null))
|
||||||
|
{
|
||||||
|
// always deleted
|
||||||
|
shapes.Remove(key);
|
||||||
|
deletedShapes.Add(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move deleted shapes to the end of the list, so they override all Set actions
|
||||||
|
info.actionGroups = info.actionGroups.Where(agk => !agk.IsDelete).Concat(deletions).ToList();
|
||||||
|
|
||||||
|
var initialState = info.actionGroups.Where(agk => agk.InitiallyActive)
|
||||||
|
.Select(agk => agk.Value)
|
||||||
|
.Prepend(info.currentState) // use scene state if everything is disabled
|
||||||
|
.Last();
|
||||||
|
|
||||||
|
initialStates[key] = initialState;
|
||||||
|
|
||||||
|
// If we're now constant-on, we can skip animation generation
|
||||||
|
if (info.actionGroups[^1].ControllingObject == null)
|
||||||
|
{
|
||||||
|
shapes.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessInitialStates(Dictionary<TargetProp, float> initialStates)
|
||||||
|
{
|
||||||
|
var clips = context.Extension<AnimationServicesContext>().AnimationDatabase;
|
||||||
|
var initialStateHolder = clips.ClipsForPath(PropertyOverlayPrePass.TAG_PATH).FirstOrDefault();
|
||||||
|
if (initialStateHolder == null) return;
|
||||||
|
|
||||||
|
_initialStateClip = new AnimationClip();
|
||||||
|
_initialStateClip.name = "MA Shape Changer Defaults";
|
||||||
|
initialStateHolder.CurrentClip = _initialStateClip;
|
||||||
|
|
||||||
|
foreach (var (key, initialState) in initialStates)
|
||||||
|
{
|
||||||
|
var curve = new AnimationCurve();
|
||||||
|
curve.AddKey(0, initialState);
|
||||||
|
curve.AddKey(1, initialState);
|
||||||
|
|
||||||
|
string path;
|
||||||
|
Type componentType;
|
||||||
|
if (key.TargetObject is GameObject go)
|
||||||
|
{
|
||||||
|
path = RuntimeUtil.RelativePath(context.AvatarRootObject, go);
|
||||||
|
componentType = typeof(GameObject);
|
||||||
|
}
|
||||||
|
else if (key.TargetObject is SkinnedMeshRenderer smr)
|
||||||
|
{
|
||||||
|
path = RuntimeUtil.RelativePath(context.AvatarRootObject, smr.gameObject);
|
||||||
|
componentType = typeof(SkinnedMeshRenderer);
|
||||||
|
|
||||||
|
if (key.PropertyName.StartsWith("blendShape."))
|
||||||
|
{
|
||||||
|
var blendShape = key.PropertyName.Substring("blendShape.".Length);
|
||||||
|
var index = smr.sharedMesh?.GetBlendShapeIndex(blendShape);
|
||||||
|
|
||||||
|
if (index != null && index >= 0) smr.SetBlendShapeWeight(index.Value, initialState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Invalid target object: " + key.TargetObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
var binding = EditorCurveBinding.FloatCurve(
|
||||||
|
path,
|
||||||
|
componentType,
|
||||||
|
key.PropertyName
|
||||||
|
);
|
||||||
|
|
||||||
|
AnimationUtility.SetEditorCurve(_initialStateClip, binding, curve);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Mesh processing
|
||||||
|
|
||||||
|
private void ProcessMeshDeletion(HashSet<TargetProp> deletedKeys)
|
||||||
|
{
|
||||||
|
ImmutableDictionary<SkinnedMeshRenderer, List<TargetProp>> renderers = deletedKeys
|
||||||
|
.GroupBy(
|
||||||
|
v => (SkinnedMeshRenderer) v.TargetObject
|
||||||
|
).ToImmutableDictionary(
|
||||||
|
g => (SkinnedMeshRenderer) g.Key,
|
||||||
|
g => g.ToList()
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var (renderer, infos) in renderers)
|
||||||
|
{
|
||||||
|
if (renderer == null) continue;
|
||||||
|
|
||||||
|
var mesh = renderer.sharedMesh;
|
||||||
|
if (mesh == null) continue;
|
||||||
|
|
||||||
|
renderer.sharedMesh = RemoveBlendShapeFromMesh.RemoveBlendshapes(
|
||||||
|
mesh,
|
||||||
|
infos
|
||||||
|
.Select(i => mesh.GetBlendShapeIndex(i.PropertyName.Substring("blendShape.".Length)))
|
||||||
|
.Where(k => k >= 0)
|
||||||
|
.ToList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private void ProcessShapeKey(PropGroup info)
|
||||||
|
{
|
||||||
|
// TODO: prune non-animated keys
|
||||||
|
|
||||||
|
// Check if this is non-animated and skip most processing if so
|
||||||
|
if (info.alwaysDeleted) return;
|
||||||
|
if (info.actionGroups[^1].ControllingObject == null)
|
||||||
|
{
|
||||||
|
info.TargetProp.ApplyImmediate(info.actionGroups[0].Value);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var asm = GenerateStateMachine(info);
|
||||||
|
ApplyController(asm, "MA Responsive: " + info.TargetProp.TargetObject.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnimatorStateMachine GenerateStateMachine(PropGroup info)
|
||||||
|
{
|
||||||
|
var asc = context.Extension<AnimationServicesContext>();
|
||||||
|
var asm = new AnimatorStateMachine();
|
||||||
|
asm.name = "MA Shape Changer " + info.TargetProp.TargetObject.name;
|
||||||
|
|
||||||
|
var x = 200;
|
||||||
|
var y = 0;
|
||||||
|
var yInc = 60;
|
||||||
|
|
||||||
|
asm.anyStatePosition = new Vector3(-200, 0);
|
||||||
|
|
||||||
|
var initial = new AnimationClip();
|
||||||
|
var initialState = new AnimatorState();
|
||||||
|
initialState.motion = initial;
|
||||||
|
initialState.writeDefaultValues = false;
|
||||||
|
initialState.name = "<default>";
|
||||||
|
asm.defaultState = initialState;
|
||||||
|
|
||||||
|
asm.entryPosition = new Vector3(0, 0);
|
||||||
|
|
||||||
|
var states = new List<ChildAnimatorState>();
|
||||||
|
states.Add(new ChildAnimatorState
|
||||||
|
{
|
||||||
|
position = new Vector3(x, y),
|
||||||
|
state = initialState
|
||||||
|
});
|
||||||
|
|
||||||
|
var lastConstant = info.actionGroups.FindLastIndex(agk => agk.ControllingObject == null);
|
||||||
|
var transitionBuffer = new List<(AnimatorState, List<AnimatorStateTransition>)>();
|
||||||
|
var entryTransitions = new List<AnimatorTransition>();
|
||||||
|
|
||||||
|
transitionBuffer.Add((initialState, new List<AnimatorStateTransition>()));
|
||||||
|
|
||||||
|
foreach (var group in info.actionGroups.Skip(lastConstant))
|
||||||
|
{
|
||||||
|
y += yInc;
|
||||||
|
|
||||||
|
var clip = AnimResult(group.TargetProp, group.Value);
|
||||||
|
|
||||||
|
if (group.ControllingObject == null)
|
||||||
|
{
|
||||||
|
clip.name = "Property Overlay constant " + group.Value;
|
||||||
|
initialState.motion = clip;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
clip.name = "Property Overlay controlled by " + group.ControllingObject.name + " " + group.Value;
|
||||||
|
|
||||||
|
var conditions = GetTransitionConditions(asc, group);
|
||||||
|
|
||||||
|
foreach (var (st, transitions) in transitionBuffer)
|
||||||
|
{
|
||||||
|
var transition = new AnimatorStateTransition
|
||||||
|
{
|
||||||
|
isExit = true,
|
||||||
|
hasExitTime = false,
|
||||||
|
duration = 0,
|
||||||
|
hasFixedDuration = true,
|
||||||
|
conditions = (AnimatorCondition[])conditions.Clone()
|
||||||
|
};
|
||||||
|
transitions.Add(transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = new AnimatorState();
|
||||||
|
state.name = group.ControllingObject.name;
|
||||||
|
state.motion = clip;
|
||||||
|
state.writeDefaultValues = false;
|
||||||
|
states.Add(new ChildAnimatorState
|
||||||
|
{
|
||||||
|
position = new Vector3(x, y),
|
||||||
|
state = state
|
||||||
|
});
|
||||||
|
|
||||||
|
var transitionList = new List<AnimatorStateTransition>();
|
||||||
|
transitionBuffer.Add((state, transitionList));
|
||||||
|
entryTransitions.Add(new AnimatorTransition
|
||||||
|
{
|
||||||
|
destinationState = state,
|
||||||
|
conditions = conditions
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var cond in conditions)
|
||||||
|
{
|
||||||
|
var inverted = new AnimatorCondition
|
||||||
|
{
|
||||||
|
parameter = cond.parameter,
|
||||||
|
mode = AnimatorConditionMode.Less,
|
||||||
|
threshold = cond.threshold
|
||||||
|
};
|
||||||
|
transitionList.Add(new AnimatorStateTransition
|
||||||
|
{
|
||||||
|
isExit = true,
|
||||||
|
hasExitTime = false,
|
||||||
|
duration = 0,
|
||||||
|
hasFixedDuration = true,
|
||||||
|
conditions = new[] { inverted }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (st, transitions) in transitionBuffer) st.transitions = transitions.ToArray();
|
||||||
|
|
||||||
|
asm.states = states.ToArray();
|
||||||
|
entryTransitions.Reverse();
|
||||||
|
asm.entryTransitions = entryTransitions.ToArray();
|
||||||
|
asm.exitPosition = new Vector3(500, 0);
|
||||||
|
|
||||||
|
return asm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnimatorCondition[] GetTransitionConditions(AnimationServicesContext asc, ActionGroupKey group)
|
||||||
|
{
|
||||||
|
var conditions = new List<AnimatorCondition>();
|
||||||
|
|
||||||
|
var controller = group.ControllingObject.transform;
|
||||||
|
while (controller != null && !RuntimeUtil.IsAvatarRoot(controller))
|
||||||
|
{
|
||||||
|
if (asc.TryGetActiveSelfProxy(controller.gameObject, out var paramName))
|
||||||
|
{
|
||||||
|
initialValues[paramName] = controller.gameObject.activeSelf ? 1 : 0;
|
||||||
|
conditions.Add(new AnimatorCondition
|
||||||
|
{
|
||||||
|
parameter = paramName,
|
||||||
|
mode = AnimatorConditionMode.Greater,
|
||||||
|
threshold = 0.5f
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
controller = controller.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.Count == 0) throw new InvalidOperationException("No controlling object found for " + group);
|
||||||
|
|
||||||
|
return conditions.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Motion AnimResult(TargetProp key, float value)
|
||||||
|
{
|
||||||
|
string path;
|
||||||
|
Type componentType;
|
||||||
|
|
||||||
|
if (key.TargetObject is GameObject go)
|
||||||
|
{
|
||||||
|
path = RuntimeUtil.RelativePath(context.AvatarRootObject, go);
|
||||||
|
componentType = typeof(GameObject);
|
||||||
|
}
|
||||||
|
else if (key.TargetObject is SkinnedMeshRenderer smr)
|
||||||
|
{
|
||||||
|
path = RuntimeUtil.RelativePath(context.AvatarRootObject, smr.gameObject);
|
||||||
|
componentType = typeof(SkinnedMeshRenderer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Invalid target object: " + key.TargetObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
var clip = new AnimationClip();
|
||||||
|
clip.name = $"Set {path}:{key.PropertyName}={value}";
|
||||||
|
|
||||||
|
var curve = new AnimationCurve();
|
||||||
|
curve.AddKey(0, value);
|
||||||
|
curve.AddKey(1, value);
|
||||||
|
|
||||||
|
var binding = EditorCurveBinding.FloatCurve(path, componentType, key.PropertyName);
|
||||||
|
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
||||||
|
|
||||||
|
return clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyController(AnimatorStateMachine asm, string layerName)
|
||||||
|
{
|
||||||
|
var fx = context.AvatarDescriptor.baseAnimationLayers
|
||||||
|
.FirstOrDefault(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX);
|
||||||
|
if (fx.animatorController == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("No FX layer found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.IsTemporaryAsset(fx.animatorController))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("FX layer is not a temporary asset");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(fx.animatorController is AnimatorController animController))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("FX layer is not an animator controller");
|
||||||
|
}
|
||||||
|
|
||||||
|
var paramList = animController.parameters.ToList();
|
||||||
|
var paramSet = paramList.Select(p => p.name).ToHashSet();
|
||||||
|
|
||||||
|
foreach (var paramName in initialValues.Keys.Except(paramSet))
|
||||||
|
{
|
||||||
|
paramList.Add(new AnimatorControllerParameter()
|
||||||
|
{
|
||||||
|
name = paramName,
|
||||||
|
type = AnimatorControllerParameterType.Float,
|
||||||
|
defaultFloat = initialValues[paramName], // TODO
|
||||||
|
});
|
||||||
|
paramSet.Add(paramName);
|
||||||
|
}
|
||||||
|
|
||||||
|
animController.parameters = paramList.ToArray();
|
||||||
|
|
||||||
|
animController.layers = animController.layers.Append(
|
||||||
|
new AnimatorControllerLayer
|
||||||
|
{
|
||||||
|
stateMachine = asm,
|
||||||
|
name = "MA Shape Changer " + layerName,
|
||||||
|
defaultWeight = 1
|
||||||
|
}
|
||||||
|
).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnimationClip AnimParam(string param, float val)
|
||||||
|
{
|
||||||
|
return AnimParam((param, val));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnimationClip AnimParam(params (string param, float val)[] pairs)
|
||||||
|
{
|
||||||
|
AnimationClip clip = new AnimationClip();
|
||||||
|
clip.name = "Set " + string.Join(", ", pairs.Select(p => $"{p.param}={p.val}"));
|
||||||
|
|
||||||
|
// TODO - check property syntax
|
||||||
|
foreach (var (param, val) in pairs)
|
||||||
|
{
|
||||||
|
var curve = new AnimationCurve();
|
||||||
|
curve.AddKey(0, val);
|
||||||
|
curve.AddKey(1, val);
|
||||||
|
clip.SetCurve("", typeof(Animator), "" + param, curve);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<TargetProp, PropGroup> FindShapes(ndmf.BuildContext context)
|
||||||
|
{
|
||||||
|
var asc = context.Extension<AnimationServicesContext>();
|
||||||
|
|
||||||
|
var changers = context.AvatarRootObject.GetComponentsInChildren<ModularAvatarShapeChanger>(true);
|
||||||
|
|
||||||
|
Dictionary<TargetProp, PropGroup> shapeKeys = new();
|
||||||
|
|
||||||
|
foreach (var changer in changers)
|
||||||
|
{
|
||||||
|
var renderer = changer.targetRenderer.Get(changer)?.GetComponent<SkinnedMeshRenderer>();
|
||||||
|
if (renderer == null) continue;
|
||||||
|
|
||||||
|
int rendererInstanceId = renderer.GetInstanceID();
|
||||||
|
var mesh = renderer.sharedMesh;
|
||||||
|
|
||||||
|
if (mesh == null) continue;
|
||||||
|
|
||||||
|
foreach (var shape in changer.Shapes)
|
||||||
|
{
|
||||||
|
var shapeId = mesh.GetBlendShapeIndex(shape.ShapeName);
|
||||||
|
if (shapeId < 0) continue;
|
||||||
|
|
||||||
|
var key = new TargetProp
|
||||||
|
{
|
||||||
|
TargetObject = renderer,
|
||||||
|
PropertyName = "blendShape." + shape.ShapeName,
|
||||||
|
};
|
||||||
|
|
||||||
|
var value = shape.ChangeType == ShapeChangeType.Delete ? 100 : shape.Value;
|
||||||
|
if (!shapeKeys.TryGetValue(key, out var info))
|
||||||
|
{
|
||||||
|
info = new PropGroup(key, renderer.GetBlendShapeWeight(shapeId));
|
||||||
|
shapeKeys[key] = info;
|
||||||
|
|
||||||
|
// Add initial state
|
||||||
|
var agk = new ActionGroupKey(asc, key, null, value);
|
||||||
|
agk.InitiallyActive = true;
|
||||||
|
agk.Value = renderer.GetBlendShapeWeight(shapeId);
|
||||||
|
info.actionGroups.Add(agk);
|
||||||
|
}
|
||||||
|
|
||||||
|
var action = new ActionGroupKey(asc, key, changer.gameObject, value);
|
||||||
|
var isCurrentlyActive = changer.gameObject.activeInHierarchy;
|
||||||
|
|
||||||
|
if (shape.ChangeType == ShapeChangeType.Delete)
|
||||||
|
{
|
||||||
|
action.IsDelete = true;
|
||||||
|
|
||||||
|
if (isCurrentlyActive) info.currentState = 100;
|
||||||
|
|
||||||
|
info.actionGroups.Add(action); // Never merge
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changer.gameObject.activeInHierarchy) info.currentState = action.Value;
|
||||||
|
|
||||||
|
// TODO: lift controlling object resolution out of loop?
|
||||||
|
if (action.ControllingObject == null)
|
||||||
|
{
|
||||||
|
if (action.InitiallyActive)
|
||||||
|
{
|
||||||
|
// always active control
|
||||||
|
info.actionGroups.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// never active control
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log("Trying merge: " + action);
|
||||||
|
if (info.actionGroups.Count == 0)
|
||||||
|
{
|
||||||
|
info.actionGroups.Add(action);
|
||||||
|
}
|
||||||
|
else if (!info.actionGroups[^1].TryMerge(action))
|
||||||
|
{
|
||||||
|
Debug.Log("Failed merge");
|
||||||
|
info.actionGroups.Add(action);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Log("Post merge: " + info.actionGroups[^1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shapeKeys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/ShapeChanger/PropertyOverlayPass.cs.meta
Normal file
3
Editor/ShapeChanger/PropertyOverlayPass.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fe9990c696424aada1212aebd23f4c64
|
||||||
|
timeCreated: 1719196895
|
@ -1,708 +0,0 @@
|
|||||||
#region
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Linq;
|
|
||||||
using nadena.dev.modular_avatar.animation;
|
|
||||||
using nadena.dev.ndmf;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditor.Animations;
|
|
||||||
using UnityEngine;
|
|
||||||
using VRC.SDK3.Avatars.Components;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Reserve an animator layer for Shape Changer's use. We do this here so that we can take advantage of MergeAnimator's
|
|
||||||
/// layer reference correction logic; this can go away once we have a more unified animation services API.
|
|
||||||
/// </summary>
|
|
||||||
internal class ShapeChangerPrePass : Pass<ShapeChangerPrePass>
|
|
||||||
{
|
|
||||||
internal const string TAG_PATH = "__MA/ShapeChanger/PrepassPlaceholder";
|
|
||||||
|
|
||||||
protected override void Execute(ndmf.BuildContext context)
|
|
||||||
{
|
|
||||||
if (context.AvatarRootObject.GetComponentInChildren<ModularAvatarShapeChanger>() != null)
|
|
||||||
{
|
|
||||||
var clip = new AnimationClip();
|
|
||||||
clip.name = "MA Shape Changer Defaults";
|
|
||||||
|
|
||||||
var curve = new AnimationCurve();
|
|
||||||
curve.AddKey(0, 0);
|
|
||||||
clip.SetCurve(TAG_PATH, typeof(Transform), "localPosition.x", curve);
|
|
||||||
|
|
||||||
// Merge using a null blend tree. This also ensures that we initialize the Merge Blend Tree system.
|
|
||||||
var bt = new BlendTree();
|
|
||||||
bt.name = "MA Shape Changer Defaults";
|
|
||||||
bt.blendType = BlendTreeType.Direct;
|
|
||||||
bt.children = new[]
|
|
||||||
{
|
|
||||||
new ChildMotion
|
|
||||||
{
|
|
||||||
motion = clip,
|
|
||||||
timeScale = 1,
|
|
||||||
cycleOffset = 0,
|
|
||||||
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE
|
|
||||||
}
|
|
||||||
};
|
|
||||||
bt.useAutomaticThresholds = false;
|
|
||||||
|
|
||||||
// This is a hack and a half - put in a dummy path so we can find the cloned clip later on...
|
|
||||||
var obj = new GameObject("MA SC Defaults");
|
|
||||||
obj.transform.SetParent(context.AvatarRootTransform);
|
|
||||||
var mambt = obj.AddComponent<ModularAvatarMergeBlendTree>();
|
|
||||||
mambt.BlendTree = bt;
|
|
||||||
mambt.PathMode = MergeAnimatorPathMode.Absolute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ShapeChangerPass
|
|
||||||
{
|
|
||||||
struct ShapeKey
|
|
||||||
{
|
|
||||||
public int RendererInstanceId;
|
|
||||||
public int ShapeIndex;
|
|
||||||
public string ShapeKeyName; // not equated
|
|
||||||
|
|
||||||
public bool Equals(ShapeKey other)
|
|
||||||
{
|
|
||||||
return RendererInstanceId == other.RendererInstanceId && ShapeIndex == other.ShapeIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return obj is ShapeKey other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
unchecked
|
|
||||||
{
|
|
||||||
return (RendererInstanceId * 397) ^ ShapeIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShapeKeyInfo
|
|
||||||
{
|
|
||||||
public ShapeKey ShapeKey { get; }
|
|
||||||
public string SetParam { get; set; }
|
|
||||||
public string DeleteParam { get; set; }
|
|
||||||
|
|
||||||
public bool alwaysDeleted;
|
|
||||||
public float currentState;
|
|
||||||
|
|
||||||
// Objects which trigger deletion of this shape key.
|
|
||||||
public List<GameObject> deletionObjects = new List<GameObject>();
|
|
||||||
public List<ActionGroupKey> setObjects = new List<ActionGroupKey>();
|
|
||||||
|
|
||||||
public ShapeKeyInfo(ShapeKey key)
|
|
||||||
{
|
|
||||||
ShapeKey = key;
|
|
||||||
currentState = (EditorUtility.InstanceIDToObject(key.RendererInstanceId) as SkinnedMeshRenderer)
|
|
||||||
?.GetBlendShapeWeight(key.ShapeIndex)
|
|
||||||
?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ActionGroupKey
|
|
||||||
{
|
|
||||||
public ActionGroupKey(AnimationServicesContext asc, ShapeKey key, GameObject controllingObject,
|
|
||||||
ChangedShape shape)
|
|
||||||
{
|
|
||||||
ShapeKey = key;
|
|
||||||
InitiallyActive = controllingObject?.activeInHierarchy == true;
|
|
||||||
|
|
||||||
var origControlling = controllingObject?.name ?? "<null>";
|
|
||||||
while (controllingObject != null && !asc.TryGetActiveSelfProxy(controllingObject, out _))
|
|
||||||
{
|
|
||||||
controllingObject = controllingObject.transform.parent?.gameObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newControlling = controllingObject?.name ?? "<null>";
|
|
||||||
Debug.Log("AGK: Controlling object " + origControlling + " => " + newControlling);
|
|
||||||
|
|
||||||
ControllingObject = controllingObject;
|
|
||||||
IsDelete = shape.ChangeType == ShapeChangeType.Delete;
|
|
||||||
Value = IsDelete ? 100 : shape.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ShapeKey ShapeKey;
|
|
||||||
public bool IsDelete;
|
|
||||||
public float Value;
|
|
||||||
|
|
||||||
public float ConditionKey;
|
|
||||||
// When constructing the 1D blend tree to interpret the sum-of-condition-keys value, we need to ensure that
|
|
||||||
// all valid values are solidly between two control points with the same animation clip, to avoid undesired
|
|
||||||
// interpolation. This is done by constructing a "guard band":
|
|
||||||
// [ valid range ] [ guard band ] [ valid range ]
|
|
||||||
//
|
|
||||||
// The valid range must contain all values that could be created by valid summations. We therefore reserve
|
|
||||||
// a "guard band" in between; by reserving the exponent below each valid stop, we can put our guard bands
|
|
||||||
// in there.
|
|
||||||
// [ valid ] [ guard ] [ valid ]
|
|
||||||
// ^-r0 ^-g0 ^-g1
|
|
||||||
// ^- r1
|
|
||||||
// g0 = r1 / 2 = r0 * 2
|
|
||||||
// g1 = BitDecrement(r1) (we don't actually use this currently as r0-g0 is enough)
|
|
||||||
|
|
||||||
public float Guard => ConditionKey * 2;
|
|
||||||
|
|
||||||
public bool ConditionKeyIsValid => float.IsFinite(ConditionKey)
|
|
||||||
&& float.IsFinite(Guard)
|
|
||||||
&& ConditionKey > 0;
|
|
||||||
|
|
||||||
public GameObject ControllingObject;
|
|
||||||
public bool InitiallyActive;
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
var obj = ControllingObject?.name ?? "<null>";
|
|
||||||
|
|
||||||
return $"AGK: {ShapeKey.RendererInstanceId}:{ShapeKey.ShapeIndex} ({ShapeKey.ShapeKeyName})={Value} " +
|
|
||||||
$"range={ConditionKey}/{Guard} controlling object={obj}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryMerge(ActionGroupKey other)
|
|
||||||
{
|
|
||||||
if (!ShapeKey.Equals(other.ShapeKey)) return false;
|
|
||||||
if (Mathf.Abs(Value - other.Value) > 0.001f) return false;
|
|
||||||
if (ControllingObject != other.ControllingObject) return false;
|
|
||||||
|
|
||||||
IsDelete |= other.IsDelete;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly ndmf.BuildContext context;
|
|
||||||
private Dictionary<string, float> initialValues = new();
|
|
||||||
|
|
||||||
private AnimationClip _initialStateClip;
|
|
||||||
|
|
||||||
public ShapeChangerPass(ndmf.BuildContext context)
|
|
||||||
{
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Execute()
|
|
||||||
{
|
|
||||||
Dictionary<ShapeKey, ShapeKeyInfo> shapes = FindShapes(context);
|
|
||||||
|
|
||||||
ProcessInitialStates(shapes);
|
|
||||||
|
|
||||||
foreach (var groups in shapes.Values)
|
|
||||||
{
|
|
||||||
ProcessShapeKey(groups);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessMeshDeletion(shapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ProcessInitialStates(Dictionary<ShapeKey, ShapeKeyInfo> shapes)
|
|
||||||
{
|
|
||||||
var clips = context.Extension<AnimationServicesContext>().AnimationDatabase;
|
|
||||||
var initialStateHolder = clips.ClipsForPath(ShapeChangerPrePass.TAG_PATH).FirstOrDefault();
|
|
||||||
if (initialStateHolder == null) return;
|
|
||||||
|
|
||||||
_initialStateClip = new AnimationClip();
|
|
||||||
_initialStateClip.name = "MA Shape Changer Defaults";
|
|
||||||
initialStateHolder.CurrentClip = _initialStateClip;
|
|
||||||
|
|
||||||
foreach (var (key, info) in shapes)
|
|
||||||
{
|
|
||||||
if (info.alwaysDeleted) continue;
|
|
||||||
|
|
||||||
var curve = new AnimationCurve();
|
|
||||||
curve.AddKey(0, info.currentState);
|
|
||||||
curve.AddKey(1, info.currentState);
|
|
||||||
|
|
||||||
var renderer = (SkinnedMeshRenderer)EditorUtility.InstanceIDToObject(key.RendererInstanceId);
|
|
||||||
|
|
||||||
var binding = EditorCurveBinding.FloatCurve(
|
|
||||||
RuntimeUtil.RelativePath(context.AvatarRootObject, renderer.gameObject),
|
|
||||||
typeof(SkinnedMeshRenderer),
|
|
||||||
$"blendShape.{key.ShapeKeyName}"
|
|
||||||
);
|
|
||||||
|
|
||||||
AnimationUtility.SetEditorCurve(_initialStateClip, binding, curve);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Mesh processing
|
|
||||||
|
|
||||||
private void ProcessMeshDeletion(Dictionary<ShapeKey, ShapeKeyInfo> shapes)
|
|
||||||
{
|
|
||||||
ImmutableDictionary<int /* renderer */, List<ShapeKeyInfo>> renderers = shapes.Values.GroupBy(
|
|
||||||
v => v.ShapeKey.RendererInstanceId
|
|
||||||
).ToImmutableDictionary(
|
|
||||||
g => g.Key,
|
|
||||||
g => g.ToList()
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach (var (rendererId, infos) in renderers)
|
|
||||||
{
|
|
||||||
var renderer = (SkinnedMeshRenderer)EditorUtility.InstanceIDToObject(rendererId);
|
|
||||||
if (renderer == null) continue;
|
|
||||||
|
|
||||||
var mesh = renderer.sharedMesh;
|
|
||||||
if (mesh == null) continue;
|
|
||||||
|
|
||||||
renderer.sharedMesh = RemoveBlendShapeFromMesh.RemoveBlendshapes(
|
|
||||||
mesh,
|
|
||||||
infos.Where(i => i.alwaysDeleted).Select(i => i.ShapeKey.ShapeIndex)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private void ProcessShapeKey(ShapeKeyInfo info)
|
|
||||||
{
|
|
||||||
// TODO: prune non-animated keys
|
|
||||||
|
|
||||||
// Check if this is non-animated and skip most processing if so
|
|
||||||
if (info.alwaysDeleted) return;
|
|
||||||
if (info.setObjects[^1].ControllingObject == null)
|
|
||||||
{
|
|
||||||
var renderer = (SkinnedMeshRenderer)EditorUtility.InstanceIDToObject(info.ShapeKey.RendererInstanceId);
|
|
||||||
renderer.SetBlendShapeWeight(info.ShapeKey.ShapeIndex, info.setObjects[0].Value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This value is the first non-subnormal float32
|
|
||||||
float shift = BitConverter.Int32BitsToSingle(0x00800000);
|
|
||||||
|
|
||||||
foreach (var group in info.setObjects)
|
|
||||||
{
|
|
||||||
group.ConditionKey = shift;
|
|
||||||
shift *= 4;
|
|
||||||
|
|
||||||
if (!group.ConditionKeyIsValid)
|
|
||||||
{
|
|
||||||
throw new ArithmeticException("Floating point overflow - too many shape key controls");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info.SetParam =
|
|
||||||
$"_MA/ShapeChanger/{info.ShapeKey.RendererInstanceId}/{info.ShapeKey.ShapeIndex}/set";
|
|
||||||
info.DeleteParam = $"_MA/ShapeChanger/{info.ShapeKey.RendererInstanceId}/{info.ShapeKey.ShapeIndex}/delete";
|
|
||||||
|
|
||||||
var summationTree = BuildSummationTree(info);
|
|
||||||
var applyTree = BuildApplyTree(info);
|
|
||||||
var merged = BuildMergeTree(summationTree, applyTree);
|
|
||||||
|
|
||||||
ApplyController(merged, "ShapeChanger Apply: " + info.ShapeKey.ShapeKeyName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BlendTree BuildMergeTree(BlendTree summationTree, BlendTree applyTree)
|
|
||||||
{
|
|
||||||
var bt = new BlendTree();
|
|
||||||
bt.blendType = BlendTreeType.Direct;
|
|
||||||
bt.blendParameter = MergeBlendTreePass.ALWAYS_ONE;
|
|
||||||
bt.useAutomaticThresholds = false;
|
|
||||||
|
|
||||||
bt.children = new[]
|
|
||||||
{
|
|
||||||
new ChildMotion()
|
|
||||||
{
|
|
||||||
motion = summationTree,
|
|
||||||
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
|
|
||||||
timeScale = 1,
|
|
||||||
},
|
|
||||||
new ChildMotion()
|
|
||||||
{
|
|
||||||
motion = applyTree,
|
|
||||||
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
|
|
||||||
timeScale = 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return bt;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BlendTree BuildApplyTree(ShapeKeyInfo info)
|
|
||||||
{
|
|
||||||
var groups = info.setObjects;
|
|
||||||
|
|
||||||
var setTree = new BlendTree();
|
|
||||||
setTree.blendType = BlendTreeType.Simple1D;
|
|
||||||
setTree.blendParameter = info.SetParam;
|
|
||||||
setTree.useAutomaticThresholds = false;
|
|
||||||
|
|
||||||
var childMotions = new List<ChildMotion>();
|
|
||||||
|
|
||||||
childMotions.Add(new ChildMotion()
|
|
||||||
{
|
|
||||||
motion = AnimResult(groups.First().ShapeKey, 0),
|
|
||||||
timeScale = 1,
|
|
||||||
threshold = 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var group in groups)
|
|
||||||
{
|
|
||||||
var lo = group.ConditionKey;
|
|
||||||
var hi = group.Guard;
|
|
||||||
|
|
||||||
Debug.Log("Threshold: [" + lo + ", " + hi + "]: " + group);
|
|
||||||
|
|
||||||
childMotions.Add(new ChildMotion()
|
|
||||||
{
|
|
||||||
motion = AnimResult(group.ShapeKey, group.Value),
|
|
||||||
timeScale = 1,
|
|
||||||
threshold = lo,
|
|
||||||
});
|
|
||||||
childMotions.Add(new ChildMotion()
|
|
||||||
{
|
|
||||||
motion = AnimResult(group.ShapeKey, group.Value),
|
|
||||||
timeScale = 1,
|
|
||||||
threshold = hi,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setTree.children = childMotions.ToArray();
|
|
||||||
|
|
||||||
var delTree = new BlendTree();
|
|
||||||
delTree.blendType = BlendTreeType.Simple1D;
|
|
||||||
delTree.blendParameter = info.DeleteParam;
|
|
||||||
delTree.useAutomaticThresholds = false;
|
|
||||||
|
|
||||||
delTree.children = new[]
|
|
||||||
{
|
|
||||||
new ChildMotion()
|
|
||||||
{
|
|
||||||
motion = setTree,
|
|
||||||
timeScale = 1,
|
|
||||||
threshold = 0
|
|
||||||
},
|
|
||||||
new ChildMotion()
|
|
||||||
{
|
|
||||||
motion = setTree,
|
|
||||||
timeScale = 1,
|
|
||||||
threshold = 0.4f
|
|
||||||
},
|
|
||||||
new ChildMotion()
|
|
||||||
{
|
|
||||||
motion = AnimResult(info.ShapeKey, 100),
|
|
||||||
timeScale = 1,
|
|
||||||
threshold = 0.6f
|
|
||||||
},
|
|
||||||
new ChildMotion()
|
|
||||||
{
|
|
||||||
motion = AnimResult(info.ShapeKey, 100),
|
|
||||||
timeScale = 1,
|
|
||||||
threshold = 1
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return delTree;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Motion AnimResult(ShapeKey key, float value)
|
|
||||||
{
|
|
||||||
var renderer = EditorUtility.InstanceIDToObject(key.RendererInstanceId) as SkinnedMeshRenderer;
|
|
||||||
if (renderer == null) throw new InvalidOperationException("Failed to get object");
|
|
||||||
|
|
||||||
var obj = renderer.gameObject;
|
|
||||||
var path = RuntimeUtil.RelativePath(context.AvatarRootObject, obj);
|
|
||||||
|
|
||||||
var clip = new AnimationClip();
|
|
||||||
clip.name = $"Set {obj.name}:{key.ShapeKeyName}={value}";
|
|
||||||
|
|
||||||
var curve = new AnimationCurve();
|
|
||||||
curve.AddKey(0, value);
|
|
||||||
curve.AddKey(1, value);
|
|
||||||
|
|
||||||
var binding =
|
|
||||||
EditorCurveBinding.FloatCurve(path, typeof(SkinnedMeshRenderer), $"blendShape.{key.ShapeKeyName}");
|
|
||||||
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
|
||||||
|
|
||||||
return clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BlendTree BuildSummationTree(ShapeKeyInfo info)
|
|
||||||
{
|
|
||||||
var groups = info.setObjects;
|
|
||||||
|
|
||||||
var setParam = info.SetParam;
|
|
||||||
var delParam = info.DeleteParam;
|
|
||||||
|
|
||||||
var asc = context.Extension<AnimationServicesContext>();
|
|
||||||
|
|
||||||
BlendTree bt = new BlendTree();
|
|
||||||
bt.blendType = BlendTreeType.Direct;
|
|
||||||
|
|
||||||
HashSet<string> paramNames = new HashSet<string>();
|
|
||||||
|
|
||||||
var childMotions = new List<ChildMotion>();
|
|
||||||
|
|
||||||
// TODO eliminate excess motion field
|
|
||||||
var initMotion = new ChildMotion()
|
|
||||||
{
|
|
||||||
motion = AnimParam((setParam, 0), (delParam, 0)),
|
|
||||||
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
|
|
||||||
timeScale = 1,
|
|
||||||
};
|
|
||||||
childMotions.Add(initMotion);
|
|
||||||
paramNames.Add(MergeBlendTreePass.ALWAYS_ONE);
|
|
||||||
initialValues[MergeBlendTreePass.ALWAYS_ONE] = 1;
|
|
||||||
initialValues[setParam] = 0;
|
|
||||||
initialValues[delParam] = 0;
|
|
||||||
|
|
||||||
foreach (var group in groups)
|
|
||||||
{
|
|
||||||
Debug.Log("Group: " + group);
|
|
||||||
string controllingParam;
|
|
||||||
if (group.ControllingObject == null)
|
|
||||||
{
|
|
||||||
controllingParam = MergeBlendTreePass.ALWAYS_ONE;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// TODO: path evaluation
|
|
||||||
if (!asc.TryGetActiveSelfProxy(group.ControllingObject, out controllingParam))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Failed to get active self proxy");
|
|
||||||
}
|
|
||||||
|
|
||||||
initialValues[controllingParam] = group.ControllingObject.activeSelf ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var childMotion = new ChildMotion()
|
|
||||||
{
|
|
||||||
motion = AnimParam(setParam, group.ConditionKey),
|
|
||||||
directBlendParameter = controllingParam,
|
|
||||||
timeScale = 1,
|
|
||||||
};
|
|
||||||
childMotions.Add(childMotion);
|
|
||||||
paramNames.Add(controllingParam);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var delController in info.deletionObjects)
|
|
||||||
{
|
|
||||||
if (!asc.TryGetActiveSelfProxy(delController, out var controllingParam))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Failed to get active self proxy");
|
|
||||||
}
|
|
||||||
|
|
||||||
initialValues[controllingParam] = delController.activeSelf ? 1 : 0;
|
|
||||||
|
|
||||||
var childMotion = new ChildMotion()
|
|
||||||
{
|
|
||||||
motion = AnimParam(delParam, 1),
|
|
||||||
directBlendParameter = controllingParam,
|
|
||||||
timeScale = 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
childMotions.Add(childMotion);
|
|
||||||
paramNames.Add(controllingParam);
|
|
||||||
}
|
|
||||||
|
|
||||||
bt.children = childMotions.ToArray();
|
|
||||||
|
|
||||||
return bt;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyController(BlendTree bt, string stateName)
|
|
||||||
{
|
|
||||||
var fx = context.AvatarDescriptor.baseAnimationLayers
|
|
||||||
.FirstOrDefault(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX);
|
|
||||||
if (fx.animatorController == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("No FX layer found");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!context.IsTemporaryAsset(fx.animatorController))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("FX layer is not a temporary asset");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(fx.animatorController is AnimatorController animController))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("FX layer is not an animator controller");
|
|
||||||
}
|
|
||||||
|
|
||||||
var stateMachine = new AnimatorStateMachine();
|
|
||||||
var layers = animController.layers.ToList();
|
|
||||||
layers.Add(
|
|
||||||
new AnimatorControllerLayer() { defaultWeight = 1, name = stateName, stateMachine = stateMachine }
|
|
||||||
);
|
|
||||||
var state = new AnimatorState();
|
|
||||||
state.name = stateName;
|
|
||||||
state.motion = bt;
|
|
||||||
state.writeDefaultValues = true;
|
|
||||||
stateMachine.states = new[] { new ChildAnimatorState() { state = state } };
|
|
||||||
stateMachine.defaultState = state;
|
|
||||||
|
|
||||||
var paramList = animController.parameters.ToList();
|
|
||||||
var paramSet = paramList.Select(p => p.name).ToHashSet();
|
|
||||||
|
|
||||||
foreach (var paramName in initialValues.Keys.Except(paramSet))
|
|
||||||
{
|
|
||||||
paramList.Add(new AnimatorControllerParameter()
|
|
||||||
{
|
|
||||||
name = paramName,
|
|
||||||
type = AnimatorControllerParameterType.Float,
|
|
||||||
defaultFloat = initialValues[paramName], // TODO
|
|
||||||
});
|
|
||||||
paramSet.Add(paramName);
|
|
||||||
}
|
|
||||||
|
|
||||||
animController.parameters = paramList.ToArray();
|
|
||||||
|
|
||||||
animController.layers = layers.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<string> FindParams(BlendTree bt)
|
|
||||||
{
|
|
||||||
if (bt == null) yield break;
|
|
||||||
|
|
||||||
if (bt.blendType == BlendTreeType.Direct)
|
|
||||||
{
|
|
||||||
foreach (var child in bt.children)
|
|
||||||
{
|
|
||||||
yield return child.directBlendParameter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
yield return bt.blendParameter;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var child in bt.children)
|
|
||||||
{
|
|
||||||
foreach (var param in FindParams(child.motion as BlendTree))
|
|
||||||
{
|
|
||||||
yield return param;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private AnimationClip AnimParam(string param, float val)
|
|
||||||
{
|
|
||||||
return AnimParam((param, val));
|
|
||||||
}
|
|
||||||
|
|
||||||
private AnimationClip AnimParam(params (string param, float val)[] pairs)
|
|
||||||
{
|
|
||||||
AnimationClip clip = new AnimationClip();
|
|
||||||
clip.name = "Set " + string.Join(", ", pairs.Select(p => $"{p.param}={p.val}"));
|
|
||||||
|
|
||||||
// TODO - check property syntax
|
|
||||||
foreach (var (param, val) in pairs)
|
|
||||||
{
|
|
||||||
var curve = new AnimationCurve();
|
|
||||||
curve.AddKey(0, val);
|
|
||||||
curve.AddKey(1, val);
|
|
||||||
clip.SetCurve("", typeof(Animator), "" + param, curve);
|
|
||||||
}
|
|
||||||
|
|
||||||
return clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<ShapeKey, ShapeKeyInfo> FindShapes(ndmf.BuildContext context)
|
|
||||||
{
|
|
||||||
var asc = context.Extension<AnimationServicesContext>();
|
|
||||||
|
|
||||||
var changers = context.AvatarRootObject.GetComponentsInChildren<ModularAvatarShapeChanger>(true);
|
|
||||||
|
|
||||||
Dictionary<ShapeKey, ShapeKeyInfo> shapeKeys = new();
|
|
||||||
|
|
||||||
foreach (var changer in changers)
|
|
||||||
{
|
|
||||||
var renderer = changer.targetRenderer.Get(changer)?.GetComponent<SkinnedMeshRenderer>();
|
|
||||||
if (renderer == null) continue;
|
|
||||||
|
|
||||||
int rendererInstanceId = renderer.GetInstanceID();
|
|
||||||
var mesh = renderer.sharedMesh;
|
|
||||||
|
|
||||||
if (mesh == null) continue;
|
|
||||||
|
|
||||||
foreach (var shape in changer.Shapes)
|
|
||||||
{
|
|
||||||
var shapeId = mesh.GetBlendShapeIndex(shape.ShapeName);
|
|
||||||
if (shapeId < 0) continue;
|
|
||||||
|
|
||||||
var key = new ShapeKey
|
|
||||||
{
|
|
||||||
RendererInstanceId = rendererInstanceId,
|
|
||||||
ShapeIndex = shapeId,
|
|
||||||
ShapeKeyName = shape.ShapeName,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!shapeKeys.TryGetValue(key, out var info))
|
|
||||||
{
|
|
||||||
info = new ShapeKeyInfo(key);
|
|
||||||
shapeKeys[key] = info;
|
|
||||||
|
|
||||||
// Add initial state
|
|
||||||
var agk = new ActionGroupKey(asc, key, null, shape);
|
|
||||||
agk.IsDelete = false;
|
|
||||||
agk.InitiallyActive = true;
|
|
||||||
agk.Value = renderer.GetBlendShapeWeight(shapeId);
|
|
||||||
info.setObjects.Add(agk);
|
|
||||||
}
|
|
||||||
|
|
||||||
var action = new ActionGroupKey(asc, key, changer.gameObject, shape);
|
|
||||||
var isCurrentlyActive = changer.gameObject.activeInHierarchy;
|
|
||||||
|
|
||||||
if (action.IsDelete)
|
|
||||||
{
|
|
||||||
if (isCurrentlyActive) info.currentState = 100;
|
|
||||||
|
|
||||||
if (action.ControllingObject == null)
|
|
||||||
{
|
|
||||||
// always active?
|
|
||||||
info.alwaysDeleted |= changer.gameObject.activeInHierarchy;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
info.deletionObjects.Add(action.ControllingObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changer.gameObject.activeInHierarchy) info.currentState = action.Value;
|
|
||||||
|
|
||||||
// TODO: lift controlling object resolution out of loop?
|
|
||||||
if (action.ControllingObject == null)
|
|
||||||
{
|
|
||||||
if (action.InitiallyActive)
|
|
||||||
{
|
|
||||||
// always active control
|
|
||||||
info.setObjects.Clear();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// never active control
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.Log("Trying merge: " + action);
|
|
||||||
if (info.setObjects.Count == 0)
|
|
||||||
{
|
|
||||||
info.setObjects.Add(action);
|
|
||||||
}
|
|
||||||
else if (!info.setObjects[^1].TryMerge(action))
|
|
||||||
{
|
|
||||||
Debug.Log("Failed merge");
|
|
||||||
info.setObjects.Add(action);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.Log("Post merge: " + info.setObjects[^1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return shapeKeys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 740a387c9d934623a6b06d945b38d8d0
|
|
||||||
timeCreated: 1717123900
|
|
@ -6,6 +6,7 @@ using nadena.dev.modular_avatar.core.editor;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using EditorCurveBinding = UnityEditor.EditorCurveBinding;
|
||||||
|
|
||||||
public class ActiveAnimationRetargeterTests : TestBase
|
public class ActiveAnimationRetargeterTests : TestBase
|
||||||
{
|
{
|
||||||
|
@ -5,6 +5,7 @@ using NUnit.Framework;
|
|||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.Animations;
|
using UnityEditor.Animations;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using EditorCurveBinding = UnityEditor.EditorCurveBinding;
|
||||||
|
|
||||||
namespace ShapeChangerTests
|
namespace ShapeChangerTests
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user