mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-29 18:55:06 +08:00
Detect and display unconfigured properties
This commit is contained in:
parent
8ee6771229
commit
b3bf7ea600
@ -1,7 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace net.fushizen.modular_avatar.core.editor
|
||||
{
|
||||
@ -17,27 +21,81 @@ namespace net.fushizen.modular_avatar.core.editor
|
||||
private ReorderableList _reorderableList;
|
||||
private SerializedProperty _parameters;
|
||||
|
||||
private bool _needsRebuild = false;
|
||||
private float elemHeight;
|
||||
|
||||
private readonly List<int> _selectedIndices = new List<int>();
|
||||
private ImplicitParams _implicitParams = null;
|
||||
private SerializedObject _implicitParamsObj;
|
||||
private SerializedProperty _implicitParamsProp;
|
||||
|
||||
private class ImplicitParams : ScriptableObject
|
||||
{
|
||||
public List<ParameterConfig> ImplicitParameters = new List<ParameterConfig>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
SetupList();
|
||||
}
|
||||
|
||||
private SerializedProperty GetParamByIndex(int idx)
|
||||
{
|
||||
if (idx >= 0)
|
||||
{
|
||||
return _parameters.GetArrayElementAtIndex(idx);
|
||||
}
|
||||
else
|
||||
{
|
||||
idx = -(idx + 1);
|
||||
return _implicitParamsProp.GetArrayElementAtIndex(idx);
|
||||
}
|
||||
}
|
||||
|
||||
private void DetectParameters()
|
||||
{
|
||||
if (_implicitParams == null || _implicitParamsProp == null)
|
||||
{
|
||||
_implicitParams = CreateInstance<ImplicitParams>();
|
||||
_implicitParamsObj = new SerializedObject(_implicitParams);
|
||||
_implicitParamsProp = _implicitParamsObj.FindProperty(nameof(ImplicitParams.ImplicitParameters));
|
||||
}
|
||||
|
||||
var target = (ModularAvatarParameters) this.target;
|
||||
var registered = target.parameters.Select(p => p.nameOrPrefix + (p.isPrefix ? "*" : ""))
|
||||
.ToImmutableHashSet();
|
||||
|
||||
_implicitParams.ImplicitParameters.Clear();
|
||||
|
||||
var detected = ParameterPolicy.ProbeParameters(((ModularAvatarParameters) target).gameObject);
|
||||
foreach (var param in detected.Values)
|
||||
{
|
||||
if (registered.Contains(param.MapKey)) continue;
|
||||
|
||||
var config = new ParameterConfig()
|
||||
{
|
||||
internalParameter = false,
|
||||
isPrefix = param.IsPrefix,
|
||||
nameOrPrefix = param.OriginalName,
|
||||
remapTo = "",
|
||||
};
|
||||
|
||||
_implicitParams.ImplicitParameters.Add(config);
|
||||
_selectedIndices.Add(-_implicitParams.ImplicitParameters.Count);
|
||||
}
|
||||
|
||||
_implicitParamsObj.Update();
|
||||
}
|
||||
|
||||
private void SetupList()
|
||||
{
|
||||
_parameters = serializedObject.FindProperty(nameof(ModularAvatarParameters.parameters));
|
||||
if (_devMode)
|
||||
{
|
||||
_reorderableList = new ReorderableList(
|
||||
serializedObject,
|
||||
_parameters,
|
||||
true, true, true, true
|
||||
);
|
||||
_reorderableList.drawHeaderCallback = DrawHeader;
|
||||
_reorderableList.drawElementCallback = DrawElement;
|
||||
_reorderableList.onAddCallback = AddElement;
|
||||
_reorderableList.onRemoveCallback = RemoveElement;
|
||||
var target = (ModularAvatarParameters) this.target;
|
||||
|
||||
_selectedIndices.Clear();
|
||||
_selectedIndices.AddRange(Enumerable.Range(0, target.parameters.Count));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -50,42 +108,120 @@ namespace net.fushizen.modular_avatar.core.editor
|
||||
|
||||
_selectedIndices.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
DetectParameters();
|
||||
|
||||
if (_reorderableList == null)
|
||||
{
|
||||
_reorderableList = new ReorderableList(
|
||||
_selectedIndices,
|
||||
typeof(int),
|
||||
false, true, false, false
|
||||
false, true, _devMode, _devMode
|
||||
);
|
||||
_reorderableList.drawHeaderCallback = DrawHeader;
|
||||
_reorderableList.drawElementCallback = DrawElement;
|
||||
_reorderableList.onAddCallback = AddElement;
|
||||
_reorderableList.onRemoveCallback = RemoveElement;
|
||||
_reorderableList.elementHeightCallback = ElementHeight;
|
||||
_reorderableList.onReorderCallbackWithDetails = ReorderElements;
|
||||
elemHeight = _reorderableList.elementHeight;
|
||||
}
|
||||
|
||||
_reorderableList.displayAdd = _devMode;
|
||||
_reorderableList.displayRemove = _devMode;
|
||||
_reorderableList.draggable = _devMode;
|
||||
|
||||
_needsRebuild = false;
|
||||
}
|
||||
|
||||
private void ReorderElements(ReorderableList list, int oldindex, int newindex)
|
||||
{
|
||||
if (_selectedIndices[oldindex] >= 0 && _selectedIndices[newindex] >= 0)
|
||||
{
|
||||
// We're in dev mode, so the "real" entries are in the same order in _selectedIndices
|
||||
// as the underlying. So just reorder them in the underlying object.
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
var target = (ModularAvatarParameters) this.target;
|
||||
var tmp = target.parameters[oldindex];
|
||||
target.parameters.RemoveAt(oldindex);
|
||||
target.parameters.Insert(newindex, tmp);
|
||||
|
||||
serializedObject.Update();
|
||||
}
|
||||
|
||||
// The reorderable list trashed our internal indices, so force a rebuild on next repaint.
|
||||
_needsRebuild = true;
|
||||
}
|
||||
|
||||
private void AddElement(ReorderableList list)
|
||||
{
|
||||
_parameters.arraySize += 1;
|
||||
_selectedIndices.Insert(_parameters.arraySize - 1, _parameters.arraySize - 1);
|
||||
list.index = _parameters.arraySize - 1;
|
||||
}
|
||||
|
||||
private void RemoveElement(ReorderableList list)
|
||||
{
|
||||
if (list.index < 0) return;
|
||||
if (list.index < 0 || _selectedIndices[list.index] < 0) return;
|
||||
_parameters.DeleteArrayElementAtIndex(list.index);
|
||||
|
||||
_needsRebuild = true;
|
||||
}
|
||||
|
||||
private float ElementHeight(int index)
|
||||
{
|
||||
if (_selectedIndices[index] == -1)
|
||||
{
|
||||
return elemHeight * 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
return elemHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAutodetectHeader(ref Rect rect)
|
||||
{
|
||||
Rect top = rect;
|
||||
top.height /= 2;
|
||||
Rect bottom = rect;
|
||||
bottom.y += top.height;
|
||||
bottom.height -= top.height;
|
||||
rect = bottom;
|
||||
|
||||
var style = new GUIStyle(EditorStyles.label);
|
||||
style.fontStyle = FontStyle.Italic;
|
||||
var content = new GUIContent(" Autodetected Parameters ");
|
||||
var size = style.CalcSize(content);
|
||||
|
||||
var centeredRect = new Rect(
|
||||
top.x + (top.width - size.x) / 2,
|
||||
top.y + (top.height - size.y) / 2,
|
||||
size.x,
|
||||
size.y
|
||||
);
|
||||
|
||||
EditorGUI.LabelField(centeredRect, content, style);
|
||||
}
|
||||
|
||||
private void DrawElement(Rect rect, int index, bool isactive, bool isfocused)
|
||||
{
|
||||
var originalIndex = index;
|
||||
index = _selectedIndices[index];
|
||||
var elem = GetParamByIndex(index);
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
DrawAutodetectHeader(ref rect);
|
||||
}
|
||||
|
||||
var margin = 20;
|
||||
var halfMargin = margin / 2;
|
||||
var leftHalf = new Rect(rect.x, rect.y, rect.width / 2 - halfMargin, rect.height);
|
||||
var rightHalf = new Rect(rect.x + leftHalf.width + halfMargin, rect.y, leftHalf.width, rect.height);
|
||||
|
||||
if (!_devMode) index = _selectedIndices[index];
|
||||
|
||||
var elem = _parameters.GetArrayElementAtIndex(index);
|
||||
|
||||
var nameOrPrefix = elem.FindPropertyRelative(nameof(ParameterConfig.nameOrPrefix));
|
||||
var remapTo = elem.FindPropertyRelative(nameof(ParameterConfig.remapTo));
|
||||
var internalParameter = elem.FindPropertyRelative(nameof(ParameterConfig.internalParameter));
|
||||
@ -94,6 +230,8 @@ namespace net.fushizen.modular_avatar.core.editor
|
||||
var indentLevel = EditorGUI.indentLevel;
|
||||
try
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
indentLevel = 0;
|
||||
if (_devMode)
|
||||
{
|
||||
@ -114,6 +252,42 @@ namespace net.fushizen.modular_avatar.core.editor
|
||||
EditorGUI.LabelField(leftHalf,
|
||||
isPrefix.boolValue ? nameOrPrefix.stringValue + "*" : nameOrPrefix.stringValue);
|
||||
EditorGUI.PropertyField(rightHalf, remapTo, GUIContent.none);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(remapTo.stringValue))
|
||||
{
|
||||
var style = new GUIStyle(EditorStyles.label);
|
||||
style.fontStyle = FontStyle.Italic;
|
||||
var oldColor = GUI.color;
|
||||
var color = GUI.color;
|
||||
color.a = 0.5f;
|
||||
GUI.color = color;
|
||||
|
||||
EditorGUI.LabelField(rightHalf, nameOrPrefix.stringValue, style);
|
||||
|
||||
GUI.color = oldColor;
|
||||
}
|
||||
}
|
||||
|
||||
if (EditorGUI.EndChangeCheck() && index < 0)
|
||||
{
|
||||
var target = (ModularAvatarParameters) this.target;
|
||||
|
||||
// Create this implicit parameter in the backing object
|
||||
var implicitIndex = -(index + 1);
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
Undo.RecordObject(target, "Updating parameters");
|
||||
_implicitParamsObj.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
var config = _implicitParams.ImplicitParameters[implicitIndex];
|
||||
target.parameters.Add(config);
|
||||
_selectedIndices.RemoveAt(originalIndex);
|
||||
_selectedIndices.Insert(target.parameters.Count - 1, target.parameters.Count - 1);
|
||||
|
||||
serializedObject.Update();
|
||||
_reorderableList.index = target.parameters.Count - 1;
|
||||
|
||||
_needsRebuild = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
@ -135,7 +309,7 @@ namespace net.fushizen.modular_avatar.core.editor
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
_devMode = EditorGUILayout.Toggle("Developer mode", _devMode);
|
||||
if (EditorGUI.EndChangeCheck()) SetupList();
|
||||
if (EditorGUI.EndChangeCheck() || _reorderableList == null || _needsRebuild) SetupList();
|
||||
_reorderableList.DoLayoutList();
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
251
Packages/net.fushizen.modular-avatar/Editor/ParameterPolicy.cs
Normal file
251
Packages/net.fushizen.modular-avatar/Editor/ParameterPolicy.cs
Normal file
@ -0,0 +1,251 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using VRC.SDK3.Dynamics.Contact.Components;
|
||||
using VRC.SDK3.Dynamics.PhysBone.Components;
|
||||
|
||||
namespace net.fushizen.modular_avatar.core.editor
|
||||
{
|
||||
public struct DetectedParameter
|
||||
{
|
||||
public string OriginalName;
|
||||
public bool IsPrefix;
|
||||
|
||||
public string MapKey => IsPrefix ? OriginalName + "*" : OriginalName;
|
||||
}
|
||||
|
||||
public static class ParameterPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters predefined by the VRChat SDK which should not be offered as remappable.
|
||||
/// </summary>
|
||||
public static ImmutableHashSet<string> VRCSDKParameters = new string[]
|
||||
{
|
||||
"IsLocal",
|
||||
"Viseme",
|
||||
"Voice",
|
||||
"GestureLeft",
|
||||
"GestureRight",
|
||||
"GestureLeftWeight",
|
||||
"GestureRightWeight",
|
||||
"AngularY",
|
||||
"VelocityX",
|
||||
"VelocityY",
|
||||
"VelocityZ",
|
||||
"Upright",
|
||||
"Grounded",
|
||||
"Seated",
|
||||
"AFK",
|
||||
"TrackingType",
|
||||
"VRMode",
|
||||
"MuteSelf",
|
||||
"InStation",
|
||||
"Earmuffs",
|
||||
}.ToImmutableHashSet();
|
||||
|
||||
public static ImmutableList<string> PhysBoneSuffixes = new string[]
|
||||
{
|
||||
"_IsGrabbed",
|
||||
"_Angle",
|
||||
"_Stretch",
|
||||
}.ToImmutableList();
|
||||
|
||||
public static ImmutableDictionary<string, DetectedParameter> ProbeParameters(GameObject root)
|
||||
{
|
||||
Dictionary<string, DetectedParameter> parameters = new Dictionary<string, DetectedParameter>();
|
||||
|
||||
WalkTree(ref parameters, root, false);
|
||||
|
||||
CleanPhysBoneParams(parameters);
|
||||
|
||||
return parameters.ToImmutableDictionary();
|
||||
}
|
||||
|
||||
private static void WalkTree(ref Dictionary<string, DetectedParameter> parameters, GameObject root,
|
||||
bool applyRemappings)
|
||||
{
|
||||
ModularAvatarParameters parametersComponent = null;
|
||||
|
||||
foreach (var component in root.GetComponents<Component>())
|
||||
{
|
||||
switch (component)
|
||||
{
|
||||
case ModularAvatarParameters p:
|
||||
{
|
||||
parametersComponent = p;
|
||||
break;
|
||||
}
|
||||
|
||||
case VRCPhysBone bone:
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(bone.parameter))
|
||||
{
|
||||
var param = new DetectedParameter()
|
||||
{
|
||||
OriginalName = bone.parameter,
|
||||
IsPrefix = true,
|
||||
};
|
||||
|
||||
parameters[param.MapKey] = param;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case VRCContactReceiver contact:
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(contact.parameter))
|
||||
{
|
||||
var param = new DetectedParameter()
|
||||
{
|
||||
OriginalName = contact.parameter,
|
||||
IsPrefix = false,
|
||||
};
|
||||
|
||||
parameters[param.MapKey] = param;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Animator anim:
|
||||
{
|
||||
WalkAnimator(parameters, anim.runtimeAnimatorController as AnimatorController);
|
||||
break;
|
||||
}
|
||||
|
||||
case ModularAvatarMenuInstaller installer:
|
||||
{
|
||||
WalkMenu(parameters, installer.menuToAppend, new HashSet<VRCExpressionsMenu>());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Transform child in root.transform)
|
||||
{
|
||||
WalkTree(ref parameters, child.gameObject, true);
|
||||
}
|
||||
|
||||
if (parametersComponent != null && applyRemappings)
|
||||
{
|
||||
CleanPhysBoneParams(parameters);
|
||||
ApplyRemappings(ref parameters, parametersComponent);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WalkMenu(Dictionary<string, DetectedParameter> parameters, VRCExpressionsMenu menu,
|
||||
HashSet<VRCExpressionsMenu> visited)
|
||||
{
|
||||
if (menu == null || visited.Contains(menu)) return;
|
||||
visited.Add(menu);
|
||||
|
||||
void AddParam(string name)
|
||||
{
|
||||
var param = new DetectedParameter()
|
||||
{
|
||||
OriginalName = name,
|
||||
IsPrefix = false
|
||||
};
|
||||
parameters[param.MapKey] = param;
|
||||
}
|
||||
|
||||
foreach (var control in menu.controls)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(control.parameter.name))
|
||||
{
|
||||
AddParam(control.parameter.name);
|
||||
}
|
||||
|
||||
foreach (var subParam in control.subParameters)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(subParam.name))
|
||||
{
|
||||
AddParam(subParam.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
|
||||
{
|
||||
WalkMenu(parameters, control.subMenu, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CleanPhysBoneParams(Dictionary<string, DetectedParameter> parameters)
|
||||
{
|
||||
var physBonePrefixes = parameters.Values.Where(p => p.IsPrefix)
|
||||
.Select(p => p.OriginalName);
|
||||
|
||||
foreach (var prefix in physBonePrefixes)
|
||||
{
|
||||
foreach (var suffix in PhysBoneSuffixes)
|
||||
{
|
||||
var key = prefix + suffix;
|
||||
if (parameters.ContainsKey(key))
|
||||
{
|
||||
parameters.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void WalkAnimator(
|
||||
Dictionary<string, DetectedParameter> parameters,
|
||||
AnimatorController controller
|
||||
)
|
||||
{
|
||||
if (controller == null) return;
|
||||
|
||||
foreach (var parameter in controller.parameters)
|
||||
{
|
||||
if (VRCSDKParameters.Contains(parameter.name)) continue;
|
||||
var param = new DetectedParameter()
|
||||
{
|
||||
OriginalName = parameter.name,
|
||||
IsPrefix = false
|
||||
};
|
||||
parameters[param.MapKey] = param;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyRemappings(ref Dictionary<string, DetectedParameter> parameters,
|
||||
ModularAvatarParameters parametersComponent)
|
||||
{
|
||||
Dictionary<string, DetectedParameter> newParams = new Dictionary<string, DetectedParameter>();
|
||||
|
||||
foreach (var map in parametersComponent.parameters)
|
||||
{
|
||||
var dictKey = map.nameOrPrefix + (map.isPrefix ? "*" : "");
|
||||
if (!parameters.ContainsKey(dictKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (map.internalParameter)
|
||||
{
|
||||
parameters.Remove(dictKey);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(map.remapTo))
|
||||
{
|
||||
var param = parameters[dictKey];
|
||||
param.OriginalName = map.remapTo;
|
||||
newParams[param.MapKey] = param;
|
||||
parameters.Remove(dictKey);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - warn of overlap? could be intentional...
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
newParams[param.Key] = param.Value;
|
||||
}
|
||||
|
||||
parameters = newParams;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb45d824aec1444fb553cecca1e042e3
|
||||
timeCreated: 1665870025
|
Loading…
Reference in New Issue
Block a user