feat: add the Replace Object component

This commit is contained in:
bd_ 2023-07-23 14:22:25 +09:00
parent 4b40c5197a
commit 4240a4f4cf
10 changed files with 463 additions and 0 deletions

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4c1526a4a3c8483696aed2fa0c98e3da
timeCreated: 1690621713

View File

@ -0,0 +1,169 @@
using System;
using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using NUnit.Framework;
using UnityEditor.VersionControl;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
namespace modular_avatar_tests.ReplaceObject
{
public class ReplaceObjectTests : TestBase
{
void Process(GameObject root)
{
var avDesc = root.GetComponent<VRCAvatarDescriptor>();
new ReplaceObjectPass(new BuildContext(avDesc)).Process();
}
[Test]
public void smokeTest()
{
var root = CreateRoot("root");
var replacee = CreateChild(root, "replacee");
var replacement = CreateChild(root, "replacement");
replacee.AddComponent<SphereCollider>();
replacement.AddComponent<BoxCollider>();
var replaceObject = root.AddComponent<ModularAvatarReplaceObject>();
replaceObject.targetObject.referencePath = RuntimeUtil.AvatarRootPath(replacee);
Process(root);
Assert.AreEqual(1, root.transform.childCount);
Assert.AreEqual(root.transform.GetChild(0).gameObject, replacement);
//Assert.AreEqual(replacement.name, "replacee");
Assert.IsTrue(replacee == null);
Assert.NotNull(replacement.GetComponent<BoxCollider>());
Assert.IsNull(root.GetComponentInChildren<SphereCollider>());
}
public class TestComponent : MonoBehaviour
{
public Transform transformRef;
public GameObject gameObjectRef;
public BoxCollider memberRef;
public BoxCollider secondMemberRef;
public SphereCollider lostRef;
public Transform rootRef;
}
[Test]
public void rewritesReferences()
{
var root = CreateRoot("root");
var replacee = CreateChild(root, "replacee");
var replacement = CreateChild(root, "replacement");
var reference = CreateChild(root, "reference");
var testComponent = reference.AddComponent<TestComponent>();
testComponent.transformRef = replacee.transform;
testComponent.gameObjectRef = replacee;
testComponent.memberRef = replacee.AddComponent<BoxCollider>();
testComponent.secondMemberRef = replacee.AddComponent<BoxCollider>();
testComponent.lostRef = replacee.AddComponent<SphereCollider>();
testComponent.rootRef = root.transform;
var newBoxCollider1 = replacement.AddComponent<BoxCollider>();
var newBoxCollider2 = replacement.AddComponent<BoxCollider>();
newBoxCollider1.center = Vector3.up; // just to make it easier to observe in the debugger
newBoxCollider2.center = Vector3.up * 2;
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
replaceObject.targetObject.Set(replacee);
Process(root);
Assert.AreEqual(2, root.transform.childCount);
Assert.AreEqual(root.transform.GetChild(0).gameObject, replacement);
Assert.AreEqual(testComponent.transformRef, replacement.transform);
Assert.AreEqual(testComponent.gameObjectRef, replacement);
Assert.AreEqual(testComponent.memberRef, newBoxCollider1);
Assert.AreEqual(testComponent.secondMemberRef, newBoxCollider2);
Assert.AreEqual(testComponent.lostRef, null);
Assert.AreEqual(testComponent.rootRef, root.transform);
}
// Test: disallow replacing parent of replacee
[Test]
public void disallowReplacingParent()
{
var root = CreateRoot("root");
var replacee = CreateChild(root, "replacee");
var replacement = CreateChild(root, "replacement");
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
replaceObject.targetObject.Set(root);
BuildReport.Clear();
Assert.Throws<Exception>(() =>
{
using (BuildReport.CurrentReport.ReportingOnAvatar(root.GetComponent<VRCAvatarDescriptor>()))
{
Process(root);
}
});
}
[Test]
public void abortWhenTargetNull()
{
var root = CreateRoot("root");
var replacee = CreateChild(root, "replacee");
var replacement = CreateChild(root, "replacement");
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
replaceObject.targetObject.Set(null);
BuildReport.Clear();
Assert.Throws<Exception>(() =>
{
using (BuildReport.CurrentReport.ReportingOnAvatar(root.GetComponent<VRCAvatarDescriptor>()))
{
Process(root);
}
});
}
// Test: child object handling
[Test]
public void preservesExistingChildObjects()
{
var root = CreateRoot("root");
var replacee = CreateChild(root, "replacee");
var replacement = CreateChild(root, "replacement");
var child1 = CreateChild(replacee, "child1");
var child2 = CreateChild(replacement, "child2");
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
replaceObject.targetObject.Set(replacee);
Process(root);
Assert.AreEqual(1, root.transform.childCount);
Assert.AreEqual(replacement.transform, child1.transform.parent);
Assert.AreEqual(replacement.transform, child2.transform.parent);
}
// Test: PathMappings
[Test]
public void updatesPathMappings()
{
var root = CreateRoot("root");
var replacee = CreateChild(root, "replacee");
var replacement = CreateChild(root, "replacement");
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
replaceObject.targetObject.Set(replacee);
PathMappings.Init(root);
Process(root);
Assert.AreEqual("replacement", PathMappings.MapPath("replacee"));
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bdc6f8e53d1843ec8d22d7f5141aff52
timeCreated: 1690621719

View File

@ -207,6 +207,7 @@ namespace nadena.dev.modular_avatar.core.editor
new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
new VisibleHeadAccessoryProcessor(vrcAvatarDescriptor).Process(context);
new MeshSettingsPass(context).OnPreprocessAvatar();
new ReplaceObjectPass(context).Process();
new RemapAnimationPass(vrcAvatarDescriptor).Process(context.AnimationDatabase);
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context);
PhysboneBlockerPass.Process(avatarGameObject);

View File

@ -114,6 +114,37 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
/// <summary>
/// Marks an object as having been replaced by another object. All references to the old object will be replaced
/// by the new object. References originally to the new object will continue to point to the new object.
/// </summary>
/// <param name="old"></param>
/// <param name="newObject"></param>
public static void ReplaceObject(GameObject old, GameObject newObject)
{
ClearCache();
if (_objectToOriginalPaths.TryGetValue(old, out var paths))
{
if (!_objectToOriginalPaths.TryGetValue(newObject, out var newObjectPaths))
{
newObjectPaths = new List<string>();
_objectToOriginalPaths.Add(newObject, newObjectPaths);
}
newObjectPaths.AddRange(paths);
_objectToOriginalPaths.Remove(old);
}
if (_transformLookthroughObjects.Contains(old))
{
_transformLookthroughObjects.Remove(old);
_transformLookthroughObjects.Add(newObject);
}
}
private static ImmutableDictionary<string, string> BuildMapping(ref ImmutableDictionary<string, string> cache,
bool transformLookup)
{

View File

@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
{
using UnityObject = UnityEngine.Object;
// ReSharper disable once RedundantUsingDirective
using Object = System.Object;
internal class ReplaceObjectPass
{
private readonly BuildContext _buildContext;
public ReplaceObjectPass(BuildContext context)
{
_buildContext = context;
}
struct Reference
{
public UnityObject Source;
public string PropPath;
}
public void Process()
{
var avatarDescriptor = _buildContext.AvatarDescriptor;
var replacementComponents =
avatarDescriptor.GetComponentsInChildren<ModularAvatarReplaceObject>(true);
if (replacementComponents.Length == 0) return;
// Build an index of object references within the avatar that we might need to fix
Dictionary<UnityObject, List<Reference>> refIndex = BuildReferenceIndex();
Dictionary<GameObject, (ModularAvatarReplaceObject, GameObject)> replacements
= new Dictionary<GameObject, (ModularAvatarReplaceObject, GameObject)>();
foreach (var component in replacementComponents)
{
var targetObject = component.targetObject?.Get(_buildContext.AvatarDescriptor);
if (targetObject == null)
{
BuildReport.LogFatal("replace_object.null_target", new string[0],
component, targetObject);
UnityObject.DestroyImmediate(component.gameObject);
continue;
}
if (component.transform.GetComponentsInParent<Transform>().Contains(targetObject.transform))
{
BuildReport.LogFatal("replace_object.parent_of_target", new string[0],
component, targetObject);
UnityObject.DestroyImmediate(component.gameObject);
continue;
}
if (replacements.TryGetValue(targetObject, out var existingReplacement))
{
BuildReport.LogFatal("replace_object.replacing_replacement", new string[0],
component, existingReplacement.Item1);
UnityObject.DestroyImmediate(component);
continue;
}
replacements[targetObject] = (component, component.gameObject);
}
// Execute replacement. For now, we reparent children.
// TODO: Handle replacing recursively.
foreach (var kvp in replacements)
{
var original = kvp.Key;
var replacement = kvp.Value.Item2;
replacement.transform.SetParent(original.transform.parent, true);
var siblingIndex = original.transform.GetSiblingIndex();
// Move children of original parent
foreach (Transform child in original.transform)
{
child.SetParent(replacement.transform, true);
}
// Update property references
foreach (var refKey in GetIndexedComponents(original))
{
var (component, index) = refKey;
if (!refIndex.TryGetValue(component, out var references))
{
continue;
}
UnityObject newValue = null;
if (component is GameObject)
{
newValue = replacement;
}
else
{
var replacementCandidates = replacement.GetComponents(component.GetType());
if (replacementCandidates.Length > index)
{
newValue = replacementCandidates[index];
}
}
foreach (var reference in references)
{
SerializedObject so = new SerializedObject(reference.Source);
SerializedProperty prop = so.FindProperty(reference.PropPath);
prop.objectReferenceValue = newValue;
so.ApplyModifiedPropertiesWithoutUndo();
}
}
PathMappings.ReplaceObject(original, replacement);
// Destroy original
UnityObject.DestroyImmediate(original);
replacement.transform.SetSiblingIndex(siblingIndex);
}
}
private IEnumerable<(UnityObject, int)> GetIndexedComponents(GameObject original)
{
yield return (original, -1);
Dictionary<Type, int> componentTypeIndex = new Dictionary<Type, int>();
foreach (var component in original.GetComponents<Component>())
{
if (!componentTypeIndex.TryGetValue(component.GetType(), out int index))
{
index = 0;
}
componentTypeIndex[component.GetType()] = index + 1;
yield return (component, index);
}
}
private Dictionary<UnityObject, List<Reference>> BuildReferenceIndex()
{
Dictionary<UnityObject, List<Reference>> refIndex = new Dictionary<UnityObject, List<Reference>>();
IndexObject(_buildContext.AvatarDescriptor.gameObject);
return refIndex;
void IndexObject(GameObject obj)
{
foreach (Transform child in obj.transform)
{
IndexObject(child.gameObject);
}
Dictionary<Type, int> componentIndex = new Dictionary<Type, int>();
foreach (Component c in obj.GetComponents(typeof(Component)))
{
if (c == null) continue;
if (c is Transform) continue;
if (!componentIndex.TryGetValue(c.GetType(), out int index)) index = 0;
componentIndex[c.GetType()] = index + 1;
var so = new SerializedObject(c);
var sp = so.GetIterator();
bool enterChildren = true;
while (sp.Next(enterChildren))
{
enterChildren = true;
if (sp.propertyType == SerializedPropertyType.String) enterChildren = false;
if (sp.propertyType != SerializedPropertyType.ObjectReference) continue;
if (sp.objectReferenceValue == null) continue;
string path = sp.propertyPath;
if (path == "m_GameObject") continue;
Reference reference;
if (sp.objectReferenceValue is GameObject)
{
// ok
}
else if (sp.objectReferenceValue is Component)
{
// ok
}
else
{
continue;
}
reference.Source = c;
reference.PropPath = sp.propertyPath;
var refKey = sp.objectReferenceValue;
if (!refIndex.TryGetValue(refKey, out var refList))
{
refList = new List<Reference>();
refIndex[refKey] = refList;
}
refList.Add(reference);
}
}
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b119c0a381384ad88e78550d23997aa7
timeCreated: 1690004198

View File

@ -1,5 +1,6 @@
using System;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
namespace nadena.dev.modular_avatar.core
{
@ -41,6 +42,24 @@ namespace nadena.dev.modular_avatar.core
return (_cachedReference = avatar.transform.Find(referencePath)?.gameObject);
}
public void Set(GameObject target)
{
if (target == null)
{
referencePath = "";
}
else if (target.GetComponent<VRCAvatarDescriptor>() != null)
{
referencePath = AVATAR_ROOT;
}
else
{
referencePath = RuntimeUtil.AvatarRootPath(target);
}
_cacheValid = false;
}
private void InvalidateCache()
{
RuntimeUtil.OnHierarchyChanged -= InvalidateCache;

View File

@ -0,0 +1,12 @@
using UnityEngine;
namespace nadena.dev.modular_avatar.core
{
[AddComponentMenu("Modular Avatar/MA Replace Object")]
[DisallowMultipleComponent]
public class ModularAvatarReplaceObject : AvatarTagComponent
{
public AvatarObjectReference targetObject = new AvatarObjectReference();
// public bool deleteChildren;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7e949680c0864ee7b441d9b2c93b890b
timeCreated: 1690004129