mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-01-31 02:32:53 +08:00
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:
parent
bf9266f054
commit
8e7526e711
2
.github/ProjectRoot/vpm-manifest-2022.json
vendored
2
.github/ProjectRoot/vpm-manifest-2022.json
vendored
@ -19,7 +19,7 @@
|
||||
"dependencies": {}
|
||||
},
|
||||
"nadena.dev.ndmf": {
|
||||
"version": "1.5.0-beta.2"
|
||||
"version": "1.5.0-beta.3"
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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 =>
|
||||
{
|
||||
|
10
Editor/ReactiveObjects/ControlCondition.cs
Normal file
10
Editor/ReactiveObjects/ControlCondition.cs
Normal 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;
|
||||
}
|
||||
}
|
3
Editor/ReactiveObjects/ControlCondition.cs.meta
Normal file
3
Editor/ReactiveObjects/ControlCondition.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fbd0a833d92c4e67a94d10bab41939b4
|
||||
timeCreated: 1722812671
|
73
Editor/ReactiveObjects/MenuItemPreviewCondition.cs
Normal file
73
Editor/ReactiveObjects/MenuItemPreviewCondition.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/ReactiveObjects/MenuItemPreviewCondition.cs.meta
Normal file
3
Editor/ReactiveObjects/MenuItemPreviewCondition.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c16bb1ac308244a7b118931dab9d23ff
|
||||
timeCreated: 1722821807
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
102
Editor/ReactiveObjects/ParameterAssignerPass.cs
Normal file
102
Editor/ReactiveObjects/ParameterAssignerPass.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/ReactiveObjects/ParameterAssignerPass.cs.meta
Normal file
3
Editor/ReactiveObjects/ParameterAssignerPass.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c93adffdb4384590830c5bd200fb08b5
|
||||
timeCreated: 1722812355
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
12
Runtime/ReactiveComponent.cs
Normal file
12
Runtime/ReactiveComponent.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
3
Runtime/ReactiveComponent.cs.meta
Normal file
3
Runtime/ReactiveComponent.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6d2893b7921475d80282ecea6929f6a
|
||||
timeCreated: 1722812955
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user