mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-01-04 13:45:04 +08:00
566 lines
22 KiB
C#
566 lines
22 KiB
C#
#region
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using nadena.dev.modular_avatar.animation;
|
|
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
|
|
{
|
|
internal partial class ReactiveObjectPass
|
|
{
|
|
private readonly ndmf.BuildContext context;
|
|
private Dictionary<string, float> initialValues = new();
|
|
|
|
// Properties that are being driven, either by foreign animations or Object Toggles
|
|
private HashSet<string> activeProps = new();
|
|
|
|
private AnimationClip _initialStateClip;
|
|
|
|
public ReactiveObjectPass(ndmf.BuildContext context)
|
|
{
|
|
this.context = context;
|
|
}
|
|
|
|
internal void Execute()
|
|
{
|
|
var analysis = new ReactiveObjectAnalyzer(context).Analyze(context.AvatarRootObject);
|
|
|
|
var shapes = analysis.Shapes;
|
|
var initialStates = analysis.InitialStates;
|
|
var deletedShapes = analysis.DeletedShapes;
|
|
|
|
GenerateActiveSelfProxies(shapes);
|
|
|
|
ProcessInitialStates(initialStates, shapes);
|
|
ProcessInitialAnimatorVariables(shapes);
|
|
|
|
foreach (var groups in shapes.Values)
|
|
{
|
|
ProcessShapeKey(groups);
|
|
}
|
|
|
|
ProcessMeshDeletion(deletedShapes);
|
|
}
|
|
|
|
private void GenerateActiveSelfProxies(Dictionary<TargetProp, AnimatedProperty> shapes)
|
|
{
|
|
var asc = context.Extension<AnimationServicesContext>();
|
|
|
|
foreach (var prop in shapes.Keys)
|
|
{
|
|
if (prop.TargetObject is GameObject go && prop.PropertyName == "m_IsActive")
|
|
{
|
|
// Ensure a proxy exists for each object we're going to be toggling.
|
|
// TODO: is this still needed?
|
|
asc.GetActiveSelfProxy(go);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ProcessInitialAnimatorVariables(Dictionary<TargetProp, AnimatedProperty> shapes)
|
|
{
|
|
foreach (var group in shapes.Values)
|
|
foreach (var agk in group.actionGroups)
|
|
foreach (var condition in agk.ControllingConditions)
|
|
{
|
|
if (condition.IsConstant) continue;
|
|
|
|
if (!initialValues.ContainsKey(condition.Parameter) && condition.InitialValue > -999f)
|
|
initialValues[condition.Parameter] = condition.InitialValue;
|
|
}
|
|
}
|
|
|
|
private void ProcessInitialStates(Dictionary<TargetProp, object> initialStates,
|
|
Dictionary<TargetProp, AnimatedProperty> shapes)
|
|
{
|
|
var asc = context.Extension<AnimationServicesContext>();
|
|
|
|
// We need to track _two_ initial states: the initial state we'll apply at build time (which applies
|
|
// when animations are disabled) and the animation base state. Confusingly, the animation base state
|
|
// should be the state that is currently applied to the object...
|
|
|
|
var clips = context.Extension<AnimationServicesContext>().AnimationDatabase;
|
|
var initialStateHolder = clips.ClipsForPath(ReactiveObjectPrepass.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)
|
|
{
|
|
string path;
|
|
Type componentType;
|
|
|
|
var applied = false;
|
|
object animBaseState = (float) 0;
|
|
|
|
if (key.TargetObject is GameObject go)
|
|
{
|
|
path = RuntimeUtil.RelativePath(context.AvatarRootObject, go);
|
|
componentType = typeof(GameObject);
|
|
}
|
|
else if (key.TargetObject is Renderer r)
|
|
{
|
|
path = RuntimeUtil.RelativePath(context.AvatarRootObject, r.gameObject);
|
|
componentType = r.GetType();
|
|
|
|
if (r is SkinnedMeshRenderer smr && key.PropertyName.StartsWith("blendShape."))
|
|
{
|
|
var blendShape = key.PropertyName.Substring("blendShape.".Length);
|
|
var index = smr.sharedMesh?.GetBlendShapeIndex(blendShape);
|
|
|
|
if (index != null && index >= 0)
|
|
{
|
|
animBaseState = smr.GetBlendShapeWeight(index.Value);
|
|
smr.SetBlendShapeWeight(index.Value, (float) initialState);
|
|
}
|
|
|
|
applied = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException("Invalid target object: " + key.TargetObject);
|
|
}
|
|
|
|
if (!applied)
|
|
{
|
|
var serializedObject = new SerializedObject(key.TargetObject);
|
|
var prop = serializedObject.FindProperty(key.PropertyName);
|
|
|
|
if (prop != null)
|
|
{
|
|
switch (prop.propertyType)
|
|
{
|
|
case SerializedPropertyType.Boolean:
|
|
animBaseState = prop.boolValue ? 1.0f : 0.0f;
|
|
prop.boolValue = ((float)initialState) > 0.5f;
|
|
break;
|
|
case SerializedPropertyType.Float:
|
|
animBaseState = prop.floatValue;
|
|
prop.floatValue = (float) initialState;
|
|
break;
|
|
case SerializedPropertyType.ObjectReference:
|
|
animBaseState = prop.objectReferenceValue;
|
|
prop.objectReferenceValue = (Object) initialState;
|
|
break;
|
|
}
|
|
|
|
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
}
|
|
|
|
if (!shapes.ContainsKey(key))
|
|
// Do not generate any animation base state if the property is set to a constant value,
|
|
// because we won't generate any override layers.
|
|
continue;
|
|
|
|
if (animBaseState is float f)
|
|
{
|
|
var binding = EditorCurveBinding.FloatCurve(
|
|
path,
|
|
componentType,
|
|
key.PropertyName
|
|
);
|
|
|
|
var curve = new AnimationCurve();
|
|
curve.AddKey(0, f);
|
|
curve.AddKey(1, f);
|
|
|
|
AnimationUtility.SetEditorCurve(_initialStateClip, binding, curve);
|
|
|
|
if (componentType == typeof(GameObject) && key.PropertyName == "m_IsActive")
|
|
{
|
|
binding = EditorCurveBinding.FloatCurve(
|
|
"",
|
|
typeof(Animator),
|
|
asc.GetActiveSelfProxy((GameObject)key.TargetObject)
|
|
);
|
|
|
|
AnimationUtility.SetEditorCurve(_initialStateClip, binding, curve);
|
|
}
|
|
}
|
|
else if (animBaseState is Object obj)
|
|
{
|
|
var binding = EditorCurveBinding.PPtrCurve(
|
|
path,
|
|
componentType,
|
|
key.PropertyName
|
|
);
|
|
|
|
AnimationUtility.SetObjectReferenceCurve(_initialStateClip, binding, new []
|
|
{
|
|
new ObjectReferenceKeyframe()
|
|
{
|
|
value = obj,
|
|
time = 0
|
|
},
|
|
new ObjectReferenceKeyframe()
|
|
{
|
|
value = obj,
|
|
time = 1
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
#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(AnimatedProperty info)
|
|
{
|
|
// TODO: prune non-animated keys
|
|
|
|
// Check if this is non-animated and skip most processing if so
|
|
if (info.alwaysDeleted || info.actionGroups[^1].IsConstant) return;
|
|
|
|
var asm = GenerateStateMachine(info);
|
|
ApplyController(asm, "MA Responsive: " + info.TargetProp.TargetObject.name);
|
|
}
|
|
|
|
private AnimatorStateMachine GenerateStateMachine(AnimatedProperty 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
|
|
});
|
|
asc.AnimationDatabase.RegisterState(states[^1].state);
|
|
|
|
var lastConstant = info.actionGroups.FindLastIndex(agk => agk.IsConstant);
|
|
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.IsConstant)
|
|
{
|
|
clip.name = "Property Overlay constant " + group.Value;
|
|
initialState.motion = clip;
|
|
}
|
|
else
|
|
{
|
|
clip.name = "Property Overlay controlled by " + group.ControllingConditions[0].DebugName + " " +
|
|
group.Value;
|
|
|
|
var conditions = GetTransitionConditions(asc, group);
|
|
|
|
foreach (var (st, transitions) in transitionBuffer)
|
|
{
|
|
if (!group.Inverted)
|
|
{
|
|
var transition = new AnimatorStateTransition
|
|
{
|
|
isExit = true,
|
|
hasExitTime = false,
|
|
duration = 0,
|
|
hasFixedDuration = true,
|
|
conditions = (AnimatorCondition[])conditions.Clone()
|
|
};
|
|
transitions.Add(transition);
|
|
}
|
|
else
|
|
{
|
|
foreach (var cond in conditions)
|
|
{
|
|
transitions.Add(new AnimatorStateTransition
|
|
{
|
|
isExit = true,
|
|
hasExitTime = false,
|
|
duration = 0,
|
|
hasFixedDuration = true,
|
|
conditions = new[] { InvertCondition(cond) }
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
var state = new AnimatorState();
|
|
state.name = group.ControllingConditions[0].DebugName;
|
|
state.motion = clip;
|
|
state.writeDefaultValues = false;
|
|
states.Add(new ChildAnimatorState
|
|
{
|
|
position = new Vector3(x, y),
|
|
state = state
|
|
});
|
|
asc.AnimationDatabase.RegisterState(states[^1].state);
|
|
|
|
var transitionList = new List<AnimatorStateTransition>();
|
|
transitionBuffer.Add((state, transitionList));
|
|
|
|
if (!group.Inverted)
|
|
{
|
|
entryTransitions.Add(new AnimatorTransition
|
|
{
|
|
destinationState = state,
|
|
conditions = conditions
|
|
});
|
|
|
|
foreach (var cond in conditions)
|
|
{
|
|
var inverted = InvertCondition(cond);
|
|
transitionList.Add(new AnimatorStateTransition
|
|
{
|
|
isExit = true,
|
|
hasExitTime = false,
|
|
duration = 0,
|
|
hasFixedDuration = true,
|
|
conditions = new[] { inverted }
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// inverted condition
|
|
foreach (var cond in conditions)
|
|
{
|
|
entryTransitions.Add(new AnimatorTransition()
|
|
{
|
|
destinationState = state,
|
|
conditions = new[] { InvertCondition(cond) }
|
|
});
|
|
}
|
|
|
|
transitionList.Add(new AnimatorStateTransition
|
|
{
|
|
isExit = true,
|
|
hasExitTime = false,
|
|
duration = 0,
|
|
hasFixedDuration = true,
|
|
conditions = conditions
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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 static AnimatorCondition InvertCondition(AnimatorCondition cond)
|
|
{
|
|
return new AnimatorCondition
|
|
{
|
|
parameter = cond.parameter,
|
|
mode = cond.mode == AnimatorConditionMode.Greater
|
|
? AnimatorConditionMode.Less
|
|
: AnimatorConditionMode.Greater,
|
|
threshold = cond.threshold
|
|
};
|
|
}
|
|
|
|
private AnimatorCondition[] GetTransitionConditions(AnimationServicesContext asc, ReactionRule group)
|
|
{
|
|
var conditions = new List<AnimatorCondition>();
|
|
|
|
foreach (var condition in group.ControllingConditions)
|
|
{
|
|
if (condition.IsConstant) continue;
|
|
|
|
if (float.IsFinite(condition.ParameterValueLo))
|
|
{
|
|
conditions.Add(new AnimatorCondition
|
|
{
|
|
parameter = condition.Parameter,
|
|
mode = AnimatorConditionMode.Greater,
|
|
threshold = condition.ParameterValueLo
|
|
});
|
|
}
|
|
|
|
if (float.IsFinite(condition.ParameterValueHi))
|
|
{
|
|
conditions.Add(new AnimatorCondition
|
|
{
|
|
parameter = condition.Parameter,
|
|
mode = AnimatorConditionMode.Less,
|
|
threshold = condition.ParameterValueHi
|
|
});
|
|
}
|
|
}
|
|
|
|
if (conditions.Count == 0)
|
|
throw new InvalidOperationException("No controlling parameters found for " + group);
|
|
|
|
return conditions.ToArray();
|
|
}
|
|
|
|
private Motion AnimResult(TargetProp key, object 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 Renderer r)
|
|
{
|
|
path = RuntimeUtil.RelativePath(context.AvatarRootObject, r.gameObject);
|
|
componentType = r.GetType();
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException("Invalid target object: " + key.TargetObject);
|
|
}
|
|
|
|
var clip = new AnimationClip();
|
|
clip.name = $"Set {path}:{key.PropertyName}={value}";
|
|
|
|
if (value is Object obj)
|
|
{
|
|
var binding = EditorCurveBinding.PPtrCurve(path, componentType, key.PropertyName);
|
|
AnimationUtility.SetObjectReferenceCurve(clip, binding, new []
|
|
{
|
|
new ObjectReferenceKeyframe()
|
|
{
|
|
value = obj,
|
|
time = 0
|
|
},
|
|
new ObjectReferenceKeyframe()
|
|
{
|
|
value = obj,
|
|
time = 1
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
var curve = new AnimationCurve();
|
|
curve.AddKey(0, (float) value);
|
|
curve.AddKey(1, (float) value);
|
|
|
|
var binding = EditorCurveBinding.FloatCurve(path, componentType, key.PropertyName);
|
|
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
|
|
|
if (key.TargetObject is GameObject targetObject && key.PropertyName == "m_IsActive")
|
|
{
|
|
var asc = context.Extension<AnimationServicesContext>();
|
|
var propName = asc.GetActiveSelfProxy(targetObject);
|
|
binding = EditorCurveBinding.FloatCurve("", typeof(Animator), propName);
|
|
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();
|
|
}
|
|
}
|
|
} |