mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-02-02 19:52:51 +08:00
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:
parent
55a2229bc6
commit
76edc43aca
@ -23,8 +23,10 @@
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||
@ -144,6 +146,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
var vrcAvatarDescriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
|
||||
|
||||
using (BuildReport.CurrentReport.ReportingOnAvatar(vrcAvatarDescriptor))
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -174,6 +178,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
var context = new BuildContext(vrcAvatarDescriptor);
|
||||
|
||||
new ActionGenerator(context).OnPreprocessAvatar(vrcAvatarDescriptor);
|
||||
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject, context);
|
||||
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject, context);
|
||||
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
|
||||
// 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);
|
||||
madeProgress = true;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
retryDestroy.Add(component);
|
||||
}
|
||||
}
|
||||
|
||||
toDestroy = retryDestroy;
|
||||
retryDestroy = new List<AvatarTagComponent>();
|
||||
}
|
||||
|
||||
var activator = avatarGameObject.GetComponent<AvatarActivator>();
|
||||
@ -220,6 +254,17 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
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)
|
||||
|
@ -293,7 +293,8 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
|
||||
|
||||
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;
|
||||
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)
|
||||
{
|
||||
Log(ReportLevel.Error, code, strings, objects);
|
||||
Log(ReportLevel.Error, code, strings: strings, objects: objects);
|
||||
if (CurrentReport._currentAvatar != null)
|
||||
{
|
||||
CurrentReport._currentAvatar.successful = false;
|
||||
|
@ -81,10 +81,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
protected virtual VisualElement CreateInnerInspectorGUI()
|
||||
{
|
||||
var throwaway = new InspectorElement();
|
||||
MethodInfo m = typeof(InspectorElement).GetMethod("CreateIMGUIInspectorFromEditor",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
return m.Invoke(throwaway, new object[] {serializedObject, this, false}) as VisualElement;
|
||||
return null;
|
||||
}
|
||||
|
||||
public sealed override VisualElement CreateInspectorGUI()
|
||||
@ -94,10 +91,19 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
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.Add(inner);
|
||||
|
||||
_suppressOnceDefaultMargins = true;
|
||||
_suppressOnceDefaultMargins = innerIsImgui;
|
||||
return _visualElement;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df175549a5ea45d59d9f2daa031bbbf1
|
||||
timeCreated: 1677901132
|
@ -4,13 +4,13 @@ using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using nadena.dev.modular_avatar.core.editor.menu;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using VRC.SDKBase;
|
||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
@ -73,7 +73,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
Action<object> OnSelect)
|
||||
{
|
||||
var window = GetWindow<AvMenuTreeViewWindow>();
|
||||
window.titleContent = new GUIContent("Select menu");
|
||||
window.titleContent = G("menu_tree.title");
|
||||
|
||||
window.Avatar = Avatar;
|
||||
window.TargetInstaller = Installer;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 357527439f084cd6812d6e5dcd9692f8
|
||||
timeCreated: 1677595893
|
@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using Codice.CM.Common.Tree.Partial;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.PlayerLoop;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
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 _value;
|
||||
private readonly SerializedProperty _submenu;
|
||||
private readonly SerializedProperty _controlGroup;
|
||||
private readonly SerializedProperty _isDefault;
|
||||
|
||||
private readonly ParameterGUI _parameterGUI;
|
||||
|
||||
@ -48,6 +50,27 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
public bool AlwaysExpandContents = 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)
|
||||
{
|
||||
_obj = obj;
|
||||
@ -68,6 +91,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
);
|
||||
|
||||
_name = gameObjects.FindProperty("m_Name");
|
||||
_controlGroup = obj.FindProperty(nameof(ModularAvatarMenuItem.controlGroup));
|
||||
_isDefault = obj.FindProperty(nameof(ModularAvatarMenuItem.isDefault));
|
||||
|
||||
var control = obj.FindProperty(nameof(ModularAvatarMenuItem.Control));
|
||||
|
||||
_texture = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
|
||||
@ -87,6 +113,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
_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)
|
||||
{
|
||||
_obj = _control.serializedObject;
|
||||
@ -100,6 +132,9 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
_value = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
|
||||
_submenu = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subMenu));
|
||||
|
||||
_controlGroup = null;
|
||||
_isDefault = null;
|
||||
|
||||
_parameterGUI = new ParameterGUI(parameterReference, parameter, redraw);
|
||||
|
||||
_subParamsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters));
|
||||
@ -112,15 +147,39 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
public void DoGUI()
|
||||
{
|
||||
_hasActions = HasActions();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
EditorGUILayout.BeginVertical();
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
_name.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
EditorGUILayout.PropertyField(_texture, G("menuitem.prop.icon"));
|
||||
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();
|
||||
|
||||
@ -172,6 +231,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
case VRCExpressionsMenu.Control.ControlType.SubMenu:
|
||||
{
|
||||
object menuSource = null;
|
||||
bool canExpand = false;
|
||||
|
||||
if (_prop_submenuSource != null)
|
||||
{
|
||||
@ -204,6 +264,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
case SubmenuSource.MenuAsset:
|
||||
{
|
||||
EditorGUILayout.PropertyField(_submenu, G("menuitem.prop.submenu_asset"));
|
||||
canExpand = true;
|
||||
|
||||
if (_submenu.hasMultipleDifferentValues) break;
|
||||
menuSource = _submenu.objectReferenceValue;
|
||||
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;
|
||||
}
|
||||
case VRCExpressionsMenu.Control.ControlType.RadialPuppet:
|
||||
{
|
||||
EnsureParameterCount(1);
|
||||
|
||||
_subParams[0].DoGUI(G("menuitem.param.rotation"));
|
||||
_subParams[0].DoGUI(!_hasActions || !HasActions(TargetParameter.RadialParam),
|
||||
G("menuitem.param.rotation"));
|
||||
|
||||
break;
|
||||
}
|
||||
@ -253,11 +345,13 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
EnsureParameterCount(2);
|
||||
EnsureLabelCount(4);
|
||||
|
||||
EditorGUILayout.LabelField("Parameters", EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField(G("menuitem.label.parameters"), EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space(2);
|
||||
|
||||
_subParams[0].DoGUI(G("menuitem.param.horizontal"));
|
||||
_subParams[1].DoGUI(G("menuitem.param.vertical"));
|
||||
_subParams[0].DoGUI(!_hasActions || !HasActions(TargetParameter.Horizontal),
|
||||
G("menuitem.param.horizontal"));
|
||||
_subParams[1].DoGUI(!_hasActions || !HasActions(TargetParameter.Vertical),
|
||||
G("menuitem.param.vertical"));
|
||||
|
||||
DoFourAxisLabels(false);
|
||||
|
||||
@ -354,10 +448,10 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
center.xMin += blockWidth;
|
||||
center.xMax -= blockWidth;
|
||||
|
||||
SingleLabel(0, up);
|
||||
SingleLabel(1, right);
|
||||
SingleLabel(2, down);
|
||||
SingleLabel(3, left);
|
||||
SingleLabel(0, up, TargetParameter.Up);
|
||||
SingleLabel(1, right, TargetParameter.Right);
|
||||
SingleLabel(2, down, TargetParameter.Down);
|
||||
SingleLabel(3, left, TargetParameter.Left);
|
||||
|
||||
var rect_param_l = center;
|
||||
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);
|
||||
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_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);
|
||||
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;
|
||||
|
@ -7,11 +7,86 @@ using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal class MenuPreviewGUI
|
||||
internal class MenuObjectHeader
|
||||
{
|
||||
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 float _indentLevel = 0;
|
||||
private readonly Dictionary<object, Action> _guiNodes = new Dictionary<object, Action>();
|
||||
|
||||
public MenuPreviewGUI(Action redraw)
|
||||
@ -21,19 +96,16 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
public void DoGUI(MenuSource root)
|
||||
{
|
||||
_indentLevel = 0;
|
||||
new VisitorContext(this).PushNode(root);
|
||||
}
|
||||
|
||||
public void DoGUI(ModularAvatarMenuInstaller root)
|
||||
{
|
||||
_indentLevel = 0;
|
||||
new VisitorContext(this).PushMenuInstaller(root);
|
||||
}
|
||||
|
||||
public void DoGUI(VRCExpressionsMenu menu, GameObject parameterReference = null)
|
||||
{
|
||||
_indentLevel = 0;
|
||||
new VisitorContext(this).PushNode(menu);
|
||||
}
|
||||
|
||||
@ -48,70 +120,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
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
|
||||
{
|
||||
@ -127,7 +135,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
_gui.PushGuiNode((expMenu, parameterReference), () =>
|
||||
{
|
||||
var header = new Header(_gui, expMenu);
|
||||
var header = new MenuObjectHeader(expMenu);
|
||||
var obj = new SerializedObject(expMenu);
|
||||
var controls = obj.FindProperty(nameof(expMenu.controls));
|
||||
var subGui = new List<MenuItemCoreGUI>();
|
||||
@ -143,7 +151,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
foreach (var gui in subGui)
|
||||
{
|
||||
using (new Header(_gui, null).Scope())
|
||||
using (new MenuObjectHeader(null).Scope())
|
||||
{
|
||||
gui.DoGUI();
|
||||
}
|
||||
@ -164,7 +172,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
_gui.PushGuiNode(item, () =>
|
||||
{
|
||||
var header = new Header(_gui, item,
|
||||
var header = new MenuObjectHeader(item,
|
||||
new SerializedObject(item.gameObject).FindProperty("m_IsActive"));
|
||||
var gui = new MenuItemCoreGUI(new SerializedObject(item), _gui._redraw);
|
||||
return () =>
|
||||
@ -178,7 +186,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
else
|
||||
{
|
||||
using (new Header(_gui, source as UnityEngine.Object).Scope())
|
||||
using (new MenuObjectHeader(source as UnityEngine.Object).Scope())
|
||||
{
|
||||
if (_visited.Contains(source)) return;
|
||||
_visited.Add(source);
|
||||
@ -190,7 +198,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
public void PushNode(ModularAvatarMenuInstaller installer)
|
||||
{
|
||||
using (new Header(_gui, installer).Scope())
|
||||
using (new MenuObjectHeader(installer).Scope())
|
||||
{
|
||||
PushMenuInstaller(installer);
|
||||
}
|
||||
|
@ -23,17 +23,26 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
_redraw = redraw;
|
||||
}
|
||||
|
||||
public void DoGUI(GUIContent label = null)
|
||||
public void DoGUI(bool enabled, GUIContent label = null)
|
||||
{
|
||||
DoGUI(EditorGUILayout.GetControlRect(
|
||||
true,
|
||||
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);
|
||||
GUILayout.BeginHorizontal();
|
||||
|
@ -106,5 +106,15 @@
|
||||
"menuitem.label.control_labels_and_params": "Control Labels and Parameters",
|
||||
"menuitem.label.control_labels": "Control Labels",
|
||||
"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"
|
||||
}
|
@ -103,5 +103,15 @@
|
||||
"menuitem.label.control_labels_and_params": "表示名・パラメーター",
|
||||
"menuitem.label.control_labels": "表示名",
|
||||
"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": "グループの初期設定にする"
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1cf0a36e200446cb099ec448b446495
|
||||
timeCreated: 1677334996
|
||||
folderAsset: yes
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be5c2a67d3b448c5bd8c439f537a1766
|
||||
timeCreated: 1677335026
|
@ -0,0 +1,4 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31ce1123a74443c1ba7a126d4b8919b1
|
||||
timeCreated: 1677317241
|
||||
folderAsset: yes
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fcac4d9412424173bd294ffd5fc5f9db
|
||||
timeCreated: 1677316809
|
@ -0,0 +1,9 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
[AddComponentMenu("Modular Avatar/MA Control Group")]
|
||||
public class ControlGroup : AvatarTagComponent
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 99c60d83ad614e81a0488d98b83b5c1c
|
||||
timeCreated: 1677317301
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33005b045768474d8ae6f789aa03361b
|
||||
timeCreated: 1677595006
|
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
@ -20,6 +21,9 @@ namespace nadena.dev.modular_avatar.core
|
||||
|
||||
public GameObject menuSource_otherObjectChildren;
|
||||
|
||||
[FormerlySerializedAs("toggleGroup")] public ControlGroup controlGroup;
|
||||
public bool isDefault;
|
||||
|
||||
public override void Visit(NodeContext context)
|
||||
{
|
||||
var cloned = new VirtualControl(Control);
|
||||
|
Loading…
Reference in New Issue
Block a user