mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-28 10:15:06 +08:00
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:
parent
32c538a536
commit
8d3da50b37
2
.github/ProjectRoot/vpm-manifest-2022.json
vendored
2
.github/ProjectRoot/vpm-manifest-2022.json
vendored
@ -19,7 +19,7 @@
|
||||
"dependencies": {}
|
||||
},
|
||||
"nadena.dev.ndmf": {
|
||||
"version": "1.4.0"
|
||||
"version": "1.5.0-alpha.0"
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 807736f252df4b1b8402827257dcbea3
|
||||
timeCreated: 1709354699
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42f70698a5df48c0908400c425a2f6ee
|
||||
timeCreated: 1709356304
|
@ -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]
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf06818f1c0c436fbae7f755d7110aba
|
||||
timeCreated: 1709359553
|
3
Editor/Inspector/ShapeChanger.meta
Normal file
3
Editor/Inspector/ShapeChanger.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2400605596824043a59b55dcb2a8e89a
|
||||
timeCreated: 1717196942
|
99
Editor/Inspector/ShapeChanger/AddShapePopup.cs
Normal file
99
Editor/Inspector/ShapeChanger/AddShapePopup.cs
Normal 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!"));
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/Inspector/ShapeChanger/AddShapePopup.cs.meta
Normal file
3
Editor/Inspector/ShapeChanger/AddShapePopup.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a8351fafb3740918363f60365adfeda
|
||||
timeCreated: 1717205112
|
11
Editor/Inspector/ShapeChanger/AddShapePopup.uxml
Normal file
11
Editor/Inspector/ShapeChanger/AddShapePopup.uxml
Normal 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="<none remaining>" class="placeholder"/>
|
||||
</ui:ScrollView>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
3
Editor/Inspector/ShapeChanger/AddShapePopup.uxml.meta
Normal file
3
Editor/Inspector/ShapeChanger/AddShapePopup.uxml.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6753a7b3eae1416cb04786cf53778c33
|
||||
timeCreated: 1717205258
|
44
Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs
Normal file
44
Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs.meta
Normal file
3
Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0778099a393468c9775fd9a99b321af
|
||||
timeCreated: 1717198244
|
8
Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml
Normal file
8
Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml
Normal file
@ -0,0 +1,8 @@
|
||||
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
|
||||
<ui:VisualElement class="changed-shape-editor">
|
||||
<ui:Label text="<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>
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d8131b91fff04f9e94b71d70105ae05b
|
||||
timeCreated: 1717198707
|
24
Editor/Inspector/ShapeChanger/ShapeChanger.uxml
Normal file
24
Editor/Inspector/ShapeChanger/ShapeChanger.uxml
Normal 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>
|
3
Editor/Inspector/ShapeChanger/ShapeChanger.uxml.meta
Normal file
3
Editor/Inspector/ShapeChanger/ShapeChanger.uxml.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8267295b2dc4d90b92dcc289f6d31c4
|
||||
timeCreated: 1717197307
|
52
Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs
Normal file
52
Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
13
Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs.meta
Normal file
13
Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs.meta
Normal 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:
|
112
Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss
Normal file
112
Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss
Normal 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;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b52b9cfc8514c55af39aeb11de7f279
|
||||
timeCreated: 1717197307
|
@ -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))
|
||||
{
|
||||
|
@ -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);
|
||||
|
155
Editor/ScaleAdjuster/ScaleAdjustedBones.cs
Normal file
155
Editor/ScaleAdjuster/ScaleAdjustedBones.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/ScaleAdjuster/ScaleAdjustedBones.cs.meta
Normal file
3
Editor/ScaleAdjuster/ScaleAdjustedBones.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b62de718b16e4f47b48e2354235ee158
|
||||
timeCreated: 1716607885
|
83
Editor/ScaleAdjuster/ScaleAdjusterPreview.cs
Normal file
83
Editor/ScaleAdjuster/ScaleAdjusterPreview.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
3
Editor/ScaleAdjuster/ScaleAdjusterPreview.cs.meta
Normal file
3
Editor/ScaleAdjuster/ScaleAdjusterPreview.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54077f1844e74fa6a344b94d83a2044f
|
||||
timeCreated: 1716174270
|
@ -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
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfa3ba0c82bc4439aa86228715f61831
|
||||
timeCreated: 1709376243
|
@ -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
3
Editor/ShapeChanger.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57bb6d1a371c4993bf7cfed797b2eb65
|
||||
timeCreated: 1717105136
|
33
Editor/ShapeChanger/DESIGN.md
Normal file
33
Editor/ShapeChanger/DESIGN.md
Normal 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.
|
3
Editor/ShapeChanger/DESIGN.md.meta
Normal file
3
Editor/ShapeChanger/DESIGN.md.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca1da54f230b498fa21d1a70d196a12a
|
||||
timeCreated: 1717123475
|
320
Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs
Normal file
320
Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs.meta
Normal file
3
Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e45c8fdd80a4a37bd7f2bd3616886bb
|
||||
timeCreated: 1717209652
|
616
Editor/ShapeChanger/ShapeChangerPass.cs
Normal file
616
Editor/ShapeChanger/ShapeChangerPass.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/ShapeChanger/ShapeChangerPass.cs.meta
Normal file
3
Editor/ShapeChanger/ShapeChangerPass.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 740a387c9d934623a6b06d945b38d8d0
|
||||
timeCreated: 1717123900
|
198
Editor/ShapeChanger/ShapeChangerPreview.cs
Normal file
198
Editor/ShapeChanger/ShapeChangerPreview.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/ShapeChanger/ShapeChangerPreview.cs.meta
Normal file
3
Editor/ShapeChanger/ShapeChangerPreview.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01cd76f7b31543b9bba5690ba478f865
|
||||
timeCreated: 1717105143
|
@ -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"
|
||||
|
@ -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);
|
||||
|
||||
|
49
Runtime/ModularAvatarShapeChanger.cs
Normal file
49
Runtime/ModularAvatarShapeChanger.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
3
Runtime/ModularAvatarShapeChanger.cs.meta
Normal file
3
Runtime/ModularAvatarShapeChanger.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2db441f589c3407bb6fb5f02ff8ab541
|
||||
timeCreated: 1717105020
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 117b3ad981cb487aa5029043f7482a94
|
||||
timeCreated: 1709447257
|
@ -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
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8bc16baa6c345eea5edf47232ee4069
|
||||
timeCreated: 1708232586
|
@ -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
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f0c19b32ba845a2a84f37f48e4ec4d5
|
||||
timeCreated: 1703659053
|
@ -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>();
|
||||
|
Loading…
Reference in New Issue
Block a user