mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-01-04 13:45:04 +08:00
809 lines
31 KiB
C#
809 lines
31 KiB
C#
#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)
|
|
{
|
|
var hasShapeChanger = context.AvatarRootObject.GetComponentInChildren<ModularAvatarShapeChanger>() != null;
|
|
var hasObjectSwitcher =
|
|
context.AvatarRootObject.GetComponentInChildren<ModularAvatarObjectToggle>() != null;
|
|
if (hasShapeChanger || hasObjectSwitcher)
|
|
{
|
|
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(ndmf.BuildContext context, TargetProp key, GameObject controllingObject, float value)
|
|
{
|
|
var asc = context.Extension<AnimationServicesContext>();
|
|
|
|
TargetProp = key;
|
|
|
|
var conditions = new List<ControlCondition>();
|
|
|
|
var cursor = controllingObject?.transform;
|
|
|
|
while (cursor != null && !RuntimeUtil.IsAvatarRoot(cursor))
|
|
{
|
|
conditions.Add(new ControlCondition
|
|
{
|
|
Parameter = asc.GetActiveSelfProxy(cursor.gameObject),
|
|
DebugName = cursor.gameObject.name,
|
|
IsConstant = false,
|
|
InitialValue = cursor.gameObject.activeSelf ? 1.0f : 0.0f,
|
|
ParameterValueLo = 0.5f,
|
|
ParameterValueHi = 1.5f,
|
|
ReferenceObject = cursor.gameObject
|
|
});
|
|
|
|
foreach (var mami in cursor.GetComponents<ModularAvatarMenuItem>())
|
|
conditions.Add(ParameterAssignerPass.AssignMenuItemParameter(context, mami));
|
|
|
|
cursor = cursor.parent;
|
|
}
|
|
|
|
ControllingConditions = conditions;
|
|
|
|
Value = value;
|
|
}
|
|
|
|
public TargetProp TargetProp;
|
|
public float Value;
|
|
|
|
public readonly List<ControlCondition> ControllingConditions;
|
|
|
|
public bool InitiallyActive =>
|
|
ControllingConditions.Count == 0 || ControllingConditions.All(c => c.InitiallyActive);
|
|
public bool IsDelete;
|
|
|
|
public bool IsConstant => ControllingConditions.Count == 0 || ControllingConditions.All(c => c.IsConstant);
|
|
public bool IsConstantOn => IsConstant && InitiallyActive;
|
|
|
|
public override string ToString()
|
|
{
|
|
return $"AGK: {TargetProp}={Value}";
|
|
}
|
|
|
|
public bool TryMerge(ActionGroupKey other)
|
|
{
|
|
if (!TargetProp.Equals(other.TargetProp)) return false;
|
|
if (Mathf.Abs(Value - other.Value) > 0.001f) return false;
|
|
if (!ControllingConditions.SequenceEqual(other.ControllingConditions)) return false;
|
|
if (IsDelete || other.IsDelete) return false;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
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 PropertyOverlayPass(ndmf.BuildContext context)
|
|
{
|
|
this.context = context;
|
|
}
|
|
|
|
internal void Execute()
|
|
{
|
|
Dictionary<TargetProp, PropGroup> shapes = FindShapes(context);
|
|
FindObjectToggles(shapes, context);
|
|
|
|
AnalyzeConstants(shapes);
|
|
|
|
PreprocessShapes(shapes, out var initialStates, out var deletedShapes);
|
|
|
|
ProcessInitialStates(initialStates);
|
|
ProcessInitialAnimatorVariables(shapes);
|
|
|
|
foreach (var groups in shapes.Values)
|
|
{
|
|
ProcessShapeKey(groups);
|
|
}
|
|
|
|
ProcessMeshDeletion(deletedShapes);
|
|
}
|
|
|
|
private void AnalyzeConstants(Dictionary<TargetProp, PropGroup> shapes)
|
|
{
|
|
HashSet<GameObject> toggledObjects = new();
|
|
|
|
foreach (var targetProp in shapes.Keys)
|
|
if (targetProp is { TargetObject: GameObject go, PropertyName: "m_IsActive" })
|
|
toggledObjects.Add(go);
|
|
|
|
foreach (var group in shapes.Values)
|
|
{
|
|
foreach (var actionGroup in group.actionGroups)
|
|
{
|
|
foreach (var condition in actionGroup.ControllingConditions)
|
|
if (condition.ReferenceObject != null && !toggledObjects.Contains(condition.ReferenceObject))
|
|
condition.IsConstant = true;
|
|
|
|
var firstAlwaysOn =
|
|
actionGroup.ControllingConditions.FindIndex(c => c.InitiallyActive && c.IsConstant);
|
|
if (firstAlwaysOn > 0) actionGroup.ControllingConditions.RemoveRange(0, firstAlwaysOn - 1);
|
|
}
|
|
|
|
// Remove any action groups with always-off conditions
|
|
group.actionGroups.RemoveAll(agk =>
|
|
agk.ControllingConditions.Any(c => !c.InitiallyActive && c.IsConstant));
|
|
}
|
|
|
|
// Remove shapes with no action groups
|
|
foreach (var kvp in shapes.ToList())
|
|
if (kvp.Value.actionGroups.Count == 0)
|
|
shapes.Remove(kvp.Key);
|
|
}
|
|
|
|
private void ProcessInitialAnimatorVariables(Dictionary<TargetProp, PropGroup> 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))
|
|
initialValues[condition.Parameter] = condition.InitialValue;
|
|
}
|
|
}
|
|
|
|
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())
|
|
{
|
|
if (info.actionGroups.Count == 0)
|
|
{
|
|
// never active control; ignore it entirely
|
|
shapes.Remove(key);
|
|
continue;
|
|
}
|
|
|
|
var deletions = info.actionGroups.Where(agk => agk.IsDelete).ToList();
|
|
if (deletions.Any(d => d.ControllingConditions.All(c => c.IsConstantActive)))
|
|
{
|
|
// 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].IsConstant)
|
|
{
|
|
shapes.Remove(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ProcessInitialStates(Dictionary<TargetProp, float> initialStates)
|
|
{
|
|
// 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(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)
|
|
{
|
|
string path;
|
|
Type componentType;
|
|
|
|
var applied = false;
|
|
float animBaseState = 0;
|
|
|
|
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)
|
|
{
|
|
animBaseState = smr.GetBlendShapeWeight(index.Value);
|
|
smr.SetBlendShapeWeight(index.Value, 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 : 0;
|
|
prop.boolValue = initialState > 0.5f;
|
|
break;
|
|
case SerializedPropertyType.Float:
|
|
animBaseState = prop.floatValue;
|
|
prop.floatValue = initialState;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var curve = new AnimationCurve();
|
|
curve.AddKey(0, animBaseState);
|
|
curve.AddKey(1, animBaseState);
|
|
|
|
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].IsConstant)
|
|
{
|
|
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.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)
|
|
{
|
|
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.ControllingConditions[0].DebugName;
|
|
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 = cond.mode == AnimatorConditionMode.Greater
|
|
? AnimatorConditionMode.Less
|
|
: AnimatorConditionMode.Greater,
|
|
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>();
|
|
|
|
foreach (var condition in group.ControllingConditions)
|
|
{
|
|
if (condition.IsConstant) continue;
|
|
|
|
conditions.Add(new AnimatorCondition
|
|
{
|
|
parameter = condition.Parameter,
|
|
mode = AnimatorConditionMode.Greater,
|
|
threshold = condition.ParameterValueLo
|
|
});
|
|
|
|
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, 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);
|
|
|
|
if (key.TargetObject is GameObject obj && key.PropertyName == "m_IsActive")
|
|
{
|
|
var asc = context.Extension<AnimationServicesContext>();
|
|
var propName = asc.GetActiveSelfProxy(obj);
|
|
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();
|
|
}
|
|
|
|
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 void FindObjectToggles(Dictionary<TargetProp, PropGroup> objectGroups, ndmf.BuildContext context)
|
|
{
|
|
var asc = context.Extension<AnimationServicesContext>();
|
|
|
|
var toggles = this.context.AvatarRootObject.GetComponentsInChildren<ModularAvatarObjectToggle>(true);
|
|
|
|
foreach (var toggle in toggles)
|
|
{
|
|
if (toggle.Objects == null) continue;
|
|
|
|
foreach (var obj in toggle.Objects)
|
|
{
|
|
var target = obj.Object.Get(toggle);
|
|
if (target == null) continue;
|
|
|
|
// Make sure we generate an animator prop for each controlled object, as we intend to generate
|
|
// animations for them.
|
|
asc.GetActiveSelfProxy(target);
|
|
|
|
var key = new TargetProp
|
|
{
|
|
TargetObject = target,
|
|
PropertyName = "m_IsActive"
|
|
};
|
|
|
|
if (!objectGroups.TryGetValue(key, out var group))
|
|
{
|
|
group = new PropGroup(key, target.activeSelf ? 1 : 0);
|
|
objectGroups[key] = group;
|
|
}
|
|
|
|
var value = obj.Active ? 1 : 0;
|
|
var action = new ActionGroupKey(context, key, toggle.gameObject, value);
|
|
|
|
if (group.actionGroups.Count == 0)
|
|
group.actionGroups.Add(action);
|
|
else if (!group.actionGroups[^1].TryMerge(action)) group.actionGroups.Add(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
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(context, key, null, value);
|
|
agk.Value = renderer.GetBlendShapeWeight(shapeId);
|
|
info.actionGroups.Add(agk);
|
|
}
|
|
|
|
var action = new ActionGroupKey(context, 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;
|
|
|
|
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;
|
|
}
|
|
}
|
|
} |