fix: improved Scale Adjuster implementation (#730)

This new implementation avoids creating any internal objects within the avatar
itself, instead creating them as hidden scene root objects. We also move all update
processing to be driven by camera callbacks as well. These changes help improve
compatibility with tools such as FaceEmo, as well as being less likely to break when
other tools manipulate the avatar's hierarchy directly.
This commit is contained in:
bd_ 2024-03-05 00:19:54 -08:00 committed by GitHub
parent e39a77855a
commit f077d24d48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 469 additions and 545 deletions

View File

@ -18,8 +18,9 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
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));
h.Patch(original: m_orig, prefix: new HarmonyMethod(m_prefix), postfix: new HarmonyMethod(m_postfix));
}
[UsedImplicitly]
@ -34,13 +35,41 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
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 (ScaleAdjusterRenderer.originalObjects.TryGetValue(obj, out var proxy) && proxy != null)
if (ProxyManager.OriginalToProxyObject.TryGetValue(obj, out var proxy) && proxy != null)
{
return proxy.gameObject;
}

View File

@ -1,56 +0,0 @@
#region
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using JetBrains.Annotations;
using UnityEditor.SceneManagement;
using UnityEngine;
#endregion
namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
{
/// <summary>
/// Try to prevent various internal objects from showing up in the Prefab Overrides window...
/// </summary>
internal class HideScaleAdjusterFromPrefabOverrideView
{
internal static Type t_PrefabOverrides;
internal static PropertyInfo p_AddedGameObjects, p_ObjectOverrides;
internal static void Patch(Harmony harmony)
{
var t_PrefabOverridesTreeView = AccessTools.TypeByName("UnityEditor.PrefabOverridesTreeView");
var m_GetPrefabOverrides = AccessTools.Method(t_PrefabOverridesTreeView, "GetPrefabOverrides");
var m_postfix = AccessTools.Method(typeof(HideScaleAdjusterFromPrefabOverrideView), "Postfix");
t_PrefabOverrides = AccessTools.TypeByName("UnityEditor.PrefabOverridesTreeView+PrefabOverrides");
p_AddedGameObjects = AccessTools.Property(t_PrefabOverrides, "addedGameObjects");
p_ObjectOverrides = AccessTools.Property(t_PrefabOverrides, "objectOverrides");
harmony.Patch(original: m_GetPrefabOverrides, postfix: new HarmonyMethod(m_postfix));
}
[UsedImplicitly]
private static void Postfix(GameObject prefabInstance, object __result)
{
List<AddedGameObject> added = p_AddedGameObjects.GetValue(__result) as List<AddedGameObject>;
if (added == null) return;
added.RemoveAll(obj => ScaleAdjusterRenderer.proxyObjects.ContainsKey(obj.instanceGameObject));
List<ObjectOverride> objectOverrides = p_ObjectOverrides.GetValue(__result) as List<ObjectOverride>;
if (objectOverrides == null) return;
objectOverrides.RemoveAll(oo =>
{
var c = oo.instanceObject as Component;
return c != null && ScaleAdjusterRenderer.proxyObjects.ContainsKey(c.gameObject);
});
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 0893522c012a46358e5ecf1df6628b2e
timeCreated: 1708237029

View File

@ -143,7 +143,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
var pptrValue = p_pptrValue.GetValue(hierarchyProperty);
if (pptrValue == null) return false;
var skip = ScaleAdjusterRenderer.proxyObjects.ContainsKey((GameObject)pptrValue);
var skip = ProxyManager.ProxyToOriginalObject.ContainsKey((GameObject)pptrValue);
if (skip) skipped++;
return skip;
@ -163,7 +163,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
{
if (pptrObject == null || isSceneHeader) return true;
if (hasChildren && ScaleAdjusterRenderer.originalObjects.ContainsKey((GameObject)pptrObject))
if (hasChildren && ProxyManager.ProxyToOriginalObject.ContainsKey((GameObject)pptrObject))
{
// See if there are any other children...
hasChildren = ((GameObject)pptrObject).transform.childCount > 1;

View File

@ -13,7 +13,6 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
{
private static readonly Action<Harmony>[] patches = new Action<Harmony>[]
{
HideScaleAdjusterFromPrefabOverrideView.Patch,
HierarchyViewPatches.Patch,
#if UNITY_2022_3_OR_NEWER
HandleUtilityPatches.Patch_FilterInstanceIDs,

View File

@ -58,11 +58,11 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
if ((bool)m_TryGetGameObject.Invoke(obj, args))
{
var go = args[0] as GameObject;
if (go != null && ScaleAdjusterRenderer.proxyObjects.ContainsKey(go))
if (go != null && ProxyManager.ProxyToOriginalObject.TryGetValue(go, out var original))
{
list.Add(ctor_PickingObject.Invoke(new[]
{
go.transform.parent.gameObject,
original,
p_materialIndex.GetValue(obj)
}));
continue;

View File

@ -1,9 +1,11 @@
using System.Collections.Generic;
using System.Linq;
#region
using System.Collections.Generic;
using nadena.dev.ndmf;
using UnityEditor.EditorTools;
using UnityEngine;
#endregion
namespace nadena.dev.modular_avatar.core.editor
{
internal class ScaleAdjusterPass : Pass<ScaleAdjusterPass>
@ -11,25 +13,30 @@ namespace nadena.dev.modular_avatar.core.editor
protected override void Execute(ndmf.BuildContext context)
{
Dictionary<Transform, Transform> boneMappings = new Dictionary<Transform, Transform>();
foreach (var component in context.AvatarRootObject.GetComponentsInChildren<ScaleProxy>())
foreach (var adjuster in context.AvatarRootObject.GetComponentsInChildren<ModularAvatarScaleAdjuster>(true))
{
var proxyTransform = component.transform;
var parentAdjuster = component.transform.parent?.GetComponent<ModularAvatarScaleAdjuster>();
if (parentAdjuster != null)
{
UnityEngine.Object.DestroyImmediate(component);
proxyTransform.localScale = parentAdjuster.Scale;
parentAdjuster.scaleProxy = null; // prevent destruction of the ScaleProxy itself
UnityEngine.Object.DestroyImmediate(parentAdjuster);
boneMappings.Add(proxyTransform.parent, proxyTransform);
}
var proxyObject = new GameObject("ScaleProxy");
var proxyTransform = proxyObject.transform;
proxyTransform.SetParent(adjuster.transform, false);
proxyTransform.localPosition = Vector3.zero;
proxyTransform.localRotation = Quaternion.identity;
proxyTransform.localScale = adjuster.Scale;
boneMappings.Add(adjuster.transform, proxyTransform);
Object.DestroyImmediate(adjuster);
}
// Legacy cleanup
foreach (var sar in context.AvatarRootObject.GetComponentsInChildren<ScaleAdjusterRenderer>())
{
UnityEngine.Object.DestroyImmediate(sar.gameObject);
Object.DestroyImmediate(sar.gameObject);
}
foreach (var sar in context.AvatarRootObject.GetComponentsInChildren<ScaleProxy>())
{
Object.DestroyImmediate(sar.gameObject);
}
if (boneMappings.Count == 0)

View File

@ -1,86 +0,0 @@
using System.Collections.Generic;
using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections;
using UnityEngine;
namespace nadena.dev.modular_avatar.core
{
internal static class CameraHooks
{
private static Dictionary<SkinnedMeshRenderer, SkinnedMeshRenderer> originalToProxy
= new Dictionary<SkinnedMeshRenderer, SkinnedMeshRenderer>(
new ObjectIdentityComparer<SkinnedMeshRenderer>()
);
#if UNITY_EDITOR
[UnityEditor.InitializeOnLoadMethod]
private static void Init()
{
Camera.onPreCull += OnPreCull;
Camera.onPostRender += OnPostRender;
UnityEditor.AssemblyReloadEvents.beforeAssemblyReload += ClearStates;
UnityEditor.SceneManagement.EditorSceneManager.sceneSaving += (scene, path) => ClearStates();
}
#endif
internal static void RegisterProxy(SkinnedMeshRenderer original, SkinnedMeshRenderer proxy)
{
originalToProxy[original] = proxy;
}
internal static void UnregisterProxy(SkinnedMeshRenderer original)
{
originalToProxy.Remove(original);
}
private static List<(SkinnedMeshRenderer, bool)> statesToRestore = new List<(SkinnedMeshRenderer, bool)>();
private static List<SkinnedMeshRenderer> toDeregister = new List<SkinnedMeshRenderer>();
private static void OnPreCull(Camera camera)
{
ClearStates();
toDeregister.Clear();
foreach (var kvp in originalToProxy)
{
var original = kvp.Key;
var proxy = kvp.Value;
if (original == null || proxy == null)
{
toDeregister.Add(original);
continue;
}
proxy.enabled = original.enabled;
if (original.enabled && original.gameObject.activeInHierarchy)
{
statesToRestore.Add((original, original.enabled));
original.enabled = false;
}
}
foreach (var original in toDeregister)
{
originalToProxy.Remove(original);
}
}
private static void OnPostRender(Camera camera)
{
ClearStates();
}
private static void ClearStates()
{
foreach (var (original, state) in statesToRestore)
{
original.enabled = state;
}
statesToRestore.Clear();
}
}
}

View File

@ -1,6 +1,8 @@
#region
using System;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using UnityEditor;
@ -25,105 +27,100 @@ namespace nadena.dev.modular_avatar.core
set
{
m_Scale = value;
Update();
PreCull();
}
}
[SerializeField] [FormerlySerializedAs("scaleProxy")]
internal Transform legacyScaleProxy;
internal Transform scaleProxy;
internal Transform scaleProxyParent, scaleProxyChild;
[NonSerialized]
private bool initialized = false;
#if UNITY_EDITOR
void OnValidate()
void Awake()
{
base.OnValidate();
ProxyManager.RegisterAdjuster(this);
initialized = false;
}
private void Update()
void OnValidate()
{
if (scaleProxy == null || initialized == false)
ProxyManager.RegisterAdjuster(this);
initialized = false;
}
internal void PreCull()
{
if (PrefabUtility.IsPartOfPrefabAsset(this)) return;
if (scaleProxyParent == null || initialized == false)
{
InitializeProxy();
}
var xform = transform;
scaleProxyParent.position = transform.position;
scaleProxyParent.rotation = transform.rotation;
scaleProxyParent.localScale = transform.localScale;
scaleProxyChild.localScale = m_Scale;
ProxyManager.RegisterBone(xform, scaleProxyChild);
if (legacyScaleProxy != null && !PrefabUtility.IsPartOfPrefabAsset(legacyScaleProxy))
{
DestroyImmediate(legacyScaleProxy.gameObject);
legacyScaleProxy = null;
}
scaleProxy.localScale = m_Scale;
}
private void InitializeProxy()
{
if (scaleProxy == null)
if (scaleProxyParent == null)
{
scaleProxy = new GameObject(gameObject.name + " (Scale Proxy)").transform;
scaleProxy.SetParent(transform, false);
scaleProxy.localPosition = Vector3.zero;
scaleProxy.localRotation = Quaternion.identity;
scaleProxy.localScale = m_Scale;
scaleProxy.gameObject.AddComponent<ScaleProxy>();
PrefabUtility.RecordPrefabInstancePropertyModifications(this);
scaleProxyParent = new GameObject(gameObject.name + " (Scale Proxy)").transform;
scaleProxyChild = new GameObject("Child").transform;
scaleProxyChild.transform.SetParent(scaleProxyParent, false);
#if MODULAR_AVATAR_DEBUG_HIDDEN
scaleProxyParent.gameObject.hideFlags = HideFlags.DontSave;
scaleProxyChild.gameObject.hideFlags = HideFlags.DontSave;
#else
scaleProxyParent.gameObject.hideFlags = HideFlags.HideAndDontSave;
scaleProxyChild.gameObject.hideFlags = HideFlags.HideAndDontSave;
#endif
if (scaleProxyParent.gameObject.scene != gameObject.scene && gameObject.scene.IsValid())
{
SceneManager.MoveGameObjectToScene(scaleProxyParent.gameObject, gameObject.scene);
}
}
ConfigureRenderers();
initialized = true;
}
private void OnDestroy()
{
if (scaleProxy != null)
ProxyManager.UnregisterAdjuster(this);
if (scaleProxyParent != null)
{
DestroyImmediate(scaleProxy.gameObject);
DestroyImmediate(scaleProxyParent.gameObject);
}
if (transform != null)
{
ProxyManager.UnregisterBone(transform);
}
ScaleAdjusterRenderer.InvalidateAll();
base.OnDestroy();
}
private void ConfigureRenderers()
{
var avatar = RuntimeUtil.FindAvatarInParents(transform);
if (avatar == null) return;
foreach (var smr in avatar.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
if (smr.GetComponent<ScaleAdjusterRenderer>() != null) continue;
var child = smr.transform.Find(ADJUSTER_OBJECT)?.GetComponent<ScaleAdjusterRenderer>();
if (child == null)
{
var childObj = new GameObject(ADJUSTER_OBJECT);
Undo.RegisterCreatedObjectUndo(childObj, "");
var childSmr = childObj.AddComponent<SkinnedMeshRenderer>();
EditorUtility.CopySerialized(smr, childSmr);
childObj.transform.SetParent(smr.transform, false);
childObj.transform.localPosition = Vector3.zero;
childObj.transform.localRotation = Quaternion.identity;
childObj.transform.localScale = Vector3.one;
child = childObj.AddComponent<ScaleAdjusterRenderer>();
}
child.BoneMappings[transform] = scaleProxy;
child.ClearBoneCache();
}
}
#endif
#if !UNITY_EDITOR
private void Update()
{
// placeholder to make builds work
#else
internal void PreCull() {
// build time stub
}
#endif
}

View File

@ -0,0 +1,338 @@
#region
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif
using UnityEngine;
using UnityEngine.SceneManagement;
#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)
{
_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;
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)
{
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(RuntimeUtil.FindAvatarTransformInParents).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 (_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 (_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)
{
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.enabled = false;
}
}
}
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.enabled = state;
}
statesToRestore.Clear();
}
#endif
}
}

View File

@ -1,300 +1,33 @@
#region
using UnityEngine;
using VRC.SDKBase;
#if UNITY_EDITOR
using UnityEditor;
#endif
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections;
using UnityEngine;
using VRC.SDKBase;
using Object = System.Object;
#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
{
internal static Dictionary<ScaleAdjusterRenderer, GameObject> originalParent =
new Dictionary<ScaleAdjusterRenderer, GameObject>(new ObjectIdentityComparer<ScaleAdjusterRenderer>());
internal static Dictionary<GameObject, GameObject> proxyObjects = new Dictionary<GameObject, GameObject>(
new ObjectIdentityComparer<GameObject>());
internal static Dictionary<GameObject, ScaleAdjusterRenderer> originalObjects =
new Dictionary<GameObject, ScaleAdjusterRenderer>(
new ObjectIdentityComparer<GameObject>()
);
private static int RecreateHierarchyIndexCount = 0;
#if UNITY_EDITOR
[InitializeOnLoadMethod]
static void Setup()
{
EditorApplication.hierarchyChanged += InvalidateAll;
}
#endif
internal static void InvalidateAll()
{
RecreateHierarchyIndexCount++;
}
private SkinnedMeshRenderer myRenderer;
private SkinnedMeshRenderer parentRenderer;
private bool wasActive = false;
private bool redoBoneMappings = true;
private bool hasRelevantBones = false;
private int lastRecreateHierarchyIndex = -1;
internal Dictionary<Transform, Transform> BoneMappings = new Dictionary<Transform, Transform>(
new ObjectIdentityComparer<Transform>()
);
#if UNITY_EDITOR
private void OnValidate()
{
if (PrefabUtility.IsPartOfPrefabAsset(this)) return;
redoBoneMappings = true;
EditorApplication.delayCall += () =>
{
if (this == null) return;
// We hide this in Harmony, not here, so it is eligible for click-to-select.
gameObject.hideFlags = HideFlags.DontSaveInBuild;
if (BoneMappings == null)
{
BoneMappings = new Dictionary<Transform, Transform>();
}
Configure();
if (this != null) DestroyImmediate(gameObject);
};
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
redoBoneMappings = true;
}
private void OnPlayModeStateChanged(PlayModeStateChange change)
{
if (change == PlayModeStateChange.ExitingEditMode)
{
ClearHooks();
}
}
#endif
private Transform MapBone(Transform bone)
{
if (bone == null) return null;
if (BoneMappings.TryGetValue(bone, out var newBone) && newBone != null) return newBone;
return bone;
}
private void OnDestroy()
{
ClearHooks();
#if UNITY_EDITOR
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
#endif
}
private void Configure()
{
if (originalParent.TryGetValue(this, out var prevParent) && transform.parent?.gameObject == prevParent)
{
return;
}
if (prevParent != null)
{
ClearHooks();
}
if (!hasRelevantBones)
{
return;
}
if (transform.parent == null)
{
return;
}
#if UNITY_EDITOR
AssemblyReloadEvents.beforeAssemblyReload += ClearHooks;
#endif
var parent = transform.parent.gameObject;
proxyObjects[gameObject] = parent;
originalObjects[parent] = this;
originalParent[this] = parent;
CheckBoneUsage(); // register the proxy if needed
}
private void ClearHooks()
{
if (originalParent.TryGetValue(this, out var prevParent))
{
if (parentRenderer != null)
{
CameraHooks.UnregisterProxy(parentRenderer);
}
if ((Object)prevParent != null)
{
originalObjects.Remove(prevParent);
}
originalParent.Remove(this);
if (gameObject != null)
{
proxyObjects.Remove(gameObject);
}
else
{
CleanDeadObjects();
}
}
}
#if UNITY_EDITOR
private void Update()
{
if (EditorApplication.isPlayingOrWillChangePlaymode) return;
if (transform.parent == null)
{
DestroyImmediate(gameObject);
return;
}
if (myRenderer == null)
{
myRenderer = GetComponent<SkinnedMeshRenderer>();
}
if (parentRenderer == null)
{
parentRenderer = transform.parent.GetComponent<SkinnedMeshRenderer>();
}
Configure();
myRenderer.sharedMaterials = parentRenderer.sharedMaterials;
myRenderer.localBounds = parentRenderer.localBounds;
if (redoBoneMappings || lastRecreateHierarchyIndex != RecreateHierarchyIndexCount
|| myRenderer.sharedMesh != parentRenderer.sharedMesh)
{
CleanDeadObjects(BoneMappings);
if (BoneMappings.Count == 0)
{
#if UNITY_2022_3_OR_NEWER
DestroyImmediate(gameObject);
return;
#endif
}
myRenderer.sharedMesh = parentRenderer.sharedMesh;
myRenderer.rootBone = MapBone(parentRenderer.rootBone);
myRenderer.bones = parentRenderer.bones.Select(MapBone).ToArray();
redoBoneMappings = false;
lastRecreateHierarchyIndex = RecreateHierarchyIndexCount;
CheckBoneUsage();
}
if (!hasRelevantBones) return;
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.sharedMesh != null)
{
var blendShapeCount = myRenderer.sharedMesh.blendShapeCount;
for (int i = 0; i < blendShapeCount; i++)
{
myRenderer.SetBlendShapeWeight(i, parentRenderer.GetBlendShapeWeight(i));
}
}
}
#endif
private void CheckBoneUsage()
{
hasRelevantBones = false;
if (myRenderer.sharedMesh != null)
{
var weights = myRenderer.sharedMesh.GetAllBoneWeights();
var parentBones = parentRenderer.bones;
foreach (var weight in weights)
{
if (weight.weight < 0.0001f) continue;
if (weight.boneIndex < 0 || weight.boneIndex >= parentBones.Length) continue;
var bone = parentBones[weight.boneIndex];
if (BoneMappings.ContainsKey(bone))
{
hasRelevantBones = true;
break;
}
}
}
if (hasRelevantBones)
{
CameraHooks.RegisterProxy(parentRenderer, myRenderer);
}
else
{
CameraHooks.UnregisterProxy(parentRenderer);
myRenderer.enabled = false;
}
}
public void ClearBoneCache()
{
redoBoneMappings = true;
}
private static void CleanDeadObjects()
{
CleanDeadObjects(originalParent);
CleanDeadObjects(originalObjects);
CleanDeadObjects(proxyObjects);
}
private static int lastCleanedFrame = 0;
private static void CleanDeadObjects<K, V>(IDictionary<K, V> dict)
where K: UnityEngine.Object
where V: UnityEngine.Object
{
// Avoid any O(n^2) behavior if we have lots of cleanup calls happening at the same instant
if (Time.frameCount == lastCleanedFrame) return;
lastCleanedFrame = Time.frameCount;
var dead = dict.Where(kvp => kvp.Key == null || kvp.Value == null).ToList();
foreach (var kvp in dead)
{
dict.Remove(kvp.Key);
}
}
}
}

View File

@ -16,13 +16,12 @@ namespace nadena.dev.modular_avatar.core
#if UNITY_EDITOR
void OnValidate()
{
base.OnDestroy();
base.OnValidate();
EditorApplication.delayCall += DeferredValidate;
}
void OnDestroy()
{
ScaleAdjusterRenderer.InvalidateAll();
base.OnDestroy();
}
@ -30,44 +29,12 @@ namespace nadena.dev.modular_avatar.core
{
if (this == null) return;
if (GetComponent<ModularAvatarPBBlocker>() == null)
// Avoid logspam on Unity 2019
if (PrefabUtility.IsPartOfPrefabInstance(gameObject)) return;
if (!ProxyManager.ShouldRetain(gameObject))
{
gameObject.AddComponent<ModularAvatarPBBlocker>();
}
var avatar = ndmf.runtime.RuntimeUtil.FindAvatarInParents(transform);
ClearOverrides(avatar);
gameObject.hideFlags = HideFlags.HideInHierarchy;
#if MODULAR_AVATAR_DEBUG_HIDDEN
gameObject.hideFlags = HideFlags.None;
#endif
hideFlags = HideFlags.None;
var parentObject = transform.parent;
var parentScaleAdjuster =
parentObject != null ? parentObject.GetComponent<ModularAvatarScaleAdjuster>() : null;
if (parentScaleAdjuster == null || parentScaleAdjuster.scaleProxy != transform)
{
if (PrefabUtility.IsPartOfPrefabAsset(this))
{
var path = AssetDatabase.GetAssetPath(this);
var root = PrefabUtility.LoadPrefabContents(path);
foreach (var obj in root.GetComponentsInChildren<ScaleProxy>())
{
obj.DeferredValidate();
}
PrefabUtility.SaveAsPrefabAsset(root, path);
PrefabUtility.UnloadPrefabContents(root);
}
else
{
SelfDestruct();
}
SelfDestruct();
}
}
@ -79,10 +46,9 @@ namespace nadena.dev.modular_avatar.core
root = transform;
while (root.parent != null) root = root.parent;
}
ClearOverrides(root);
// Avoid logspam on Unity 2019
if (PrefabUtility.IsPartOfPrefabInstance(gameObject)) return;
DestroyImmediate(gameObject);