modular-avatar/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs
bd_ f7b12d7f82
fix: ScaleAdjuster breaks scene view selection (#718)
... fixed by reimplementing ScaleAdjuster (again!)
2024-03-03 17:26:23 +09:00

260 lines
8.0 KiB
C#

#region
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using VRC.SDKBase;
using Object = System.Object;
#endregion
namespace nadena.dev.modular_avatar.core
{
[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 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();
};
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 (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;
}
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>();
}
CameraHooks.RegisterProxy(parentRenderer, myRenderer);
Configure();
myRenderer.sharedMaterials = parentRenderer.sharedMaterials;
myRenderer.sharedMesh = parentRenderer.sharedMesh;
myRenderer.localBounds = parentRenderer.localBounds;
if (redoBoneMappings || lastRecreateHierarchyIndex != RecreateHierarchyIndexCount)
{
CleanDeadObjects(BoneMappings);
if (BoneMappings.Count == 0)
{
#if UNITY_2022_3_OR_NEWER
DestroyImmediate(gameObject);
return;
#endif
}
myRenderer.rootBone = MapBone(parentRenderer.rootBone);
myRenderer.bones = parentRenderer.bones.Select(MapBone).ToArray();
redoBoneMappings = false;
lastRecreateHierarchyIndex = RecreateHierarchyIndexCount;
}
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
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);
}
}
}
}