Connect reactive components to MenuItems (#944)

* refactor: generalize support for arbitrary parameters as conditions

* feat: automatically assign Menu Item parameters

* feat: ReactiveComponents respond to MenuItems

* feat: AvatarObjectReference tracks both paths and direct object references

* feat: set isSaved/isSynced/default values from MenuItem

* feat: Object Toggle preview supports menu items and manipulating parent objects

* feat: reactive previews respond to menu item default value states

* chore: update NDMF dependency
This commit is contained in:
bd_ 2024-08-04 19:31:43 -07:00 committed by GitHub
parent bf9266f054
commit 8e7526e711
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 481 additions and 129 deletions

View File

@ -19,7 +19,7 @@
"dependencies": {}
},
"nadena.dev.ndmf": {
"version": "1.5.0-beta.2"
"version": "1.5.0-beta.3"
}
}
}

View File

@ -29,6 +29,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
var color = GUI.contentColor;
var targetObjectProp = property.FindPropertyRelative(nameof(AvatarObjectReference.targetObject));
property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath));
try
@ -43,6 +44,14 @@ namespace nadena.dev.modular_avatar.core.editor
else if (isRoot) target = avatarTransform;
else target = avatarTransform.Find(property.stringValue);
if (targetObjectProp.objectReferenceValue is GameObject go &&
(go.transform == avatarTransform || go.transform.IsChildOf(avatarTransform)))
{
target = go.transform;
isNull = false;
isRoot = target == avatarTransform;
}
var labelRect = position;
position = EditorGUI.PrefixLabel(position, label);
labelRect.width = position.x - labelRect.x;
@ -73,6 +82,8 @@ namespace nadena.dev.modular_avatar.core.editor
property.stringValue = relPath;
}
targetObjectProp.objectReferenceValue = ((Transform)newTarget)?.gameObject;
}
}
else
@ -104,6 +115,8 @@ namespace nadena.dev.modular_avatar.core.editor
property.stringValue = relPath;
}
targetObjectProp.objectReferenceValue = ((Transform)newTarget)?.gameObject;
}
else
{

View File

@ -1,13 +1,16 @@
#if MA_VRCSDK3_AVATARS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using nadena.dev.modular_avatar.core.menu;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
using static nadena.dev.modular_avatar.core.editor.Localization;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
@ -32,6 +35,7 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly SerializedProperty _submenu;
private readonly ParameterGUI _parameterGUI;
private readonly SerializedProperty _parameterName;
private readonly SerializedProperty _subParamsRoot;
private readonly SerializedProperty _labelsRoot;
@ -46,9 +50,15 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly SerializedProperty _prop_submenuSource;
private readonly SerializedProperty _prop_otherObjSource;
private readonly SerializedProperty _prop_isSynced;
private readonly SerializedProperty _prop_isSaved;
private readonly SerializedProperty _prop_isDefault;
public bool AlwaysExpandContents = false;
public bool ExpandContents = false;
private readonly HashSet<string> _knownParameters = new();
public MenuItemCoreGUI(SerializedObject obj, Action redraw)
{
_obj = obj;
@ -62,9 +72,11 @@ namespace nadena.dev.modular_avatar.core.editor
_parameterReference = parameterReference;
_redraw = redraw;
InitKnownParameters();
var gameObjects = new SerializedObject(
obj.targetObjects.Select(o =>
(UnityEngine.Object) ((ModularAvatarMenuItem) o).gameObject
(Object) ((ModularAvatarMenuItem) o).gameObject
).ToArray()
);
@ -74,21 +86,47 @@ namespace nadena.dev.modular_avatar.core.editor
_texture = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
_type = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.type));
var parameter = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
_parameterName = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter.name));
_value = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
_submenu = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subMenu));
_parameterGUI = new ParameterGUI(parameterReference, parameter, redraw);
_parameterGUI = new ParameterGUI(parameterReference, _parameterName, redraw);
_subParamsRoot = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters));
_labelsRoot = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.labels));
_prop_submenuSource = obj.FindProperty(nameof(ModularAvatarMenuItem.MenuSource));
_prop_otherObjSource = obj.FindProperty(nameof(ModularAvatarMenuItem.menuSource_otherObjectChildren));
_prop_isSynced = obj.FindProperty(nameof(ModularAvatarMenuItem.isSynced));
_prop_isSaved = obj.FindProperty(nameof(ModularAvatarMenuItem.isSaved));
_prop_isDefault = obj.FindProperty(nameof(ModularAvatarMenuItem.isDefault));
_previewGUI = new MenuPreviewGUI(redraw);
}
private void InitKnownParameters()
{
if (_parameterReference == null) return;
var rootParameters = ParameterInfo.ForUI.GetParametersForObject(
RuntimeUtil.FindAvatarInParents(_parameterReference.transform).gameObject
).Select(p => p.EffectiveName).ToHashSet();
var remaps = ParameterInfo.ForUI.GetParameterRemappingsAt(_parameterReference);
foreach (var remap in remaps)
{
if (remap.Key.Item1 != ParameterNamespace.Animator) continue;
if (rootParameters.Contains(remap.Value.ParameterName)) _knownParameters.Add(remap.Key.Item2);
}
foreach (var rootParam in rootParameters)
if (!remaps.ContainsKey((ParameterNamespace.Animator, rootParam)))
_knownParameters.Add(rootParam);
}
/// <summary>
/// Builds a menu item GUI for a raw VRCExpressionsMenu.Control reference.
/// </summary>
@ -99,25 +137,48 @@ namespace nadena.dev.modular_avatar.core.editor
{
_obj = _control.serializedObject;
_parameterReference = parameterReference;
InitKnownParameters();
_redraw = redraw;
_name = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.name));
_texture = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
_type = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.type));
var parameter = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
_parameterName = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter.name));
_value = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
_submenu = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subMenu));
_parameterGUI = new ParameterGUI(parameterReference, parameter, redraw);
_parameterGUI = new ParameterGUI(parameterReference, _parameterName, redraw);
_subParamsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters));
_labelsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.labels));
_prop_isSynced = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isSynced));
_prop_isSaved = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isSaved));
_prop_isDefault = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isDefault));
_prop_submenuSource = null;
_prop_otherObjSource = null;
_previewGUI = new MenuPreviewGUI(redraw);
}
private void DrawHorizontalToggleProp(SerializedProperty prop, GUIContent label)
{
var toggleSize = EditorStyles.toggle.CalcSize(new GUIContent());
var labelSize = EditorStyles.label.CalcSize(label);
var width = toggleSize.x + labelSize.x + 4;
var rect = EditorGUILayout.GetControlRect(GUILayout.Width(width));
EditorGUI.BeginProperty(rect, label, prop);
prop.boolValue = EditorGUI.ToggleLeft(rect, label, prop.boolValue);
EditorGUI.EndProperty();
}
private float lastWidth;
public void DoGUI()
{
EditorGUILayout.BeginHorizontal();
@ -136,6 +197,18 @@ namespace nadena.dev.modular_avatar.core.editor
_parameterGUI.DoGUI(true);
var paramName = _parameterName.stringValue;
if (!_parameterName.hasMultipleDifferentValues && !_knownParameters.Contains(paramName))
{
EditorGUILayout.BeginHorizontal();
DrawHorizontalToggleProp(_prop_isDefault, new GUIContent("Default"));
GUILayout.FlexibleSpace();
DrawHorizontalToggleProp(_prop_isSaved, new GUIContent("Saved"));
GUILayout.FlexibleSpace();
DrawHorizontalToggleProp(_prop_isSynced, new GUIContent("Synced"));
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
if (_texture != null)

View File

@ -11,7 +11,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
[CustomPropertyDrawer(typeof(ToggledObject))]
public class ToggledObjectEditor : PropertyDrawer
{
private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/ObjectSwitcher/";
private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/ObjectToggle/";
private const string UxmlPath = Root + "ToggledObjectEditor.uxml";
private const string UssPath = Root + "ObjectSwitcherStyles.uss";

View File

@ -5,7 +5,6 @@ using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.core.editor.plugin;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEngine;
#endregion
@ -54,6 +53,7 @@ namespace nadena.dev.modular_avatar.core.editor
IsAnimatorOnly = animatorOnly,
WantSynced = !p.localOnly,
IsHidden = p.internalParameter,
DefaultValue = p.defaultValue
};
});
}
@ -76,7 +76,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
else
{
remapTo = p.nameOrPrefix + "$" + GUID.Generate();
remapTo = p.nameOrPrefix + "$" + _component.GetInstanceID();
}
}
else if (string.IsNullOrEmpty(p.remapTo))

View File

@ -1,14 +1,12 @@
#region
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.core.ArmatureAwase;
using nadena.dev.modular_avatar.core.editor.plugin;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using nadena.dev.ndmf.fluent;
using nadena.dev.ndmf.preview;
using UnityEngine;
using Object = UnityEngine.Object;
@ -52,10 +50,10 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
#if MA_VRCSDK3_AVATARS
seq.Run(PropertyOverlayPrePass.Instance);
seq.Run(RenameParametersPluginPass.Instance);
seq.Run(ParameterAssignerPass.Instance);
seq.Run(MergeBlendTreePass.Instance);
seq.Run(MergeAnimatorPluginPass.Instance);
seq.Run(ApplyAnimatorDefaultValuesPass.Instance);
seq.Run(MenuInstallPluginPass.Instance);
#endif
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
{
@ -74,6 +72,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
seq.Run(GameObjectDelayDisablePass.Instance);
});
#if MA_VRCSDK3_AVATARS
seq.Run(MenuInstallPluginPass.Instance);
seq.Run(PhysbonesBlockerPluginPass.Instance);
seq.Run("Fixup Expressions Menu", ctx =>
{

View File

@ -0,0 +1,10 @@
namespace nadena.dev.modular_avatar.core.editor
{
internal struct ControlCondition
{
public string Parameter, DebugName;
public bool IsConstant;
public float ParameterValueLo, ParameterValueHi, InitialValue;
public bool InitiallyActive => InitialValue > ParameterValueLo && InitialValue < ParameterValueHi;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fbd0a833d92c4e67a94d10bab41939b4
timeCreated: 1722812671

View File

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using nadena.dev.ndmf;
using nadena.dev.ndmf.preview;
using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
{
internal class MenuItemPreviewCondition
{
private readonly ComputeContext _context;
private readonly ParameterInfo _info;
// avatar root => params
private readonly Dictionary<GameObject, Dictionary<string, ProvidedParameter>> _registeredParameters = new();
public MenuItemPreviewCondition(ComputeContext computeContext)
{
if (computeContext == null) throw new ArgumentNullException(nameof(computeContext));
_info = ParameterInfo.ForPreview(computeContext);
_context = computeContext;
}
private Dictionary<string, ProvidedParameter> RegisteredParameters(GameObject obj)
{
_context.ObservePath(obj.transform);
var root = RuntimeUtil.FindAvatarInParents(obj.transform)?.gameObject;
if (root == null) return new Dictionary<string, ProvidedParameter>();
if (_registeredParameters.TryGetValue(root, out var parameters))
return parameters;
parameters = new Dictionary<string, ProvidedParameter>();
foreach (var param in _info.GetParametersForObject(root)) parameters[param.EffectiveName] = param;
_registeredParameters[root] = parameters;
return parameters;
}
private bool TryGetRegisteredParam(ModularAvatarMenuItem mami, string paramName,
out ProvidedParameter providedParameter)
{
providedParameter = default;
if (string.IsNullOrWhiteSpace(mami.Control?.parameter?.name)) return false;
var remaps = _info.GetParameterRemappingsAt(mami.gameObject);
if (remaps.TryGetValue((ParameterNamespace.Animator, paramName), out var remap))
paramName = remap.ParameterName;
return RegisteredParameters(mami.gameObject).TryGetValue(paramName, out providedParameter);
}
public bool IsEnabledForPreview(ModularAvatarMenuItem mami)
{
_context.ObservePath(mami.transform);
if (_context.Observe(mami, _ => mami.Control == null)) return false;
var (paramName, value) = _context.Observe(mami, m => (m.Control.parameter.name, m.Control.value));
if (TryGetRegisteredParam(mami, paramName, out var providedParameter))
{
var defaultValue = providedParameter.DefaultValue ?? 0;
return Mathf.Abs(defaultValue - value) < 0.01f;
}
return _context.Observe(mami, _ => mami.isDefault);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c16bb1ac308244a7b118931dab9d23ff
timeCreated: 1722821807

View File

@ -27,6 +27,7 @@ namespace nadena.dev.modular_avatar.core.editor
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext context)
{
var menuItemPreview = new MenuItemPreviewCondition(context);
var allToggles = context.GetComponentsByType<ModularAvatarObjectToggle>();
var objectGroups =
@ -35,6 +36,13 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var toggle in allToggles)
{
if (!context.ActiveAndEnabled(toggle)) continue;
var mami = context.GetComponent<ModularAvatarMenuItem>(toggle.gameObject);
if (mami != null)
if (!menuItemPreview.IsEnabledForPreview(mami))
continue;
context.Observe(toggle,
t => t.Objects.Select(o => o.Object.referencePath).ToList(),
(x, y) => x.SequenceEqual(y)
@ -69,24 +77,45 @@ namespace nadena.dev.modular_avatar.core.editor
// the child. We do this by simply looking at how many times we observe each renderer.
.GroupBy(r => r)
.Select(g => g.Key)
.ToList();
.ToHashSet();
var renderGroups = new List<RenderGroup>();
foreach (var r in affectedRenderers)
{
var switchers = new List<(ModularAvatarObjectToggle, int)>();
var shouldEnable = true;
var obj = r.gameObject;
context.ActiveInHierarchy(obj); // observe path changes & object state changes
while (obj != null)
{
var enableAtNode = obj.activeSelf;
var group = objectGroups.GetValueOrDefault(obj);
if (group != null) switchers.AddRange(group);
if (group == null && !obj.activeSelf)
{
// always inactive
shouldEnable = false;
break;
}
if (group != null)
{
var (toggle, index) = group[^1];
enableAtNode = context.Observe(toggle, t => t.Objects[index].Active);
}
if (!enableAtNode)
{
shouldEnable = false;
break;
}
obj = obj.transform.parent?.gameObject;
}
renderGroups.Add(RenderGroup.For(r).WithData(switchers.ToImmutableList()));
if (shouldEnable) renderGroups.Add(RenderGroup.For(r));
}
return renderGroups.ToImmutableList();
@ -95,48 +124,17 @@ namespace nadena.dev.modular_avatar.core.editor
public Task<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs,
ComputeContext context)
{
var data = group.GetData<ImmutableList<(ModularAvatarObjectToggle, int)>>();
return new Node(data).Refresh(proxyPairs, context, 0);
return Task.FromResult<IRenderFilterNode>(new Node());
}
private class Node : IRenderFilterNode
{
public RenderAspects WhatChanged => 0;
private readonly ImmutableList<(ModularAvatarObjectToggle, int)> _controllers;
public Node(ImmutableList<(ModularAvatarObjectToggle, int)> controllers)
{
_controllers = controllers;
}
public Task<IRenderFilterNode> Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context,
RenderAspects updatedAspects)
{
foreach (var controller in _controllers)
{
// Ensure we get awoken whenever there's a change in a controlling component, or its enabled state.
context.Observe(controller.Item1);
context.ActiveAndEnabled(controller.Item1);
}
return Task.FromResult<IRenderFilterNode>(this);
}
public void OnFrame(Renderer original, Renderer proxy)
{
var shouldEnable = true;
foreach (var (controller, index) in _controllers)
{
if (controller == null) continue;
if (!controller.gameObject.activeInHierarchy) continue;
if (controller.Objects == null || index >= controller.Objects.Count) continue;
var obj = controller.Objects[index];
shouldEnable = obj.Active;
}
proxy.gameObject.SetActive(shouldEnable);
proxy.gameObject.SetActive(true);
}
}
}

View File

@ -0,0 +1,102 @@
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
namespace nadena.dev.modular_avatar.core.editor
{
/// <summary>
/// Creates/allocates parameters to any Menu Items that need them.
/// </summary>
internal class ParameterAssignerPass : Pass<ParameterAssignerPass>
{
protected override void Execute(ndmf.BuildContext context)
{
var paramIndex = 0;
var declaredParams = context.AvatarDescriptor.expressionParameters.parameters.Select(p => p.name)
.ToHashSet();
Dictionary<string, VRCExpressionParameters.Parameter> newParameters = new();
foreach (var mami in context.AvatarRootTransform.GetComponentsInChildren<ModularAvatarMenuItem>(true))
{
if (string.IsNullOrWhiteSpace(mami.Control?.parameter?.name))
{
if (mami.Control == null) mami.Control = new VRCExpressionsMenu.Control();
mami.Control.parameter = new VRCExpressionsMenu.Control.Parameter
{
name = $"__MA/AutoParam/{mami.gameObject.name}${paramIndex++}"
};
}
var paramName = mami.Control.parameter.name;
if (!declaredParams.Contains(paramName))
{
newParameters.TryGetValue(paramName, out var existingNewParam);
var wantedType = existingNewParam?.valueType ?? VRCExpressionParameters.ValueType.Bool;
if (wantedType != VRCExpressionParameters.ValueType.Float &&
(mami.Control.value > 1.01 || mami.Control.value < -0.01))
wantedType = VRCExpressionParameters.ValueType.Int;
if (Mathf.Abs(Mathf.Round(mami.Control.value) - mami.Control.value) > 0.01f)
wantedType = VRCExpressionParameters.ValueType.Float;
if (existingNewParam == null)
{
existingNewParam = new VRCExpressionParameters.Parameter
{
name = paramName,
valueType = wantedType,
saved = mami.isSaved,
defaultValue = -1,
networkSynced = mami.isSynced
};
newParameters[paramName] = existingNewParam;
}
else
{
existingNewParam.valueType = wantedType;
}
// TODO: warn on inconsistent configuration
existingNewParam.saved = existingNewParam.saved || mami.isSaved;
existingNewParam.networkSynced = existingNewParam.networkSynced || mami.isSynced;
existingNewParam.defaultValue = mami.isDefault ? mami.Control.value : existingNewParam.defaultValue;
}
}
if (newParameters.Count > 0)
{
foreach (var p in newParameters)
if (p.Value.defaultValue < 0)
p.Value.defaultValue = 0;
var expParams = context.AvatarDescriptor.expressionParameters;
if (!context.IsTemporaryAsset(expParams))
{
expParams = Object.Instantiate(expParams);
context.AvatarDescriptor.expressionParameters = expParams;
}
expParams.parameters = expParams.parameters.Concat(newParameters.Values).ToArray();
}
}
internal static ControlCondition AssignMenuItemParameter(ndmf.BuildContext context, ModularAvatarMenuItem mami)
{
return new ControlCondition
{
Parameter = mami.Control.parameter.name,
DebugName = mami.gameObject.name,
IsConstant = false,
InitialValue = 0, // TODO
ParameterValueLo = mami.Control.value - 0.5f,
ParameterValueHi = mami.Control.value + 0.5f
};
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c93adffdb4384590830c5bd200fb08b5
timeCreated: 1722812355

View File

@ -121,69 +121,74 @@ namespace nadena.dev.modular_avatar.core.editor
class ActionGroupKey
{
public ActionGroupKey(AnimationServicesContext asc, TargetProp key, GameObject controllingObject, float value)
public ActionGroupKey(ndmf.BuildContext context, TargetProp key, GameObject controllingObject, float value)
{
TargetProp = key;
InitiallyActive = controllingObject?.activeInHierarchy == true;
var asc = context.Extension<AnimationServicesContext>();
var origControlling = controllingObject?.name ?? "<null>";
while (controllingObject != null && !asc.TryGetActiveSelfProxy(controllingObject, out _))
TargetProp = key;
var conditions = new List<ControlCondition>();
var cursor = controllingObject?.transform;
while (cursor != null && !RuntimeUtil.IsAvatarRoot(cursor))
{
controllingObject = controllingObject.transform.parent?.gameObject;
if (controllingObject != null && RuntimeUtil.IsAvatarRoot(controllingObject.transform))
{
controllingObject = null;
}
if (asc.TryGetActiveSelfProxy(cursor.gameObject, out var paramName))
conditions.Add(new ControlCondition
{
Parameter = paramName,
DebugName = cursor.gameObject.name,
IsConstant = false,
InitialValue = cursor.gameObject.activeSelf ? 1.0f : 0.0f,
ParameterValueLo = 0.5f,
ParameterValueHi = 1.5f
});
else if (!cursor.gameObject.activeSelf)
conditions = new List<ControlCondition>
{
new ControlCondition
{
Parameter = "",
DebugName = cursor.gameObject.name,
IsConstant = true,
InitialValue = 0,
ParameterValueLo = 0.5f,
ParameterValueHi = 1.5f
}
};
foreach (var mami in cursor.GetComponents<ModularAvatarMenuItem>())
conditions.Add(ParameterAssignerPass.AssignMenuItemParameter(context, mami));
cursor = cursor.parent;
}
var newControlling = controllingObject?.name ?? "<null>";
Debug.Log("AGK: Controlling object " + origControlling + " => " + newControlling);
ControllingConditions = conditions;
ControllingObject = controllingObject;
Value = value;
}
public TargetProp TargetProp;
public float Value;
public float ConditionKey;
// When constructing the 1D blend tree to interpret the sum-of-condition-keys value, we need to ensure that
// all valid values are solidly between two control points with the same animation clip, to avoid undesired
// interpolation. This is done by constructing a "guard band":
// [ valid range ] [ guard band ] [ valid range ]
//
// The valid range must contain all values that could be created by valid summations. We therefore reserve
// a "guard band" in between; by reserving the exponent below each valid stop, we can put our guard bands
// in there.
// [ valid ] [ guard ] [ valid ]
// ^-r0 ^-g0 ^-g1
// ^- r1
// g0 = r1 / 2 = r0 * 2
// g1 = BitDecrement(r1) (we don't actually use this currently as r0-g0 is enough)
public readonly List<ControlCondition> ControllingConditions;
public float Guard => ConditionKey * 2;
public bool ConditionKeyIsValid => float.IsFinite(ConditionKey)
&& float.IsFinite(Guard)
&& ConditionKey > 0;
public GameObject ControllingObject;
public bool InitiallyActive;
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 override string ToString()
{
var obj = ControllingObject?.name ?? "<null>";
return $"AGK: {TargetProp}={Value} " +
$"range={ConditionKey}/{Guard} controlling object={obj}";
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 (ControllingObject != other.ControllingObject) return false;
if (!ControllingConditions.SequenceEqual(other.ControllingConditions)) return false;
if (IsDelete || other.IsDelete) return false;
return true;
@ -208,6 +213,7 @@ namespace nadena.dev.modular_avatar.core.editor
PreprocessShapes(shapes, out var initialStates, out var deletedShapes);
ProcessInitialStates(initialStates);
ProcessInitialAnimatorVariables(shapes);
foreach (var groups in shapes.Values)
{
@ -217,6 +223,19 @@ namespace nadena.dev.modular_avatar.core.editor
ProcessMeshDeletion(deletedShapes);
}
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
@ -235,7 +254,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
var deletions = info.actionGroups.Where(agk => agk.IsDelete).ToList();
if (deletions.Any(d => d.ControllingObject == null))
if (deletions.Any(d => d.ControllingConditions.Count == 0))
{
// always deleted
shapes.Remove(key);
@ -254,7 +273,7 @@ namespace nadena.dev.modular_avatar.core.editor
initialStates[key] = initialState;
// If we're now constant-on, we can skip animation generation
if (info.actionGroups[^1].ControllingObject == null)
if (info.actionGroups[^1].IsConstant)
{
shapes.Remove(key);
}
@ -382,7 +401,7 @@ namespace nadena.dev.modular_avatar.core.editor
// Check if this is non-animated and skip most processing if so
if (info.alwaysDeleted) return;
if (info.actionGroups[^1].ControllingObject == null)
if (info.actionGroups[^1].IsConstant)
{
info.TargetProp.ApplyImmediate(info.actionGroups[0].Value);
@ -421,7 +440,7 @@ namespace nadena.dev.modular_avatar.core.editor
state = initialState
});
var lastConstant = info.actionGroups.FindLastIndex(agk => agk.ControllingObject == null);
var lastConstant = info.actionGroups.FindLastIndex(agk => agk.IsConstant);
var transitionBuffer = new List<(AnimatorState, List<AnimatorStateTransition>)>();
var entryTransitions = new List<AnimatorTransition>();
@ -433,14 +452,15 @@ namespace nadena.dev.modular_avatar.core.editor
var clip = AnimResult(group.TargetProp, group.Value);
if (group.ControllingObject == null)
if (group.IsConstant)
{
clip.name = "Property Overlay constant " + group.Value;
initialState.motion = clip;
}
else
{
clip.name = "Property Overlay controlled by " + group.ControllingObject.name + " " + group.Value;
clip.name = "Property Overlay controlled by " + group.ControllingConditions[0].DebugName + " " +
group.Value;
var conditions = GetTransitionConditions(asc, group);
@ -458,7 +478,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
var state = new AnimatorState();
state.name = group.ControllingObject.name;
state.name = group.ControllingConditions[0].DebugName;
state.motion = clip;
state.writeDefaultValues = false;
states.Add(new ChildAnimatorState
@ -480,7 +500,9 @@ namespace nadena.dev.modular_avatar.core.editor
var inverted = new AnimatorCondition
{
parameter = cond.parameter,
mode = AnimatorConditionMode.Less,
mode = cond.mode == AnimatorConditionMode.Greater
? AnimatorConditionMode.Less
: AnimatorConditionMode.Greater,
threshold = cond.threshold
};
transitionList.Add(new AnimatorStateTransition
@ -509,24 +531,27 @@ namespace nadena.dev.modular_avatar.core.editor
{
var conditions = new List<AnimatorCondition>();
var controller = group.ControllingObject.transform;
while (controller != null && !RuntimeUtil.IsAvatarRoot(controller))
foreach (var condition in group.ControllingConditions)
{
if (asc.TryGetActiveSelfProxy(controller.gameObject, out var paramName))
{
initialValues[paramName] = controller.gameObject.activeSelf ? 1 : 0;
conditions.Add(new AnimatorCondition
{
parameter = paramName,
mode = AnimatorConditionMode.Greater,
threshold = 0.5f
});
}
if (condition.IsConstant) continue;
controller = controller.parent;
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 object found for " + group);
if (conditions.Count == 0)
throw new InvalidOperationException("No controlling parameters found for " + group);
return conditions.ToArray();
}
@ -673,9 +698,9 @@ namespace nadena.dev.modular_avatar.core.editor
}
var value = obj.Active ? 1 : 0;
var action = new ActionGroupKey(asc, key, toggle.gameObject, value);
var action = new ActionGroupKey(context, key, toggle.gameObject, value);
if (action.ControllingObject == null)
if (action.IsConstant)
{
if (action.InitiallyActive)
// always active control
@ -728,13 +753,12 @@ namespace nadena.dev.modular_avatar.core.editor
shapeKeys[key] = info;
// Add initial state
var agk = new ActionGroupKey(asc, key, null, value);
agk.InitiallyActive = true;
var agk = new ActionGroupKey(context, key, null, value);
agk.Value = renderer.GetBlendShapeWeight(shapeId);
info.actionGroups.Add(agk);
}
var action = new ActionGroupKey(asc, key, changer.gameObject, value);
var action = new ActionGroupKey(context, key, changer.gameObject, value);
var isCurrentlyActive = changer.gameObject.activeInHierarchy;
if (shape.ChangeType == ShapeChangeType.Delete)
@ -751,7 +775,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (changer.gameObject.activeInHierarchy) info.currentState = action.Value;
// TODO: lift controlling object resolution out of loop?
if (action.ControllingObject == null)
if (action.IsConstant)
{
if (action.InitiallyActive)
{

View File

@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using nadena.dev.modular_avatar.core.editor.plugin;
using nadena.dev.ndmf.preview;
using UnityEngine;
using Object = UnityEngine.Object;
@ -34,6 +33,8 @@ namespace nadena.dev.modular_avatar.core.editor
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext ctx)
{
var menuItemPreviewCondition = new MenuItemPreviewCondition(ctx);
var allChangers = ctx.GetComponentsByType<ModularAvatarShapeChanger>();
var groups =
@ -47,6 +48,9 @@ namespace nadena.dev.modular_avatar.core.editor
// TODO: observe avatar root
if (!ctx.ActiveAndEnabled(changer)) continue;
var mami = ctx.GetComponent<ModularAvatarMenuItem>(changer.gameObject);
if (mami != null && !menuItemPreviewCondition.IsEnabledForPreview(mami)) continue;
var target = ctx.Observe(changer, _ => changer.targetRenderer.Get(changer));
var renderer = ctx.GetComponent<SkinnedMeshRenderer>(target);

View File

@ -173,6 +173,10 @@ namespace nadena.dev.modular_avatar.core.editor
p.Value.ResolvedParameter.HasDefaultValue &&
p.Value.ResolvedParameter.OverrideAnimatorDefaults)
.ToImmutableDictionary(p => p.Key, p => p.Value.ResolvedParameter.defaultValue);
// clean up all parameters objects before the ParameterAssignerPass runs
foreach (var p in avatar.GetComponentsInChildren<ModularAvatarParameters>())
UnityObject.DestroyImmediate(p);
}
private void SetExpressionParameters(GameObject avatarRoot, ImmutableDictionary<string, ParameterInfo> allParams)

View File

@ -11,6 +11,8 @@ namespace nadena.dev.modular_avatar.core
public static string AVATAR_ROOT = "$$$AVATAR_ROOT$$$";
public string referencePath;
[SerializeField] internal GameObject targetObject;
private bool _cacheValid;
private string _cachedPath;
private GameObject _cachedReference;
@ -36,6 +38,9 @@ namespace nadena.dev.modular_avatar.core
var avatarTransform = RuntimeUtil.FindAvatarTransformInParents(container.transform);
if (avatarTransform == null) return (_cachedReference = null);
if (targetObject != null && targetObject.transform.IsChildOf(avatarTransform))
return _cachedReference = targetObject;
if (referencePath == AVATAR_ROOT)
{
_cachedReference = avatarTransform.gameObject;
@ -82,6 +87,7 @@ namespace nadena.dev.modular_avatar.core
_cachedReference = target;
_cacheValid = true;
targetObject = target;
}
private void InvalidateCache()
@ -92,7 +98,12 @@ namespace nadena.dev.modular_avatar.core
protected bool Equals(AvatarObjectReference other)
{
return referencePath == other.referencePath;
return GetDirectTarget() == other.GetDirectTarget() && referencePath == other.referencePath;
}
private GameObject GetDirectTarget()
{
return targetObject != null ? targetObject : null;
}
public override bool Equals(object obj)

View File

@ -23,12 +23,25 @@ namespace nadena.dev.modular_avatar.core
public GameObject menuSource_otherObjectChildren;
/// <summary>
/// If no control group is set (and an action is linked), this controls whether this control is synced.
/// If this MenuItem references a parameter that does not exist, it is created automatically.
/// In this case, isSynced controls whether the parameter is network synced.
/// </summary>
public bool isSynced = true;
/// <summary>
/// If this MenuItem references a parameter that does not exist, it is created automatically.
/// In this case, isSaved controls whether the parameter is saved across avatar changes.
/// </summary>
public bool isSaved = true;
/// <summary>
/// If this MenuItem references a parameter that does not exist, it is created automatically.
/// In this case, isDefault controls whether the parameter is set, by default, to the value for this
/// menu item. If multiple menu items reference the same parameter, the last menu item in hierarchy order
/// with isDefault = true is selected.
/// </summary>
public bool isDefault;
protected override void OnValidate()
{
base.OnValidate();

View File

@ -13,7 +13,7 @@ namespace nadena.dev.modular_avatar.core
[AddComponentMenu("Modular Avatar/MA Object Toggle")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/object-toggle?lang=auto")]
public class ModularAvatarObjectToggle : AvatarTagComponent
public class ModularAvatarObjectToggle : ReactiveComponent
{
[SerializeField] private List<ToggledObject> m_objects = new();

View File

@ -46,7 +46,7 @@ namespace nadena.dev.modular_avatar.core
[AddComponentMenu("Modular Avatar/MA Shape Changer")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/shape-changer?lang=auto")]
public class ModularAvatarShapeChanger : AvatarTagComponent
public class ModularAvatarShapeChanger : ReactiveComponent
{
[SerializeField] [FormerlySerializedAs("targetRenderer")]
private AvatarObjectReference m_targetRenderer;

View File

@ -0,0 +1,12 @@
namespace nadena.dev.modular_avatar.core
{
/// <summary>
/// Tag class used internally to mark reactive components. Not publicly extensible.
/// </summary>
public abstract class ReactiveComponent : AvatarTagComponent
{
internal ReactiveComponent()
{
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c6d2893b7921475d80282ecea6929f6a
timeCreated: 1722812955

View File

@ -583,17 +583,21 @@ MonoBehaviour:
Bindings:
- ReferenceMesh:
referencePath: BaseMesh
targetObject: {fileID: 0}
Blendshape: shape_0
LocalBlendshape: shape_0_local
- ReferenceMesh:
referencePath: BaseMesh
targetObject: {fileID: 0}
Blendshape: shape_1
LocalBlendshape: shape_1
- ReferenceMesh:
referencePath: MissingMesh
targetObject: {fileID: 0}
Blendshape: missing_mesh_shape
LocalBlendshape: missing_mesh_shape
- ReferenceMesh:
referencePath:
targetObject: {fileID: 0}
Blendshape: missing_mesh_shape_2
LocalBlendshape: missing_mesh_shape_2

View File

@ -16,6 +16,6 @@
},
"vpmDependencies": {
"com.vrchat.avatars": ">=3.4.0",
"nadena.dev.ndmf": ">=1.5.0-beta.2 <2.0.0-a"
"nadena.dev.ndmf": ">=1.5.0-beta.3 <2.0.0-a"
}
}