feat: MA Shape Changer (#863)

* chore: migrate Scale Adjuster to NDMF preview system

* feat: MA Shape Changer

* chore: update NDMF dependency

* fix: unit test failure
This commit is contained in:
bd_ 2024-06-03 09:52:08 +09:00 committed by GitHub
parent 32c538a536
commit 8d3da50b37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 1993 additions and 1023 deletions

View File

@ -19,7 +19,7 @@
"dependencies": {}
},
"nadena.dev.ndmf": {
"version": "1.4.0"
"version": "1.5.0-alpha.0"
}
}
}

View File

@ -1,19 +1,20 @@
using System;
#region
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Data.Odbc;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using BuildContext = nadena.dev.ndmf.BuildContext;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
#endif
using Object = UnityEngine.Object;
#endregion
namespace nadena.dev.modular_avatar.animation
{
@ -72,9 +73,14 @@ namespace nadena.dev.modular_avatar.animation
{
return _currentClip;
}
public void SetCurrentNoInvalidate(Motion newMotion)
{
_currentClip = newMotion;
}
}
private ndmf.BuildContext _context;
private BuildContext _context;
private List<Action> _clipCommitActions = new List<Action>();
private List<ClipHolder> _clips = new List<ClipHolder>();
@ -119,7 +125,7 @@ namespace nadena.dev.modular_avatar.animation
}
}
internal void OnActivate(ndmf.BuildContext context)
internal void OnActivate(BuildContext context)
{
_context = context;

View File

@ -1,5 +1,14 @@
using System;
#region
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
#endregion
namespace nadena.dev.modular_avatar.animation
{
@ -17,11 +26,15 @@ namespace nadena.dev.modular_avatar.animation
/// </summary>
internal sealed class AnimationServicesContext : IExtensionContext
{
private BuildContext _context;
private AnimationDatabase _animationDatabase;
private PathMappings _pathMappings;
private Dictionary<GameObject, string> _selfProxies = new();
public void OnActivate(BuildContext context)
{
_context = context;
_animationDatabase = new AnimationDatabase();
_animationDatabase.OnActivate(context);
@ -65,5 +78,96 @@ namespace nadena.dev.modular_avatar.animation
return _pathMappings;
}
}
/// <summary>
/// Returns a parameter which proxies the "activeSelf" state of the specified GameObject.
/// </summary>
/// <param name="obj"></param>
/// <param name="paramName"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public bool TryGetActiveSelfProxy(GameObject obj, out string paramName)
{
if (_selfProxies.TryGetValue(obj, out paramName)) return !string.IsNullOrEmpty(paramName);
var path = PathMappings.GetObjectIdentifier(obj);
var clips = AnimationDatabase.ClipsForPath(path);
if (clips == null || clips.IsEmpty)
{
_selfProxies[obj] = "";
return false;
}
var iid = obj.GetInstanceID();
paramName = $"_MA/ActiveSelf/{iid}";
var binding = EditorCurveBinding.FloatCurve(path, typeof(GameObject), "m_IsActive");
bool hadAnyClip = false;
foreach (var clip in clips)
{
Motion newMotion = ProcessActiveSelf(clip.CurrentClip, paramName, binding);
if (newMotion != clip.CurrentClip)
{
clip.SetCurrentNoInvalidate(newMotion);
hadAnyClip = true;
}
}
if (hadAnyClip)
{
_selfProxies[obj] = paramName;
return true;
}
else
{
_selfProxies[obj] = "";
return false;
}
}
private Motion ProcessActiveSelf(Motion motion, string paramName, EditorCurveBinding binding)
{
if (motion is AnimationClip clip)
{
var curve = AnimationUtility.GetEditorCurve(clip, binding);
if (curve == null) return motion;
var newClip = new AnimationClip();
EditorUtility.CopySerialized(motion, newClip);
newClip.SetCurve("", typeof(Animator), paramName, curve);
return newClip;
}
else if (motion is BlendTree bt)
{
bool anyChanged = false;
var motions = bt.children.Select(c => // c is struct ChildMotion
{
var newMotion = ProcessActiveSelf(c.motion, paramName, binding);
anyChanged |= newMotion != c.motion;
c.motion = newMotion;
return c;
}).ToArray();
if (anyChanged)
{
var newBt = new BlendTree();
EditorUtility.CopySerialized(bt, newBt);
newBt.children = motions;
return newBt;
}
else
{
return bt;
}
}
else
{
return motion;
}
}
}
}

View File

@ -1,84 +0,0 @@
#region
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using JetBrains.Annotations;
using UnityEngine;
#endregion
namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
{
internal static class HandleUtilityPatches
{
internal static void Patch_FilterInstanceIDs(Harmony h)
{
var t_HandleUtility = AccessTools.TypeByName("UnityEditor.HandleUtility");
var m_orig = AccessTools.Method(t_HandleUtility, "FilterInstanceIDs");
var m_prefix = AccessTools.Method(typeof(HandleUtilityPatches), "Prefix_FilterInstanceIDs");
var m_postfix = AccessTools.Method(typeof(HandleUtilityPatches), "Postfix_FilterInstanceIDs");
h.Patch(original: m_orig, prefix: new HarmonyMethod(m_prefix), postfix: new HarmonyMethod(m_postfix));
}
[UsedImplicitly]
private static bool Prefix_FilterInstanceIDs(
ref IEnumerable<GameObject> gameObjects,
out int[] parentInstanceIDs,
out int[] childInstanceIDs
)
{
gameObjects = RemapObjects(gameObjects);
parentInstanceIDs = childInstanceIDs = null;
return true;
}
private static void Postfix_FilterInstanceIDs(
ref IEnumerable<GameObject> gameObjects,
ref int[] parentInstanceIDs,
ref int[] childInstanceIDs
)
{
HashSet<int> newChildInstanceIDs = null;
foreach (var parent in gameObjects)
{
foreach (var renderer in parent.GetComponentsInChildren<Renderer>())
{
if (renderer is SkinnedMeshRenderer smr &&
ProxyManager.OriginalToProxyRenderer.TryGetValue(smr, out var proxy) &&
proxy != null)
{
if (newChildInstanceIDs == null) newChildInstanceIDs = new HashSet<int>(childInstanceIDs);
newChildInstanceIDs.Add(proxy.GetInstanceID());
}
}
}
if (newChildInstanceIDs != null)
{
childInstanceIDs = newChildInstanceIDs.ToArray();
}
}
private static IEnumerable<GameObject> RemapObjects(IEnumerable<GameObject> objs)
{
return objs.Select(
obj =>
{
if (obj == null) return obj;
if (ProxyManager.OriginalToProxyObject.TryGetValue(obj, out var proxy) && proxy != null)
{
return proxy.gameObject;
}
else
{
return obj;
}
}
).ToArray();
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 807736f252df4b1b8402827257dcbea3
timeCreated: 1709354699

View File

@ -1,175 +0,0 @@
#region
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using JetBrains.Annotations;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
#endregion
namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
{
internal static class HierarchyViewPatches
{
private static readonly Type t_HierarchyProperty = AccessTools.TypeByName("UnityEditor.HierarchyProperty");
private static readonly PropertyInfo p_pptrValue = AccessTools.Property(t_HierarchyProperty, "pptrValue");
private static FieldInfo f_m_Rows; // List<TreeViewItem>
private static FieldInfo f_m_RowCount; // int
private static PropertyInfo p_objectPPTR;
internal static void Patch(Harmony h)
{
#if MODULAR_AVATAR_DEBUG_HIDDEN
return;
#endif
var t_GameObjectTreeViewDataSource = AccessTools.TypeByName("UnityEditor.GameObjectTreeViewDataSource");
var t_GameObjectTreeViewItem = AccessTools.TypeByName("UnityEditor.GameObjectTreeViewItem");
f_m_Rows = t_GameObjectTreeViewDataSource.GetField("m_Rows",
BindingFlags.NonPublic | BindingFlags.Instance);
f_m_RowCount =
t_GameObjectTreeViewDataSource.GetField("m_RowCount", BindingFlags.NonPublic | BindingFlags.Instance);
p_objectPPTR = t_GameObjectTreeViewItem.GetProperty("objectPPTR");
var m_orig = AccessTools.Method(t_GameObjectTreeViewDataSource, "InitTreeViewItem",
new[]
{
t_GameObjectTreeViewItem,
typeof(int),
typeof(Scene),
typeof(bool),
typeof(int),
typeof(Object),
typeof(bool),
typeof(int)
});
var m_patch = AccessTools.Method(typeof(HierarchyViewPatches), "Prefix_InitTreeViewItem");
h.Patch(original: m_orig, prefix: new HarmonyMethod(m_patch));
var m_InitRows = AccessTools.Method(t_GameObjectTreeViewDataSource, "InitializeRows");
var m_transpiler = AccessTools.Method(typeof(HierarchyViewPatches), "Transpile_InitializeRows");
h.Patch(original: m_InitRows,
transpiler: new HarmonyMethod(m_transpiler),
postfix: new HarmonyMethod(AccessTools.Method(typeof(HierarchyViewPatches), "Postfix_InitializeRows")),
prefix: new HarmonyMethod(AccessTools.Method(typeof(HierarchyViewPatches), "Prefix_InitializeRows"))
);
}
private static int skipped = 0;
private static void Prefix_InitializeRows()
{
skipped = 0;
}
[UsedImplicitly]
private static void Postfix_InitializeRows(object __instance)
{
var rows = (IList<TreeViewItem>)f_m_Rows.GetValue(__instance);
var rowCount = (int)f_m_RowCount.GetValue(__instance);
f_m_RowCount.SetValue(__instance, rowCount - skipped);
for (int i = 0; i < skipped; i++)
{
rows.RemoveAt(rows.Count - 1);
}
}
[UsedImplicitly]
private static IEnumerable<CodeInstruction> Transpile_InitializeRows(IEnumerable<CodeInstruction> instructions,
ILGenerator generator)
{
foreach (var c in Transpile_InitializeRows0(instructions, generator))
{
//Debug.Log(c);
yield return c;
}
}
[UsedImplicitly]
private static IEnumerable<CodeInstruction> Transpile_InitializeRows0(IEnumerable<CodeInstruction> instructions,
ILGenerator generator)
{
var m_shouldLoop = AccessTools.Method(typeof(HierarchyViewPatches), "ShouldLoop");
var m_Next = AccessTools.Method(t_HierarchyProperty, "Next", new[] { typeof(int[]) });
foreach (var c in instructions)
{
if (c.Is(OpCodes.Callvirt, m_Next))
{
var loopLabel = generator.DefineLabel();
var stash_arg = generator.DeclareLocal(typeof(int[]));
var stash_obj = generator.DeclareLocal(t_HierarchyProperty);
yield return new CodeInstruction(OpCodes.Stloc, stash_arg);
yield return new CodeInstruction(OpCodes.Stloc, stash_obj);
var tmp = new CodeInstruction(OpCodes.Ldloc, stash_obj);
tmp.labels.Add(loopLabel);
yield return tmp;
yield return new CodeInstruction(OpCodes.Ldloc, stash_arg);
yield return new CodeInstruction(OpCodes.Call, m_Next);
// Check if this item should be ignored.
yield return new CodeInstruction(OpCodes.Ldloc, stash_obj);
yield return new CodeInstruction(OpCodes.Call, m_shouldLoop);
yield return new CodeInstruction(OpCodes.Brtrue_S, loopLabel);
}
else
{
yield return c;
}
}
}
[UsedImplicitly]
private static bool ShouldLoop(object hierarchyProperty)
{
if (hierarchyProperty == null) return false;
var pptrValue = p_pptrValue.GetValue(hierarchyProperty);
if (pptrValue == null) return false;
var skip = ProxyManager.ProxyToOriginalObject.ContainsKey((GameObject)pptrValue);
if (skip) skipped++;
return skip;
}
private static bool Prefix_InitTreeViewItem(
object __instance,
ref object item,
int itemID,
Scene scene,
bool isSceneHeader,
int colorCode,
Object pptrObject,
ref bool hasChildren,
int depth
)
{
if (pptrObject == null || isSceneHeader) return true;
if (hasChildren && ProxyManager.ProxyToOriginalObject.ContainsKey((GameObject)pptrObject))
{
// See if there are any other children...
hasChildren = ((GameObject)pptrObject).transform.childCount > 1;
}
return true;
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 42f70698a5df48c0908400c425a2f6ee
timeCreated: 1709356304

View File

@ -13,11 +13,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
{
private static readonly Action<Harmony>[] patches = new Action<Harmony>[]
{
HierarchyViewPatches.Patch,
#if UNITY_2022_3_OR_NEWER
HandleUtilityPatches.Patch_FilterInstanceIDs,
PickingObjectPatch.Patch,
#endif
//HierarchyViewPatches.Patch,
};
[InitializeOnLoadMethod]

View File

@ -1,78 +0,0 @@
#region
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using UnityEngine;
using Object = UnityEngine.Object;
#endregion
namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
{
internal static class PickingObjectPatch
{
private static Type t_PickingObject = AccessTools.TypeByName("UnityEditor.PickingObject");
private static Type l_PickingObject =
typeof(List<>).MakeGenericType(new[] { t_PickingObject });
private static ConstructorInfo ctor_l = AccessTools.Constructor(l_PickingObject);
private static ConstructorInfo ctor_PickingObject =
AccessTools.Constructor(t_PickingObject, new[] { typeof(Object), typeof(int) });
private static PropertyInfo p_materialIndex = AccessTools.Property(t_PickingObject, "materialIndex");
private static MethodInfo m_TryGetGameObject = AccessTools.Method(t_PickingObject, "TryGetGameObject");
internal static void Patch(Harmony h)
{
var t_PickingObject = AccessTools.TypeByName("UnityEditor.PickingObject");
var ctor_PickingObject = AccessTools.Constructor(t_PickingObject, new[] { typeof(Object), typeof(int) });
var t_SceneViewPicking = AccessTools.TypeByName("UnityEditor.SceneViewPicking");
var m_GetAllOverlapping = AccessTools.Method(t_SceneViewPicking, "GetAllOverlapping");
var m_postfix = AccessTools.Method(typeof(PickingObjectPatch), nameof(Postfix_GetAllOverlapping));
h.Patch(original: m_GetAllOverlapping, postfix: new HarmonyMethod(m_postfix));
}
private static void Postfix_GetAllOverlapping(ref object __result)
{
var erased = (IEnumerable)__result;
var list = (IList)ctor_l.Invoke(new object[0]);
foreach (var obj in erased)
{
if (obj == null)
{
list.Add(obj);
continue;
}
var args = new object[] { null };
if ((bool)m_TryGetGameObject.Invoke(obj, args))
{
var go = args[0] as GameObject;
if (go != null && ProxyManager.ProxyToOriginalObject.TryGetValue(go, out var original))
{
list.Add(ctor_PickingObject.Invoke(new[]
{
original,
p_materialIndex.GetValue(obj)
}));
continue;
}
}
list.Add(obj);
}
__result = list;
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: cf06818f1c0c436fbae7f755d7110aba
timeCreated: 1709359553

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2400605596824043a59b55dcb2a8e89a
timeCreated: 1717196942

View File

@ -0,0 +1,99 @@
#region
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
#endregion
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
{
public class AddShapePopup : PopupWindowContent
{
private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/ShapeChanger/";
const string UxmlPath = Root + "AddShapePopup.uxml";
const string UssPath = Root + "ShapeChangerStyles.uss";
private VisualElement _elem;
private ScrollView _scrollView;
public AddShapePopup(ModularAvatarShapeChanger changer)
{
if (changer == null) return;
var target = changer.targetRenderer.Get(changer)?.GetComponent<SkinnedMeshRenderer>();
if (target == null || target.sharedMesh == null) return;
var alreadyRegistered = changer.Shapes.Select(c => c.ShapeName).ToHashSet();
var keys = new List<string>();
for (int i = 0; i < target.sharedMesh.blendShapeCount; i++)
{
var name = target.sharedMesh.GetBlendShapeName(i);
if (alreadyRegistered.Contains(name)) continue;
keys.Add(name);
}
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
_elem = uxml.CloneTree();
_elem.styleSheets.Add(uss);
Localization.UI.Localize(_elem);
_scrollView = _elem.Q<ScrollView>("scroll-view");
if (keys.Count > 0)
{
_scrollView.contentContainer.Clear();
foreach (var key in keys)
{
var container = new VisualElement();
container.AddToClassList("add-shape-row");
Button btn = default;
btn = new Button(() =>
{
AddShape(changer, key);
container.RemoveFromHierarchy();
});
btn.text = "+";
container.Add(btn);
var label = new Label(key);
container.Add(label);
_scrollView.contentContainer.Add(container);
}
}
}
private void AddShape(ModularAvatarShapeChanger changer, string key)
{
Undo.RecordObject(changer, "Add Shape");
changer.Shapes.Add(new ChangedShape()
{
ShapeName = key,
ChangeType = ShapeChangeType.Delete,
Value = 100
});
}
public override void OnGUI(Rect rect)
{
}
public override void OnOpen()
{
editorWindow.rootVisualElement.Clear();
editorWindow.rootVisualElement.Add(_elem);
//editorWindow.rootVisualElement.Clear();
//editorWindow.rootVisualElement.Add(new Label("Hello, World!"));
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1a8351fafb3740918363f60365adfeda
timeCreated: 1717205112

View File

@ -0,0 +1,11 @@
<UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement class="add-shape-popup">
<ui:VisualElement class="add-shape-popup">
<ui:Label text="Select Blendshape"/>
<ui:VisualElement class="vline"/>
<ui:ScrollView show-horizontal-scroller="false" name="scroll-view">
<ui:Label text="&lt;none remaining>" class="placeholder"/>
</ui:ScrollView>
</ui:VisualElement>
</ui:VisualElement>
</UXML>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6753a7b3eae1416cb04786cf53778c33
timeCreated: 1717205258

View File

@ -0,0 +1,44 @@
#region
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
#endregion
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
{
[CustomPropertyDrawer(typeof(ChangedShape))]
public class ChangedShapeEditor : PropertyDrawer
{
private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/ShapeChanger/";
const string UxmlPath = Root + "ChangedShapeEditor.uxml";
const string UssPath = Root + "ShapeChangerStyles.uss";
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath).CloneTree();
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
Localization.UI.Localize(uxml);
uxml.styleSheets.Add(uss);
uxml.BindProperty(property);
uxml.Q<PropertyField>("f-change-type").RegisterCallback<ChangeEvent<string>>(
e =>
{
if (e.newValue == "Delete")
{
uxml.AddToClassList("change-type-delete");
}
else
{
uxml.RemoveFromClassList("change-type-delete");
}
}
);
return uxml;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c0778099a393468c9775fd9a99b321af
timeCreated: 1717198244

View File

@ -0,0 +1,8 @@
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
<ui:VisualElement class="changed-shape-editor">
<ui:Label text="&lt;shape name>" binding-path="ShapeName" name="f-name"/>
<ed:PropertyField binding-path="ChangeType" label="" name="f-change-type"/>
<ed:PropertyField binding-path="Value" label="" name="f-value" class="f-value"/>
<ui:VisualElement name="f-value-delete" class="f-value"/>
</ui:VisualElement>
</UXML>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d8131b91fff04f9e94b71d70105ae05b
timeCreated: 1717198707

View File

@ -0,0 +1,24 @@
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements"
xmlns:ma="nadena.dev.modular_avatar.core.editor">
<ui:VisualElement name="root-box">
<ui:VisualElement name="group-box">
<ed:PropertyField binding-path="m_targetRenderer"/>
<ui:VisualElement name="ListViewContainer">
<ui:ListView virtualization-method="DynamicHeight"
reorder-mode="Animated"
reorderable="true"
show-add-remove-footer="true"
show-border="true"
show-foldout-header="false"
name="Shapes"
item-height="100"
binding-path="m_shapes"
style="flex-grow: 1;"
/>
</ui:VisualElement>
</ui:VisualElement>
<ma:LanguageSwitcherElement/>
</ui:VisualElement>
</UXML>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c8267295b2dc4d90b92dcc289f6d31c4
timeCreated: 1717197307

View File

@ -0,0 +1,52 @@
#region
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using static System.Reflection.BindingFlags;
using PopupWindow = UnityEditor.PopupWindow;
#endregion
namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
{
[CustomEditor(typeof(ModularAvatarShapeChanger))]
public class ShapeChangerEditor : MAEditorBase
{
[SerializeField] private StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml;
protected override void OnInnerInspectorGUI()
{
throw new NotImplementedException();
}
protected override VisualElement CreateInnerInspectorGUI()
{
var root = uxml.CloneTree();
Localization.UI.Localize(root);
root.styleSheets.Add(uss);
root.Bind(serializedObject);
var listView = root.Q<ListView>("Shapes");
listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
// The Add button callback isn't exposed publicly for some reason...
var field_addButton = typeof(BaseListView).GetField("m_AddButton", NonPublic | Instance);
var addButton = (Button)field_addButton.GetValue(listView);
addButton.clickable = new Clickable(() =>
{
PopupWindow.Show(addButton.worldBound, new AddShapePopup(target as ModularAvatarShapeChanger));
});
return root;
}
}
}

View File

@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 419e9c8f5ec74da9a4cdd702f7e63902
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- uss: {fileID: 7433441132597879392, guid: 8b52b9cfc8514c55af39aeb11de7f279, type: 3}
- uxml: {fileID: 9197481963319205126, guid: c8267295b2dc4d90b92dcc289f6d31c4, type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,112 @@
VisualElement {
}
#group-box {
margin-top: 4px;
margin-bottom: 4px;
padding: 4px;
border-width: 3px;
border-left-color: rgba(0, 1, 0, 0.2);
border-top-color: rgba(0, 1, 0, 0.2);
border-right-color: rgba(0, 1, 0, 0.2);
border-bottom-color: rgba(0, 1, 0, 0.2);
border-radius: 4px;
/* background-color: rgba(0, 0, 0, 0.1); */
}
#ListViewContainer {
margin-top: 4px;
}
#group-box > Label {
-unity-font-style: bold;
}
.group-root {
margin-top: 4px;
}
.group-root Toggle {
margin-left: 0;
}
.group-children {
padding-left: 10px;
}
.left-toggle {
display: flex;
flex-direction: row;
}
.changed-shape-editor {
flex-direction: row;
}
.changed-shape-editor #f-name {
flex-grow: 1;
}
.changed-shape-editor #f-change-type {
flex-grow: 0;
}
.changed-shape-editor #f-value {
flex-grow: 0;
}
.changed-shape-editor PropertyField Label {
display: none;
}
#f-change-type {
width: 75px;
}
.f-value {
width: 40px;
}
#f-value-delete {
display: none;
}
.change-type-delete #f-value {
display: none;
}
.change-type-delete #f-value-delete {
display: flex;
}
/* Add shape window */
.add-shape-popup {
margin: 2px;
}
.vline {
width: 100%;
height: 4px;
border-top-width: 4px;
margin-top: 2px;
margin-bottom: 2px;
border-top-color: rgba(0, 0, 0, 0.2);
}
.add-shape-row {
flex-direction: row;
}
.add-shape-row Button {
flex-grow: 0;
}
.add-shape-popup ScrollView Label.placeholder {
-unity-text-align: middle-center;
}
.add-shape-row Label {
flex-grow: 1;
-unity-text-align: middle-left;
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8b52b9cfc8514c55af39aeb11de7f279
timeCreated: 1717197307

View File

@ -85,10 +85,10 @@ namespace nadena.dev.modular_avatar.core.editor
TopoProcessMergeArmatures(mergeArmatures);
#if MA_VRCSDK3_AVATARS
foreach (var c in avatarGameObject.transform.GetComponentsInChildren<ScaleProxy>(true))
/*foreach (var c in avatarGameObject.transform.GetComponentsInChildren<ScaleProxy>(true))
{
BoneDatabase.AddMergedBone(c.transform);
}
}*/
foreach (var c in avatarGameObject.transform.GetComponentsInChildren<VRCPhysBone>(true))
{

View File

@ -46,7 +46,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
{
seq.Run(ClearEditorOnlyTags.Instance);
seq.Run(MeshSettingsPluginPass.Instance);
seq.Run(ScaleAdjusterPass.Instance);
seq.Run(ScaleAdjusterPass.Instance).PreviewingWith(new ScaleAdjusterPreview());
#if MA_VRCSDK3_AVATARS
seq.Run(RenameParametersPluginPass.Instance);
seq.Run(MergeBlendTreePass.Instance);
@ -56,6 +56,8 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
#endif
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
{
seq.Run("Shape Changer", ctx => new ShapeChangerPass(ctx).Execute())
.PreviewingWith(new ShapeChangerPreview());
seq.Run(MergeArmaturePluginPass.Instance);
seq.Run(BoneProxyPluginPass.Instance);
seq.Run(VisibleHeadAccessoryPluginPass.Instance);

View File

@ -0,0 +1,155 @@
#region
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
#endregion
namespace nadena.dev.modular_avatar.core.editor.ScaleAdjuster
{
internal class ScaleAdjustedBones
{
private static int editorFrameCount = 0;
private static int lastMutatingUpdate = 0;
private static int mutatingUpdateCount = 0;
[InitializeOnLoadMethod]
static void Init()
{
EditorApplication.update += () => editorFrameCount++;
}
internal class BoneState
{
public Component original;
public Transform proxy;
public int lastUsedFrame;
public BoneState parentHint;
}
private Dictionary<Component, BoneState> _bones = new(new ObjectIdentityComparer<Component>());
//private List<BoneState> _states = new List<BoneState>();
public BoneState GetBone(Component src, bool force = true)
{
if (src == null) return null;
if (_bones.TryGetValue(src, out var state))
{
state.lastUsedFrame = mutatingUpdateCount;
return state;
}
if (!force) return null;
var proxyObj = new GameObject(src.name);
proxyObj.hideFlags = HideFlags.DontSave;
proxyObj.AddComponent<SelfDestructComponent>().KeepAlive = this;
#if MODULAR_AVATAR_DEBUG_HIDDEN
proxyObj.hideFlags = HideFlags.DontSave;
#endif
var boneState = new BoneState();
boneState.original = src;
boneState.proxy = proxyObj.transform;
boneState.parentHint = null;
boneState.lastUsedFrame = Time.frameCount;
_bones[src] = boneState;
CheckParent(CopyState(boneState), boneState);
return boneState;
}
private List<Component> toRemove = new List<Component>();
private List<BoneState> stateList = new List<BoneState>();
public void Update()
{
if (lastMutatingUpdate != editorFrameCount)
{
mutatingUpdateCount++;
lastMutatingUpdate = editorFrameCount;
}
toRemove.Clear();
stateList.Clear();
stateList.AddRange(_bones.Values);
foreach (var entry in stateList)
{
if (entry.original == null || entry.proxy == null)
{
if (entry.proxy != null)
{
Object.DestroyImmediate(entry.proxy.gameObject);
}
toRemove.Add(entry.original);
continue;
}
if (mutatingUpdateCount - entry.lastUsedFrame > 5 && entry.proxy.childCount == 0)
{
Object.DestroyImmediate(entry.proxy.gameObject);
toRemove.Add(entry.original);
continue;
}
if (entry.original.gameObject.scene != entry.proxy.gameObject.scene)
{
SceneManager.MoveGameObjectToScene(entry.proxy.gameObject, entry.original.gameObject.scene);
}
Transform parent = CopyState(entry);
CheckParent(parent, entry);
}
foreach (var remove in toRemove)
{
_bones.Remove(remove);
}
}
private void CheckParent(Transform parent, BoneState entry)
{
if (parent != entry.parentHint?.original)
{
entry.parentHint = GetBone(parent);
entry.proxy.SetParent(entry.parentHint?.proxy, false);
}
}
private static Transform CopyState(BoneState entry)
{
Transform parent;
if (entry.original is Transform t)
{
parent = t.parent;
entry.proxy.localPosition = t.localPosition;
entry.proxy.localRotation = t.localRotation;
entry.proxy.localScale = t.localScale;
}
else
{
parent = entry.original.transform;
if (entry.original is ModularAvatarScaleAdjuster sa)
{
entry.proxy.localPosition = Vector3.zero;
entry.proxy.localRotation = Quaternion.identity;
entry.proxy.localScale = sa.Scale;
}
}
return parent;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b62de718b16e4f47b48e2354235ee158
timeCreated: 1716607885

View File

@ -0,0 +1,83 @@
#region
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.core.editor.ScaleAdjuster;
using nadena.dev.ndmf.preview;
using nadena.dev.ndmf.rq;
using nadena.dev.ndmf.rq.unity.editor;
using UnityEditor;
using UnityEngine;
#endregion
namespace nadena.dev.modular_avatar.core.editor
{
internal class ScaleAdjusterPreview : IRenderFilter
{
private ScaleAdjustedBones _bones = new ScaleAdjustedBones();
[InitializeOnLoadMethod]
private static void StaticInit()
{
}
private static GameObject FindAvatarRootObserving(ComputeContext ctx, GameObject ptr)
{
while (ptr != null)
{
ctx.Observe(ptr);
var xform = ptr.transform;
if (RuntimeUtil.IsAvatarRoot(xform)) return ptr;
ptr = xform.parent?.gameObject;
}
return null;
}
public ReactiveValue<IImmutableList<IImmutableList<Renderer>>> TargetGroups { get; } =
ReactiveValue<IImmutableList<IImmutableList<Renderer>>>.Create(
"Scale Adjuster: Find targets",
async ctx =>
{
var scaleAdjusters = await ctx.Observe(CommonQueries.GetComponentsByType<ModularAvatarScaleAdjuster>());
HashSet<Renderer> targets = new HashSet<Renderer>();
foreach (var adjuster in scaleAdjusters)
{
// Find parent object
// TODO: Reactive helper
var root = FindAvatarRootObserving(ctx, adjuster.gameObject);
if (root == null) continue;
var renderers = ctx.GetComponentsInChildren<Renderer>(root, true);
foreach (var renderer in renderers)
{
targets.Add(renderer);
}
}
return targets.Select(r => (IImmutableList<Renderer>)ImmutableList.Create(r)).ToImmutableList();
});
public void OnFrame(Renderer original, Renderer proxy)
{
if (proxy is SkinnedMeshRenderer p_smr && original is SkinnedMeshRenderer o_smr)
{
p_smr.rootBone = _bones.GetBone(o_smr.rootBone)?.proxy ?? o_smr.rootBone;
p_smr.bones = o_smr.bones.Select(b =>
{
var sa = (Component)b?.GetComponent<ModularAvatarScaleAdjuster>();
return _bones.GetBone(sa ?? b, true)?.proxy ?? b;
}).ToArray();
}
_bones.Update();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 54077f1844e74fa6a344b94d83a2044f
timeCreated: 1716174270

View File

@ -1,28 +0,0 @@
using UnityEditor;
namespace nadena.dev.modular_avatar.core.editor.ScaleAdjuster
{
#if !UNITY_2022_3_OR_NEWER
internal static class SelectionHack
{
[InitializeOnLoadMethod]
static void Init()
{
Selection.selectionChanged += OnSelectionChanged;
}
static void OnSelectionChanged()
{
var gameObject = Selection.activeGameObject;
if (gameObject != null && gameObject.GetComponent<ScaleAdjusterRenderer>() != null)
{
EditorApplication.delayCall += () =>
{
Selection.activeGameObject = gameObject.transform.parent.gameObject;
};
}
}
}
#endif
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: cfa3ba0c82bc4439aa86228715f61831
timeCreated: 1709376243

View File

@ -30,14 +30,14 @@ namespace nadena.dev.modular_avatar.core.editor
}
// Legacy cleanup
foreach (var sar in context.AvatarRootObject.GetComponentsInChildren<ScaleAdjusterRenderer>())
/*foreach (var sar in context.AvatarRootObject.GetComponentsInChildren<ScaleAdjusterRenderer>())
{
Object.DestroyImmediate(sar.gameObject);
}
foreach (var sar in context.AvatarRootObject.GetComponentsInChildren<ScaleProxy>())
{
Object.DestroyImmediate(sar.gameObject);
}
}*/
if (boneMappings.Count == 0)
{

3
Editor/ShapeChanger.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 57bb6d1a371c4993bf7cfed797b2eb65
timeCreated: 1717105136

View File

@ -0,0 +1,33 @@
# Semantics
Shape Changer will set the specified shape keys when the GameObject it is on is active and enabled.
This analysis is based on the original hierarchy position of the GameObject in question, not the position after any
Merge Armature or Bone Proxy operations.
## Shape Key Modes
Shape keys can be set to either
* Set - Set the shape key to the given value
* Delete - If the GameObject is always active, deletes the associated triangles and vertices. Otherwise, sets the shape
key to 100%.
## Conflict resolution
First, if any Delete action is active, we will delete the affected vertices (if static) or set to 100 (if dynamic).
Otherwise, the last object in hierarchy traversal order wins.
Implementation-wise, we first determine if the result is static (ie, if the last object - or multiple objects with
consistent settings - is non-animated). If so, we take that result.
Otherwise, we:
* Create animator parameters to proxy game object active states. These are injected into any animation clips which
manipulate relevant game objects.
* Build a blend tree to determine the final state.
* For delete actions, we create a blend tree which effectively adds together all the active objects.
* For set actions, we create a blend tree which selects the last active object.
* This blend tree works by exploiting the FP32 exponent field. Each object is assigned a floating point value with a
unique exponent and a zero mantissa; we skip every other exponent here, in order to provide a "guard band" between
values. We then sum these using a direct blend tree.
* We finally use a two-level 1D blend tree to evaluate the delete action and then any set actions.

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ca1da54f230b498fa21d1a70d196a12a
timeCreated: 1717123475

View File

@ -0,0 +1,320 @@
#region
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
#endregion
namespace nadena.dev.modular_avatar.core.editor
{
internal static class RemoveBlendShapeFromMesh
{
private const float THRESHOLD = 0.001f;
public static Mesh RemoveBlendshapes(Mesh original, IEnumerable<int> targets)
{
var mesh = new Mesh();
mesh.indexFormat = original.indexFormat;
mesh.bounds = original.bounds;
bool[] toDeleteVertices = new bool[original.vertexCount];
bool[] toRetainVertices = new bool[original.vertexCount];
ProbeBlendshapes(original, toDeleteVertices, targets);
ProbeRetainedVertices(original, toDeleteVertices, toRetainVertices);
RemapVerts(toRetainVertices, out var origToNewVertIndex, out var newToOrigVertIndex);
TransferVertexData(mesh, original, toRetainVertices);
TransferBoneData(mesh, original, toRetainVertices);
mesh.bindposes = original.bindposes;
TransferShapes(mesh, original, newToOrigVertIndex);
UpdateTriangles(mesh, original, toRetainVertices, origToNewVertIndex);
return mesh;
}
private static VertexAttribute[] uvAttrs = new[]
{
VertexAttribute.TexCoord0,
VertexAttribute.TexCoord1,
VertexAttribute.TexCoord2,
VertexAttribute.TexCoord3,
VertexAttribute.TexCoord4,
VertexAttribute.TexCoord5,
VertexAttribute.TexCoord6,
VertexAttribute.TexCoord7,
};
private static void TransferVertexData(Mesh mesh, Mesh original, bool[] toRetain)
{
List<Vector2> tmpVec2 = new();
List<Vector3> tmpVec3 = new();
List<Vector4> tmpVec4 = new();
TransferData(tmpVec3, mesh.SetVertices, original.GetVertices);
TransferData(tmpVec3, mesh.SetNormals, original.GetNormals);
TransferData(tmpVec4, mesh.SetTangents, original.GetTangents);
for (int uv = 0; uv < 8; uv++)
{
if (!original.HasVertexAttribute(uvAttrs[uv])) continue;
switch (original.GetVertexAttributeDimension(uvAttrs[uv]))
{
case 2:
TransferData(tmpVec2, l => mesh.SetUVs(uv, l), l => original.GetUVs(uv, l));
break;
case 3:
TransferData(tmpVec3, l => mesh.SetUVs(uv, l), l => original.GetUVs(uv, l));
break;
case 4:
TransferData(tmpVec4, l => mesh.SetUVs(uv, l), l => original.GetUVs(uv, l));
break;
default:
throw new ArgumentOutOfRangeException();
}
}
void TransferData<T>(List<T> tmp, Action<List<T>> setter, Action<List<T>> getter)
{
getter(tmp);
int index = 0;
tmp.RemoveAll(_t => !toRetain[index++]);
setter(tmp);
}
}
private static void TransferShapes(Mesh mesh, Mesh original, int[] newToOrigVertIndex)
{
Vector3[] o_pos = new Vector3[original.vertexCount];
Vector3[] n_pos = new Vector3[mesh.vertexCount];
Vector3[] o_nrm = new Vector3[original.vertexCount];
Vector3[] n_nrm = new Vector3[mesh.vertexCount];
Vector3[] o_tan = new Vector3[original.vertexCount];
Vector3[] n_tan = new Vector3[mesh.vertexCount];
int blendshapeCount = original.blendShapeCount;
for (int s = 0; s < blendshapeCount; s++)
{
int frameCount = original.GetBlendShapeFrameCount(s);
var shapeName = original.GetBlendShapeName(s);
for (int f = 0; f < frameCount; f++)
{
original.GetBlendShapeFrameVertices(s, f, o_pos, o_nrm, o_tan);
Remap();
var frameWeight = original.GetBlendShapeFrameWeight(s, f);
mesh.AddBlendShapeFrame(shapeName, frameWeight, n_pos, n_nrm, n_tan);
}
}
void Remap()
{
for (int i = 0; i < n_pos.Length; i++)
{
try
{
n_pos[i] = o_pos[newToOrigVertIndex[i]];
n_nrm[i] = o_nrm[newToOrigVertIndex[i]];
n_tan[i] = o_tan[newToOrigVertIndex[i]];
}
catch (IndexOutOfRangeException e)
{
throw;
}
}
}
}
private static void TransferBoneData(Mesh mesh, Mesh original, bool[] toRetain)
{
var origBoneWeights = original.GetAllBoneWeights();
var origBonesPerVertex = original.GetBonesPerVertex();
List<BoneWeight1> boneWeights = new(origBoneWeights.Length);
List<byte> bonesPerVertex = new(origBonesPerVertex.Length);
int ptr = 0;
for (int i = 0; i < toRetain.Length; i++)
{
byte n_weight = origBonesPerVertex[i];
if (toRetain[i])
{
for (int j = 0; j < n_weight; j++)
{
boneWeights.Add(origBoneWeights[ptr + j]);
}
bonesPerVertex.Add(n_weight);
}
ptr += n_weight;
}
var native_boneWeights = new NativeArray<BoneWeight1>(boneWeights.ToArray(), Allocator.Temp);
var native_bonesPerVertex = new NativeArray<byte>(bonesPerVertex.ToArray(), Allocator.Temp);
mesh.SetBoneWeights(native_bonesPerVertex, native_boneWeights);
native_boneWeights.Dispose();
native_bonesPerVertex.Dispose();
}
private static void UpdateTriangles(Mesh mesh, Mesh original, bool[] toRetainVertices, int[] origToNewVertIndex)
{
int submeshCount = original.subMeshCount;
List<int> orig_tris = new List<int>();
List<int> new_tris = new List<int>();
List<ushort> orig_tris_16 = new List<ushort>();
List<ushort> new_tris_16 = new List<ushort>();
mesh.subMeshCount = submeshCount;
for (int sm = 0; sm < submeshCount; sm++)
{
if (original.indexFormat == IndexFormat.UInt32)
{
original.GetTriangles(orig_tris, sm, true);
ProcessSubmesh<int>(orig_tris, new_tris, i => i, i => i);
int min = Math.Max(0, new_tris.Min());
for (int i = 0; i < new_tris.Count; i++)
{
new_tris[i] -= min;
}
mesh.SetTriangles(new_tris, sm, true, min);
}
else
{
original.GetTriangles(orig_tris_16, sm, true);
ProcessSubmesh<ushort>(orig_tris_16, new_tris_16, i => i, i => (ushort)i);
ushort min = new_tris_16.Min();
for (int i = 0; i < new_tris_16.Count; i++)
{
new_tris_16[i] -= min;
}
mesh.SetTriangles(new_tris_16, sm, true, min);
}
}
void ProcessSubmesh<T>(List<T> orig_tri, List<T> new_tri, Func<T, int> toInt, Func<int, T> fromInt)
{
int limit = orig_tri.Count - 2;
new_tri.Clear();
for (int i = 0; i < limit; i += 3)
{
if (!toRetainVertices[toInt(orig_tri[i])]
|| !toRetainVertices[toInt(orig_tri[i + 1])]
|| !toRetainVertices[toInt(orig_tri[i + 2])]
)
{
continue;
}
new_tri.Add(fromInt(origToNewVertIndex[toInt(orig_tri[i])]));
new_tri.Add(fromInt(origToNewVertIndex[toInt(orig_tri[i + 1])]));
new_tri.Add(fromInt(origToNewVertIndex[toInt(orig_tri[i + 2])]));
}
if (new_tri.Count == 0)
{
new_tri.Add(default);
new_tri.Add(default);
new_tri.Add(default);
}
}
}
private static void RemapVerts(bool[] toRetainVertices, out int[] origToNewVertIndex,
out int[] newToOrigVertIndex)
{
List<int> n2o = new List<int>(toRetainVertices.Length);
List<int> o2n = new List<int>(toRetainVertices.Length);
int i = 0;
for (int j = 0; j < toRetainVertices.Length; j++)
{
if (toRetainVertices[j])
{
o2n.Add(n2o.Count);
n2o.Add(j);
}
else
{
o2n.Add(-1);
}
}
newToOrigVertIndex = n2o.ToArray();
origToNewVertIndex = o2n.ToArray();
}
private static void ProbeBlendshapes(Mesh mesh, bool[] toDeleteVertices, IEnumerable<int> shapes)
{
var bsPos = new Vector3[mesh.vertexCount];
foreach (var index in shapes)
{
int frames = mesh.GetBlendShapeFrameCount(index);
for (int f = 0; f < frames; f++)
{
mesh.GetBlendShapeFrameVertices(index, f, bsPos, null, null);
for (int i = 0; i < bsPos.Length; i++)
{
if (bsPos[i].sqrMagnitude > 0.0001f)
{
toDeleteVertices[i] = true;
}
}
}
}
}
private static void ProbeRetainedVertices(Mesh mesh, bool[] toDeleteVertices, bool[] toRetainVertices)
{
List<int> tris = new List<int>();
for (int subMesh = 0; subMesh < mesh.subMeshCount; subMesh++)
{
tris.Clear();
var baseVertex = (int)mesh.GetBaseVertex(subMesh);
mesh.GetTriangles(tris, subMesh, false);
for (int i = 0; i < tris.Count; i += 3)
{
if (toDeleteVertices[tris[i] + baseVertex] || toDeleteVertices[tris[i + 1] + baseVertex] ||
toDeleteVertices[tris[i + 2] + baseVertex])
{
continue;
}
else
{
toRetainVertices[tris[i] + baseVertex] = true;
toRetainVertices[tris[i + 1] + baseVertex] = true;
toRetainVertices[tris[i + 2] + baseVertex] = true;
}
}
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4e45c8fdd80a4a37bd7f2bd3616886bb
timeCreated: 1717209652

View File

@ -0,0 +1,616 @@
#region
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
#endregion
namespace nadena.dev.modular_avatar.core.editor
{
internal class ShapeChangerPass
{
struct ShapeKey
{
public int RendererInstanceId;
public int ShapeIndex;
public string ShapeKeyName; // not equated
public bool Equals(ShapeKey other)
{
return RendererInstanceId == other.RendererInstanceId && ShapeIndex == other.ShapeIndex;
}
public override bool Equals(object obj)
{
return obj is ShapeKey other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
return (RendererInstanceId * 397) ^ ShapeIndex;
}
}
}
class ShapeKeyInfo
{
public ShapeKey ShapeKey { get; }
public string SetParam { get; set; }
public string DeleteParam { get; set; }
public bool alwaysDeleted;
// Objects which trigger deletion of this shape key.
public List<GameObject> deletionObjects = new List<GameObject>();
public List<ActionGroupKey> setObjects = new List<ActionGroupKey>();
public ShapeKeyInfo(ShapeKey key)
{
ShapeKey = key;
}
}
class ActionGroupKey
{
public ActionGroupKey(AnimationServicesContext asc, ShapeKey key, GameObject controllingObject,
ChangedShape shape)
{
ShapeKey = key;
InitiallyActive = controllingObject?.activeInHierarchy == true;
var origControlling = controllingObject?.name ?? "<null>";
while (controllingObject != null && !asc.TryGetActiveSelfProxy(controllingObject, out _))
{
controllingObject = controllingObject.transform.parent?.gameObject;
}
var newControlling = controllingObject?.name ?? "<null>";
Debug.Log("AGK: Controlling object " + origControlling + " => " + newControlling);
ControllingObject = controllingObject;
IsDelete = shape.ChangeType == ShapeChangeType.Delete;
Value = IsDelete ? 100 : shape.Value;
}
public ShapeKey ShapeKey;
public bool IsDelete;
public float Value;
public float ConditionKey;
// When constructing the 1D blend tree to interpret the sum-of-condition-keys value, we need to ensure that
// all valid values are solidly between two control points with the same animation clip, to avoid undesired
// interpolation. This is done by constructing a "guard band":
// [ valid range ] [ guard band ] [ valid range ]
//
// The valid range must contain all values that could be created by valid summations. We therefore reserve
// a "guard band" in between; by reserving the exponent below each valid stop, we can put our guard bands
// in there.
// [ valid ] [ guard ] [ valid ]
// ^-r0 ^-g0 ^-g1
// ^- r1
// g0 = r1 / 2 = r0 * 2
// g1 = BitDecrement(r1) (we don't actually use this currently as r0-g0 is enough)
public float Guard => ConditionKey * 2;
public bool ConditionKeyIsValid => float.IsFinite(ConditionKey)
&& float.IsFinite(Guard)
&& ConditionKey > 0;
public GameObject ControllingObject;
public bool InitiallyActive;
public override string ToString()
{
var obj = ControllingObject?.name ?? "<null>";
return $"AGK: {ShapeKey.RendererInstanceId}:{ShapeKey.ShapeIndex} ({ShapeKey.ShapeKeyName})={Value} " +
$"range={ConditionKey}/{Guard} controlling object={obj}";
}
public bool TryMerge(ActionGroupKey other)
{
if (!ShapeKey.Equals(other.ShapeKey)) return false;
if (Mathf.Abs(Value - other.Value) > 0.001f) return false;
if (ControllingObject != other.ControllingObject) return false;
IsDelete |= other.IsDelete;
return true;
}
}
private readonly ndmf.BuildContext context;
private Dictionary<string, float> initialValues = new();
public ShapeChangerPass(ndmf.BuildContext context)
{
this.context = context;
}
internal void Execute()
{
Dictionary<ShapeKey, ShapeKeyInfo> shapes = FindShapes(context);
foreach (var groups in shapes.Values)
{
ProcessShapeKey(groups);
}
ProcessMeshDeletion(shapes);
}
#region Mesh processing
private void ProcessMeshDeletion(Dictionary<ShapeKey, ShapeKeyInfo> shapes)
{
ImmutableDictionary<int /* renderer */, List<ShapeKeyInfo>> renderers = shapes.Values.GroupBy(
v => v.ShapeKey.RendererInstanceId
).ToImmutableDictionary(
g => g.Key,
g => g.ToList()
);
foreach (var (rendererId, infos) in renderers)
{
var renderer = (SkinnedMeshRenderer)EditorUtility.InstanceIDToObject(rendererId);
if (renderer == null) continue;
var mesh = renderer.sharedMesh;
if (mesh == null) continue;
renderer.sharedMesh = RemoveBlendShapeFromMesh.RemoveBlendshapes(
mesh,
infos.Where(i => i.alwaysDeleted).Select(i => i.ShapeKey.ShapeIndex)
);
}
}
#endregion
private void ProcessShapeKey(ShapeKeyInfo info)
{
// TODO: prune non-animated keys
// Check if this is non-animated and skip most processing if so
if (info.alwaysDeleted) return;
if (info.setObjects[^1].ControllingObject == null)
{
var renderer = (SkinnedMeshRenderer)EditorUtility.InstanceIDToObject(info.ShapeKey.RendererInstanceId);
renderer.SetBlendShapeWeight(info.ShapeKey.ShapeIndex, info.setObjects[0].Value);
return;
}
// This value is the first non-subnormal float32
float shift = BitConverter.Int32BitsToSingle(0x00800000);
foreach (var group in info.setObjects)
{
group.ConditionKey = shift;
shift *= 4;
if (!group.ConditionKeyIsValid)
{
throw new ArithmeticException("Floating point overflow - too many shape key controls");
}
}
info.SetParam =
$"_MA/ShapeChanger/{info.ShapeKey.RendererInstanceId}/{info.ShapeKey.ShapeIndex}/set";
info.DeleteParam = $"_MA/ShapeChanger/{info.ShapeKey.RendererInstanceId}/{info.ShapeKey.ShapeIndex}/delete";
var summationTree = BuildSummationTree(info);
var applyTree = BuildApplyTree(info);
var merged = BuildMergeTree(summationTree, applyTree);
ApplyController(merged, "ShapeChanger Apply: " + info.ShapeKey.ShapeKeyName);
}
private BlendTree BuildMergeTree(BlendTree summationTree, BlendTree applyTree)
{
var bt = new BlendTree();
bt.blendType = BlendTreeType.Direct;
bt.blendParameter = MergeBlendTreePass.ALWAYS_ONE;
bt.useAutomaticThresholds = false;
bt.children = new[]
{
new ChildMotion()
{
motion = summationTree,
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
timeScale = 1,
},
new ChildMotion()
{
motion = applyTree,
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
timeScale = 1,
},
};
return bt;
}
private BlendTree BuildApplyTree(ShapeKeyInfo info)
{
var groups = info.setObjects;
var setTree = new BlendTree();
setTree.blendType = BlendTreeType.Simple1D;
setTree.blendParameter = info.SetParam;
setTree.useAutomaticThresholds = false;
var childMotions = new List<ChildMotion>();
childMotions.Add(new ChildMotion()
{
motion = AnimResult(groups.First().ShapeKey, 0),
timeScale = 1,
threshold = 0,
});
foreach (var group in groups)
{
var lo = group.ConditionKey;
var hi = group.Guard;
Debug.Log("Threshold: [" + lo + ", " + hi + "]: " + group);
childMotions.Add(new ChildMotion()
{
motion = AnimResult(group.ShapeKey, group.Value),
timeScale = 1,
threshold = lo,
});
childMotions.Add(new ChildMotion()
{
motion = AnimResult(group.ShapeKey, group.Value),
timeScale = 1,
threshold = hi,
});
}
setTree.children = childMotions.ToArray();
var delTree = new BlendTree();
delTree.blendType = BlendTreeType.Simple1D;
delTree.blendParameter = info.DeleteParam;
delTree.useAutomaticThresholds = false;
delTree.children = new[]
{
new ChildMotion()
{
motion = setTree,
timeScale = 1,
threshold = 0
},
new ChildMotion()
{
motion = setTree,
timeScale = 1,
threshold = 0.4f
},
new ChildMotion()
{
motion = delTree,
timeScale = 1,
threshold = 0.6f
},
new ChildMotion()
{
motion = delTree,
timeScale = 1,
threshold = 1
},
};
return delTree;
}
private Motion AnimResult(ShapeKey key, float value)
{
var renderer = EditorUtility.InstanceIDToObject(key.RendererInstanceId) as SkinnedMeshRenderer;
if (renderer == null) throw new InvalidOperationException("Failed to get object");
var obj = renderer.gameObject;
var path = RuntimeUtil.RelativePath(context.AvatarRootObject, obj);
var clip = new AnimationClip();
clip.name = $"Set {obj.name}:{key.ShapeKeyName}={value}";
var curve = new AnimationCurve();
curve.AddKey(0, value);
curve.AddKey(1, value);
var binding =
EditorCurveBinding.FloatCurve(path, typeof(SkinnedMeshRenderer), $"blendShape.{key.ShapeKeyName}");
AnimationUtility.SetEditorCurve(clip, binding, curve);
return clip;
}
private BlendTree BuildSummationTree(ShapeKeyInfo info)
{
var groups = info.setObjects;
var setParam = info.SetParam;
var delParam = info.DeleteParam;
var asc = context.Extension<AnimationServicesContext>();
BlendTree bt = new BlendTree();
bt.blendType = BlendTreeType.Direct;
HashSet<string> paramNames = new HashSet<string>();
var childMotions = new List<ChildMotion>();
// TODO eliminate excess motion field
var initMotion = new ChildMotion()
{
motion = AnimParam((setParam, 0), (delParam, 0)),
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
timeScale = 1,
};
childMotions.Add(initMotion);
paramNames.Add(MergeBlendTreePass.ALWAYS_ONE);
initialValues[MergeBlendTreePass.ALWAYS_ONE] = 1;
foreach (var group in groups)
{
Debug.Log("Group: " + group);
string controllingParam;
if (group.ControllingObject == null)
{
controllingParam = MergeBlendTreePass.ALWAYS_ONE;
}
else
{
// TODO: path evaluation
if (!asc.TryGetActiveSelfProxy(group.ControllingObject, out controllingParam))
{
throw new InvalidOperationException("Failed to get active self proxy");
}
initialValues[controllingParam] = group.ControllingObject.activeSelf ? 1 : 0;
}
var childMotion = new ChildMotion()
{
motion = AnimParam(setParam, group.ConditionKey),
directBlendParameter = controllingParam,
timeScale = 1,
};
childMotions.Add(childMotion);
paramNames.Add(controllingParam);
}
foreach (var delController in info.deletionObjects)
{
if (!asc.TryGetActiveSelfProxy(delController, out var controllingParam))
{
throw new InvalidOperationException("Failed to get active self proxy");
}
initialValues[controllingParam] = delController.activeSelf ? 1 : 0;
var childMotion = new ChildMotion()
{
motion = AnimParam(delParam, 1),
directBlendParameter = controllingParam,
timeScale = 1,
};
childMotions.Add(childMotion);
paramNames.Add(controllingParam);
}
bt.children = childMotions.ToArray();
return bt;
}
private void ApplyController(BlendTree bt, string stateName)
{
var fx = context.AvatarDescriptor.baseAnimationLayers
.FirstOrDefault(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX);
if (fx.animatorController == null)
{
throw new InvalidOperationException("No FX layer found");
}
if (!context.IsTemporaryAsset(fx.animatorController))
{
throw new InvalidOperationException("FX layer is not a temporary asset");
}
if (!(fx.animatorController is AnimatorController animController))
{
throw new InvalidOperationException("FX layer is not an animator controller");
}
var stateMachine = new AnimatorStateMachine();
var layers = animController.layers.ToList();
layers.Add(
new AnimatorControllerLayer() { defaultWeight = 1, name = stateName, stateMachine = stateMachine }
);
var state = new AnimatorState();
state.name = stateName;
state.motion = bt;
state.writeDefaultValues = true;
stateMachine.states = new[] { new ChildAnimatorState() { state = state } };
stateMachine.defaultState = state;
var paramList = animController.parameters.ToList();
var paramSet = paramList.Select(p => p.name).ToHashSet();
foreach (var paramName in FindParams(bt))
{
if (!paramSet.Contains(paramName))
{
paramList.Add(new AnimatorControllerParameter()
{
name = paramName,
type = AnimatorControllerParameterType.Float,
defaultFloat = 0, // TODO
});
paramSet.Add(paramName);
}
}
animController.parameters = paramList.ToArray();
animController.layers = layers.ToArray();
}
private static IEnumerable<string> FindParams(BlendTree bt)
{
if (bt == null) yield break;
if (bt.blendType == BlendTreeType.Direct)
{
foreach (var child in bt.children)
{
yield return child.directBlendParameter;
}
}
else
{
yield return bt.blendParameter;
}
foreach (var child in bt.children)
{
foreach (var param in FindParams(child.motion as BlendTree))
{
yield return param;
}
}
}
private AnimationClip AnimParam(string param, float val)
{
return AnimParam((param, val));
}
private AnimationClip AnimParam(params (string param, float val)[] pairs)
{
AnimationClip clip = new AnimationClip();
clip.name = "Set " + string.Join(", ", pairs.Select(p => $"{p.param}={p.val}"));
// TODO - check property syntax
foreach (var (param, val) in pairs)
{
var curve = new AnimationCurve();
curve.AddKey(0, val);
curve.AddKey(1, val);
clip.SetCurve("", typeof(Animator), "" + param, curve);
}
return clip;
}
private Dictionary<ShapeKey, ShapeKeyInfo> FindShapes(ndmf.BuildContext context)
{
var asc = context.Extension<AnimationServicesContext>();
var changers = context.AvatarRootObject.GetComponentsInChildren<ModularAvatarShapeChanger>(true);
Dictionary<ShapeKey, ShapeKeyInfo> shapeKeys = new();
foreach (var changer in changers)
{
var renderer = changer.targetRenderer.Get(changer)?.GetComponent<SkinnedMeshRenderer>();
if (renderer == null) continue;
int rendererInstanceId = renderer.GetInstanceID();
var mesh = renderer.sharedMesh;
if (mesh == null) continue;
foreach (var shape in changer.Shapes)
{
var shapeId = mesh.GetBlendShapeIndex(shape.ShapeName);
if (shapeId < 0) continue;
var key = new ShapeKey
{
RendererInstanceId = rendererInstanceId,
ShapeIndex = shapeId,
ShapeKeyName = shape.ShapeName,
};
if (!shapeKeys.TryGetValue(key, out var info))
{
info = new ShapeKeyInfo(key);
shapeKeys[key] = info;
// Add initial state
info.setObjects.Add(new ActionGroupKey(asc, key, null, shape));
}
var action = new ActionGroupKey(asc, key, changer.gameObject, shape);
if (action.IsDelete)
{
if (action.ControllingObject == null)
{
// always active?
info.alwaysDeleted |= changer.gameObject.activeInHierarchy;
}
else
{
info.deletionObjects.Add(action.ControllingObject);
}
continue;
}
// TODO: lift controlling object resolution out of loop?
if (action.ControllingObject == null)
{
if (action.InitiallyActive)
{
// always active control
info.setObjects.Clear();
}
else
{
// never active control
continue;
}
}
Debug.Log("Trying merge: " + action);
if (info.setObjects.Count == 0)
{
info.setObjects.Add(action);
}
else if (!info.setObjects[^1].TryMerge(action))
{
Debug.Log("Failed merge");
info.setObjects.Add(action);
}
else
{
Debug.Log("Post merge: " + info.setObjects[^1]);
}
}
}
return shapeKeys;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 740a387c9d934623a6b06d945b38d8d0
timeCreated: 1717123900

View File

@ -0,0 +1,198 @@
#region
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using nadena.dev.ndmf.preview;
using nadena.dev.ndmf.rq;
using nadena.dev.ndmf.rq.unity.editor;
using UnityEngine;
#endregion
namespace nadena.dev.modular_avatar.core.editor
{
public class ShapeChangerPreview : IRenderFilter
{
private static ReactiveValue<ImmutableDictionary<Renderer, ImmutableList<ModularAvatarShapeChanger>>>
InternalTargetGroups
= ReactiveValue<ImmutableDictionary<Renderer, ImmutableList<ModularAvatarShapeChanger>>>.Create(
"ShapeChangerPreview.TargetGroups", async ctx =>
{
var allChangers =
await ctx.Observe(CommonQueries.GetComponentsByType<ModularAvatarShapeChanger>());
Dictionary<Renderer, ImmutableList<ModularAvatarShapeChanger>.Builder> groups =
new Dictionary<Renderer, ImmutableList<ModularAvatarShapeChanger>.Builder>(
new ObjectIdentityComparer<Renderer>());
foreach (var changer in allChangers)
{
// TODO: observe avatar root
ctx.Observe(changer);
var target = ctx.Observe(changer.targetRenderer.Get(changer));
var renderer = ctx.GetComponent<SkinnedMeshRenderer>(target);
if (renderer == null) continue;
if (!groups.TryGetValue(renderer, out var group))
{
group = ImmutableList.CreateBuilder<ModularAvatarShapeChanger>();
groups[renderer] = group;
}
group.Add(changer);
}
return groups.ToImmutableDictionary(p => p.Key, p => p.Value.ToImmutable());
});
public ReactiveValue<IImmutableList<IImmutableList<Renderer>>> TargetGroups { get; } =
ReactiveValue<IImmutableList<IImmutableList<Renderer>>>.Create(
"ShapeChangerPreview.TargetGroups", async ctx =>
{
var targetGroups = await ctx.Observe(InternalTargetGroups);
return targetGroups.Keys
.Select(v => (IImmutableList<Renderer>)ImmutableList.Create(v))
.ToImmutableList();
});
private bool IsChangerActive(ModularAvatarShapeChanger changer, ComputeContext context)
{
if (context != null)
{
if (!context.ActiveAndEnabled(changer)) return false;
}
else
{
if (!changer.isActiveAndEnabled) return false;
}
var changerRenderer = context != null
? context.GetComponent<Renderer>(changer.gameObject)
: changer.GetComponent<Renderer>();
context?.Observe(changerRenderer);
if (changerRenderer == null) return false;
return changerRenderer.enabled && (context?.ActiveAndEnabled(changer) ?? changer.isActiveAndEnabled);
}
public async Task MutateMeshData(IList<MeshState> states, ComputeContext context)
{
var targetGroups = await context.Observe(InternalTargetGroups);
var state = states[0];
var renderer = state.Original;
if (renderer == null) return;
if (!targetGroups.TryGetValue(renderer, out var changers)) return;
if (!(renderer is SkinnedMeshRenderer smr)) return;
HashSet<int> toDelete = new HashSet<int>();
foreach (var changer in changers)
{
if (!IsChangerActive(changer, context)) continue;
foreach (var shape in changer.Shapes)
{
if (shape.ChangeType == ShapeChangeType.Delete)
{
var index = state.Mesh.GetBlendShapeIndex(shape.ShapeName);
if (index < 0) continue;
toDelete.Add(index);
}
}
}
if (toDelete.Count > 0)
{
var mesh = Object.Instantiate(state.Mesh);
var bsPos = new Vector3[mesh.vertexCount];
bool[] targetVertex = new bool[mesh.vertexCount];
foreach (var bs in toDelete)
{
int frames = mesh.GetBlendShapeFrameCount(bs);
for (int f = 0; f < frames; f++)
{
mesh.GetBlendShapeFrameVertices(bs, f, bsPos, null, null);
for (int i = 0; i < bsPos.Length; i++)
{
if (bsPos[i].sqrMagnitude > 0.0001f)
{
targetVertex[i] = true;
}
}
}
}
List<int> tris = new List<int>();
for (int subMesh = 0; subMesh < mesh.subMeshCount; subMesh++)
{
tris.Clear();
var baseVertex = (int)mesh.GetBaseVertex(subMesh);
mesh.GetTriangles(tris, subMesh, false);
for (int i = 0; i < tris.Count; i += 3)
{
if (targetVertex[tris[i] + baseVertex] || targetVertex[tris[i + 1] + baseVertex] ||
targetVertex[tris[i + 2] + baseVertex])
{
tris.RemoveRange(i, 3);
i -= 3;
}
}
mesh.SetTriangles(tris, subMesh, false, baseVertex: baseVertex);
}
state.Mesh = mesh;
state.OnDispose += () =>
{
if (mesh != null) Object.Destroy(mesh);
};
}
}
public void OnFrame(Renderer original, Renderer proxy)
{
if (!InternalTargetGroups.TryGetValue(out var targetGroups)) return;
if (!targetGroups.TryGetValue(original, out var changers)) return;
if (!(proxy is SkinnedMeshRenderer smr)) return;
var mesh = smr.sharedMesh;
if (mesh == null) return;
foreach (var changer in changers)
{
if (!IsChangerActive(changer, null)) continue;
foreach (var shape in changer.Shapes)
{
var index = mesh.GetBlendShapeIndex(shape.ShapeName);
if (index < 0) continue;
float setToValue = -1;
switch (shape.ChangeType)
{
case ShapeChangeType.Delete:
setToValue = 100;
break;
case ShapeChangeType.Set:
setToValue = shape.Value;
break;
}
smr.SetBlendShapeWeight(index, setToValue);
}
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 01cd76f7b31543b9bba5690ba478f865
timeCreated: 1717105143

View File

@ -6,7 +6,9 @@
"VRC.SDK3A",
"VRC.SDKBase",
"nadena.dev.ndmf",
"nadena.dev.ndmf.vrchat"
"nadena.dev.ndmf.vrchat",
"nadena.dev.ndmf.reactive-query.core",
"nadena.dev.ndmf.runtime"
],
"includePlatforms": [
"Editor"

View File

@ -2,7 +2,6 @@
using System.Collections.Generic;
using nadena.dev.modular_avatar.core.armature_lock;
using UnityEngine;
#if MA_VRCSDK3_AVATARS
using VRC.SDKBase;
#endif
@ -128,7 +127,6 @@ namespace nadena.dev.modular_avatar.core.ArmatureAwase
foreach (Transform child in parent)
{
if (_excluded.Contains(child)) continue;
if (child.GetComponent<ScaleProxy>() != null) continue;
_observed.Add(child);

View File

@ -0,0 +1,49 @@
#region
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
#endregion
namespace nadena.dev.modular_avatar.core
{
[Serializable]
public enum ShapeChangeType
{
Delete,
Set
}
[Serializable]
public struct ChangedShape
{
public string ShapeName;
public ShapeChangeType ChangeType;
public float Value;
}
[AddComponentMenu("Modular Avatar/MA Shape Changer")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/shape-changer?lang=auto")]
public class ModularAvatarShapeChanger : AvatarTagComponent
{
[SerializeField] [FormerlySerializedAs("targetRenderer")]
private AvatarObjectReference m_targetRenderer;
public AvatarObjectReference targetRenderer
{
get => m_targetRenderer;
set => m_targetRenderer = value;
}
[SerializeField] [FormerlySerializedAs("Shapes")]
private List<ChangedShape> m_shapes = new();
public List<ChangedShape> Shapes
{
get => m_shapes;
set => m_shapes = value;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2db441f589c3407bb6fb5f02ff8ab541
timeCreated: 1717105020

View File

@ -1,11 +1,8 @@
#region
using System;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using UnityEditor;
#endif
#endregion
@ -24,146 +21,10 @@ namespace nadena.dev.modular_avatar.core
public Vector3 Scale
{
get => m_Scale;
set
{
m_Scale = value;
PreCull();
}
set => m_Scale = value;
}
[SerializeField] [FormerlySerializedAs("scaleProxy")]
internal Transform legacyScaleProxy;
internal Transform scaleProxyChild;
[NonSerialized]
private bool initialized = false;
#if UNITY_EDITOR
void Awake()
{
ProxyManager.RegisterAdjuster(this);
initialized = false;
}
void OnValidate()
{
ProxyManager.RegisterAdjuster(this);
initialized = false;
}
internal void PreCull()
{
if (this == null)
{
EditorApplication.delayCall += () => ProxyManager.UnregisterAdjuster(this);
return;
}
if (PrefabUtility.IsPartOfPrefabAsset(this)) return;
if (scaleProxyChild == null || initialized == false)
{
InitializeProxy();
}
UpdateProxyParent(scaleProxyChild, transform);
var xform = transform;
scaleProxyChild.localScale = m_Scale;
ProxyManager.RegisterBone(xform, scaleProxyChild);
if (legacyScaleProxy != null && !PrefabUtility.IsPartOfPrefabAsset(legacyScaleProxy))
{
DestroyImmediate(legacyScaleProxy.gameObject);
legacyScaleProxy = null;
}
}
private void UpdateProxyParent(Transform proxyChild, Transform trueParent)
{
while (trueParent != null)
{
Transform parent = proxyChild.parent;
if (parent == null)
{
GameObject obj = new GameObject();
proxyChild.transform.SetParent(obj.transform, false);
#if MODULAR_AVATAR_DEBUG_HIDDEN
obj.hideFlags = HideFlags.DontSave;
#else
obj.hideFlags = HideFlags.HideAndDontSave;
#endif
parent = obj.transform;
if (obj.scene != gameObject.scene && gameObject.scene.IsValid())
{
SceneManager.MoveGameObjectToScene(obj, gameObject.scene);
}
}
parent.gameObject.name = "Proxy object for " + trueParent.gameObject.name;
parent.localPosition = trueParent.localPosition;
parent.localRotation = trueParent.localRotation;
parent.localScale = trueParent.localScale;
proxyChild = parent;
trueParent = trueParent.parent;
}
// Clean up any additional parentage we might not want
if (proxyChild.parent != null)
{
Transform parent = proxyChild.parent;
// Reparent to root
proxyChild.SetParent(null, false);
// Destroy old hierarchy
while (parent.parent != null) parent = parent.parent;
DestroyImmediate(parent.gameObject);
}
}
private void InitializeProxy()
{
if (scaleProxyChild == null)
{
scaleProxyChild = new GameObject("Child").transform;
#if MODULAR_AVATAR_DEBUG_HIDDEN
scaleProxyChild.gameObject.hideFlags = HideFlags.DontSave;
#else
scaleProxyChild.gameObject.hideFlags = HideFlags.HideAndDontSave;
#endif
}
initialized = true;
}
private void OnDestroy()
{
ProxyManager.UnregisterAdjuster(this);
if (scaleProxyChild != null)
{
Transform parent = scaleProxyChild.parent;
while (parent.parent != null) parent = parent.parent;
DestroyImmediate(parent.gameObject);
}
if (transform != null)
{
ProxyManager.UnregisterBone(transform);
}
base.OnDestroy();
}
#else
internal void PreCull() {
// build time stub
}
#endif
}
}

View File

@ -1,359 +0,0 @@
#region
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif
#endregion
namespace nadena.dev.modular_avatar.core
{
internal static class ProxyManager
{
#region Accessible from multiple threads
private static bool _dirty = false;
private static readonly object _lock = new object();
private static ImmutableHashSet<ModularAvatarScaleAdjuster> _adjusters
= ImmutableHashSet<ModularAvatarScaleAdjuster>.Empty;
private static ImmutableDictionary<Transform, Transform> _originalToReplacementBone
= ImmutableDictionary<Transform, Transform>.Empty.WithComparers(new ObjectIdentityComparer<Transform>());
internal static void RegisterAdjuster(ModularAvatarScaleAdjuster adjuster)
{
lock (_lock)
{
if (_adjusters.Contains(adjuster)) return;
_adjusters = _adjusters.Add(adjuster);
_dirty = true;
}
}
internal static void UnregisterAdjuster(ModularAvatarScaleAdjuster adjuster)
{
lock (_lock)
{
_adjusters = _adjusters.Remove(adjuster);
_dirty = true;
}
}
internal static void RegisterBone(Transform original, Transform replacement)
{
lock (_lock)
{
if (_originalToReplacementBone.TryGetValue(original, out var val) && val == replacement)
{
return;
}
_originalToReplacementBone = _originalToReplacementBone.Add(original, replacement);
_dirty = true;
}
}
internal static void UnregisterBone(Transform original)
{
lock (_lock)
{
_originalToReplacementBone = _originalToReplacementBone.Remove(original);
_dirty = true;
}
}
#endregion
private static ImmutableHashSet<ModularAvatarScaleAdjuster> _capturedAdjusters =
ImmutableHashSet<ModularAvatarScaleAdjuster>.Empty;
private static ImmutableDictionary<Transform, Transform> _capturedBones =
ImmutableDictionary<Transform, Transform>.Empty;
private static ImmutableDictionary<SkinnedMeshRenderer, SkinnedMeshRenderer> _originalToReplacementRenderer
= ImmutableDictionary<SkinnedMeshRenderer, SkinnedMeshRenderer>.Empty.WithComparers(
new ObjectIdentityComparer<SkinnedMeshRenderer>());
internal static ImmutableDictionary<GameObject, GameObject> ProxyToOriginalObject { get; private set; } =
ImmutableDictionary<GameObject, GameObject>.Empty;
internal static ImmutableDictionary<GameObject, GameObject> OriginalToProxyObject { get; private set; } =
ImmutableDictionary<GameObject, GameObject>.Empty;
internal static ImmutableDictionary<SkinnedMeshRenderer, SkinnedMeshRenderer> OriginalToProxyRenderer =>
_originalToReplacementRenderer;
internal static ImmutableHashSet<GameObject> RetainedObjects = ImmutableHashSet<GameObject>.Empty;
internal static bool ShouldRetain(GameObject obj) => RetainedObjects.Contains(obj);
private static void BuildRenderers()
{
lock (_lock)
{
_capturedAdjusters = _adjusters;
// Give each adjuster a chance to initialize the bone mappings first
foreach (var adj in _capturedAdjusters)
{
if (adj == null)
{
_capturedAdjusters = _adjusters = _adjusters.Remove(adj);
}
adj.PreCull();
}
foreach (var kvp in _originalToReplacementBone)
{
if (kvp.Key == null || kvp.Value == null)
{
_originalToReplacementBone = _originalToReplacementBone.Remove(kvp.Key);
}
}
_capturedBones = _originalToReplacementBone;
_dirty = false;
}
var avatarRoots = _capturedBones.Keys.Select(bone =>
{
var root = RuntimeUtil.FindAvatarTransformInParents(bone);
if (root == null)
{
root = bone;
while (root.parent != null) root = root.parent;
}
return root;
}).ToImmutableHashSet();
var potentialRenderers = avatarRoots.SelectMany(r => r.GetComponentsInChildren<SkinnedMeshRenderer>(true))
.ToList();
ImmutableDictionary<SkinnedMeshRenderer, SkinnedMeshRenderer>.Builder renderers =
ImmutableDictionary.CreateBuilder<SkinnedMeshRenderer, SkinnedMeshRenderer>(
new ObjectIdentityComparer<SkinnedMeshRenderer>()
);
foreach (var originalRenderer in potentialRenderers)
{
SkinnedMeshRenderer replacement;
if (!NeedsReplacement(originalRenderer))
{
if (_originalToReplacementRenderer.TryGetValue(originalRenderer, out replacement) &&
replacement != null)
{
Object.DestroyImmediate(replacement.gameObject);
}
continue;
}
if (!_originalToReplacementRenderer.TryGetValue(originalRenderer, out replacement) ||
replacement == null)
{
replacement = CreateReplacement(originalRenderer);
}
SetupBoneMappings(originalRenderer, replacement);
renderers.Add(originalRenderer, replacement);
}
foreach (var kvp in _originalToReplacementRenderer)
{
if (!renderers.ContainsKey(kvp.Key))
{
if (kvp.Value != null)
{
Object.DestroyImmediate(kvp.Value.gameObject);
}
}
}
_originalToReplacementRenderer = renderers.ToImmutable();
ProxyToOriginalObject = _originalToReplacementRenderer.ToImmutableDictionary(
kvp => kvp.Value.gameObject,
kvp => kvp.Key.gameObject
);
OriginalToProxyObject = _originalToReplacementRenderer.ToImmutableDictionary(
kvp => kvp.Key.gameObject,
kvp => kvp.Value.gameObject
);
RetainedObjects = ProxyToOriginalObject.Keys.Concat(
_capturedBones.Values.Where(b => b != null).Select(b => b.gameObject)
).ToImmutableHashSet(new ObjectIdentityComparer<GameObject>());
}
private static void SetupBoneMappings(SkinnedMeshRenderer originalRenderer, SkinnedMeshRenderer replacement)
{
replacement.sharedMesh = originalRenderer.sharedMesh;
replacement.bones = originalRenderer.bones.Select(MapBone).ToArray();
}
private static Transform MapBone(Transform srcBone)
{
if (srcBone == null)
{
return null;
} else if (_capturedBones.TryGetValue(srcBone, out var newBone) && newBone != null)
{
return newBone;
}
else
{
return srcBone;
}
}
private static SkinnedMeshRenderer CreateReplacement(SkinnedMeshRenderer originalRenderer)
{
var obj = new GameObject("MA Proxy Renderer for " + originalRenderer.gameObject.name);
// We can't use HideAndDontSave as this would break scene view click-to-pick handling
// (so instead this is hidden via the HierarchyViewPatches harmony hack)
obj.hideFlags = HideFlags.DontSave;
var renderer = obj.AddComponent<SkinnedMeshRenderer>();
return renderer;
}
private static bool NeedsReplacement(SkinnedMeshRenderer originalRenderer)
{
if (originalRenderer.sharedMesh == null) return false;
var bones = originalRenderer.bones;
var weights = originalRenderer.sharedMesh.GetAllBoneWeights();
for (var i = 0; i < weights.Length; i++)
{
var bone = bones[weights[i].boneIndex];
if (bone != null && _capturedBones.ContainsKey(bone)) return true;
}
return false;
}
#if UNITY_EDITOR
[InitializeOnLoadMethod]
private static void Init()
{
Camera.onPreCull += OnPreCull;
Camera.onPostRender += OnPostRender;
AssemblyReloadEvents.beforeAssemblyReload += () =>
{
ClearStates();
foreach (var renderer in _originalToReplacementRenderer.Values)
{
Object.DestroyImmediate(renderer.gameObject);
}
};
EditorSceneManager.sceneSaving += (scene, path) => ClearStates();
}
private static List<(SkinnedMeshRenderer, bool)> statesToRestore = new List<(SkinnedMeshRenderer, bool)>();
private static void OnPreCull(Camera camera)
{
if (_dirty)
{
BuildRenderers();
}
ClearStates();
foreach (var adj in _capturedAdjusters)
{
if (adj == null)
{
_capturedAdjusters = _capturedAdjusters.Remove(adj);
}
adj.PreCull(); // update scale
}
foreach (var kvp in _originalToReplacementRenderer)
{
var original = kvp.Key;
var proxy = kvp.Value;
if (original == null || proxy == null)
{
_dirty = true;
continue;
}
var originalGameObject = original.gameObject;
var proxyActive = original.enabled && originalGameObject.activeInHierarchy &&
!SceneVisibilityManager.instance.IsHidden(originalGameObject, false);
proxy.enabled = proxyActive;
if (original.enabled && originalGameObject.activeInHierarchy)
{
CopyRendererStates(original, proxy);
statesToRestore.Add((original, original.enabled));
original.forceRenderingOff = true;
}
}
}
private static void CopyRendererStates(SkinnedMeshRenderer parentRenderer, SkinnedMeshRenderer myRenderer)
{
myRenderer.transform.position = parentRenderer.transform.position;
myRenderer.transform.rotation = parentRenderer.transform.rotation;
myRenderer.sharedMaterials = parentRenderer.sharedMaterials;
myRenderer.localBounds = parentRenderer.localBounds;
myRenderer.rootBone = MapBone(parentRenderer.rootBone);
myRenderer.quality = parentRenderer.quality;
myRenderer.shadowCastingMode = parentRenderer.shadowCastingMode;
myRenderer.receiveShadows = parentRenderer.receiveShadows;
myRenderer.lightProbeUsage = parentRenderer.lightProbeUsage;
myRenderer.reflectionProbeUsage = parentRenderer.reflectionProbeUsage;
myRenderer.probeAnchor = parentRenderer.probeAnchor;
myRenderer.motionVectorGenerationMode = parentRenderer.motionVectorGenerationMode;
myRenderer.allowOcclusionWhenDynamic = parentRenderer.allowOcclusionWhenDynamic;
if (myRenderer.gameObject.scene != parentRenderer.gameObject.scene &&
parentRenderer.gameObject.scene.IsValid())
{
SceneManager.MoveGameObjectToScene(myRenderer.gameObject, parentRenderer.gameObject.scene);
}
if (myRenderer.sharedMesh != null)
{
var blendShapeCount = myRenderer.sharedMesh.blendShapeCount;
for (int i = 0; i < blendShapeCount; i++)
{
myRenderer.SetBlendShapeWeight(i, parentRenderer.GetBlendShapeWeight(i));
}
}
}
private static void OnPostRender(Camera camera)
{
ClearStates();
}
private static void ClearStates()
{
foreach (var (original, state) in statesToRestore)
{
original.forceRenderingOff = false;
}
statesToRestore.Clear();
}
#endif
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 117b3ad981cb487aa5029043f7482a94
timeCreated: 1709447257

View File

@ -1,33 +0,0 @@
#region
using UnityEngine;
using VRC.SDKBase;
#if UNITY_EDITOR
using UnityEditor;
#endif
#endregion
namespace nadena.dev.modular_avatar.core
{
/// <summary>
/// Legacy component from early 1.9.x builds.
/// </summary>
[ExecuteInEditMode]
[AddComponentMenu("")]
[RequireComponent(typeof(SkinnedMeshRenderer))]
internal class ScaleAdjusterRenderer : MonoBehaviour, IEditorOnly
{
#if UNITY_EDITOR
private void OnValidate()
{
if (PrefabUtility.IsPartOfPrefabAsset(this)) return;
EditorApplication.delayCall += () =>
{
if (this != null) DestroyImmediate(gameObject);
};
}
#endif
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: c8bc16baa6c345eea5edf47232ee4069
timeCreated: 1708232586

View File

@ -1,80 +0,0 @@

#region
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
#endregion
namespace nadena.dev.modular_avatar.core
{
[AddComponentMenu("")]
internal sealed class ScaleProxy : AvatarTagComponent
{
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
EditorApplication.delayCall += DeferredValidate;
}
private void DeferredValidate()
{
if (this == null) return;
// Avoid logspam on Unity 2019
if (PrefabUtility.IsPartOfPrefabInstance(gameObject)) return;
if (!ProxyManager.ShouldRetain(gameObject))
{
SelfDestruct();
}
}
private void SelfDestruct()
{
var root = ndmf.runtime.RuntimeUtil.FindAvatarInParents(transform);
if (root == null)
{
root = transform;
while (root.parent != null) root = root.parent;
}
ClearOverrides(root);
if (PrefabUtility.IsPartOfPrefabInstance(gameObject)) return;
DestroyImmediate(gameObject);
}
private void ClearOverrides(Transform root)
{
// This clears bone overrides that date back to the 1.9.0-rc.2 implementation, to ease rc.2 -> rc.3
// migrations. It'll be removed in 1.10.
foreach (var smr in root.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
if (smr.GetComponent<ScaleAdjusterRenderer>()) continue;
var bones = smr.bones;
bool changed = false;
for (var i = 0; i < bones.Length; i++)
{
if (bones[i] == transform)
{
bones[i] = transform.parent;
changed = true;
}
}
if (changed)
{
smr.bones = bones;
}
}
}
#endif
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 3f0c19b32ba845a2a84f37f48e4ec4d5
timeCreated: 1703659053

View File

@ -39,8 +39,7 @@ namespace modular_avatar_tests
if (type == typeof(Activator)) return;
if (type == typeof(AvatarActivator)) return;
if (type == typeof(TestComponent)) return;
if (type == typeof(ScaleProxy)) return;
if (type == typeof(ScaleAdjusterRenderer)) return;
if (type == typeof(ModularAvatarShapeChanger)) return;
// get icon
var component = (MonoBehaviour) _gameObject.AddComponent(type);
@ -65,8 +64,6 @@ namespace modular_avatar_tests
if (type == typeof(Activator)) return;
if (type == typeof(AvatarActivator)) return;
if (type == typeof(TestComponent)) return;
if (type == typeof(ScaleProxy)) return;
if (type == typeof(ScaleAdjusterRenderer)) return;
// get icon
var helpUrl = type.GetCustomAttribute<HelpURLAttribute>();