feat: menu actions (#231)

Add a new feature to generate animations for menu items automatically.
Currently this only supports generating blend-tree based GameObject toggles.
This commit is contained in:
bd_ 2023-03-04 14:31:23 +09:00 committed by GitHub
parent 55a2229bc6
commit 76edc43aca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1269 additions and 160 deletions

View File

@ -23,8 +23,10 @@
*/ */
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using nadena.dev.modular_avatar.editor.ErrorReporting; using nadena.dev.modular_avatar.editor.ErrorReporting;
@ -144,6 +146,8 @@ namespace nadena.dev.modular_avatar.core.editor
var vrcAvatarDescriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>(); var vrcAvatarDescriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
using (BuildReport.CurrentReport.ReportingOnAvatar(vrcAvatarDescriptor)) using (BuildReport.CurrentReport.ReportingOnAvatar(vrcAvatarDescriptor))
{
try
{ {
try try
{ {
@ -174,6 +178,7 @@ namespace nadena.dev.modular_avatar.core.editor
var context = new BuildContext(vrcAvatarDescriptor); var context = new BuildContext(vrcAvatarDescriptor);
new ActionGenerator(context).OnPreprocessAvatar(vrcAvatarDescriptor);
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject, context); new RenameParametersHook().OnPreprocessAvatar(avatarGameObject, context);
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject, context); new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject, context);
context.AnimationDatabase.Bootstrap(vrcAvatarDescriptor); context.AnimationDatabase.Bootstrap(vrcAvatarDescriptor);
@ -200,9 +205,38 @@ namespace nadena.dev.modular_avatar.core.editor
// Ensure that we clean up AvatarTagComponents after failed processing. This ensures we don't re-enter // Ensure that we clean up AvatarTagComponents after failed processing. This ensures we don't re-enter
// processing from the Awake method on the unprocessed AvatarTagComponents // processing from the Awake method on the unprocessed AvatarTagComponents
foreach (var component in avatarGameObject.GetComponentsInChildren<AvatarTagComponent>(true)) var toDestroy = avatarGameObject.GetComponentsInChildren<AvatarTagComponent>(true).ToList();
var retryDestroy = new List<AvatarTagComponent>();
// Sometimes AvatarTagComponents have interdependencies and need to be deleted in the right order;
// retry until we purge them all.
bool madeProgress = true;
while (toDestroy.Count > 0)
{
if (!madeProgress)
{
throw new Exception("One or more components failed to destroy." +
RuntimeUtil.AvatarRootPath(toDestroy[0].gameObject));
}
foreach (var component in toDestroy)
{
try
{
if (component != null)
{ {
UnityEngine.Object.DestroyImmediate(component); UnityEngine.Object.DestroyImmediate(component);
madeProgress = true;
}
}
catch (Exception e)
{
retryDestroy.Add(component);
}
}
toDestroy = retryDestroy;
retryDestroy = new List<AvatarTagComponent>();
} }
var activator = avatarGameObject.GetComponent<AvatarActivator>(); var activator = avatarGameObject.GetComponent<AvatarActivator>();
@ -220,6 +254,17 @@ namespace nadena.dev.modular_avatar.core.editor
Resources.UnloadUnusedAssets(); Resources.UnloadUnusedAssets();
} }
} }
catch (Exception e)
{
BuildReport.LogException(e);
throw;
}
if (!BuildReport.CurrentReport.CurrentAvatar.successful)
{
throw new Exception("Fatal error reported during avatar processing.");
}
}
} }
private static void ClearEditorOnlyTagComponents(Transform obj) private static void ClearEditorOnlyTagComponents(Transform obj)

View File

@ -293,7 +293,8 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
internal static void Log(ReportLevel level, string code, object[] strings, params Object[] objects) internal static void Log(ReportLevel level, string code, object[] strings, params Object[] objects)
{ {
ErrorLog errorLog = new ErrorLog(level, code, strings, objects); ErrorLog errorLog =
new ErrorLog(level, code, strings: strings.Select(s => s.ToString()).ToArray(), objects);
var avatarReport = CurrentReport._currentAvatar; var avatarReport = CurrentReport._currentAvatar;
if (avatarReport == null) if (avatarReport == null)
@ -307,7 +308,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
internal static void LogFatal(string code, object[] strings, params Object[] objects) internal static void LogFatal(string code, object[] strings, params Object[] objects)
{ {
Log(ReportLevel.Error, code, strings, objects); Log(ReportLevel.Error, code, strings: strings, objects: objects);
if (CurrentReport._currentAvatar != null) if (CurrentReport._currentAvatar != null)
{ {
CurrentReport._currentAvatar.successful = false; CurrentReport._currentAvatar.successful = false;

View File

@ -81,10 +81,7 @@ namespace nadena.dev.modular_avatar.core.editor
protected virtual VisualElement CreateInnerInspectorGUI() protected virtual VisualElement CreateInnerInspectorGUI()
{ {
var throwaway = new InspectorElement(); return null;
MethodInfo m = typeof(InspectorElement).GetMethod("CreateIMGUIInspectorFromEditor",
BindingFlags.NonPublic | BindingFlags.Instance);
return m.Invoke(throwaway, new object[] {serializedObject, this, false}) as VisualElement;
} }
public sealed override VisualElement CreateInspectorGUI() public sealed override VisualElement CreateInspectorGUI()
@ -94,10 +91,19 @@ namespace nadena.dev.modular_avatar.core.editor
var inner = CreateInnerInspectorGUI(); var inner = CreateInnerInspectorGUI();
bool innerIsImgui = (inner == null);
if (innerIsImgui)
{
var throwaway = new InspectorElement();
MethodInfo m = typeof(InspectorElement).GetMethod("CreateIMGUIInspectorFromEditor",
BindingFlags.NonPublic | BindingFlags.Instance);
inner = m.Invoke(throwaway, new object[] {serializedObject, this, false}) as VisualElement;
}
_visualElement = new MAVisualElement(); _visualElement = new MAVisualElement();
_visualElement.Add(inner); _visualElement.Add(inner);
_suppressOnceDefaultMargins = true; _suppressOnceDefaultMargins = innerIsImgui;
return _visualElement; return _visualElement;
} }

View File

@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomEditor(typeof(ActionToggleObject))]
internal class ActionToggleObjectInspector : MAEditorBase
{
private ReorderableList _list;
private SerializedProperty _listProp;
private void OnEnable()
{
_listProp = serializedObject.FindProperty(nameof(ActionToggleObject.Objects));
_list = new ReorderableList(
serializedObject,
_listProp,
true,
true,
true,
true
);
_list.drawHeaderCallback += DrawListHeader;
_list.drawElementCallback += DrawElement;
_list.onRemoveCallback += OnRemoveElement;
_list.onAddCallback += OnAddElement;
_list.elementHeight += 2;
}
private void OnAddElement(ReorderableList list)
{
_listProp.arraySize++;
}
private void OnRemoveElement(ReorderableList reorderableList)
{
if (reorderableList.index < _listProp.arraySize)
{
_listProp.DeleteArrayElementAtIndex(reorderableList.index);
}
}
private Rect _checkRect, _objectRect;
private void DrawListHeader(Rect rect)
{
var margin = 2;
var t = EditorStyles.toggle;
var toggleSize = t.CalcSize(GUIContent.none);
_checkRect = new Rect(0, 0, toggleSize.x, toggleSize.y);
_checkRect.y += (rect.height - _checkRect.height);
_objectRect = new Rect(_checkRect.xMax + margin, 0, rect.width - _checkRect.width - margin,
_list.elementHeight);
EditorGUI.LabelField(rect, G("action.toggle_object.header"));
}
private void DrawElement(Rect rect, int index, bool isactive, bool isfocused)
{
var element = _listProp.GetArrayElementAtIndex(index);
var activeProp = element.FindPropertyRelative(nameof(ActionToggleObject.ObjectEntry.Active));
var targetProp = element.FindPropertyRelative(nameof(ActionToggleObject.ObjectEntry.target));
var checkRect = _checkRect;
checkRect.x += rect.x;
checkRect.y += rect.y;
var objectRect = _objectRect;
objectRect.x += rect.x;
objectRect.y += rect.y;
objectRect.xMax = rect.xMax;
objectRect.yMin += 1;
objectRect.yMax -= 1;
using (new ZeroIndentScope())
{
EditorGUI.PropertyField(checkRect, activeProp, GUIContent.none);
EditorGUI.PropertyField(objectRect, targetProp, GUIContent.none);
}
}
protected override void OnInnerInspectorGUI()
{
serializedObject.Update();
Rect rect = GUILayoutUtility.GetRect(
10f,
_list.headerHeight + _list.elementHeight * Math.Max(1, _list.serializedProperty.arraySize) +
_list.footerHeight,
GUILayout.ExpandWidth(true)
);
_list.DoList(rect);
EditorGUILayout.Space(8);
Localization.ShowLanguageUI();
serializedObject.ApplyModifiedProperties();
if (rect.Contains(Event.current.mousePosition))
{
switch (Event.current.type)
{
case EventType.DragUpdated:
if (!DragIsGameObject()) break;
DragAndDrop.visualMode = DragAndDropVisualMode.Link;
Event.current.Use();
break;
case EventType.DragPerform:
{
if (!DragIsGameObject()) break;
var targetObj = (ActionToggleObject) target;
if (targetObj.Objects == null)
{
targetObj.Objects = new List<ActionToggleObject.ObjectEntry>();
}
HashSet<GameObject> currentObjects = new HashSet<GameObject>();
foreach (var obj in targetObj.Objects)
{
if (obj != null && obj.target != null)
{
currentObjects.Add(obj.target);
}
}
var objects = targetObj.Objects.ToList();
foreach (var obj in DragAndDrop.objectReferences)
{
if (obj is GameObject go && !currentObjects.Contains(go))
{
objects.Add(new ActionToggleObject.ObjectEntry()
{
target = go,
Active = go.activeSelf
});
}
}
Undo.RecordObject(targetObj, "Add objects");
targetObj.Objects = objects;
EditorUtility.SetDirty(targetObj);
PrefabUtility.RecordPrefabInstancePropertyModifications(targetObj);
break;
}
}
}
}
private bool DragIsGameObject()
{
foreach (var obj in DragAndDrop.objectReferences)
{
if (obj is GameObject) return true;
}
return false;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: df175549a5ea45d59d9f2daa031bbbf1
timeCreated: 1677901132

View File

@ -4,13 +4,13 @@ using System.Collections.Immutable;
using System.Linq; using System.Linq;
using nadena.dev.modular_avatar.core.editor.menu; using nadena.dev.modular_avatar.core.editor.menu;
using nadena.dev.modular_avatar.core.menu; using nadena.dev.modular_avatar.core.menu;
using NUnit.Framework;
using UnityEditor; using UnityEditor;
using UnityEditor.IMGUI.Controls; using UnityEditor.IMGUI.Controls;
using UnityEngine; using UnityEngine;
using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects; using VRC.SDK3.Avatars.ScriptableObjects;
using VRC.SDKBase; using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor namespace nadena.dev.modular_avatar.core.editor
{ {
@ -73,7 +73,7 @@ namespace nadena.dev.modular_avatar.core.editor
Action<object> OnSelect) Action<object> OnSelect)
{ {
var window = GetWindow<AvMenuTreeViewWindow>(); var window = GetWindow<AvMenuTreeViewWindow>();
window.titleContent = new GUIContent("Select menu"); window.titleContent = G("menu_tree.title");
window.Avatar = Avatar; window.Avatar = Avatar;
window.TargetInstaller = Installer; window.TargetInstaller = Installer;

View File

@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomEditor(typeof(ControlGroup))]
internal class ControlGroupInspector : MAEditorBase
{
private bool _showInner;
private void OnEnable()
{
EditorApplication.hierarchyChanged += Invalidate;
}
private void OnDisable()
{
EditorApplication.hierarchyChanged -= Invalidate;
}
private List<Action> _menuItemActions = null;
private void Invalidate()
{
var target = (ControlGroup) this.target;
var avatar = RuntimeUtil.FindAvatarInParents(target.transform);
var menuItems = avatar.GetComponentsInChildren<ModularAvatarMenuItem>(true);
_menuItemActions = new List<Action>();
foreach (var menuItem in menuItems.Where(item => item.controlGroup == target))
{
var node = CreateMenuItemNode(menuItem);
_menuItemActions.Add(node);
}
}
private Action CreateMenuItemNode(ModularAvatarMenuItem menuItem)
{
bool foldout = false;
var coreUI = new MenuItemCoreGUI(new SerializedObject(menuItem), Repaint);
var enableMenuItem = new SerializedObject(menuItem.gameObject).FindProperty("m_IsActive");
List<Action> foldoutInspectors = null;
return () =>
{
using (new MenuObjectHeader(menuItem, enableMenuItem).Scope())
{
coreUI.DoGUI();
foldout = EditorGUILayout.Foldout(foldout, G("control_group.foldout.actions"));
if (foldout)
{
if (foldoutInspectors == null)
{
foldoutInspectors = menuItem.GetComponents<MenuAction>()
.Select(action =>
{
var component = (Component) action;
var editor = CreateEditor(component);
var enabled_prop = new SerializedObject(component).FindProperty("m_Enabled");
return (Action) (() =>
{
using (new MenuObjectHeader(component, enabled_prop).Scope())
{
editor.OnInspectorGUI();
}
});
})
.ToList();
}
foreach (var inspector in foldoutInspectors)
{
inspector();
}
}
}
};
}
protected override void OnInnerInspectorGUI()
{
if (_menuItemActions == null) Invalidate();
_showInner = EditorGUILayout.Foldout(_showInner, G("control_group.foldout.menu_items"));
if (_showInner)
{
foreach (var action in _menuItemActions)
{
try
{
EditorGUI.indentLevel++;
action();
}
finally
{
EditorGUI.indentLevel--;
}
EditorGUILayout.Space(4);
}
}
Localization.ShowLanguageUI();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 357527439f084cd6812d6e5dcd9692f8
timeCreated: 1677595893

View File

@ -1,10 +1,10 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Codice.CM.Common.Tree.Partial;
using nadena.dev.modular_avatar.core.menu; using nadena.dev.modular_avatar.core.menu;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityEngine.PlayerLoop;
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;
@ -29,6 +29,8 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly SerializedProperty _type; private readonly SerializedProperty _type;
private readonly SerializedProperty _value; private readonly SerializedProperty _value;
private readonly SerializedProperty _submenu; private readonly SerializedProperty _submenu;
private readonly SerializedProperty _controlGroup;
private readonly SerializedProperty _isDefault;
private readonly ParameterGUI _parameterGUI; private readonly ParameterGUI _parameterGUI;
@ -48,6 +50,27 @@ namespace nadena.dev.modular_avatar.core.editor
public bool AlwaysExpandContents = false; public bool AlwaysExpandContents = false;
public bool ExpandContents = false; public bool ExpandContents = false;
private bool _hasActions;
private bool HasActions(TargetParameter? p = null)
{
return _controlGroup != null && _obj.targetObjects.Any(o =>
{
if (!(o is ModularAvatarMenuItem c)) return false;
if (p.HasValue)
{
foreach (var component in c.GetComponents<MenuAction>())
{
if (component.BindsParameter(p.Value)) return true;
}
return false;
}
return true;
});
}
public MenuItemCoreGUI(SerializedObject obj, Action redraw) public MenuItemCoreGUI(SerializedObject obj, Action redraw)
{ {
_obj = obj; _obj = obj;
@ -68,6 +91,9 @@ namespace nadena.dev.modular_avatar.core.editor
); );
_name = gameObjects.FindProperty("m_Name"); _name = gameObjects.FindProperty("m_Name");
_controlGroup = obj.FindProperty(nameof(ModularAvatarMenuItem.controlGroup));
_isDefault = obj.FindProperty(nameof(ModularAvatarMenuItem.isDefault));
var control = obj.FindProperty(nameof(ModularAvatarMenuItem.Control)); var control = obj.FindProperty(nameof(ModularAvatarMenuItem.Control));
_texture = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon)); _texture = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
@ -87,6 +113,12 @@ namespace nadena.dev.modular_avatar.core.editor
_previewGUI = new MenuPreviewGUI(redraw); _previewGUI = new MenuPreviewGUI(redraw);
} }
/// <summary>
/// Builds a menu item GUI for a raw VRCExpressionsMenu.Control reference.
/// </summary>
/// <param name="parameterReference"></param>
/// <param name="_control"></param>
/// <param name="redraw"></param>
public MenuItemCoreGUI(GameObject parameterReference, SerializedProperty _control, Action redraw) public MenuItemCoreGUI(GameObject parameterReference, SerializedProperty _control, Action redraw)
{ {
_obj = _control.serializedObject; _obj = _control.serializedObject;
@ -100,6 +132,9 @@ namespace nadena.dev.modular_avatar.core.editor
_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));
_controlGroup = null;
_isDefault = null;
_parameterGUI = new ParameterGUI(parameterReference, parameter, redraw); _parameterGUI = new ParameterGUI(parameterReference, parameter, redraw);
_subParamsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters)); _subParamsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters));
@ -112,15 +147,39 @@ namespace nadena.dev.modular_avatar.core.editor
public void DoGUI() public void DoGUI()
{ {
_hasActions = HasActions();
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
EditorGUILayout.BeginVertical(); EditorGUILayout.BeginVertical();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name")); EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
if (EditorGUI.EndChangeCheck())
{
_name.serializedObject.ApplyModifiedProperties();
}
EditorGUILayout.PropertyField(_texture, G("menuitem.prop.icon")); EditorGUILayout.PropertyField(_texture, G("menuitem.prop.icon"));
EditorGUILayout.PropertyField(_type, G("menuitem.prop.type")); EditorGUILayout.PropertyField(_type, G("menuitem.prop.type"));
EditorGUILayout.PropertyField(_value, G("menuitem.prop.value"));
_parameterGUI.DoGUI(); if (_hasActions)
{
EditorGUILayout.PropertyField(_controlGroup, G("menuitem.prop.control_group"));
if (_controlGroup.hasMultipleDifferentValues || _controlGroup.objectReferenceValue != null)
{
using (new EditorGUI.DisabledScope(_obj.isEditingMultipleObjects))
{
EditorGUILayout.PropertyField(_isDefault, G("menuitem.prop.is_default"));
}
}
}
if (!_hasActions || !HasActions(TargetParameter.BaseParameter))
{
EditorGUILayout.PropertyField(_value, G("menuitem.prop.value"));
}
_parameterGUI.DoGUI(!_hasActions || !HasActions(TargetParameter.BaseParameter));
EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical();
@ -172,6 +231,7 @@ namespace nadena.dev.modular_avatar.core.editor
case VRCExpressionsMenu.Control.ControlType.SubMenu: case VRCExpressionsMenu.Control.ControlType.SubMenu:
{ {
object menuSource = null; object menuSource = null;
bool canExpand = false;
if (_prop_submenuSource != null) if (_prop_submenuSource != null)
{ {
@ -204,6 +264,8 @@ namespace nadena.dev.modular_avatar.core.editor
case SubmenuSource.MenuAsset: case SubmenuSource.MenuAsset:
{ {
EditorGUILayout.PropertyField(_submenu, G("menuitem.prop.submenu_asset")); EditorGUILayout.PropertyField(_submenu, G("menuitem.prop.submenu_asset"));
canExpand = true;
if (_submenu.hasMultipleDifferentValues) break; if (_submenu.hasMultipleDifferentValues) break;
menuSource = _submenu.objectReferenceValue; menuSource = _submenu.objectReferenceValue;
break; break;
@ -238,13 +300,43 @@ namespace nadena.dev.modular_avatar.core.editor
} }
} }
if (canExpand && (_submenu.hasMultipleDifferentValues || _submenu.objectReferenceValue != null))
{
if (GUILayout.Button(G("menuitem.misc.extract")))
{
_obj.ApplyModifiedProperties();
foreach (var targetObj in _obj.targetObjects)
{
var menuItem = (ModularAvatarMenuItem) targetObj;
if (menuItem.Control.type == VRCExpressionsMenu.Control.ControlType.SubMenu
&& menuItem.Control.subMenu != null
&& menuItem.MenuSource == SubmenuSource.MenuAsset
)
{
Undo.RecordObject(menuItem, "Extract menu");
MenuExtractor.ExtractSingleLayerMenu(menuItem.Control.subMenu,
menuItem.gameObject);
menuItem.Control.subMenu = null;
menuItem.MenuSource = SubmenuSource.Children;
menuItem.menuSource_otherObjectChildren = null;
EditorUtility.SetDirty(menuItem);
PrefabUtility.RecordPrefabInstancePropertyModifications(menuItem);
}
}
_obj.Update();
}
}
break; break;
} }
case VRCExpressionsMenu.Control.ControlType.RadialPuppet: case VRCExpressionsMenu.Control.ControlType.RadialPuppet:
{ {
EnsureParameterCount(1); EnsureParameterCount(1);
_subParams[0].DoGUI(G("menuitem.param.rotation")); _subParams[0].DoGUI(!_hasActions || !HasActions(TargetParameter.RadialParam),
G("menuitem.param.rotation"));
break; break;
} }
@ -253,11 +345,13 @@ namespace nadena.dev.modular_avatar.core.editor
EnsureParameterCount(2); EnsureParameterCount(2);
EnsureLabelCount(4); EnsureLabelCount(4);
EditorGUILayout.LabelField("Parameters", EditorStyles.boldLabel); EditorGUILayout.LabelField(G("menuitem.label.parameters"), EditorStyles.boldLabel);
EditorGUILayout.Space(2); EditorGUILayout.Space(2);
_subParams[0].DoGUI(G("menuitem.param.horizontal")); _subParams[0].DoGUI(!_hasActions || !HasActions(TargetParameter.Horizontal),
_subParams[1].DoGUI(G("menuitem.param.vertical")); G("menuitem.param.horizontal"));
_subParams[1].DoGUI(!_hasActions || !HasActions(TargetParameter.Vertical),
G("menuitem.param.vertical"));
DoFourAxisLabels(false); DoFourAxisLabels(false);
@ -354,10 +448,10 @@ namespace nadena.dev.modular_avatar.core.editor
center.xMin += blockWidth; center.xMin += blockWidth;
center.xMax -= blockWidth; center.xMax -= blockWidth;
SingleLabel(0, up); SingleLabel(0, up, TargetParameter.Up);
SingleLabel(1, right); SingleLabel(1, right, TargetParameter.Right);
SingleLabel(2, down); SingleLabel(2, down, TargetParameter.Down);
SingleLabel(3, left); SingleLabel(3, left, TargetParameter.Left);
var rect_param_l = center; var rect_param_l = center;
rect_param_l.yMin = rect_param_l.yMax - EditorGUIUtility.singleLineHeight; rect_param_l.yMin = rect_param_l.yMax - EditorGUIUtility.singleLineHeight;
@ -367,7 +461,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (showParams) CenterLabel(rect_param_l, G("menuitem.prop.parameter"), EditorStyles.label); if (showParams) CenterLabel(rect_param_l, G("menuitem.prop.parameter"), EditorStyles.label);
CenterLabel(rect_name_l, G("menuitem.prop.label"), EditorStyles.label); CenterLabel(rect_name_l, G("menuitem.prop.label"), EditorStyles.label);
void SingleLabel(int index, Rect block) void SingleLabel(int index, Rect block, TargetParameter parameter)
{ {
var prop_name = _labels[index].FindPropertyRelative(nameof(VRCExpressionsMenu.Control.Label.name)); var prop_name = _labels[index].FindPropertyRelative(nameof(VRCExpressionsMenu.Control.Label.name));
var prop_icon = _labels[index].FindPropertyRelative(nameof(VRCExpressionsMenu.Control.Label.icon)); var prop_icon = _labels[index].FindPropertyRelative(nameof(VRCExpressionsMenu.Control.Label.icon));
@ -384,7 +478,7 @@ namespace nadena.dev.modular_avatar.core.editor
EditorGUI.PropertyField(rect_name, prop_name, GUIContent.none); EditorGUI.PropertyField(rect_name, prop_name, GUIContent.none);
if (showParams) if (showParams)
{ {
_subParams[index].DoGUI(rect_param, GUIContent.none); _subParams[index].DoGUI(rect_param, !_hasActions || !HasActions(parameter), GUIContent.none);
} }
var tex = prop_icon.objectReferenceValue as Texture; var tex = prop_icon.objectReferenceValue as Texture;

View File

@ -7,11 +7,86 @@ using VRC.SDK3.Avatars.ScriptableObjects;
namespace nadena.dev.modular_avatar.core.editor namespace nadena.dev.modular_avatar.core.editor
{ {
internal class MenuPreviewGUI internal class MenuObjectHeader
{ {
private const float INDENT_PER_LEVEL = 2; private const float INDENT_PER_LEVEL = 2;
private static float _indentLevel = 0;
private UnityEngine.Object _headerObj;
private SerializedProperty _disableProp;
public MenuObjectHeader(UnityEngine.Object headerObj, SerializedProperty disableProp = null)
{
_headerObj = headerObj;
_disableProp = disableProp;
}
public static void ClearIndent()
{
_indentLevel = 0;
}
public IDisposable Scope()
{
GUILayout.BeginHorizontal();
GUILayout.Space(_indentLevel);
_indentLevel += INDENT_PER_LEVEL;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
if (_headerObj != null)
{
var oldIndent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
try
{
GUILayout.BeginHorizontal();
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.ObjectField(new GUIContent(), _headerObj, _headerObj.GetType(),
true,
GUILayout.ExpandWidth(true));
}
if (_disableProp != null)
{
_disableProp.serializedObject.Update();
GUILayout.Space(10);
GUILayout.Label("Enabled", GUILayout.Width(50));
EditorGUILayout.PropertyField(_disableProp, GUIContent.none,
GUILayout.Width(EditorGUIUtility.singleLineHeight));
_disableProp.serializedObject.ApplyModifiedProperties();
}
GUILayout.EndHorizontal();
}
finally
{
EditorGUI.indentLevel = oldIndent;
}
}
return new ScopeSentinel();
}
private class ScopeSentinel : IDisposable
{
public ScopeSentinel()
{
}
public void Dispose()
{
GUILayout.EndVertical();
_indentLevel -= INDENT_PER_LEVEL;
GUILayout.EndHorizontal();
}
}
}
internal class MenuPreviewGUI
{
private Action _redraw; private Action _redraw;
private float _indentLevel = 0;
private readonly Dictionary<object, Action> _guiNodes = new Dictionary<object, Action>(); private readonly Dictionary<object, Action> _guiNodes = new Dictionary<object, Action>();
public MenuPreviewGUI(Action redraw) public MenuPreviewGUI(Action redraw)
@ -21,19 +96,16 @@ namespace nadena.dev.modular_avatar.core.editor
public void DoGUI(MenuSource root) public void DoGUI(MenuSource root)
{ {
_indentLevel = 0;
new VisitorContext(this).PushNode(root); new VisitorContext(this).PushNode(root);
} }
public void DoGUI(ModularAvatarMenuInstaller root) public void DoGUI(ModularAvatarMenuInstaller root)
{ {
_indentLevel = 0;
new VisitorContext(this).PushMenuInstaller(root); new VisitorContext(this).PushMenuInstaller(root);
} }
public void DoGUI(VRCExpressionsMenu menu, GameObject parameterReference = null) public void DoGUI(VRCExpressionsMenu menu, GameObject parameterReference = null)
{ {
_indentLevel = 0;
new VisitorContext(this).PushNode(menu); new VisitorContext(this).PushNode(menu);
} }
@ -48,70 +120,6 @@ namespace nadena.dev.modular_avatar.core.editor
gui(); gui();
} }
private class Header
{
private MenuPreviewGUI _gui;
private UnityEngine.Object _headerObj;
private SerializedProperty _disableProp;
public Header(MenuPreviewGUI gui, UnityEngine.Object headerObj, SerializedProperty disableProp = null)
{
_gui = gui;
_headerObj = headerObj;
_disableProp = disableProp;
}
public IDisposable Scope()
{
GUILayout.BeginHorizontal();
GUILayout.Space(_gui._indentLevel);
_gui._indentLevel += INDENT_PER_LEVEL;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
if (_headerObj != null)
{
GUILayout.BeginHorizontal();
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.ObjectField(new GUIContent(), _headerObj, _headerObj.GetType(),
true,
GUILayout.ExpandWidth(true));
}
if (_disableProp != null)
{
_disableProp.serializedObject.Update();
GUILayout.Space(20);
GUILayout.Label("Enabled", GUILayout.Width(50));
EditorGUILayout.PropertyField(_disableProp, GUIContent.none,
GUILayout.Width(EditorGUIUtility.singleLineHeight));
_disableProp.serializedObject.ApplyModifiedProperties();
}
GUILayout.EndHorizontal();
}
return new ScopeSentinel(_gui);
}
private class ScopeSentinel : IDisposable
{
private readonly MenuPreviewGUI _gui;
public ScopeSentinel(MenuPreviewGUI gui)
{
_gui = gui;
}
public void Dispose()
{
GUILayout.EndVertical();
_gui._indentLevel -= INDENT_PER_LEVEL;
GUILayout.EndHorizontal();
}
}
}
private class VisitorContext : NodeContext private class VisitorContext : NodeContext
{ {
@ -127,7 +135,7 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
_gui.PushGuiNode((expMenu, parameterReference), () => _gui.PushGuiNode((expMenu, parameterReference), () =>
{ {
var header = new Header(_gui, expMenu); var header = new MenuObjectHeader(expMenu);
var obj = new SerializedObject(expMenu); var obj = new SerializedObject(expMenu);
var controls = obj.FindProperty(nameof(expMenu.controls)); var controls = obj.FindProperty(nameof(expMenu.controls));
var subGui = new List<MenuItemCoreGUI>(); var subGui = new List<MenuItemCoreGUI>();
@ -143,7 +151,7 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
foreach (var gui in subGui) foreach (var gui in subGui)
{ {
using (new Header(_gui, null).Scope()) using (new MenuObjectHeader(null).Scope())
{ {
gui.DoGUI(); gui.DoGUI();
} }
@ -164,7 +172,7 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
_gui.PushGuiNode(item, () => _gui.PushGuiNode(item, () =>
{ {
var header = new Header(_gui, item, var header = new MenuObjectHeader(item,
new SerializedObject(item.gameObject).FindProperty("m_IsActive")); new SerializedObject(item.gameObject).FindProperty("m_IsActive"));
var gui = new MenuItemCoreGUI(new SerializedObject(item), _gui._redraw); var gui = new MenuItemCoreGUI(new SerializedObject(item), _gui._redraw);
return () => return () =>
@ -178,7 +186,7 @@ namespace nadena.dev.modular_avatar.core.editor
} }
else else
{ {
using (new Header(_gui, source as UnityEngine.Object).Scope()) using (new MenuObjectHeader(source as UnityEngine.Object).Scope())
{ {
if (_visited.Contains(source)) return; if (_visited.Contains(source)) return;
_visited.Add(source); _visited.Add(source);
@ -190,7 +198,7 @@ namespace nadena.dev.modular_avatar.core.editor
public void PushNode(ModularAvatarMenuInstaller installer) public void PushNode(ModularAvatarMenuInstaller installer)
{ {
using (new Header(_gui, installer).Scope()) using (new MenuObjectHeader(installer).Scope())
{ {
PushMenuInstaller(installer); PushMenuInstaller(installer);
} }

View File

@ -23,17 +23,26 @@ namespace nadena.dev.modular_avatar.core.editor
_redraw = redraw; _redraw = redraw;
} }
public void DoGUI(GUIContent label = null) public void DoGUI(bool enabled, GUIContent label = null)
{ {
DoGUI(EditorGUILayout.GetControlRect( DoGUI(EditorGUILayout.GetControlRect(
true, true,
EditorGUIUtility.singleLineHeight EditorGUIUtility.singleLineHeight
), label); ), enabled, label);
} }
public void DoGUI(Rect rect, GUIContent label = null) public void DoGUI(Rect rect, bool enabled, GUIContent label = null)
{ {
if (label == null) label = G("menuitem.prop.parameter"); label = label ?? G("menuitem.prop.parameter");
if (!enabled)
{
using (new EditorGUI.DisabledScope(true))
{
EditorGUI.TextField(rect, label, S("menuitem.param.controlled_by_action"));
return;
}
}
if (_parameterReference != null) GUILayout.Space(-2); if (_parameterReference != null) GUILayout.Space(-2);
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();

View File

@ -106,5 +106,15 @@
"menuitem.label.control_labels_and_params": "Control Labels and Parameters", "menuitem.label.control_labels_and_params": "Control Labels and Parameters",
"menuitem.label.control_labels": "Control Labels", "menuitem.label.control_labels": "Control Labels",
"menuitem.misc.multiple": "(multiple)", "menuitem.misc.multiple": "(multiple)",
"menuitem.misc.no_icon": "(no icon)" "menuitem.misc.no_icon": "(no icon)",
"menuitem.misc.extract": "Extract to objects",
"menuitem.label.parameters": "Parameters",
"action.toggle_object.header": "Objects to show/hide",
"menu_tree.title": "Select menu",
"menuitem.param.controlled_by_action": "<controlled by action>",
"control_group.foldout.actions": "Actions",
"control_group.foldout.menu_items": "Bound menu items",
"menuitem.prop.control_group": "Control Group",
"menuitem.prop.control_group.tooltip": "Only one toggle in a given group can be selected at the same time",
"menuitem.prop.is_default": "Is Group Default"
} }

View File

@ -103,5 +103,15 @@
"menuitem.label.control_labels_and_params": "表示名・パラメーター", "menuitem.label.control_labels_and_params": "表示名・パラメーター",
"menuitem.label.control_labels": "表示名", "menuitem.label.control_labels": "表示名",
"menuitem.misc.multiple": "(複数設定)", "menuitem.misc.multiple": "(複数設定)",
"menuitem.misc.no_icon": "(アイコン無し)" "menuitem.misc.no_icon": "(アイコン無し)",
"menuitem.misc.extract": "オブジェクトに展開",
"menuitem.label.parameters": "パラメーター",
"action.toggle_object.header": "表示・非表示にするオブジェクト",
"menu_tree.title": "メニューを選択",
"menuitem.param.controlled_by_action": "<アクションで制御されています>",
"control_group.foldout.actions": "アクション",
"control_group.foldout.menu_items": "関連付けされたメニューアイテム",
"menuitem.prop.control_group": "コントロールグループ",
"menuitem.prop.control_group.tooltip": "同じグループ内では、一つのトグルしか起動できません",
"menuitem.prop.is_default": "グループの初期設定にする"
} }

View File

@ -0,0 +1,4 @@
fileFormatVersion: 2
guid: d1cf0a36e200446cb099ec448b446495
timeCreated: 1677334996
folderAsset: yes

View File

@ -0,0 +1,430 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
using VRC.SDKBase;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
internal class ActionGenerator
{
private const string DIRECT_BLEND_TREE_PARAM = "_MA/ONE";
private readonly BuildContext _context;
public ActionGenerator(BuildContext context)
{
_context = context;
}
public void OnPreprocessAvatar(VRCAvatarDescriptor avatar)
{
// Locate MenuActions
var actionMenus = avatar.GetComponentsInChildren<MenuAction>(true)
.Select(a => ((Component) a).gameObject.GetComponent<ModularAvatarMenuItem>())
.Where(item => item != null)
.ToImmutableHashSet();
// Generate the root blendtree and animation; insert into the FX layer
var animLayers = avatar.baseAnimationLayers;
int fxLayerIndex = -1;
AnimatorController controller = null;
// TODO: refactor out layer manipulation here (+ the base state generator)
for (int i = 0; i < animLayers.Length; i++)
{
if (animLayers[i].type == VRCAvatarDescriptor.AnimLayerType.FX)
{
fxLayerIndex = i;
controller = _context.DeepCloneAnimator(animLayers[i].animatorController);
break;
}
}
if (controller == null)
{
controller = new AnimatorController();
controller.name = "FX Controller";
_context.SaveAsset(controller);
}
animLayers[fxLayerIndex].animatorController = controller;
avatar.baseAnimationLayers = animLayers;
var parameters = controller.parameters.ToList();
parameters.Add(new AnimatorControllerParameter()
{
name = DIRECT_BLEND_TREE_PARAM,
type = AnimatorControllerParameterType.Float,
defaultFloat = 1,
});
var actions = GenerateActions(avatar, actionMenus, parameters);
controller.parameters = parameters.ToArray();
int layersToInsert = 2; // TODO
var rootBlendTree = GenerateRootBlendLayer(actions);
AdjustAllBehaviors(controller, b =>
{
if (b is VRCAnimatorLayerControl lc && lc.playable == VRC_AnimatorLayerControl.BlendableLayer.FX)
{
lc.layer += layersToInsert;
}
});
foreach (var layer in controller.layers)
{
layer.syncedLayerIndex += layersToInsert;
}
var layerList = controller.layers.ToList();
layerList.Insert(0, GenerateBlendshapeBaseLayer(avatar));
rootBlendTree.defaultWeight = 1;
layerList.Insert(0, rootBlendTree);
layerList[2].defaultWeight = 1;
controller.layers = layerList.ToArray();
foreach (var action in avatar.GetComponentsInChildren<MenuAction>(true))
{
Object.DestroyImmediate((UnityEngine.Object) action);
}
}
private AnimatorControllerLayer GenerateBlendshapeBaseLayer(VRCAvatarDescriptor avatar)
{
AnimatorControllerLayer layer = new AnimatorControllerLayer();
AnimationClip clip = new AnimationClip();
foreach (var renderer in avatar.GetComponentsInChildren<SkinnedMeshRenderer>())
{
int nShapes = renderer.sharedMesh.blendShapeCount;
for (int i = 0; i < nShapes; i++)
{
float value = renderer.GetBlendShapeWeight(i);
if (value > 0.000001f)
{
clip.SetCurve(
RuntimeUtil.AvatarRootPath(renderer.gameObject),
typeof(SkinnedMeshRenderer),
"blendShape." + renderer.sharedMesh.GetBlendShapeName(i),
AnimationCurve.Constant(0, 1, value)
);
}
}
}
layer.stateMachine = new AnimatorStateMachine();
_context.SaveAsset(layer.stateMachine);
var state = layer.stateMachine.AddState("Base");
state.motion = clip;
state.writeDefaultValues = false;
layer.defaultWeight = 1;
return layer;
}
private void AdjustAllBehaviors(AnimatorController controller, Action<StateMachineBehaviour> action)
{
HashSet<object> visited = new HashSet<object>();
foreach (var layer in controller.layers)
{
VisitStateMachine(layer.stateMachine);
}
void VisitStateMachine(AnimatorStateMachine layerStateMachine)
{
if (!visited.Add(layerStateMachine)) return;
foreach (var state in layerStateMachine.states)
{
foreach (var behaviour in state.state.behaviours)
{
action(behaviour);
}
}
foreach (var child in layerStateMachine.stateMachines)
{
VisitStateMachine(child.stateMachine);
}
}
}
private AnimatorControllerLayer GenerateRootBlendLayer(List<BlendTree> actions)
{
var motion = new BlendTree();
motion.name = "Menu Actions (generated)";
motion.blendParameter = DIRECT_BLEND_TREE_PARAM;
motion.blendType = BlendTreeType.Direct;
motion.children = actions.Select(a => new ChildMotion()
{
motion = a,
directBlendParameter = DIRECT_BLEND_TREE_PARAM,
timeScale = 1,
}).ToArray();
var layer = new AnimatorControllerLayer();
layer.name = "Menu Actions (generated)";
layer.defaultWeight = 1;
layer.blendingMode = AnimatorLayerBlendingMode.Override;
layer.stateMachine = new AnimatorStateMachine();
layer.stateMachine.name = "Menu Actions (generated)";
//layer.stateMachine.hideFlags = HideFlags.HideInHierarchy;
_context.SaveAsset(layer.stateMachine);
var rootState = layer.stateMachine.AddState("Root");
rootState.motion = motion;
return layer;
}
private List<BlendTree> GenerateActions(
VRCAvatarDescriptor descriptor,
IEnumerable<ModularAvatarMenuItem> items,
List<AnimatorControllerParameter> acParameters)
{
var expParams = descriptor.expressionParameters;
if (expParams != null)
{
expParams = Object.Instantiate(expParams);
_context.SaveAsset(expParams);
}
else
{
expParams = ScriptableObject.CreateInstance<VRCExpressionParameters>();
expParams.name = "Expression Parameters";
_context.SaveAsset(expParams);
descriptor.expressionParameters = expParams;
}
List<VRCExpressionParameters.Parameter> expParameters = expParams.parameters.ToList();
List<BlendTree> blendTrees = new List<BlendTree>();
Dictionary<Component, List<ModularAvatarMenuItem>> groupedItems =
new Dictionary<Component, List<ModularAvatarMenuItem>>();
foreach (var item in items)
{
List<ModularAvatarMenuItem> group;
if (item.controlGroup)
{
if (!groupedItems.TryGetValue(item.controlGroup, out group))
{
group = new List<ModularAvatarMenuItem>();
groupedItems.Add(item.controlGroup, group);
}
}
else
{
group = new List<ModularAvatarMenuItem>();
groupedItems.Add(item, group);
}
group.Add(item);
}
int paramIndex = 0;
foreach (var kvp in groupedItems)
{
// sort default first
var group = kvp.Value;
group.Sort((a, b) => b.isDefault.CompareTo(a.isDefault));
// Generate parameter
var paramname = "_MA/A/" + kvp.Key.gameObject.name + "/" + (paramIndex++);
var expParamType = group.Count > 1
? VRCExpressionParameters.ValueType.Int
: VRCExpressionParameters.ValueType.Bool;
expParameters.Add(new VRCExpressionParameters.Parameter()
{
name = paramname,
defaultValue = 0, // TODO
valueType = expParamType,
saved = false, // TODO
});
acParameters.Add(new AnimatorControllerParameter()
{
name = paramname,
type = AnimatorControllerParameterType.Float,
defaultFloat = 0, // TODO
});
var hasDefault = group[0].isDefault;
for (int i = 0; i < group.Count; i++)
{
var control = group[i].Control;
control.parameter = new VRCExpressionsMenu.Control.Parameter() {name = paramname};
control.value = hasDefault ? i : i + 1;
}
var blendTree = new BlendTree();
blendTree.name = paramname;
blendTree.blendParameter = paramname;
blendTree.blendType = BlendTreeType.Simple1D;
blendTree.useAutomaticThresholds = false;
List<ChildMotion> children = new List<ChildMotion>();
List<Motion> motions = GenerateMotions(group, out var inactiveMotion);
if (!hasDefault)
{
motions.Insert(0, inactiveMotion);
}
for (int i = 0; i < motions.Count; i++)
{
children.Add(new ChildMotion()
{
motion = motions[i],
position = new Vector2(i, 0),
threshold = i - 0.1f,
timeScale = 1,
});
children.Add(new ChildMotion()
{
motion = motions[i],
position = new Vector2(i, 0),
threshold = i + 0.1f,
timeScale = 1,
});
}
blendTree.children = children.ToArray();
_context.SaveAsset(blendTree);
blendTrees.Add(blendTree);
}
expParams.parameters = expParameters.ToArray();
descriptor.expressionParameters = expParams;
return blendTrees;
}
void MergeCurves(
IDictionary<MenuCurveBinding, (Component, AnimationCurve)> curves,
ModularAvatarMenuItem item,
Func<SwitchedMenuAction, IDictionary<MenuCurveBinding, AnimationCurve>> getCurves,
bool ignoreDuplicates
)
{
foreach (var action in item.GetComponents<SwitchedMenuAction>())
{
var newCurves = getCurves(action);
foreach (var curvePair in newCurves)
{
var binding = curvePair.Key;
var curve = curvePair.Value;
if (curves.TryGetValue(binding, out var existing))
{
if (!ignoreDuplicates)
{
BuildReport.LogFatal("animation_gen.conflict", new object[]
{
binding,
existing.Item1.gameObject.name,
existing.Item1.GetType().Name,
item.gameObject.name,
item.GetType().Name
}, binding.target, existing.Item1, item);
}
}
else
{
curves.Add(binding, (item, curve));
}
}
}
}
private List<Motion> GenerateMotions(List<ModularAvatarMenuItem> items, out Motion inactiveMotion)
{
Dictionary<MenuCurveBinding, (Component, AnimationCurve)> inactiveCurves =
new Dictionary<MenuCurveBinding, (Component, AnimationCurve)>();
var defaultItems = items.Where(i => i.isDefault).ToList();
if (defaultItems.Count > 1)
{
BuildReport.LogFatal("animation_gen.multiple_defaults",
strings: Array.Empty<object>(),
objects: defaultItems.ToArray<UnityEngine.Object>()
);
defaultItems.RemoveRange(1, defaultItems.Count - 1);
}
if (defaultItems.Count > 0)
{
MergeCurves(inactiveCurves, defaultItems[0], a => a.GetInactiveCurves(true), false);
}
foreach (var item in items)
{
if (defaultItems.Count == 0 || defaultItems[0] != item)
{
MergeCurves(inactiveCurves, item, a => a.GetInactiveCurves(false), true);
}
}
inactiveMotion = CurvesToMotion(inactiveCurves);
var groupName = (items[0].controlGroup != null
? items[0].controlGroup.gameObject.name
: items[0].gameObject.name);
inactiveMotion.name =
groupName
+ " (Inactive)";
List<Motion> motions = new List<Motion>();
foreach (var item in items)
{
Dictionary<MenuCurveBinding, (Component, AnimationCurve)> activeCurves =
new Dictionary<MenuCurveBinding, (Component, AnimationCurve)>();
MergeCurves(activeCurves, item, a => a.GetCurves(), false);
foreach (var kvp in inactiveCurves)
{
if (!activeCurves.ContainsKey(kvp.Key))
{
activeCurves.Add(kvp.Key, kvp.Value);
}
}
var clip = CurvesToMotion(activeCurves);
clip.name = groupName + " (" + item.gameObject.name + ")";
motions.Add(clip);
}
return motions;
}
Motion CurvesToMotion(IDictionary<MenuCurveBinding, (Component, AnimationCurve)> curves)
{
var clip = new AnimationClip();
_context.SaveAsset(clip);
foreach (var entry in curves)
{
clip.SetCurve(
RuntimeUtil.AvatarRootPath(entry.Key.target),
entry.Key.type,
entry.Key.property,
entry.Value.Item2 // curve
);
}
return clip;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: be5c2a67d3b448c5bd8c439f537a1766
timeCreated: 1677335026

View File

@ -0,0 +1,4 @@
fileFormatVersion: 2
guid: 31ce1123a74443c1ba7a126d4b8919b1
timeCreated: 1677317241
folderAsset: yes

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using UnityEngine;
using Object = System.Object;
namespace nadena.dev.modular_avatar.core
{
[RequireComponent(typeof(ModularAvatarMenuItem))]
[AddComponentMenu("Modular Avatar/MA Action Toggle Object")]
public class ActionToggleObject : AvatarTagComponent, SwitchedMenuAction
{
[Serializable]
public class ObjectEntry
{
public GameObject target;
public bool Active;
}
public List<ObjectEntry> Objects;
protected override void OnValidate()
{
base.OnValidate();
if (Objects == null)
{
Objects = new List<ObjectEntry>();
}
}
public ImmutableDictionary<MenuCurveBinding, AnimationCurve> GetCurves()
{
return Objects.Select(obj =>
new KeyValuePair<MenuCurveBinding, AnimationCurve>(
new MenuCurveBinding(obj.target, typeof(GameObject), "m_IsActive"),
AnimationCurve.Constant(0, 1, obj.Active ? 1 : 0))
).ToImmutableDictionary();
}
public ImmutableDictionary<MenuCurveBinding, AnimationCurve> GetInactiveCurves(bool isDefault)
{
return Objects.Select(obj =>
{
bool active;
if (isDefault)
{
active = !obj.Active; // inactive state is the opposite of the default state
}
else
{
active = obj.target.activeSelf; // inactive state is the current state
}
return new KeyValuePair<MenuCurveBinding, AnimationCurve>(
new MenuCurveBinding(obj.target, typeof(GameObject), "m_IsActive"),
AnimationCurve.Constant(0, 1, active ? 1 : 0));
}
).ToImmutableDictionary();
}
public bool BindsParameter(TargetParameter parameter)
{
return parameter == TargetParameter.BaseParameter;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fcac4d9412424173bd294ffd5fc5f9db
timeCreated: 1677316809

View File

@ -0,0 +1,9 @@
using UnityEngine;
namespace nadena.dev.modular_avatar.core
{
[AddComponentMenu("Modular Avatar/MA Control Group")]
public class ControlGroup : AvatarTagComponent
{
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 99c60d83ad614e81a0488d98b83b5c1c
timeCreated: 1677317301

View File

@ -0,0 +1,102 @@
using System;
using System.Collections.Immutable;
using UnityEngine;
namespace nadena.dev.modular_avatar.core
{
public interface MenuAction
{
/// <summary>
/// Returns whether this action binds to the given parameter.
/// </summary>
/// <param name="parameter"></param>
/// <returns></returns>
bool BindsParameter(TargetParameter parameter);
}
public interface SwitchedMenuAction : MenuAction
{
/// <summary>
/// Returns the curves applied when this action is active
/// </summary>
/// <returns></returns>
ImmutableDictionary<MenuCurveBinding, AnimationCurve> GetCurves();
/// <summary>
/// Returns the curves applied when this action is inactive (and no other actions override).
/// </summary>
/// <param name="isDefault">True if this action is part of the default toggle option.</param>
/// <returns></returns>
ImmutableDictionary<MenuCurveBinding, AnimationCurve> GetInactiveCurves(bool isDefault);
}
public enum TargetParameter
{
BaseParameter,
RadialParam,
Horizontal,
Vertical,
Up,
Right,
Down,
Left
}
public static class TargetParameterExtension
{
public static int Index(this TargetParameter p)
{
switch (p)
{
case TargetParameter.BaseParameter:
return -1;
case TargetParameter.RadialParam: return 0;
case TargetParameter.Horizontal: return 0;
case TargetParameter.Vertical: return 1;
case TargetParameter.Up: return 0;
case TargetParameter.Right: return 1;
case TargetParameter.Down: return 2;
case TargetParameter.Left: return 3;
default: throw new NotImplementedException();
}
}
}
public sealed class MenuCurveBinding
{
public readonly GameObject target;
public readonly Type type;
public readonly string property;
public MenuCurveBinding(GameObject target, Type type, string property)
{
this.target = target;
this.type = type;
this.property = property;
}
private bool Equals(MenuCurveBinding other)
{
return target == other.target && type == other.type && property == other.property;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((MenuCurveBinding) obj);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = (target != null ? target.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (type != null ? type.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (property != null ? property.GetHashCode() : 0);
return hashCode;
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 33005b045768474d8ae6f789aa03361b
timeCreated: 1677595006

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using nadena.dev.modular_avatar.core.menu; using nadena.dev.modular_avatar.core.menu;
using UnityEngine; using UnityEngine;
using UnityEngine.Serialization;
using VRC.SDK3.Avatars.ScriptableObjects; using VRC.SDK3.Avatars.ScriptableObjects;
namespace nadena.dev.modular_avatar.core namespace nadena.dev.modular_avatar.core
@ -20,6 +21,9 @@ namespace nadena.dev.modular_avatar.core
public GameObject menuSource_otherObjectChildren; public GameObject menuSource_otherObjectChildren;
[FormerlySerializedAs("toggleGroup")] public ControlGroup controlGroup;
public bool isDefault;
public override void Visit(NodeContext context) public override void Visit(NodeContext context)
{ {
var cloned = new VirtualControl(Control); var cloned = new VirtualControl(Control);