diff --git a/.github/ProjectRoot/vpm-manifest-2022.json b/.github/ProjectRoot/vpm-manifest-2022.json index 9e90164d..e2bb6817 100644 --- a/.github/ProjectRoot/vpm-manifest-2022.json +++ b/.github/ProjectRoot/vpm-manifest-2022.json @@ -19,7 +19,7 @@ "dependencies": {} }, "nadena.dev.ndmf": { - "version": "1.5.0-beta.2" + "version": "1.5.0-beta.3" } } } \ No newline at end of file diff --git a/Editor/Inspector/AvatarObjectReferenceDrawer.cs b/Editor/Inspector/AvatarObjectReferenceDrawer.cs index 25bf2720..3543681e 100644 --- a/Editor/Inspector/AvatarObjectReferenceDrawer.cs +++ b/Editor/Inspector/AvatarObjectReferenceDrawer.cs @@ -29,13 +29,14 @@ 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 { var avatarTransform = findContainingAvatarTransform(property); if (avatarTransform == null) return false; - + bool isRoot = property.stringValue == AvatarObjectReference.AVATAR_ROOT; bool isNull = string.IsNullOrEmpty(property.stringValue); Transform target; @@ -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 { diff --git a/Editor/Inspector/Menu/MenuItemGUI.cs b/Editor/Inspector/Menu/MenuItemGUI.cs index ee658158..15f93df7 100644 --- a/Editor/Inspector/Menu/MenuItemGUI.cs +++ b/Editor/Inspector/Menu/MenuItemGUI.cs @@ -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 _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); + } + /// /// Builds a menu item GUI for a raw VRCExpressionsMenu.Control reference. /// @@ -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) diff --git a/Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs b/Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs index 62c03c65..cdb3e622 100644 --- a/Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs +++ b/Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs @@ -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"; diff --git a/Editor/ParamsUsage/MAParametersIntrospection.cs b/Editor/ParamsUsage/MAParametersIntrospection.cs index 3eeee897..a5500df7 100644 --- a/Editor/ParamsUsage/MAParametersIntrospection.cs +++ b/Editor/ParamsUsage/MAParametersIntrospection.cs @@ -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)) diff --git a/Editor/PluginDefinition/PluginDefinition.cs b/Editor/PluginDefinition/PluginDefinition.cs index ecd10290..dc69c1e5 100644 --- a/Editor/PluginDefinition/PluginDefinition.cs +++ b/Editor/PluginDefinition/PluginDefinition.cs @@ -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 => { diff --git a/Editor/ReactiveObjects/ControlCondition.cs b/Editor/ReactiveObjects/ControlCondition.cs new file mode 100644 index 00000000..ba9904c8 --- /dev/null +++ b/Editor/ReactiveObjects/ControlCondition.cs @@ -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; + } +} \ No newline at end of file diff --git a/Editor/ReactiveObjects/ControlCondition.cs.meta b/Editor/ReactiveObjects/ControlCondition.cs.meta new file mode 100644 index 00000000..8b621fa6 --- /dev/null +++ b/Editor/ReactiveObjects/ControlCondition.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fbd0a833d92c4e67a94d10bab41939b4 +timeCreated: 1722812671 \ No newline at end of file diff --git a/Editor/ReactiveObjects/MenuItemPreviewCondition.cs b/Editor/ReactiveObjects/MenuItemPreviewCondition.cs new file mode 100644 index 00000000..1d8f8629 --- /dev/null +++ b/Editor/ReactiveObjects/MenuItemPreviewCondition.cs @@ -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> _registeredParameters = new(); + + public MenuItemPreviewCondition(ComputeContext computeContext) + { + if (computeContext == null) throw new ArgumentNullException(nameof(computeContext)); + _info = ParameterInfo.ForPreview(computeContext); + _context = computeContext; + } + + private Dictionary RegisteredParameters(GameObject obj) + { + _context.ObservePath(obj.transform); + + var root = RuntimeUtil.FindAvatarInParents(obj.transform)?.gameObject; + if (root == null) return new Dictionary(); + + if (_registeredParameters.TryGetValue(root, out var parameters)) + return parameters; + + parameters = new Dictionary(); + + 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); + } + } +} \ No newline at end of file diff --git a/Editor/ReactiveObjects/MenuItemPreviewCondition.cs.meta b/Editor/ReactiveObjects/MenuItemPreviewCondition.cs.meta new file mode 100644 index 00000000..0ebce9db --- /dev/null +++ b/Editor/ReactiveObjects/MenuItemPreviewCondition.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c16bb1ac308244a7b118931dab9d23ff +timeCreated: 1722821807 \ No newline at end of file diff --git a/Editor/ReactiveObjects/ObjectTogglePreview.cs b/Editor/ReactiveObjects/ObjectTogglePreview.cs index ac793953..08c56759 100644 --- a/Editor/ReactiveObjects/ObjectTogglePreview.cs +++ b/Editor/ReactiveObjects/ObjectTogglePreview.cs @@ -27,6 +27,7 @@ namespace nadena.dev.modular_avatar.core.editor public ImmutableList GetTargetGroups(ComputeContext context) { + var menuItemPreview = new MenuItemPreviewCondition(context); var allToggles = context.GetComponentsByType(); 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(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(); - + 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 Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context) { - var data = group.GetData>(); - return new Node(data).Refresh(proxyPairs, context, 0); + return Task.FromResult(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 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(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); } } } diff --git a/Editor/ReactiveObjects/ParameterAssignerPass.cs b/Editor/ReactiveObjects/ParameterAssignerPass.cs new file mode 100644 index 00000000..5d741e48 --- /dev/null +++ b/Editor/ReactiveObjects/ParameterAssignerPass.cs @@ -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 +{ + /// + /// Creates/allocates parameters to any Menu Items that need them. + /// + internal class ParameterAssignerPass : Pass + { + protected override void Execute(ndmf.BuildContext context) + { + var paramIndex = 0; + + var declaredParams = context.AvatarDescriptor.expressionParameters.parameters.Select(p => p.name) + .ToHashSet(); + + Dictionary newParameters = new(); + + foreach (var mami in context.AvatarRootTransform.GetComponentsInChildren(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 + }; + } + } +} \ No newline at end of file diff --git a/Editor/ReactiveObjects/ParameterAssignerPass.cs.meta b/Editor/ReactiveObjects/ParameterAssignerPass.cs.meta new file mode 100644 index 00000000..9c918e7a --- /dev/null +++ b/Editor/ReactiveObjects/ParameterAssignerPass.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c93adffdb4384590830c5bd200fb08b5 +timeCreated: 1722812355 \ No newline at end of file diff --git a/Editor/ReactiveObjects/PropertyOverlayPass.cs b/Editor/ReactiveObjects/PropertyOverlayPass.cs index 9b6e1521..8d479303 100644 --- a/Editor/ReactiveObjects/PropertyOverlayPass.cs +++ b/Editor/ReactiveObjects/PropertyOverlayPass.cs @@ -118,72 +118,77 @@ namespace nadena.dev.modular_avatar.core.editor this.currentState = currentState; } } - + class ActionGroupKey { - public ActionGroupKey(AnimationServicesContext asc, TargetProp key, GameObject controllingObject, float value) + public ActionGroupKey(ndmf.BuildContext context, TargetProp key, GameObject controllingObject, float value) { + var asc = context.Extension(); + TargetProp = key; - InitiallyActive = controllingObject?.activeInHierarchy == true; - var origControlling = controllingObject?.name ?? ""; - while (controllingObject != null && !asc.TryGetActiveSelfProxy(controllingObject, out _)) + var conditions = new List(); + + 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 + { + new ControlCondition + { + Parameter = "", + DebugName = cursor.gameObject.name, + IsConstant = true, + InitialValue = 0, + ParameterValueLo = 0.5f, + ParameterValueHi = 1.5f + } + }; + + foreach (var mami in cursor.GetComponents()) + conditions.Add(ParameterAssignerPass.AssignMenuItemParameter(context, mami)); + + cursor = cursor.parent; } - var newControlling = controllingObject?.name ?? ""; - Debug.Log("AGK: Controlling object " + origControlling + " => " + newControlling); - - ControllingObject = controllingObject; + ControllingConditions = conditions; + 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 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 ?? ""; - - 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 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 shapes, out Dictionary initialStates, out HashSet 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)>(); var entryTransitions = new List(); @@ -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(); - 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) { diff --git a/Editor/ReactiveObjects/ShapeChangerPreview.cs b/Editor/ReactiveObjects/ShapeChangerPreview.cs index b2b6d8e5..8162a7d9 100644 --- a/Editor/ReactiveObjects/ShapeChangerPreview.cs +++ b/Editor/ReactiveObjects/ShapeChangerPreview.cs @@ -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 GetTargetGroups(ComputeContext ctx) { + var menuItemPreviewCondition = new MenuItemPreviewCondition(ctx); + var allChangers = ctx.GetComponentsByType(); 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(changer.gameObject); + if (mami != null && !menuItemPreviewCondition.IsEnabledForPreview(mami)) continue; + var target = ctx.Observe(changer, _ => changer.targetRenderer.Get(changer)); var renderer = ctx.GetComponent(target); diff --git a/Editor/RenameParametersHook.cs b/Editor/RenameParametersHook.cs index 1ff6d7cc..b2bb3e21 100644 --- a/Editor/RenameParametersHook.cs +++ b/Editor/RenameParametersHook.cs @@ -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()) + UnityObject.DestroyImmediate(p); } private void SetExpressionParameters(GameObject avatarRoot, ImmutableDictionary allParams) diff --git a/Runtime/AvatarObjectReference.cs b/Runtime/AvatarObjectReference.cs index f1f224d7..baedbb7c 100644 --- a/Runtime/AvatarObjectReference.cs +++ b/Runtime/AvatarObjectReference.cs @@ -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; @@ -18,7 +20,7 @@ namespace nadena.dev.modular_avatar.core public GameObject Get(Component container) { bool cacheValid = _cacheValid || ReferencesLockedAtFrame == Time.frameCount; - + if (cacheValid && _cachedPath == referencePath && _cachedReference != null) return _cachedReference; _cacheValid = true; @@ -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) diff --git a/Runtime/ModularAvatarMenuItem.cs b/Runtime/ModularAvatarMenuItem.cs index 750b4383..13fedb3c 100644 --- a/Runtime/ModularAvatarMenuItem.cs +++ b/Runtime/ModularAvatarMenuItem.cs @@ -23,12 +23,25 @@ namespace nadena.dev.modular_avatar.core public GameObject menuSource_otherObjectChildren; /// - /// 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. /// public bool isSynced = true; + /// + /// 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. + /// public bool isSaved = true; + /// + /// 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. + /// + public bool isDefault; + protected override void OnValidate() { base.OnValidate(); diff --git a/Runtime/ModularAvatarObjectToggle.cs b/Runtime/ModularAvatarObjectToggle.cs index 532add7b..c87451d3 100644 --- a/Runtime/ModularAvatarObjectToggle.cs +++ b/Runtime/ModularAvatarObjectToggle.cs @@ -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 m_objects = new(); diff --git a/Runtime/ModularAvatarShapeChanger.cs b/Runtime/ModularAvatarShapeChanger.cs index 0b0bb8e5..f21adabe 100644 --- a/Runtime/ModularAvatarShapeChanger.cs +++ b/Runtime/ModularAvatarShapeChanger.cs @@ -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; diff --git a/Runtime/ReactiveComponent.cs b/Runtime/ReactiveComponent.cs new file mode 100644 index 00000000..ce778cfe --- /dev/null +++ b/Runtime/ReactiveComponent.cs @@ -0,0 +1,12 @@ +namespace nadena.dev.modular_avatar.core +{ + /// + /// Tag class used internally to mark reactive components. Not publicly extensible. + /// + public abstract class ReactiveComponent : AvatarTagComponent + { + internal ReactiveComponent() + { + } + } +} \ No newline at end of file diff --git a/Runtime/ReactiveComponent.cs.meta b/Runtime/ReactiveComponent.cs.meta new file mode 100644 index 00000000..36109990 --- /dev/null +++ b/Runtime/ReactiveComponent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c6d2893b7921475d80282ecea6929f6a +timeCreated: 1722812955 \ No newline at end of file diff --git a/UnitTests~/BlendshapeSyncTests/BlendshapeSyncIntegrationTest.prefab b/UnitTests~/BlendshapeSyncTests/BlendshapeSyncIntegrationTest.prefab index 46e1bb93..97234940 100644 --- a/UnitTests~/BlendshapeSyncTests/BlendshapeSyncIntegrationTest.prefab +++ b/UnitTests~/BlendshapeSyncTests/BlendshapeSyncIntegrationTest.prefab @@ -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 diff --git a/package.json b/package.json index 95ad726c..f2988e02 100644 --- a/package.json +++ b/package.json @@ -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" } }