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": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
"nadena.dev.ndmf": {
|
"nadena.dev.ndmf": {
|
||||||
"version": "1.5.0-beta.2"
|
"version": "1.5.0-beta.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -29,13 +29,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
var color = GUI.contentColor;
|
var color = GUI.contentColor;
|
||||||
|
|
||||||
|
var targetObjectProp = property.FindPropertyRelative(nameof(AvatarObjectReference.targetObject));
|
||||||
property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath));
|
property = property.FindPropertyRelative(nameof(AvatarObjectReference.referencePath));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var avatarTransform = findContainingAvatarTransform(property);
|
var avatarTransform = findContainingAvatarTransform(property);
|
||||||
if (avatarTransform == null) return false;
|
if (avatarTransform == null) return false;
|
||||||
|
|
||||||
bool isRoot = property.stringValue == AvatarObjectReference.AVATAR_ROOT;
|
bool isRoot = property.stringValue == AvatarObjectReference.AVATAR_ROOT;
|
||||||
bool isNull = string.IsNullOrEmpty(property.stringValue);
|
bool isNull = string.IsNullOrEmpty(property.stringValue);
|
||||||
Transform target;
|
Transform target;
|
||||||
@ -43,6 +44,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
else if (isRoot) target = avatarTransform;
|
else if (isRoot) target = avatarTransform;
|
||||||
else target = avatarTransform.Find(property.stringValue);
|
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;
|
var labelRect = position;
|
||||||
position = EditorGUI.PrefixLabel(position, label);
|
position = EditorGUI.PrefixLabel(position, label);
|
||||||
labelRect.width = position.x - labelRect.x;
|
labelRect.width = position.x - labelRect.x;
|
||||||
@ -73,6 +82,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
property.stringValue = relPath;
|
property.stringValue = relPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetObjectProp.objectReferenceValue = ((Transform)newTarget)?.gameObject;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -104,6 +115,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
property.stringValue = relPath;
|
property.stringValue = relPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetObjectProp.objectReferenceValue = ((Transform)newTarget)?.gameObject;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
#if MA_VRCSDK3_AVATARS
|
#if MA_VRCSDK3_AVATARS
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using nadena.dev.modular_avatar.core.menu;
|
using nadena.dev.modular_avatar.core.menu;
|
||||||
|
using nadena.dev.ndmf;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
namespace nadena.dev.modular_avatar.core.editor
|
namespace nadena.dev.modular_avatar.core.editor
|
||||||
{
|
{
|
||||||
@ -32,6 +35,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
private readonly SerializedProperty _submenu;
|
private readonly SerializedProperty _submenu;
|
||||||
|
|
||||||
private readonly ParameterGUI _parameterGUI;
|
private readonly ParameterGUI _parameterGUI;
|
||||||
|
private readonly SerializedProperty _parameterName;
|
||||||
|
|
||||||
private readonly SerializedProperty _subParamsRoot;
|
private readonly SerializedProperty _subParamsRoot;
|
||||||
private readonly SerializedProperty _labelsRoot;
|
private readonly SerializedProperty _labelsRoot;
|
||||||
@ -46,9 +50,15 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
private readonly SerializedProperty _prop_submenuSource;
|
private readonly SerializedProperty _prop_submenuSource;
|
||||||
private readonly SerializedProperty _prop_otherObjSource;
|
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 AlwaysExpandContents = false;
|
||||||
public bool ExpandContents = false;
|
public bool ExpandContents = false;
|
||||||
|
|
||||||
|
private readonly HashSet<string> _knownParameters = new();
|
||||||
|
|
||||||
public MenuItemCoreGUI(SerializedObject obj, Action redraw)
|
public MenuItemCoreGUI(SerializedObject obj, Action redraw)
|
||||||
{
|
{
|
||||||
_obj = obj;
|
_obj = obj;
|
||||||
@ -62,9 +72,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
_parameterReference = parameterReference;
|
_parameterReference = parameterReference;
|
||||||
_redraw = redraw;
|
_redraw = redraw;
|
||||||
|
|
||||||
|
InitKnownParameters();
|
||||||
|
|
||||||
var gameObjects = new SerializedObject(
|
var gameObjects = new SerializedObject(
|
||||||
obj.targetObjects.Select(o =>
|
obj.targetObjects.Select(o =>
|
||||||
(UnityEngine.Object) ((ModularAvatarMenuItem) o).gameObject
|
(Object) ((ModularAvatarMenuItem) o).gameObject
|
||||||
).ToArray()
|
).ToArray()
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -74,21 +86,47 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
_texture = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
|
_texture = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
|
||||||
_type = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.type));
|
_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));
|
.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter.name));
|
||||||
|
|
||||||
_value = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
|
_value = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
|
||||||
_submenu = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subMenu));
|
_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));
|
_subParamsRoot = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters));
|
||||||
_labelsRoot = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.labels));
|
_labelsRoot = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.labels));
|
||||||
|
|
||||||
_prop_submenuSource = obj.FindProperty(nameof(ModularAvatarMenuItem.MenuSource));
|
_prop_submenuSource = obj.FindProperty(nameof(ModularAvatarMenuItem.MenuSource));
|
||||||
_prop_otherObjSource = obj.FindProperty(nameof(ModularAvatarMenuItem.menuSource_otherObjectChildren));
|
_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);
|
_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>
|
/// <summary>
|
||||||
/// Builds a menu item GUI for a raw VRCExpressionsMenu.Control reference.
|
/// Builds a menu item GUI for a raw VRCExpressionsMenu.Control reference.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -99,25 +137,48 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
_obj = _control.serializedObject;
|
_obj = _control.serializedObject;
|
||||||
_parameterReference = parameterReference;
|
_parameterReference = parameterReference;
|
||||||
|
InitKnownParameters();
|
||||||
|
|
||||||
_redraw = redraw;
|
_redraw = redraw;
|
||||||
_name = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.name));
|
_name = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.name));
|
||||||
_texture = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
|
_texture = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
|
||||||
_type = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.type));
|
_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));
|
.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter.name));
|
||||||
|
|
||||||
_value = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
|
_value = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
|
||||||
_submenu = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subMenu));
|
_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));
|
_subParamsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters));
|
||||||
_labelsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.labels));
|
_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_submenuSource = null;
|
||||||
_prop_otherObjSource = null;
|
_prop_otherObjSource = null;
|
||||||
_previewGUI = new MenuPreviewGUI(redraw);
|
_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()
|
public void DoGUI()
|
||||||
{
|
{
|
||||||
EditorGUILayout.BeginHorizontal();
|
EditorGUILayout.BeginHorizontal();
|
||||||
@ -136,6 +197,18 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
_parameterGUI.DoGUI(true);
|
_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();
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
if (_texture != null)
|
if (_texture != null)
|
||||||
|
@ -11,7 +11,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
|
|||||||
[CustomPropertyDrawer(typeof(ToggledObject))]
|
[CustomPropertyDrawer(typeof(ToggledObject))]
|
||||||
public class ToggledObjectEditor : PropertyDrawer
|
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 UxmlPath = Root + "ToggledObjectEditor.uxml";
|
||||||
private const string UssPath = Root + "ObjectSwitcherStyles.uss";
|
private const string UssPath = Root + "ObjectSwitcherStyles.uss";
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ using System.Collections.Immutable;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using nadena.dev.modular_avatar.core.editor.plugin;
|
using nadena.dev.modular_avatar.core.editor.plugin;
|
||||||
using nadena.dev.ndmf;
|
using nadena.dev.ndmf;
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -54,6 +53,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
IsAnimatorOnly = animatorOnly,
|
IsAnimatorOnly = animatorOnly,
|
||||||
WantSynced = !p.localOnly,
|
WantSynced = !p.localOnly,
|
||||||
IsHidden = p.internalParameter,
|
IsHidden = p.internalParameter,
|
||||||
|
DefaultValue = p.defaultValue
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
remapTo = p.nameOrPrefix + "$" + GUID.Generate();
|
remapTo = p.nameOrPrefix + "$" + _component.GetInstanceID();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (string.IsNullOrEmpty(p.remapTo))
|
else if (string.IsNullOrEmpty(p.remapTo))
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
#region
|
#region
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using nadena.dev.modular_avatar.animation;
|
using nadena.dev.modular_avatar.animation;
|
||||||
using nadena.dev.modular_avatar.core.ArmatureAwase;
|
using nadena.dev.modular_avatar.core.ArmatureAwase;
|
||||||
using nadena.dev.modular_avatar.core.editor.plugin;
|
using nadena.dev.modular_avatar.core.editor.plugin;
|
||||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||||
using nadena.dev.ndmf;
|
using nadena.dev.ndmf;
|
||||||
using nadena.dev.ndmf.fluent;
|
using nadena.dev.ndmf.fluent;
|
||||||
using nadena.dev.ndmf.preview;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
@ -52,10 +50,10 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
|
|||||||
#if MA_VRCSDK3_AVATARS
|
#if MA_VRCSDK3_AVATARS
|
||||||
seq.Run(PropertyOverlayPrePass.Instance);
|
seq.Run(PropertyOverlayPrePass.Instance);
|
||||||
seq.Run(RenameParametersPluginPass.Instance);
|
seq.Run(RenameParametersPluginPass.Instance);
|
||||||
|
seq.Run(ParameterAssignerPass.Instance);
|
||||||
seq.Run(MergeBlendTreePass.Instance);
|
seq.Run(MergeBlendTreePass.Instance);
|
||||||
seq.Run(MergeAnimatorPluginPass.Instance);
|
seq.Run(MergeAnimatorPluginPass.Instance);
|
||||||
seq.Run(ApplyAnimatorDefaultValuesPass.Instance);
|
seq.Run(ApplyAnimatorDefaultValuesPass.Instance);
|
||||||
seq.Run(MenuInstallPluginPass.Instance);
|
|
||||||
#endif
|
#endif
|
||||||
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
|
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
|
||||||
{
|
{
|
||||||
@ -74,6 +72,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
|
|||||||
seq.Run(GameObjectDelayDisablePass.Instance);
|
seq.Run(GameObjectDelayDisablePass.Instance);
|
||||||
});
|
});
|
||||||
#if MA_VRCSDK3_AVATARS
|
#if MA_VRCSDK3_AVATARS
|
||||||
|
seq.Run(MenuInstallPluginPass.Instance);
|
||||||
seq.Run(PhysbonesBlockerPluginPass.Instance);
|
seq.Run(PhysbonesBlockerPluginPass.Instance);
|
||||||
seq.Run("Fixup Expressions Menu", ctx =>
|
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)
|
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext context)
|
||||||
{
|
{
|
||||||
|
var menuItemPreview = new MenuItemPreviewCondition(context);
|
||||||
var allToggles = context.GetComponentsByType<ModularAvatarObjectToggle>();
|
var allToggles = context.GetComponentsByType<ModularAvatarObjectToggle>();
|
||||||
|
|
||||||
var objectGroups =
|
var objectGroups =
|
||||||
@ -35,6 +36,13 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
foreach (var toggle in allToggles)
|
foreach (var toggle in allToggles)
|
||||||
{
|
{
|
||||||
|
if (!context.ActiveAndEnabled(toggle)) continue;
|
||||||
|
|
||||||
|
var mami = context.GetComponent<ModularAvatarMenuItem>(toggle.gameObject);
|
||||||
|
if (mami != null)
|
||||||
|
if (!menuItemPreview.IsEnabledForPreview(mami))
|
||||||
|
continue;
|
||||||
|
|
||||||
context.Observe(toggle,
|
context.Observe(toggle,
|
||||||
t => t.Objects.Select(o => o.Object.referencePath).ToList(),
|
t => t.Objects.Select(o => o.Object.referencePath).ToList(),
|
||||||
(x, y) => x.SequenceEqual(y)
|
(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.
|
// the child. We do this by simply looking at how many times we observe each renderer.
|
||||||
.GroupBy(r => r)
|
.GroupBy(r => r)
|
||||||
.Select(g => g.Key)
|
.Select(g => g.Key)
|
||||||
.ToList();
|
.ToHashSet();
|
||||||
|
|
||||||
var renderGroups = new List<RenderGroup>();
|
var renderGroups = new List<RenderGroup>();
|
||||||
|
|
||||||
foreach (var r in affectedRenderers)
|
foreach (var r in affectedRenderers)
|
||||||
{
|
{
|
||||||
var switchers = new List<(ModularAvatarObjectToggle, int)>();
|
var shouldEnable = true;
|
||||||
|
|
||||||
var obj = r.gameObject;
|
var obj = r.gameObject;
|
||||||
|
context.ActiveInHierarchy(obj); // observe path changes & object state changes
|
||||||
|
|
||||||
while (obj != null)
|
while (obj != null)
|
||||||
{
|
{
|
||||||
|
var enableAtNode = obj.activeSelf;
|
||||||
|
|
||||||
var group = objectGroups.GetValueOrDefault(obj);
|
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;
|
obj = obj.transform.parent?.gameObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGroups.Add(RenderGroup.For(r).WithData(switchers.ToImmutableList()));
|
if (shouldEnable) renderGroups.Add(RenderGroup.For(r));
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderGroups.ToImmutableList();
|
return renderGroups.ToImmutableList();
|
||||||
@ -95,48 +124,17 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
public Task<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs,
|
public Task<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs,
|
||||||
ComputeContext context)
|
ComputeContext context)
|
||||||
{
|
{
|
||||||
var data = group.GetData<ImmutableList<(ModularAvatarObjectToggle, int)>>();
|
return Task.FromResult<IRenderFilterNode>(new Node());
|
||||||
return new Node(data).Refresh(proxyPairs, context, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Node : IRenderFilterNode
|
private class Node : IRenderFilterNode
|
||||||
{
|
{
|
||||||
public RenderAspects WhatChanged => 0;
|
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)
|
public void OnFrame(Renderer original, Renderer proxy)
|
||||||
{
|
{
|
||||||
var shouldEnable = true;
|
proxy.gameObject.SetActive(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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
@ -118,72 +118,77 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
this.currentState = currentState;
|
this.currentState = currentState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ActionGroupKey
|
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<AnimationServicesContext>();
|
||||||
|
|
||||||
TargetProp = key;
|
TargetProp = key;
|
||||||
InitiallyActive = controllingObject?.activeInHierarchy == true;
|
|
||||||
|
|
||||||
var origControlling = controllingObject?.name ?? "<null>";
|
var conditions = new List<ControlCondition>();
|
||||||
while (controllingObject != null && !asc.TryGetActiveSelfProxy(controllingObject, out _))
|
|
||||||
|
var cursor = controllingObject?.transform;
|
||||||
|
|
||||||
|
while (cursor != null && !RuntimeUtil.IsAvatarRoot(cursor))
|
||||||
{
|
{
|
||||||
controllingObject = controllingObject.transform.parent?.gameObject;
|
if (asc.TryGetActiveSelfProxy(cursor.gameObject, out var paramName))
|
||||||
if (controllingObject != null && RuntimeUtil.IsAvatarRoot(controllingObject.transform))
|
conditions.Add(new ControlCondition
|
||||||
{
|
{
|
||||||
controllingObject = null;
|
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>";
|
ControllingConditions = conditions;
|
||||||
Debug.Log("AGK: Controlling object " + origControlling + " => " + newControlling);
|
|
||||||
|
|
||||||
ControllingObject = controllingObject;
|
|
||||||
Value = value;
|
Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TargetProp TargetProp;
|
public TargetProp TargetProp;
|
||||||
public float Value;
|
public float Value;
|
||||||
|
|
||||||
public float ConditionKey;
|
public readonly List<ControlCondition> ControllingConditions;
|
||||||
// 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 float Guard => ConditionKey * 2;
|
public bool InitiallyActive =>
|
||||||
|
ControllingConditions.Count == 0 || ControllingConditions.All(c => c.InitiallyActive);
|
||||||
public bool ConditionKeyIsValid => float.IsFinite(ConditionKey)
|
|
||||||
&& float.IsFinite(Guard)
|
|
||||||
&& ConditionKey > 0;
|
|
||||||
|
|
||||||
public GameObject ControllingObject;
|
|
||||||
public bool InitiallyActive;
|
|
||||||
public bool IsDelete;
|
public bool IsDelete;
|
||||||
|
|
||||||
|
public bool IsConstant => ControllingConditions.Count == 0 || ControllingConditions.All(c => c.IsConstant);
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var obj = ControllingObject?.name ?? "<null>";
|
return $"AGK: {TargetProp}={Value}";
|
||||||
|
|
||||||
return $"AGK: {TargetProp}={Value} " +
|
|
||||||
$"range={ConditionKey}/{Guard} controlling object={obj}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryMerge(ActionGroupKey other)
|
public bool TryMerge(ActionGroupKey other)
|
||||||
{
|
{
|
||||||
if (!TargetProp.Equals(other.TargetProp)) return false;
|
if (!TargetProp.Equals(other.TargetProp)) return false;
|
||||||
if (Mathf.Abs(Value - other.Value) > 0.001f) 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;
|
if (IsDelete || other.IsDelete) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -208,6 +213,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
PreprocessShapes(shapes, out var initialStates, out var deletedShapes);
|
PreprocessShapes(shapes, out var initialStates, out var deletedShapes);
|
||||||
|
|
||||||
ProcessInitialStates(initialStates);
|
ProcessInitialStates(initialStates);
|
||||||
|
ProcessInitialAnimatorVariables(shapes);
|
||||||
|
|
||||||
foreach (var groups in shapes.Values)
|
foreach (var groups in shapes.Values)
|
||||||
{
|
{
|
||||||
@ -217,6 +223,19 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
ProcessMeshDeletion(deletedShapes);
|
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)
|
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
|
// 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();
|
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
|
// always deleted
|
||||||
shapes.Remove(key);
|
shapes.Remove(key);
|
||||||
@ -254,7 +273,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
initialStates[key] = initialState;
|
initialStates[key] = initialState;
|
||||||
|
|
||||||
// If we're now constant-on, we can skip animation generation
|
// 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);
|
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
|
// Check if this is non-animated and skip most processing if so
|
||||||
if (info.alwaysDeleted) return;
|
if (info.alwaysDeleted) return;
|
||||||
if (info.actionGroups[^1].ControllingObject == null)
|
if (info.actionGroups[^1].IsConstant)
|
||||||
{
|
{
|
||||||
info.TargetProp.ApplyImmediate(info.actionGroups[0].Value);
|
info.TargetProp.ApplyImmediate(info.actionGroups[0].Value);
|
||||||
|
|
||||||
@ -421,7 +440,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
state = initialState
|
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 transitionBuffer = new List<(AnimatorState, List<AnimatorStateTransition>)>();
|
||||||
var entryTransitions = new List<AnimatorTransition>();
|
var entryTransitions = new List<AnimatorTransition>();
|
||||||
|
|
||||||
@ -433,14 +452,15 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
var clip = AnimResult(group.TargetProp, group.Value);
|
var clip = AnimResult(group.TargetProp, group.Value);
|
||||||
|
|
||||||
if (group.ControllingObject == null)
|
if (group.IsConstant)
|
||||||
{
|
{
|
||||||
clip.name = "Property Overlay constant " + group.Value;
|
clip.name = "Property Overlay constant " + group.Value;
|
||||||
initialState.motion = clip;
|
initialState.motion = clip;
|
||||||
}
|
}
|
||||||
else
|
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);
|
var conditions = GetTransitionConditions(asc, group);
|
||||||
|
|
||||||
@ -458,7 +478,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
var state = new AnimatorState();
|
var state = new AnimatorState();
|
||||||
state.name = group.ControllingObject.name;
|
state.name = group.ControllingConditions[0].DebugName;
|
||||||
state.motion = clip;
|
state.motion = clip;
|
||||||
state.writeDefaultValues = false;
|
state.writeDefaultValues = false;
|
||||||
states.Add(new ChildAnimatorState
|
states.Add(new ChildAnimatorState
|
||||||
@ -480,7 +500,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var inverted = new AnimatorCondition
|
var inverted = new AnimatorCondition
|
||||||
{
|
{
|
||||||
parameter = cond.parameter,
|
parameter = cond.parameter,
|
||||||
mode = AnimatorConditionMode.Less,
|
mode = cond.mode == AnimatorConditionMode.Greater
|
||||||
|
? AnimatorConditionMode.Less
|
||||||
|
: AnimatorConditionMode.Greater,
|
||||||
threshold = cond.threshold
|
threshold = cond.threshold
|
||||||
};
|
};
|
||||||
transitionList.Add(new AnimatorStateTransition
|
transitionList.Add(new AnimatorStateTransition
|
||||||
@ -509,24 +531,27 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
{
|
{
|
||||||
var conditions = new List<AnimatorCondition>();
|
var conditions = new List<AnimatorCondition>();
|
||||||
|
|
||||||
var controller = group.ControllingObject.transform;
|
foreach (var condition in group.ControllingConditions)
|
||||||
while (controller != null && !RuntimeUtil.IsAvatarRoot(controller))
|
|
||||||
{
|
{
|
||||||
if (asc.TryGetActiveSelfProxy(controller.gameObject, out var paramName))
|
if (condition.IsConstant) continue;
|
||||||
{
|
|
||||||
initialValues[paramName] = controller.gameObject.activeSelf ? 1 : 0;
|
|
||||||
conditions.Add(new AnimatorCondition
|
|
||||||
{
|
|
||||||
parameter = paramName,
|
|
||||||
mode = AnimatorConditionMode.Greater,
|
|
||||||
threshold = 0.5f
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
return conditions.ToArray();
|
||||||
}
|
}
|
||||||
@ -673,9 +698,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
var value = obj.Active ? 1 : 0;
|
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)
|
if (action.InitiallyActive)
|
||||||
// always active control
|
// always active control
|
||||||
@ -728,13 +753,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
shapeKeys[key] = info;
|
shapeKeys[key] = info;
|
||||||
|
|
||||||
// Add initial state
|
// Add initial state
|
||||||
var agk = new ActionGroupKey(asc, key, null, value);
|
var agk = new ActionGroupKey(context, key, null, value);
|
||||||
agk.InitiallyActive = true;
|
|
||||||
agk.Value = renderer.GetBlendShapeWeight(shapeId);
|
agk.Value = renderer.GetBlendShapeWeight(shapeId);
|
||||||
info.actionGroups.Add(agk);
|
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;
|
var isCurrentlyActive = changer.gameObject.activeInHierarchy;
|
||||||
|
|
||||||
if (shape.ChangeType == ShapeChangeType.Delete)
|
if (shape.ChangeType == ShapeChangeType.Delete)
|
||||||
@ -751,7 +775,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
if (changer.gameObject.activeInHierarchy) info.currentState = action.Value;
|
if (changer.gameObject.activeInHierarchy) info.currentState = action.Value;
|
||||||
|
|
||||||
// TODO: lift controlling object resolution out of loop?
|
// TODO: lift controlling object resolution out of loop?
|
||||||
if (action.ControllingObject == null)
|
if (action.IsConstant)
|
||||||
{
|
{
|
||||||
if (action.InitiallyActive)
|
if (action.InitiallyActive)
|
||||||
{
|
{
|
||||||
|
@ -5,7 +5,6 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using nadena.dev.modular_avatar.core.editor.plugin;
|
|
||||||
using nadena.dev.ndmf.preview;
|
using nadena.dev.ndmf.preview;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
@ -34,6 +33,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext ctx)
|
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext ctx)
|
||||||
{
|
{
|
||||||
|
var menuItemPreviewCondition = new MenuItemPreviewCondition(ctx);
|
||||||
|
|
||||||
var allChangers = ctx.GetComponentsByType<ModularAvatarShapeChanger>();
|
var allChangers = ctx.GetComponentsByType<ModularAvatarShapeChanger>();
|
||||||
|
|
||||||
var groups =
|
var groups =
|
||||||
@ -47,6 +48,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
// TODO: observe avatar root
|
// TODO: observe avatar root
|
||||||
if (!ctx.ActiveAndEnabled(changer)) continue;
|
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 target = ctx.Observe(changer, _ => changer.targetRenderer.Get(changer));
|
||||||
var renderer = ctx.GetComponent<SkinnedMeshRenderer>(target);
|
var renderer = ctx.GetComponent<SkinnedMeshRenderer>(target);
|
||||||
|
|
||||||
|
@ -173,6 +173,10 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
p.Value.ResolvedParameter.HasDefaultValue &&
|
p.Value.ResolvedParameter.HasDefaultValue &&
|
||||||
p.Value.ResolvedParameter.OverrideAnimatorDefaults)
|
p.Value.ResolvedParameter.OverrideAnimatorDefaults)
|
||||||
.ToImmutableDictionary(p => p.Key, p => p.Value.ResolvedParameter.defaultValue);
|
.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)
|
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 static string AVATAR_ROOT = "$$$AVATAR_ROOT$$$";
|
||||||
public string referencePath;
|
public string referencePath;
|
||||||
|
|
||||||
|
[SerializeField] internal GameObject targetObject;
|
||||||
|
|
||||||
private bool _cacheValid;
|
private bool _cacheValid;
|
||||||
private string _cachedPath;
|
private string _cachedPath;
|
||||||
private GameObject _cachedReference;
|
private GameObject _cachedReference;
|
||||||
@ -18,7 +20,7 @@ namespace nadena.dev.modular_avatar.core
|
|||||||
public GameObject Get(Component container)
|
public GameObject Get(Component container)
|
||||||
{
|
{
|
||||||
bool cacheValid = _cacheValid || ReferencesLockedAtFrame == Time.frameCount;
|
bool cacheValid = _cacheValid || ReferencesLockedAtFrame == Time.frameCount;
|
||||||
|
|
||||||
if (cacheValid && _cachedPath == referencePath && _cachedReference != null) return _cachedReference;
|
if (cacheValid && _cachedPath == referencePath && _cachedReference != null) return _cachedReference;
|
||||||
|
|
||||||
_cacheValid = true;
|
_cacheValid = true;
|
||||||
@ -36,6 +38,9 @@ namespace nadena.dev.modular_avatar.core
|
|||||||
var avatarTransform = RuntimeUtil.FindAvatarTransformInParents(container.transform);
|
var avatarTransform = RuntimeUtil.FindAvatarTransformInParents(container.transform);
|
||||||
if (avatarTransform == null) return (_cachedReference = null);
|
if (avatarTransform == null) return (_cachedReference = null);
|
||||||
|
|
||||||
|
if (targetObject != null && targetObject.transform.IsChildOf(avatarTransform))
|
||||||
|
return _cachedReference = targetObject;
|
||||||
|
|
||||||
if (referencePath == AVATAR_ROOT)
|
if (referencePath == AVATAR_ROOT)
|
||||||
{
|
{
|
||||||
_cachedReference = avatarTransform.gameObject;
|
_cachedReference = avatarTransform.gameObject;
|
||||||
@ -82,6 +87,7 @@ namespace nadena.dev.modular_avatar.core
|
|||||||
|
|
||||||
_cachedReference = target;
|
_cachedReference = target;
|
||||||
_cacheValid = true;
|
_cacheValid = true;
|
||||||
|
targetObject = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InvalidateCache()
|
private void InvalidateCache()
|
||||||
@ -92,7 +98,12 @@ namespace nadena.dev.modular_avatar.core
|
|||||||
|
|
||||||
protected bool Equals(AvatarObjectReference other)
|
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)
|
public override bool Equals(object obj)
|
||||||
|
@ -23,12 +23,25 @@ namespace nadena.dev.modular_avatar.core
|
|||||||
public GameObject menuSource_otherObjectChildren;
|
public GameObject menuSource_otherObjectChildren;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public bool isSynced = true;
|
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;
|
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()
|
protected override void OnValidate()
|
||||||
{
|
{
|
||||||
base.OnValidate();
|
base.OnValidate();
|
||||||
|
@ -13,7 +13,7 @@ namespace nadena.dev.modular_avatar.core
|
|||||||
|
|
||||||
[AddComponentMenu("Modular Avatar/MA Object Toggle")]
|
[AddComponentMenu("Modular Avatar/MA Object Toggle")]
|
||||||
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/object-toggle?lang=auto")]
|
[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();
|
[SerializeField] private List<ToggledObject> m_objects = new();
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ namespace nadena.dev.modular_avatar.core
|
|||||||
|
|
||||||
[AddComponentMenu("Modular Avatar/MA Shape Changer")]
|
[AddComponentMenu("Modular Avatar/MA Shape Changer")]
|
||||||
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/shape-changer?lang=auto")]
|
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/shape-changer?lang=auto")]
|
||||||
public class ModularAvatarShapeChanger : AvatarTagComponent
|
public class ModularAvatarShapeChanger : ReactiveComponent
|
||||||
{
|
{
|
||||||
[SerializeField] [FormerlySerializedAs("targetRenderer")]
|
[SerializeField] [FormerlySerializedAs("targetRenderer")]
|
||||||
private AvatarObjectReference m_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:
|
Bindings:
|
||||||
- ReferenceMesh:
|
- ReferenceMesh:
|
||||||
referencePath: BaseMesh
|
referencePath: BaseMesh
|
||||||
|
targetObject: {fileID: 0}
|
||||||
Blendshape: shape_0
|
Blendshape: shape_0
|
||||||
LocalBlendshape: shape_0_local
|
LocalBlendshape: shape_0_local
|
||||||
- ReferenceMesh:
|
- ReferenceMesh:
|
||||||
referencePath: BaseMesh
|
referencePath: BaseMesh
|
||||||
|
targetObject: {fileID: 0}
|
||||||
Blendshape: shape_1
|
Blendshape: shape_1
|
||||||
LocalBlendshape: shape_1
|
LocalBlendshape: shape_1
|
||||||
- ReferenceMesh:
|
- ReferenceMesh:
|
||||||
referencePath: MissingMesh
|
referencePath: MissingMesh
|
||||||
|
targetObject: {fileID: 0}
|
||||||
Blendshape: missing_mesh_shape
|
Blendshape: missing_mesh_shape
|
||||||
LocalBlendshape: missing_mesh_shape
|
LocalBlendshape: missing_mesh_shape
|
||||||
- ReferenceMesh:
|
- ReferenceMesh:
|
||||||
referencePath:
|
referencePath:
|
||||||
|
targetObject: {fileID: 0}
|
||||||
Blendshape: missing_mesh_shape_2
|
Blendshape: missing_mesh_shape_2
|
||||||
LocalBlendshape: missing_mesh_shape_2
|
LocalBlendshape: missing_mesh_shape_2
|
||||||
|
@ -16,6 +16,6 @@
|
|||||||
},
|
},
|
||||||
"vpmDependencies": {
|
"vpmDependencies": {
|
||||||
"com.vrchat.avatars": ">=3.4.0",
|
"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