mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-02-07 06:12:47 +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">
|
xmlns:ma="nadena.dev.modular_avatar.core.editor">
|
||||||
<ui:VisualElement name="root-box">
|
<ui:VisualElement name="root-box">
|
||||||
<ui:VisualElement name="group-box">
|
<ui:VisualElement name="group-box">
|
||||||
@ -17,6 +17,8 @@
|
|||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
|
<ed:PropertyField binding-path="m_inverted"/>
|
||||||
|
|
||||||
<ma:LanguageSwitcherElement/>
|
<ma:LanguageSwitcherElement/>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
</UXML>
|
</UXML>
|
@ -3,6 +3,7 @@
|
|||||||
<ui:VisualElement name="root-box">
|
<ui:VisualElement name="root-box">
|
||||||
<ui:VisualElement name="group-box">
|
<ui:VisualElement name="group-box">
|
||||||
<ed:PropertyField binding-path="m_targetRenderer"/>
|
<ed:PropertyField binding-path="m_targetRenderer"/>
|
||||||
|
<ed:PropertyField binding-path="m_inverted"/>
|
||||||
|
|
||||||
<ui:VisualElement name="ListViewContainer">
|
<ui:VisualElement name="ListViewContainer">
|
||||||
<ui:ListView virtualization-method="DynamicHeight"
|
<ui:ListView virtualization-method="DynamicHeight"
|
||||||
@ -19,6 +20,8 @@
|
|||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
|
<ed:PropertyField binding-path="m_inverted"/>
|
||||||
|
|
||||||
<ma:LanguageSwitcherElement/>
|
<ma:LanguageSwitcherElement/>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
</UXML>
|
</UXML>
|
@ -48,7 +48,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
|
|||||||
seq.Run(MeshSettingsPluginPass.Instance);
|
seq.Run(MeshSettingsPluginPass.Instance);
|
||||||
seq.Run(ScaleAdjusterPass.Instance).PreviewingWith(new ScaleAdjusterPreview());
|
seq.Run(ScaleAdjusterPass.Instance).PreviewingWith(new ScaleAdjusterPreview());
|
||||||
#if MA_VRCSDK3_AVATARS
|
#if MA_VRCSDK3_AVATARS
|
||||||
seq.Run(PropertyOverlayPrePass.Instance);
|
seq.Run(ReactiveObjectPrepass.Instance);
|
||||||
seq.Run(RenameParametersPluginPass.Instance);
|
seq.Run(RenameParametersPluginPass.Instance);
|
||||||
seq.Run(ParameterAssignerPass.Instance);
|
seq.Run(ParameterAssignerPass.Instance);
|
||||||
seq.Run(MergeBlendTreePass.Instance);
|
seq.Run(MergeBlendTreePass.Instance);
|
||||||
@ -57,8 +57,8 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
|
|||||||
#endif
|
#endif
|
||||||
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
|
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
|
||||||
{
|
{
|
||||||
seq.Run("Shape Changer", ctx => new PropertyOverlayPass(ctx).Execute())
|
seq.Run("Shape Changer", ctx => new ReactiveObjectPass(ctx).Execute())
|
||||||
.PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview());
|
.PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview(), new MaterialSetterPreview());
|
||||||
seq.Run(MergeArmaturePluginPass.Instance);
|
seq.Run(MergeArmaturePluginPass.Instance);
|
||||||
seq.Run(BoneProxyPluginPass.Instance);
|
seq.Run(BoneProxyPluginPass.Instance);
|
||||||
seq.Run(VisibleHeadAccessoryPluginPass.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
|
internal class ControlCondition
|
||||||
{
|
{
|
||||||
public string Parameter, DebugName;
|
public string Parameter;
|
||||||
|
public UnityEngine.Object ControllingObject;
|
||||||
|
|
||||||
|
public string DebugName;
|
||||||
public bool IsConstant;
|
public bool IsConstant;
|
||||||
public float ParameterValueLo, ParameterValueHi, InitialValue;
|
public float ParameterValueLo, ParameterValueHi, InitialValue;
|
||||||
public bool InitiallyActive => InitialValue > ParameterValueLo && InitialValue < ParameterValueHi;
|
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)
|
foreach (var toggle in allToggles)
|
||||||
{
|
{
|
||||||
if (!context.ActiveAndEnabled(toggle)) continue;
|
|
||||||
|
|
||||||
var mami = context.GetComponent<ModularAvatarMenuItem>(toggle.gameObject);
|
var mami = context.GetComponent<ModularAvatarMenuItem>(toggle.gameObject);
|
||||||
if (mami != null)
|
|
||||||
if (!menuItemPreview.IsEnabledForPreview(mami))
|
bool active = context.ActiveAndEnabled(toggle) && (mami == null || menuItemPreview.IsEnabledForPreview(mami));
|
||||||
continue;
|
if (active == context.Observe(toggle, t => t.Inverted)) continue;
|
||||||
|
|
||||||
context.Observe(toggle,
|
context.Observe(toggle,
|
||||||
t => t.Objects.Select(o => o.Object.referencePath).ToList(),
|
t => t.Objects.Select(o => o.Object.referencePath).ToList(),
|
||||||
@ -103,7 +101,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
if (group != null)
|
if (group != null)
|
||||||
{
|
{
|
||||||
var (toggle, index) = group[^1];
|
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)
|
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;
|
if (mami?.Control?.parameter?.name == null) return null;
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
Parameter = mami.Control.parameter.name,
|
Parameter = mami.Control.parameter.name,
|
||||||
DebugName = mami.gameObject.name,
|
DebugName = mami.gameObject.name,
|
||||||
IsConstant = false,
|
IsConstant = false,
|
||||||
InitialValue = 0, // TODO
|
InitialValue = mami.isDefault ? mami.Control.value : -999, // TODO
|
||||||
ParameterValueLo = mami.Control.value - 0.5f,
|
ParameterValueLo = mami.Control.value - 0.5f,
|
||||||
ParameterValueHi = 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)
|
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext ctx)
|
||||||
{
|
{
|
||||||
var menuItemPreviewCondition = new MenuItemPreviewCondition(ctx);
|
var menuItemPreview = new MenuItemPreviewCondition(ctx);
|
||||||
|
|
||||||
var allChangers = ctx.GetComponentsByType<ModularAvatarShapeChanger>();
|
var allChangers = ctx.GetComponentsByType<ModularAvatarShapeChanger>();
|
||||||
|
|
||||||
@ -45,11 +45,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
if (changer == null) continue;
|
if (changer == null) continue;
|
||||||
|
|
||||||
// TODO: observe avatar root
|
|
||||||
if (!ctx.ActiveAndEnabled(changer)) continue;
|
|
||||||
|
|
||||||
var mami = ctx.GetComponent<ModularAvatarMenuItem>(changer.gameObject);
|
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 target = ctx.Observe(changer, _ => changer.targetRenderer.Get(changer));
|
||||||
var renderer = ctx.GetComponent<SkinnedMeshRenderer>(target);
|
var renderer = ctx.GetComponent<SkinnedMeshRenderer>(target);
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
using UnityEditor;
|
||||||
|
#endif
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core
|
namespace nadena.dev.modular_avatar.core
|
||||||
@ -17,6 +20,28 @@ namespace nadena.dev.modular_avatar.core
|
|||||||
private string _cachedPath;
|
private string _cachedPath;
|
||||||
private GameObject _cachedReference;
|
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)
|
public GameObject Get(Component container)
|
||||||
{
|
{
|
||||||
bool cacheValid = _cacheValid || ReferencesLockedAtFrame == Time.frameCount;
|
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>
|
/// <summary>
|
||||||
/// Tag class used internally to mark reactive components. Not publicly extensible.
|
/// Tag class used internally to mark reactive components. Not publicly extensible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class ReactiveComponent : AvatarTagComponent
|
public abstract class ReactiveComponent : AvatarTagComponent
|
||||||
{
|
{
|
||||||
|
[SerializeField] private bool m_inverted = false;
|
||||||
|
|
||||||
|
public bool Inverted
|
||||||
|
{
|
||||||
|
get => m_inverted;
|
||||||
|
set => m_inverted = value;
|
||||||
|
}
|
||||||
|
|
||||||
internal ReactiveComponent()
|
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