mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-01-31 02:32:53 +08:00
feat: add material switcher and inverse mode (#974)
* feat: add material switcher Also refactor everything... * refactor: simplify object curve handling * refactor: additional refactoring and bugfixes * feat: inverse mode * feat: add material setter inspector UI * chore: set material setter icon * chore: fix error on build * chore: adjust order of inverted element
This commit is contained in:
parent
3d1b4f1c76
commit
d998763fbe
3
Editor/Inspector/MaterialSetter.meta
Normal file
3
Editor/Inspector/MaterialSetter.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 131d9706ddc04331bd09cf13b863c537
|
||||
timeCreated: 1723334567
|
24
Editor/Inspector/MaterialSetter/MaterialSetter.uxml
Normal file
24
Editor/Inspector/MaterialSetter/MaterialSetter.uxml
Normal file
@ -0,0 +1,24 @@
|
||||
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements"
|
||||
xmlns:ma="nadena.dev.modular_avatar.core.editor">
|
||||
<ui:VisualElement name="root-box">
|
||||
<ui:VisualElement name="group-box">
|
||||
<ui:VisualElement name="ListViewContainer">
|
||||
<ui:ListView virtualization-method="DynamicHeight"
|
||||
reorder-mode="Animated"
|
||||
reorderable="true"
|
||||
show-add-remove-footer="true"
|
||||
show-border="true"
|
||||
show-foldout-header="false"
|
||||
name="Shapes"
|
||||
item-height="100"
|
||||
binding-path="m_objects"
|
||||
style="flex-grow: 1;"
|
||||
/>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ed:PropertyField binding-path="m_inverted"/>
|
||||
|
||||
<ma:LanguageSwitcherElement/>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
3
Editor/Inspector/MaterialSetter/MaterialSetter.uxml.meta
Normal file
3
Editor/Inspector/MaterialSetter/MaterialSetter.uxml.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd5c518316b2435d8a666911d4131903
|
||||
timeCreated: 1723334567
|
41
Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs
Normal file
41
Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs
Normal file
@ -0,0 +1,41 @@
|
||||
#region
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
#endregion
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
||||
{
|
||||
[CustomEditor(typeof(ModularAvatarMaterialSetter))]
|
||||
public class MaterialSetterEditor : MAEditorBase
|
||||
{
|
||||
[SerializeField] private StyleSheet uss;
|
||||
[SerializeField] private VisualTreeAsset uxml;
|
||||
|
||||
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override VisualElement CreateInnerInspectorGUI()
|
||||
{
|
||||
var root = uxml.CloneTree();
|
||||
Localization.UI.Localize(root);
|
||||
root.styleSheets.Add(uss);
|
||||
|
||||
root.Bind(serializedObject);
|
||||
|
||||
var listView = root.Q<ListView>("Shapes");
|
||||
|
||||
listView.showBoundCollectionSize = false;
|
||||
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
}
|
13
Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs.meta
Normal file
13
Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs.meta
Normal file
@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 339dd3848a2044b1aa04f543226de0e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences:
|
||||
- uss: {fileID: 7433441132597879392, guid: fce9f3fe74434b718abac5ea66775acb, type: 3}
|
||||
- uxml: {fileID: 9197481963319205126, guid: cd5c518316b2435d8a666911d4131903, type: 3}
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
35
Editor/Inspector/MaterialSetter/MaterialSetterStyles.uss
Normal file
35
Editor/Inspector/MaterialSetter/MaterialSetterStyles.uss
Normal file
@ -0,0 +1,35 @@
|
||||
VisualElement {
|
||||
}
|
||||
|
||||
#group-box {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
padding: 4px;
|
||||
border-width: 3px;
|
||||
border-left-color: rgba(0, 1, 0, 0.2);
|
||||
border-top-color: rgba(0, 1, 0, 0.2);
|
||||
border-right-color: rgba(0, 1, 0, 0.2);
|
||||
border-bottom-color: rgba(0, 1, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
/* background-color: rgba(0, 0, 0, 0.1); */
|
||||
}
|
||||
|
||||
#ListViewContainer {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#group-box > Label {
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.horizontal #f-object {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#f-material-index-int {
|
||||
display: none;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fce9f3fe74434b718abac5ea66775acb
|
||||
timeCreated: 1723334567
|
111
Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.cs
Normal file
111
Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.cs
Normal file
@ -0,0 +1,111 @@
|
||||
#region
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
#endregion
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(MaterialSwitchObject))]
|
||||
public class MaterialSwitchObjectEditor : PropertyDrawer
|
||||
{
|
||||
private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/MaterialSetter/";
|
||||
private const string UxmlPath = Root + "MaterialSwitchObjectEditor.uxml";
|
||||
private const string UssPath = Root + "MaterialSetterStyles.uss";
|
||||
|
||||
public override VisualElement CreatePropertyGUI(SerializedProperty property)
|
||||
{
|
||||
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath).CloneTree();
|
||||
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
||||
|
||||
Localization.UI.Localize(uxml);
|
||||
uxml.styleSheets.Add(uss);
|
||||
uxml.BindProperty(property);
|
||||
|
||||
var f_material_index = uxml.Q<DropdownField>("f-material-index");
|
||||
|
||||
var f_object = uxml.Q<PropertyField>("f-object");
|
||||
f_object.RegisterValueChangeCallback(evt => { UpdateMaterialDropdown(); });
|
||||
UpdateMaterialDropdown();
|
||||
|
||||
// Link dropdown to material index field
|
||||
var f_material_index_int = uxml.Q<IntegerField>("f-material-index-int");
|
||||
f_material_index_int.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
f_material_index.SetValueWithoutNotify("" + evt.newValue);
|
||||
});
|
||||
|
||||
f_material_index.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (evt.newValue != null && int.TryParse(evt.newValue, out var i))
|
||||
{
|
||||
f_material_index_int.value = i;
|
||||
}
|
||||
});
|
||||
|
||||
return uxml;
|
||||
|
||||
void UpdateMaterialDropdown()
|
||||
{
|
||||
var toggledObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object"));
|
||||
Material[] sharedMaterials;
|
||||
try
|
||||
{
|
||||
sharedMaterials = toggledObject?.GetComponent<Renderer>()?.sharedMaterials;
|
||||
}
|
||||
catch (MissingComponentException e)
|
||||
{
|
||||
sharedMaterials = null;
|
||||
}
|
||||
|
||||
if (sharedMaterials != null)
|
||||
{
|
||||
var matCount = sharedMaterials.Length;
|
||||
|
||||
f_material_index.SetEnabled(true);
|
||||
|
||||
f_material_index.choices.Clear();
|
||||
for (int i = 0; i < matCount; i++)
|
||||
{
|
||||
f_material_index.choices.Add(i.ToString());
|
||||
}
|
||||
|
||||
f_material_index.formatListItemCallback = idx_s =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(idx_s)) return "";
|
||||
|
||||
var idx = int.Parse(idx_s);
|
||||
if (idx < 0 || idx >= sharedMaterials.Length)
|
||||
{
|
||||
return idx + ": <???>";
|
||||
}
|
||||
else if (sharedMaterials[idx] == null)
|
||||
{
|
||||
return idx + ": <none>";
|
||||
}
|
||||
else
|
||||
{
|
||||
return idx + ": " + sharedMaterials[idx].name;
|
||||
}
|
||||
};
|
||||
f_material_index.formatSelectedValueCallback = f_material_index.formatListItemCallback;
|
||||
}
|
||||
else
|
||||
{
|
||||
f_material_index.SetEnabled(false);
|
||||
if (f_material_index.choices.Count == 0)
|
||||
{
|
||||
f_material_index.choices.Add("0");
|
||||
}
|
||||
|
||||
f_material_index.formatListItemCallback = _ => "<Missing Renderer>";
|
||||
f_material_index.formatSelectedValueCallback = f_material_index.formatListItemCallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6361a17f884644988ef3ece7fbe73ab7
|
||||
timeCreated: 1723334567
|
@ -0,0 +1,10 @@
|
||||
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
|
||||
<ui:VisualElement class="toggled-object-editor">
|
||||
<ui:VisualElement class="horizontal">
|
||||
<ed:PropertyField binding-path="Object" label="" name="f-object" class="f-object"/>
|
||||
<ui:DropdownField name="f-material-index" binding-path="MaterialIndex"/>
|
||||
<ed:IntegerField binding-path="MaterialIndex" name="f-material-index-int"/>
|
||||
</ui:VisualElement>
|
||||
<ed:PropertyField binding-path="Material" label="" name="f-material"/>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55b5e53f6c364089a1871b68e0de17c6
|
||||
timeCreated: 1723334567
|
@ -1,4 +1,4 @@
|
||||
<UXML xmlns:ui="UnityEngine.UIElements"
|
||||
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements"
|
||||
xmlns:ma="nadena.dev.modular_avatar.core.editor">
|
||||
<ui:VisualElement name="root-box">
|
||||
<ui:VisualElement name="group-box">
|
||||
@ -17,6 +17,8 @@
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ed:PropertyField binding-path="m_inverted"/>
|
||||
|
||||
<ma:LanguageSwitcherElement/>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
@ -3,6 +3,7 @@
|
||||
<ui:VisualElement name="root-box">
|
||||
<ui:VisualElement name="group-box">
|
||||
<ed:PropertyField binding-path="m_targetRenderer"/>
|
||||
<ed:PropertyField binding-path="m_inverted"/>
|
||||
|
||||
<ui:VisualElement name="ListViewContainer">
|
||||
<ui:ListView virtualization-method="DynamicHeight"
|
||||
@ -19,6 +20,8 @@
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ed:PropertyField binding-path="m_inverted"/>
|
||||
|
||||
<ma:LanguageSwitcherElement/>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
@ -48,7 +48,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
|
||||
seq.Run(MeshSettingsPluginPass.Instance);
|
||||
seq.Run(ScaleAdjusterPass.Instance).PreviewingWith(new ScaleAdjusterPreview());
|
||||
#if MA_VRCSDK3_AVATARS
|
||||
seq.Run(PropertyOverlayPrePass.Instance);
|
||||
seq.Run(ReactiveObjectPrepass.Instance);
|
||||
seq.Run(RenameParametersPluginPass.Instance);
|
||||
seq.Run(ParameterAssignerPass.Instance);
|
||||
seq.Run(MergeBlendTreePass.Instance);
|
||||
@ -57,8 +57,8 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
|
||||
#endif
|
||||
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
|
||||
{
|
||||
seq.Run("Shape Changer", ctx => new PropertyOverlayPass(ctx).Execute())
|
||||
.PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview());
|
||||
seq.Run("Shape Changer", ctx => new ReactiveObjectPass(ctx).Execute())
|
||||
.PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview(), new MaterialSetterPreview());
|
||||
seq.Run(MergeArmaturePluginPass.Instance);
|
||||
seq.Run(BoneProxyPluginPass.Instance);
|
||||
seq.Run(VisibleHeadAccessoryPluginPass.Instance);
|
||||
|
3
Editor/ReactiveObjects/AnimationGeneration.meta
Normal file
3
Editor/ReactiveObjects/AnimationGeneration.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e0a91a8758c4946899e34d25372bd3e
|
||||
timeCreated: 1723325795
|
@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal class AnimatedProperty
|
||||
{
|
||||
public TargetProp TargetProp { get; }
|
||||
public string ControlParam { get; set; }
|
||||
|
||||
public bool alwaysDeleted;
|
||||
public object currentState;
|
||||
|
||||
// Objects which trigger deletion of this shape key.
|
||||
public List<ReactionRule> actionGroups = new List<ReactionRule>();
|
||||
|
||||
public AnimatedProperty(TargetProp key, float currentState)
|
||||
{
|
||||
TargetProp = key;
|
||||
this.currentState = currentState;
|
||||
}
|
||||
|
||||
public AnimatedProperty(TargetProp key, Object currentState)
|
||||
{
|
||||
TargetProp = key;
|
||||
this.currentState = currentState;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c64796ed187a4de9bca2bcb5d0c6b029
|
||||
timeCreated: 1723325881
|
@ -4,7 +4,10 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal class ControlCondition
|
||||
{
|
||||
public string Parameter, DebugName;
|
||||
public string Parameter;
|
||||
public UnityEngine.Object ControllingObject;
|
||||
|
||||
public string DebugName;
|
||||
public bool IsConstant;
|
||||
public float ParameterValueLo, ParameterValueHi, InitialValue;
|
||||
public bool InitiallyActive => InitialValue > ParameterValueLo && InitialValue < ParameterValueHi;
|
96
Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs
Normal file
96
Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using nadena.dev.modular_avatar.animation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal class ReactionRule
|
||||
{
|
||||
public ReactionRule(ndmf.BuildContext context, TargetProp key, GameObject controllingObject, float value)
|
||||
: this(context, key, controllingObject, (object)value) { }
|
||||
|
||||
public ReactionRule(ndmf.BuildContext context, TargetProp key, GameObject controllingObject, UnityEngine.Object value)
|
||||
: this(context, key, controllingObject, (object)value) { }
|
||||
|
||||
private ReactionRule(ndmf.BuildContext context, TargetProp key, GameObject controllingObject, object value)
|
||||
{
|
||||
var asc = context?.Extension<AnimationServicesContext>();
|
||||
|
||||
TargetProp = key;
|
||||
|
||||
var conditions = new List<ControlCondition>();
|
||||
|
||||
var cursor = controllingObject?.transform;
|
||||
|
||||
bool did_mami = false;
|
||||
|
||||
while (cursor != null && !RuntimeUtil.IsAvatarRoot(cursor))
|
||||
{
|
||||
// Only look at the menu item closest to the object we're directly attached to, to avoid submenus
|
||||
// causing issues...
|
||||
var mami = cursor?.GetComponent<ModularAvatarMenuItem>();
|
||||
if (mami != null && !did_mami)
|
||||
{
|
||||
did_mami = true;
|
||||
|
||||
var mami_condition = ParameterAssignerPass.AssignMenuItemParameter(mami);
|
||||
if (mami_condition != null) conditions.Add(mami_condition);
|
||||
}
|
||||
|
||||
conditions.Add(new ControlCondition
|
||||
{
|
||||
Parameter = asc?.GetActiveSelfProxy(cursor.gameObject) ?? RuntimeUtil.AvatarRootPath(cursor.gameObject),
|
||||
DebugName = cursor.gameObject.name,
|
||||
IsConstant = false,
|
||||
InitialValue = cursor.gameObject.activeSelf ? 1.0f : 0.0f,
|
||||
ParameterValueLo = 0.5f,
|
||||
ParameterValueHi = float.PositiveInfinity,
|
||||
ReferenceObject = cursor.gameObject
|
||||
});
|
||||
|
||||
cursor = cursor.parent;
|
||||
}
|
||||
|
||||
ControllingConditions = conditions;
|
||||
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public TargetProp TargetProp;
|
||||
public object Value;
|
||||
|
||||
public readonly List<ControlCondition> ControllingConditions;
|
||||
|
||||
public bool InitiallyActive =>
|
||||
((ControllingConditions.Count == 0) || ControllingConditions.All(c => c.InitiallyActive)) ^ Inverted;
|
||||
public bool IsDelete;
|
||||
|
||||
public bool Inverted;
|
||||
|
||||
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(ReactionRule other)
|
||||
{
|
||||
if (!TargetProp.Equals(other.TargetProp)) return false;
|
||||
|
||||
// Value checks
|
||||
if (Value == other.Value) { /* objects match */ }
|
||||
else if (Value is float a && other.Value is float b)
|
||||
{
|
||||
if (Mathf.Abs(a - b) > 0.001f) return false;
|
||||
}
|
||||
else return false;
|
||||
if (!ControllingConditions.SequenceEqual(other.ControllingConditions)) return false;
|
||||
if (IsDelete || other.IsDelete) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3142032fc2624e528b917973d9bc79f6
|
||||
timeCreated: 1723325905
|
@ -0,0 +1,158 @@
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.modular_avatar.animation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
partial class ReactiveObjectAnalyzer
|
||||
{
|
||||
|
||||
private Dictionary<TargetProp, AnimatedProperty> FindShapes(GameObject root)
|
||||
{
|
||||
var changers = root.GetComponentsInChildren<ModularAvatarShapeChanger>(true);
|
||||
|
||||
Dictionary<TargetProp, AnimatedProperty> 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 AnimatedProperty(key, renderer.GetBlendShapeWeight(shapeId));
|
||||
shapeKeys[key] = info;
|
||||
|
||||
// Add initial state
|
||||
var agk = new ReactionRule(context, key, null, value);
|
||||
agk.Value = renderer.GetBlendShapeWeight(shapeId);
|
||||
info.actionGroups.Add(agk);
|
||||
}
|
||||
|
||||
var action = new ReactionRule(context, key, changer.gameObject, value);
|
||||
action.Inverted = changer.Inverted;
|
||||
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;
|
||||
}
|
||||
|
||||
private void FindMaterialSetters(Dictionary<TargetProp, AnimatedProperty> objectGroups, GameObject root)
|
||||
{
|
||||
var materialSetters = root.GetComponentsInChildren<ModularAvatarMaterialSetter>(true);
|
||||
|
||||
foreach (var setter in materialSetters)
|
||||
{
|
||||
if (setter.Objects == null) continue;
|
||||
|
||||
foreach (var obj in setter.Objects)
|
||||
{
|
||||
var target = obj.Object.Get(setter);
|
||||
if (target == null) continue;
|
||||
var renderer = target.GetComponent<Renderer>();
|
||||
if (renderer == null || renderer.sharedMaterials.Length < obj.MaterialIndex) continue;
|
||||
|
||||
var key = new TargetProp
|
||||
{
|
||||
TargetObject = renderer,
|
||||
PropertyName = "m_Materials.Array.data[" + obj.MaterialIndex + "]",
|
||||
};
|
||||
|
||||
if (!objectGroups.TryGetValue(key, out var group))
|
||||
{
|
||||
group = new AnimatedProperty(key, renderer.sharedMaterials[obj.MaterialIndex]);
|
||||
objectGroups[key] = group;
|
||||
}
|
||||
|
||||
var action = new ReactionRule(context, key, setter.gameObject, obj.Material);
|
||||
action.Inverted = setter.Inverted;
|
||||
|
||||
if (group.actionGroups.Count == 0)
|
||||
group.actionGroups.Add(action);
|
||||
else if (!group.actionGroups[^1].TryMerge(action)) group.actionGroups.Add(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FindObjectToggles(Dictionary<TargetProp, AnimatedProperty> objectGroups, GameObject root)
|
||||
{
|
||||
var toggles = root.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;
|
||||
|
||||
var key = new TargetProp
|
||||
{
|
||||
TargetObject = target,
|
||||
PropertyName = "m_IsActive"
|
||||
};
|
||||
|
||||
if (!objectGroups.TryGetValue(key, out var group))
|
||||
{
|
||||
group = new AnimatedProperty(key, target.activeSelf ? 1 : 0);
|
||||
objectGroups[key] = group;
|
||||
}
|
||||
|
||||
var value = obj.Active ? 1 : 0;
|
||||
var action = new ReactionRule(context, key, toggle.gameObject, value);
|
||||
action.Inverted = toggle.Inverted;
|
||||
|
||||
if (group.actionGroups.Count == 0)
|
||||
group.actionGroups.Add(action);
|
||||
else if (!group.actionGroups[^1].TryMerge(action)) group.actionGroups.Add(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1b8b0b40f3fe4334a9a458d23baca09c
|
||||
timeCreated: 1723325966
|
@ -0,0 +1,222 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using nadena.dev.modular_avatar.animation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs analysis of reactive object rules prior to animation generation. This is used for debug
|
||||
/// displays/introspection as well.
|
||||
/// </summary>
|
||||
internal partial class ReactiveObjectAnalyzer
|
||||
{
|
||||
private readonly ndmf.BuildContext context;
|
||||
|
||||
public ReactiveObjectAnalyzer(ndmf.BuildContext context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public ReactiveObjectAnalyzer()
|
||||
{
|
||||
context = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find all reactive object rules
|
||||
/// </summary>
|
||||
/// <param name="root">The avatar root</param>
|
||||
/// <param name="initialStates">A dictionary of target property to initial state (float or UnityEngine.Object)</param>
|
||||
/// <param name="deletedShapes">A hashset of blendshape properties which are always deleted</param>
|
||||
/// <returns></returns>
|
||||
public Dictionary<TargetProp, AnimatedProperty> Analyze(
|
||||
GameObject root,
|
||||
out Dictionary<TargetProp, object> initialStates,
|
||||
out HashSet<TargetProp> deletedShapes
|
||||
)
|
||||
{
|
||||
Dictionary<TargetProp, AnimatedProperty> shapes = FindShapes(root);
|
||||
FindObjectToggles(shapes, root);
|
||||
FindMaterialSetters(shapes, root);
|
||||
|
||||
AnalyzeConstants(shapes);
|
||||
ResolveToggleInitialStates(shapes);
|
||||
PreprocessShapes(shapes, out initialStates, out deletedShapes);
|
||||
|
||||
return shapes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines which animated properties have a constant state, and prunes the set of rules appropriately.
|
||||
/// No-op if there is not build context (as animations cannot be determined)
|
||||
/// </summary>
|
||||
/// <param name="shapes"></param>
|
||||
private void AnalyzeConstants(Dictionary<TargetProp, AnimatedProperty> shapes)
|
||||
{
|
||||
var asc = context?.Extension<AnimationServicesContext>();
|
||||
HashSet<GameObject> toggledObjects = new();
|
||||
|
||||
if (asc == null) return;
|
||||
|
||||
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 = asc.AnimationDatabase.ClipsForPath(asc.PathMappings.GetObjectIdentifier(condition.ReferenceObject)).IsEmpty;
|
||||
|
||||
var i = 0;
|
||||
// Remove redundant active conditions.
|
||||
int retain = 0;
|
||||
actionGroup.ControllingConditions.RemoveAll(c => c.IsConstant && c.InitiallyActive);
|
||||
}
|
||||
|
||||
// Remove any action groups with always-unsatisfied conditions
|
||||
group.actionGroups.RemoveAll(agk => agk.IsConstant && !agk.InitiallyActive);
|
||||
|
||||
// Remove all action groups up until the last one where we're always on
|
||||
var lastAlwaysOnGroup = group.actionGroups.FindLastIndex(ag => ag.IsConstantOn);
|
||||
if (lastAlwaysOnGroup > 0)
|
||||
group.actionGroups.RemoveRange(0, lastAlwaysOnGroup - 1);
|
||||
}
|
||||
|
||||
// Remove shapes with no action groups
|
||||
foreach (var kvp in shapes.ToList())
|
||||
if (kvp.Value.actionGroups.Count == 0)
|
||||
shapes.Remove(kvp.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the initial active state of all GameObjects
|
||||
/// </summary>
|
||||
/// <param name="groups"></param>
|
||||
private void ResolveToggleInitialStates(Dictionary<TargetProp, AnimatedProperty> groups)
|
||||
{
|
||||
var asc = context?.Extension<AnimationServicesContext>();
|
||||
|
||||
Dictionary<string, bool> propStates = new Dictionary<string, bool>();
|
||||
Dictionary<string, bool> nextPropStates = new Dictionary<string, bool>();
|
||||
int loopLimit = 5;
|
||||
|
||||
bool unsettled = true;
|
||||
while (unsettled && loopLimit-- > 0)
|
||||
{
|
||||
unsettled = false;
|
||||
|
||||
foreach (var group in groups.Values)
|
||||
{
|
||||
if (group.TargetProp.PropertyName != "m_IsActive") continue;
|
||||
if (!(group.TargetProp.TargetObject is GameObject targetObject)) continue;
|
||||
|
||||
var pathKey = asc?.GetActiveSelfProxy(targetObject) ?? RuntimeUtil.AvatarRootPath(targetObject);
|
||||
|
||||
bool state;
|
||||
if (!propStates.TryGetValue(pathKey, out state)) state = targetObject.activeSelf;
|
||||
|
||||
foreach (var actionGroup in group.actionGroups)
|
||||
{
|
||||
bool evaluated = true;
|
||||
foreach (var condition in actionGroup.ControllingConditions)
|
||||
{
|
||||
if (!propStates.TryGetValue(condition.Parameter, out var propCondition))
|
||||
{
|
||||
propCondition = condition.InitiallyActive;
|
||||
}
|
||||
|
||||
if (!propCondition)
|
||||
{
|
||||
evaluated = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (actionGroup.Inverted) evaluated = !evaluated;
|
||||
|
||||
if (evaluated)
|
||||
{
|
||||
state = (float) actionGroup.Value > 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
nextPropStates[pathKey] = state;
|
||||
|
||||
if (!propStates.TryGetValue(pathKey, out var oldState) || oldState != state)
|
||||
{
|
||||
unsettled = true;
|
||||
}
|
||||
}
|
||||
|
||||
propStates = nextPropStates;
|
||||
nextPropStates = new();
|
||||
}
|
||||
|
||||
foreach (var group in groups.Values)
|
||||
{
|
||||
foreach (var action in group.actionGroups)
|
||||
{
|
||||
foreach (var condition in action.ControllingConditions)
|
||||
{
|
||||
if (propStates.TryGetValue(condition.Parameter, out var state))
|
||||
condition.InitialValue = state ? 1.0f : 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine initial state and deleted shapes for all properties
|
||||
/// </summary>
|
||||
/// <param name="shapes"></param>
|
||||
/// <param name="initialStates"></param>
|
||||
/// <param name="deletedShapes"></param>
|
||||
private void PreprocessShapes(Dictionary<TargetProp, AnimatedProperty> shapes, out Dictionary<TargetProp, object> 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, object>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0693ea763ed432d8f825999a037fae9
|
||||
timeCreated: 1723327408
|
562
Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs
Normal file
562
Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs
Normal file
@ -0,0 +1,562 @@
|
||||
#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()
|
||||
{
|
||||
Dictionary<TargetProp, AnimatedProperty> shapes =
|
||||
new ReactiveObjectAnalyzer(context).Analyze(
|
||||
context.AvatarRootObject,
|
||||
out var initialStates,
|
||||
out var deletedShapes
|
||||
);
|
||||
|
||||
GenerateActiveSelfProxies(shapes);
|
||||
|
||||
ProcessInitialStates(initialStates);
|
||||
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))
|
||||
initialValues[condition.Parameter] = condition.InitialValue;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessInitialStates(Dictionary<TargetProp, object> initialStates)
|
||||
{
|
||||
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 (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 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}";
|
||||
|
||||
if (value is UnityEngine.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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
using nadena.dev.ndmf;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Reserve an animator layer for reactive object 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 ReactiveObjectPrepass : Pass<ReactiveObjectPrepass>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b9815a0578e46d7985971eaf9a9bcb6
|
||||
timeCreated: 1723325828
|
35
Editor/ReactiveObjects/AnimationGeneration/TargetProp.cs
Normal file
35
Editor/ReactiveObjects/AnimationGeneration/TargetProp.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
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 override string ToString()
|
||||
{
|
||||
return $"{TargetObject}.{PropertyName}";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61908419c3e34e88838beb3f3d2fc706
|
||||
timeCreated: 1723325848
|
145
Editor/ReactiveObjects/MaterialSetterPreview.cs
Normal file
145
Editor/ReactiveObjects/MaterialSetterPreview.cs
Normal file
@ -0,0 +1,145 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using nadena.dev.ndmf.preview;
|
||||
using UnityEngine;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
public class MaterialSetterPreview : IRenderFilter
|
||||
{
|
||||
static TogglablePreviewNode EnableNode = TogglablePreviewNode.Create(
|
||||
() => "Material Setter",
|
||||
qualifiedName: "nadena.dev.modular-avatar/MaterialSetterPreview",
|
||||
true
|
||||
);
|
||||
|
||||
public IEnumerable<TogglablePreviewNode> GetPreviewControlNodes()
|
||||
{
|
||||
yield return EnableNode;
|
||||
}
|
||||
|
||||
public bool IsEnabled(ComputeContext context)
|
||||
{
|
||||
return context.Observe(EnableNode.IsEnabled);
|
||||
}
|
||||
|
||||
|
||||
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext context)
|
||||
{
|
||||
var menuItemPreview = new MenuItemPreviewCondition(context);
|
||||
var setters = context.GetComponentsByType<ModularAvatarMaterialSetter>();
|
||||
|
||||
var groups = new Dictionary<Renderer, ImmutableList<ModularAvatarMaterialSetter>>();
|
||||
|
||||
foreach (var setter in setters)
|
||||
{
|
||||
var mami = context.GetComponent<ModularAvatarMenuItem>(setter.gameObject);
|
||||
bool active = context.ActiveAndEnabled(setter) && (mami == null || menuItemPreview.IsEnabledForPreview(mami));
|
||||
if (active == context.Observe(setter, t => t.Inverted)) continue;
|
||||
|
||||
var objs = context.Observe(setter, s => s.Objects.Select(o => (o.Object.Get(s), o.Material, o.MaterialIndex)).ToList(), (x, y) => x.SequenceEqual(y));
|
||||
|
||||
if (setter.Objects == null) continue;
|
||||
|
||||
foreach (var (obj, mat, index) in objs)
|
||||
{
|
||||
if (obj == null) continue;
|
||||
var renderer = context.GetComponent<Renderer>(obj);
|
||||
if (renderer == null) continue;
|
||||
|
||||
var matCount = context.Observe(renderer, r => r.sharedMaterials.Length);
|
||||
|
||||
if (matCount <= index) continue;
|
||||
|
||||
if (!groups.TryGetValue(renderer, out var list))
|
||||
{
|
||||
list = ImmutableList.Create<ModularAvatarMaterialSetter>();
|
||||
groups.Add(renderer, list);
|
||||
}
|
||||
|
||||
groups[renderer] = list.Add(setter);
|
||||
}
|
||||
}
|
||||
|
||||
var finalGroups = groups.Select(g => RenderGroup.For(g.Key).WithData(g.Value)).ToImmutableList();
|
||||
return finalGroups;
|
||||
}
|
||||
|
||||
public Task<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context)
|
||||
{
|
||||
var setters = group.GetData<ImmutableList<ModularAvatarMaterialSetter>>();
|
||||
var node = new Node(setters);
|
||||
|
||||
return node.Refresh(proxyPairs, context, 0);
|
||||
}
|
||||
|
||||
private class Node : IRenderFilterNode
|
||||
{
|
||||
private readonly ImmutableList<ModularAvatarMaterialSetter> _setters;
|
||||
private ImmutableList<(int, Material)> _materials;
|
||||
|
||||
public RenderAspects WhatChanged {get; private set; }
|
||||
|
||||
public void OnFrame(Renderer original, Renderer proxy)
|
||||
{
|
||||
var mats = proxy.sharedMaterials;
|
||||
|
||||
foreach (var mat in _materials)
|
||||
{
|
||||
if (mat.Item1 <= mats.Length)
|
||||
{
|
||||
mats[mat.Item1] = mat.Item2;
|
||||
}
|
||||
}
|
||||
|
||||
proxy.sharedMaterials = mats;
|
||||
}
|
||||
|
||||
public Node(ImmutableList<ModularAvatarMaterialSetter> setters)
|
||||
{
|
||||
_setters = setters;
|
||||
_materials = ImmutableList<(int, Material)>.Empty;
|
||||
WhatChanged = RenderAspects.Material;
|
||||
}
|
||||
|
||||
public Task<IRenderFilterNode> Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context, RenderAspects updatedAspects)
|
||||
{
|
||||
var proxyPair = proxyPairs.First();
|
||||
var original = proxyPair.Item1;
|
||||
var proxy = proxyPair.Item2;
|
||||
|
||||
var mats = new Material[proxy.sharedMaterials.Length];
|
||||
|
||||
foreach (var setter in _setters)
|
||||
{
|
||||
var objects = context.Observe(setter, s => s.Objects
|
||||
.Where(obj => obj.Object.Get(s) == original.gameObject)
|
||||
.Select(obj => (obj.Material, obj.MaterialIndex)),
|
||||
(x, y) => x.SequenceEqual(y)
|
||||
);
|
||||
|
||||
foreach (var (mat, index) in objects)
|
||||
{
|
||||
if (index <= mats.Length)
|
||||
{
|
||||
mats[index] = mat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var materials = mats.Select((m, i) => (i, m)).Where(kvp => kvp.m != null).ToImmutableList();
|
||||
|
||||
if (materials.SequenceEqual(_materials))
|
||||
{
|
||||
return Task.FromResult<IRenderFilterNode>(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Task.FromResult<IRenderFilterNode>(new Node(_setters) { _materials = materials });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/ReactiveObjects/MaterialSetterPreview.cs.meta
Normal file
3
Editor/ReactiveObjects/MaterialSetterPreview.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b1909b15d714ed59e4bd64bc2f13b1a
|
||||
timeCreated: 1723255528
|
@ -36,12 +36,10 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
foreach (var toggle in allToggles)
|
||||
{
|
||||
if (!context.ActiveAndEnabled(toggle)) continue;
|
||||
|
||||
var mami = context.GetComponent<ModularAvatarMenuItem>(toggle.gameObject);
|
||||
if (mami != null)
|
||||
if (!menuItemPreview.IsEnabledForPreview(mami))
|
||||
continue;
|
||||
|
||||
bool active = context.ActiveAndEnabled(toggle) && (mami == null || menuItemPreview.IsEnabledForPreview(mami));
|
||||
if (active == context.Observe(toggle, t => t.Inverted)) continue;
|
||||
|
||||
context.Observe(toggle,
|
||||
t => t.Objects.Select(o => o.Object.referencePath).ToList(),
|
||||
@ -103,7 +101,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
if (group != null)
|
||||
{
|
||||
var (toggle, index) = group[^1];
|
||||
enableAtNode = context.Observe(toggle, t => t.Objects[index].Active);
|
||||
enableAtNode = context.Observe(toggle, t => t.Objects.Count > index && t.Objects[index].Active);
|
||||
}
|
||||
|
||||
if (!enableAtNode)
|
||||
|
@ -88,7 +88,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
|
||||
internal static ControlCondition AssignMenuItemParameter(ndmf.BuildContext context, ModularAvatarMenuItem mami)
|
||||
internal static ControlCondition AssignMenuItemParameter(ModularAvatarMenuItem mami)
|
||||
{
|
||||
if (mami?.Control?.parameter?.name == null) return null;
|
||||
|
||||
@ -97,7 +97,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
Parameter = mami.Control.parameter.name,
|
||||
DebugName = mami.gameObject.name,
|
||||
IsConstant = false,
|
||||
InitialValue = 0, // TODO
|
||||
InitialValue = mami.isDefault ? mami.Control.value : -999, // TODO
|
||||
ParameterValueLo = mami.Control.value - 0.5f,
|
||||
ParameterValueHi = mami.Control.value + 0.5f
|
||||
};
|
||||
|
@ -1,912 +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;
|
||||
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;
|
||||
|
||||
// Only look at the menu item we're directly attached to, to avoid submenus causing issues...
|
||||
var mami = cursor?.GetComponent<ModularAvatarMenuItem>();
|
||||
if (mami != null)
|
||||
{
|
||||
var mami_condition = ParameterAssignerPass.AssignMenuItemParameter(context, mami);
|
||||
if (mami_condition != null) conditions.Add(mami_condition);
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
ResolveToggleInitialStates(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)
|
||||
{
|
||||
var asc = context.Extension<AnimationServicesContext>();
|
||||
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 = asc.AnimationDatabase.ClipsForPath(asc.PathMappings.GetObjectIdentifier(condition.ReferenceObject)).IsEmpty;
|
||||
|
||||
var i = 0;
|
||||
// Remove redundant conditions
|
||||
actionGroup.ControllingConditions.RemoveAll(c => c.IsConstant && c.InitiallyActive && (i++ != 0));
|
||||
}
|
||||
|
||||
// Remove any action groups with always-off conditions
|
||||
group.actionGroups.RemoveAll(agk =>
|
||||
agk.ControllingConditions.Any(c => !c.InitiallyActive && c.IsConstant));
|
||||
|
||||
// Remove all action groups up until the last one where we're always on
|
||||
var lastAlwaysOnGroup = group.actionGroups.FindLastIndex(ag => ag.IsConstantOn);
|
||||
if (lastAlwaysOnGroup > 0)
|
||||
group.actionGroups.RemoveRange(0, lastAlwaysOnGroup - 1);
|
||||
}
|
||||
|
||||
// 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 ResolveToggleInitialStates(Dictionary<TargetProp, PropGroup> groups)
|
||||
{
|
||||
var asc = context.Extension<AnimationServicesContext>();
|
||||
|
||||
Dictionary<string, bool> propStates = new Dictionary<string, bool>();
|
||||
Dictionary<string, bool> nextPropStates = new Dictionary<string, bool>();
|
||||
int loopLimit = 5;
|
||||
|
||||
bool unsettled = true;
|
||||
while (unsettled && loopLimit-- > 0)
|
||||
{
|
||||
unsettled = false;
|
||||
|
||||
foreach (var group in groups.Values)
|
||||
{
|
||||
if (group.TargetProp.PropertyName != "m_IsActive") continue;
|
||||
if (!(group.TargetProp.TargetObject is GameObject targetObject)) continue;
|
||||
|
||||
var pathKey = asc.GetActiveSelfProxy(targetObject);
|
||||
|
||||
bool state;
|
||||
if (!propStates.TryGetValue(pathKey, out state)) state = targetObject.activeSelf;
|
||||
|
||||
foreach (var actionGroup in group.actionGroups)
|
||||
{
|
||||
bool evaluated = true;
|
||||
foreach (var condition in actionGroup.ControllingConditions)
|
||||
{
|
||||
if (!propStates.TryGetValue(condition.Parameter, out var propCondition))
|
||||
{
|
||||
propCondition = condition.InitiallyActive;
|
||||
}
|
||||
|
||||
if (!propCondition)
|
||||
{
|
||||
evaluated = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (evaluated)
|
||||
{
|
||||
state = actionGroup.Value > 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
nextPropStates[pathKey] = state;
|
||||
|
||||
if (!propStates.TryGetValue(pathKey, out var oldState) || oldState != state)
|
||||
{
|
||||
unsettled = true;
|
||||
}
|
||||
}
|
||||
|
||||
propStates = nextPropStates;
|
||||
nextPropStates = new();
|
||||
}
|
||||
|
||||
foreach (var group in groups.Values)
|
||||
{
|
||||
foreach (var action in group.actionGroups)
|
||||
{
|
||||
foreach (var condition in action.ControllingConditions)
|
||||
{
|
||||
if (propStates.TryGetValue(condition.Parameter, out var state))
|
||||
condition.InitialValue = state ? 1.0f : 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessInitialStates(Dictionary<TargetProp, float> initialStates)
|
||||
{
|
||||
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(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;
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (componentType == typeof(GameObject) && key.PropertyName == "m_IsActive")
|
||||
{
|
||||
binding = EditorCurveBinding.FloatCurve(
|
||||
"",
|
||||
typeof(Animator),
|
||||
asc.GetActiveSelfProxy((GameObject)key.TargetObject)
|
||||
);
|
||||
|
||||
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
|
||||
});
|
||||
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)
|
||||
{
|
||||
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
|
||||
});
|
||||
asc.AnimationDatabase.RegisterState(states[^1].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;
|
||||
}
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext ctx)
|
||||
{
|
||||
var menuItemPreviewCondition = new MenuItemPreviewCondition(ctx);
|
||||
var menuItemPreview = new MenuItemPreviewCondition(ctx);
|
||||
|
||||
var allChangers = ctx.GetComponentsByType<ModularAvatarShapeChanger>();
|
||||
|
||||
@ -45,11 +45,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
if (changer == null) continue;
|
||||
|
||||
// TODO: observe avatar root
|
||||
if (!ctx.ActiveAndEnabled(changer)) continue;
|
||||
|
||||
var mami = ctx.GetComponent<ModularAvatarMenuItem>(changer.gameObject);
|
||||
if (mami != null && !menuItemPreviewCondition.IsEnabledForPreview(mami)) continue;
|
||||
bool active = ctx.ActiveAndEnabled(changer) && (mami == null || menuItemPreview.IsEnabledForPreview(mami));
|
||||
if (active == ctx.Observe(changer, t => t.Inverted)) continue;
|
||||
|
||||
var target = ctx.Observe(changer, _ => changer.targetRenderer.Get(changer));
|
||||
var renderer = ctx.GetComponent<SkinnedMeshRenderer>(target);
|
||||
|
@ -1,4 +1,7 @@
|
||||
using System;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
using UnityEngine;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
@ -17,6 +20,28 @@ namespace nadena.dev.modular_avatar.core
|
||||
private string _cachedPath;
|
||||
private GameObject _cachedReference;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static GameObject Get(SerializedProperty prop)
|
||||
{
|
||||
var rootObject = prop.serializedObject.targetObject;
|
||||
if (rootObject == null) return null;
|
||||
|
||||
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents((rootObject as Component)?.transform ?? (rootObject as GameObject)?.transform);
|
||||
if (avatarRoot == null) return null;
|
||||
|
||||
var referencePath = prop.FindPropertyRelative("referencePath").stringValue;
|
||||
var targetObject = prop.FindPropertyRelative("targetObject").objectReferenceValue as GameObject;
|
||||
|
||||
if (targetObject != null && targetObject.transform.IsChildOf(avatarRoot))
|
||||
return targetObject;
|
||||
|
||||
if (referencePath == AVATAR_ROOT)
|
||||
return avatarRoot.gameObject;
|
||||
|
||||
return avatarRoot.Find(referencePath)?.gameObject;
|
||||
}
|
||||
#endif
|
||||
|
||||
public GameObject Get(Component container)
|
||||
{
|
||||
bool cacheValid = _cacheValid || ReferencesLockedAtFrame == Time.frameCount;
|
||||
|
@ -1,10 +1,21 @@
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
/// <summary>
|
||||
/// Tag class used internally to mark reactive components. Not publicly extensible.
|
||||
/// </summary>
|
||||
public abstract class ReactiveComponent : AvatarTagComponent
|
||||
{
|
||||
[SerializeField] private bool m_inverted = false;
|
||||
|
||||
public bool Inverted
|
||||
{
|
||||
get => m_inverted;
|
||||
set => m_inverted = value;
|
||||
}
|
||||
|
||||
internal ReactiveComponent()
|
||||
{
|
||||
}
|
||||
|
3
Runtime/ReactiveObjects.meta
Normal file
3
Runtime/ReactiveObjects.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd73c1b15bec4119b573ad3b12a3371c
|
||||
timeCreated: 1723330403
|
43
Runtime/ReactiveObjects/ModularAvatarMaterialSetter.cs
Normal file
43
Runtime/ReactiveObjects/ModularAvatarMaterialSetter.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
[Serializable]
|
||||
public struct MaterialSwitchObject
|
||||
{
|
||||
public AvatarObjectReference Object;
|
||||
public Material Material;
|
||||
public int MaterialIndex;
|
||||
|
||||
public bool Equals(MaterialSwitchObject other)
|
||||
{
|
||||
return Equals(Object, other.Object) && Equals(Material, other.Material) && MaterialIndex == other.MaterialIndex;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is MaterialSwitchObject other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Object, Material, MaterialIndex);
|
||||
}
|
||||
}
|
||||
|
||||
[AddComponentMenu("Modular Avatar/MA Material Setter")]
|
||||
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/material-setter?lang=auto")]
|
||||
|
||||
public class ModularAvatarMaterialSetter : ReactiveComponent
|
||||
{
|
||||
[SerializeField] private List<MaterialSwitchObject> m_objects = new();
|
||||
|
||||
public List<MaterialSwitchObject> Objects
|
||||
{
|
||||
get => m_objects;
|
||||
set => m_objects = value;
|
||||
}
|
||||
}
|
||||
}
|
11
Runtime/ReactiveObjects/ModularAvatarMaterialSetter.cs.meta
Normal file
11
Runtime/ReactiveObjects/ModularAvatarMaterialSetter.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0adf335711644e34b6c635e94ae61fa7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: a8edd5bd1a0a64a40aa99cc09fb5f198, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Loading…
Reference in New Issue
Block a user