mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-01-01 20:25:07 +08:00
02ca58a3a9
Closes: #872
708 lines
25 KiB
C#
708 lines
25 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;
|
|
|
|
#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;
|
|
}
|
|
}
|
|
} |