feat: NDMF integration

This commit is contained in:
bd_ 2023-08-05 15:47:03 +09:00
parent b155202714
commit 99386fc756
52 changed files with 512 additions and 1197 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "Packages/nadena.dev.ndmf"]
path = Packages/nadena.dev.ndmf
url = https://github.com/bdunderscore/ndmf.git

View File

@ -29,15 +29,15 @@ namespace modular_avatar_tests
var boneProxy = reference.AddComponent<ModularAvatarBoneProxy>();
boneProxy.target = root.transform;
boneProxy.ClearCache();
boneProxy.ClearCache(true);
Assert.AreEqual(root.transform, boneProxy.target);
boneProxy.target = target.transform;
boneProxy.ClearCache();
boneProxy.ClearCache(true);
Assert.AreEqual(target.transform, boneProxy.target);
target.name = "target2";
boneProxy.ClearCache();
boneProxy.ClearCache(true);
Assert.IsNull(boneProxy.target);
}

View File

@ -16,7 +16,7 @@ namespace modular_avatar_tests
Texture2D _iconTexture;
[SetUp]
public virtual void Setup()
public override void Setup()
{
base.Setup();
_gameObject = new GameObject();
@ -40,10 +40,10 @@ namespace modular_avatar_tests
if (type == typeof(TestComponent)) return;
// get icon
var component = (MonoBehaviour)_gameObject.AddComponent(type);
var component = (MonoBehaviour) _gameObject.AddComponent(type);
var monoScript = MonoScript.FromMonoBehaviour(component);
var scriptPath = AssetDatabase.GetAssetPath(monoScript);
var monoImporter = (MonoImporter)AssetImporter.GetAtPath(scriptPath);
var monoImporter = (MonoImporter) AssetImporter.GetAtPath(scriptPath);
// in Unity 2021.2, we can use monoImporter.GetIcon()
// but it's not available in unity 2019 so use SerializedObject
var serializedImporter = new SerializedObject(monoImporter);

View File

@ -6,6 +6,7 @@ namespace _ModularAvatar.EditModeTests
{
public class DuplicateObjectNameTest : TestBase
{
/* TODO - move to build framework
[Test]
public void test_duplicate_object_names()
{
@ -17,5 +18,6 @@ namespace _ModularAvatar.EditModeTests
c2.gameObject.name = "child2";
Assert.AreEqual(PathMappings.MapPath("child"), "child");
}
*/
}
}

View File

@ -1,4 +1,5 @@
using System.Linq;
using System.Linq;
using nadena.dev.ndmf.animation;
using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
@ -51,7 +52,10 @@ namespace modular_avatar_tests.MergeArmatureTests
m1_leaf2.AddComponent<TestComponentA>();
m2_leaf3.AddComponent<TestComponentB>();
BuildContext context = new BuildContext(root.GetComponent<VRCAvatarDescriptor>());
nadena.dev.ndmf.BuildContext context =
new nadena.dev.ndmf.BuildContext(root.GetComponent<VRCAvatarDescriptor>(), null);
context.ActivateExtensionContext<ModularAvatarContext>();
context.ActivateExtensionContext(typeof(TrackObjectRenamesContext));
new MergeArmatureHook().OnPreprocessAvatar(context, root);
Assert.IsTrue(bone.GetComponentInChildren<TestComponentA>() != null);
@ -76,7 +80,10 @@ namespace modular_avatar_tests.MergeArmatureTests
ma.mergeTarget.referencePath = RuntimeUtil.AvatarRootPath(armature);
ma.mangleNames = false;
BuildContext context = new BuildContext(root.GetComponent<VRCAvatarDescriptor>());
nadena.dev.ndmf.BuildContext context =
new nadena.dev.ndmf.BuildContext(root.GetComponent<VRCAvatarDescriptor>(), null);
context.ActivateExtensionContext<ModularAvatarContext>();
context.ActivateExtensionContext(typeof(TrackObjectRenamesContext));
new MergeArmatureHook().OnPreprocessAvatar(context, root);
Assert.IsTrue(m_bone == null); // destroyed by retargeting pass
@ -97,7 +104,10 @@ namespace modular_avatar_tests.MergeArmatureTests
var ma = merge.AddComponent<ModularAvatarMergeArmature>();
ma.mergeTarget.referencePath = RuntimeUtil.AvatarRootPath(armature);
BuildContext context = new BuildContext(root.GetComponent<VRCAvatarDescriptor>());
nadena.dev.ndmf.BuildContext context =
new nadena.dev.ndmf.BuildContext(root.GetComponent<VRCAvatarDescriptor>(), null);
context.ActivateExtensionContext<ModularAvatarContext>();
context.ActivateExtensionContext(typeof(TrackObjectRenamesContext));
new MergeArmatureHook().OnPreprocessAvatar(context, root);
Assert.IsTrue(m_bone == null); // destroyed by retargeting pass

View File

@ -1,69 +0,0 @@
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
using UnityEngine;
namespace modular_avatar_tests
{
public class PathMappingTest : TestBase
{
[Test]
public void TracksSimpleRenames()
{
var root = CreateRoot("root");
var a = CreateChild(root, "a");
PathMappings.Init(root);
Assert.AreEqual("a", PathMappings.MapPath("a"));
a.name = "b";
PathMappings.ClearCache();
Assert.AreEqual("b", PathMappings.MapPath("a"));
}
[Test]
public void TracksObjectMoves()
{
var root = CreateRoot("root");
var a = CreateChild(root, "a");
var b = CreateChild(root, "b");
PathMappings.Init(root);
Assert.AreEqual("a", PathMappings.MapPath("a"));
a.transform.parent = b.transform;
PathMappings.ClearCache();
Assert.AreEqual("b/a", PathMappings.MapPath("a"));
}
[Test]
public void TracksCollapses()
{
var root = CreateRoot("root");
var a = CreateChild(root, "a");
var b = CreateChild(a, "b");
var c = CreateChild(b, "c");
PathMappings.Init(root);
PathMappings.MarkRemoved(b);
c.transform.parent = a.transform;
Object.DestroyImmediate(b);
Assert.AreEqual("a/c", PathMappings.MapPath("a/b/c"));
}
[Test]
public void TransformLookthrough()
{
var root = CreateRoot("root");
var a = CreateChild(root, "a");
var b = CreateChild(a, "b");
var c = CreateChild(b, "c");
var d = CreateChild(c, "d");
PathMappings.Init(root);
PathMappings.MarkTransformLookthrough(b);
PathMappings.MarkTransformLookthrough(c);
Assert.AreEqual("a/b/c", PathMappings.MapPath("a/b/c"));
Assert.AreEqual("a", PathMappings.MapPath("a/b/c", true));
Assert.AreEqual("a/b/c/d", PathMappings.MapPath("a/b/c/d", true));
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: fc087947fd98b2b43a853f93161cfe13
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,4 +1,5 @@
using System;
using nadena.dev.ndmf.animation;
using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.modular_avatar.editor.ErrorReporting;
@ -10,10 +11,14 @@ namespace modular_avatar_tests.ReplaceObject
{
public class ReplaceObjectTests : TestBase
{
private TrackObjectRenamesContext pathMappings;
void Process(GameObject root)
{
var avDesc = root.GetComponent<VRCAvatarDescriptor>();
new ReplaceObjectPass(new BuildContext(avDesc)).Process();
var buildContext = new nadena.dev.ndmf.BuildContext(avDesc, null);
pathMappings = buildContext.ActivateExtensionContext<TrackObjectRenamesContext>();
new ReplaceObjectPass(buildContext).Process();
}
[Test]
@ -159,10 +164,9 @@ namespace modular_avatar_tests.ReplaceObject
var replaceObject = replacement.AddComponent<ModularAvatarReplaceObject>();
replaceObject.targetObject.Set(replacee);
PathMappings.Init(root);
Process(root);
Assert.AreEqual("replacement", PathMappings.MapPath("replacee"));
Assert.AreEqual("replacement", pathMappings.MapPath("replacee"));
}
}
}

View File

@ -1,4 +1,5 @@
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.ndmf.animation;
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
@ -20,9 +21,15 @@ namespace modular_avatar_tests
skinnedMeshRenderer.rootBone = b.transform;
Debug.Assert(skinnedMeshRenderer.bones.Length == 0);
BoneDatabase.AddMergedBone(b.transform);
var context = new BuildContext(root.GetComponent<VRCAvatarDescriptor>());
new RetargetMeshes().OnPreprocessAvatar(root, context);
var build_context =
new nadena.dev.ndmf.BuildContext(root.GetComponent<VRCAvatarDescriptor>(), null);
var torc = new TrackObjectRenamesContext();
torc.OnActivate(build_context);
var bonedb = new BoneDatabase();
bonedb.AddMergedBone(b.transform);
new RetargetMeshes().OnPreprocessAvatar(root, bonedb, torc);
Assert.AreEqual(a.transform, skinnedMeshRenderer.rootBone);
}
@ -41,9 +48,15 @@ namespace modular_avatar_tests
skinnedMeshRenderer.rootBone = b.transform;
Debug.Assert(skinnedMeshRenderer.bones.Length == 0);
BoneDatabase.AddMergedBone(b.transform);
var context = new BuildContext(root.GetComponent<VRCAvatarDescriptor>());
new RetargetMeshes().OnPreprocessAvatar(root, context);
var build_context =
new nadena.dev.ndmf.BuildContext(root.GetComponent<VRCAvatarDescriptor>(), null);
var torc = new TrackObjectRenamesContext();
torc.OnActivate(build_context);
var bonedb = new BoneDatabase();
bonedb.AddMergedBone(b.transform);
new RetargetMeshes().OnPreprocessAvatar(root, bonedb, torc);
Assert.AreEqual(a.transform, skinnedMeshRenderer.rootBone);
Assert.AreEqual(new Bounds(new Vector3(0, 0, 0), new Vector3(2, 2, 2)),

View File

@ -1,56 +0,0 @@
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
namespace modular_avatar_tests.serialization
{
class TestComponent : MonoBehaviour
{
public UnityEngine.Object ref1, ref2;
}
class TestScriptable : ScriptableObject
{
public UnityEngine.Object ref1, ref2;
}
public class SerializationSweepTest : TestBase
{
[Test]
public void testSerialization()
{
var root = CreateRoot("root");
var child = CreateChild(root, "child");
var testComponent = child.AddComponent<TestComponent>();
var testScriptable1 = ScriptableObject.CreateInstance<TestScriptable>();
var testScriptable2 = ScriptableObject.CreateInstance<TestScriptable>();
var testScriptable3 = ScriptableObject.CreateInstance<TestScriptable>();
var testScriptable4 = ScriptableObject.CreateInstance<TestScriptable>();
testComponent.ref1 = testScriptable1;
testComponent.ref2 = root;
testScriptable1.ref1 = testScriptable2;
testScriptable2.ref1 = testScriptable3;
testScriptable1.ref2 = testScriptable4;
testScriptable2.ref2 = testScriptable4;
testScriptable3.ref2 = testScriptable4;
BuildContext bc = new BuildContext(root.GetComponent<VRCAvatarDescriptor>());
bc.CommitReferencedAssets();
var path = AssetDatabase.GetAssetPath(testScriptable1);
Assert.IsFalse(string.IsNullOrEmpty(path));
Assert.AreEqual(path, AssetDatabase.GetAssetPath(testScriptable2));
Assert.AreEqual(path, AssetDatabase.GetAssetPath(testScriptable3));
Assert.AreEqual(path, AssetDatabase.GetAssetPath(testScriptable4));
Assert.IsTrue(string.IsNullOrEmpty(AssetDatabase.GetAssetPath(testComponent)));
Assert.IsTrue(string.IsNullOrEmpty(AssetDatabase.GetAssetPath(root)));
Assert.IsTrue(string.IsNullOrEmpty(AssetDatabase.GetAssetPath(child)));
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: a8035784f3364865a84cc938682be7a0
timeCreated: 1690804771

View File

@ -1,5 +1,6 @@
using System.Linq;
using modular_avatar_tests;
using nadena.dev.ndmf.runtime;
using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
@ -60,7 +61,7 @@ namespace _ModularAvatar.EditModeTests.SerializationTests
Assert.False(string.IsNullOrEmpty(path));
var mainAsset = AssetDatabase.LoadMainAssetAtPath(path);
Assert.IsInstanceOf<MAAssetBundle>(mainAsset);
Assert.IsInstanceOf<GeneratedAssets>(mainAsset);
}
}
}

View File

@ -8,7 +8,9 @@
"GUID:5718fb738711cd34ea54e9553040911d",
"GUID:b906909fcc54f634db50f2cad0f988d9",
"GUID:3456780c4fb2d324ab9c633d6f1b0ddb",
"GUID:e9745f6a32442194c8dc5a43e9ab86f9"
"GUID:e9745f6a32442194c8dc5a43e9ab86f9",
"GUID:62ced99b048af7f4d8dfe4bed8373d76",
"GUID:fe747755f7b44e048820525b07f9b956"
],
"includePlatforms": [
"Editor"

View File

@ -1,81 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 bd_
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
using UnityEditor;
using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
{
[InitializeOnLoad]
internal static class ApplyOnPlay
{
private const string MENU_NAME = "Tools/Modular Avatar/Apply on Play";
/**
* We need to process avatars before lyuma's av3 emulator wakes up and processes avatars; it does this in Awake,
* so we have to do our processing in Awake as well. This seems to work fine when first entering play mode, but
* if you subsequently enable an initially-disabled avatar, processing from within Awake causes an editor crash.
*
* To workaround this, we initially process in awake; then, after OnPlayModeStateChanged is invoked (ie, after
* all initially-enabled components have Awake called), we switch to processing from Start instead.
*/
private static RuntimeUtil.OnDemandSource armedSource = RuntimeUtil.OnDemandSource.Awake;
static ApplyOnPlay()
{
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
RuntimeUtil.OnDemandProcessAvatar = MaybeProcessAvatar;
EditorApplication.delayCall += () => Menu.SetChecked(MENU_NAME, ModularAvatarSettings.applyOnPlay);
}
private static void MaybeProcessAvatar(RuntimeUtil.OnDemandSource source, MonoBehaviour component)
{
if (ModularAvatarSettings.applyOnPlay && source == armedSource && component != null)
{
var avatar = RuntimeUtil.FindAvatarInParents(component.transform);
if (avatar == null) return;
AvatarProcessor.ProcessAvatar(avatar.gameObject);
}
}
[MenuItem(MENU_NAME)]
private static void ToggleApplyOnPlay()
{
ModularAvatarSettings.applyOnPlay = !ModularAvatarSettings.applyOnPlay;
Menu.SetChecked(MENU_NAME, ModularAvatarSettings.applyOnPlay);
}
private static void OnPlayModeStateChanged(PlayModeStateChange obj)
{
if (obj == PlayModeStateChange.EnteredPlayMode)
{
armedSource = RuntimeUtil.OnDemandSource.Start;
}
else if (obj == PlayModeStateChange.EnteredEditMode)
{
armedSource = RuntimeUtil.OnDemandSource.Awake;
}
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 246c8f6cb27c4758972ceac5e8700add
timeCreated: 1661822272

View File

@ -22,51 +22,16 @@
* SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDKBase.Editor.BuildPipeline;
using BuildReport = nadena.dev.modular_avatar.editor.ErrorReporting.BuildReport;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;
[assembly: InternalsVisibleTo("Tests")]
namespace nadena.dev.modular_avatar.core.editor
{
[InitializeOnLoad]
public class AvatarProcessor : IVRCSDKPreprocessAvatarCallback, IVRCSDKPostprocessAvatarCallback
public class AvatarProcessor
{
// Place after EditorOnly processing (which runs at -1024) but hopefully before most other user callbacks
public int callbackOrder => -25;
/// <summary>
/// Avoid recursive activation of avatar processing by suppressing starting processing while processing is
/// already in progress.
/// </summary>
private static bool nowProcessing = false;
internal delegate void AvatarProcessorCallback(GameObject obj, BuildContext context);
/// <summary>
/// This API is NOT stable. Do not use it yet.
/// </summary>
internal static event AvatarProcessorCallback AfterProcessing;
static AvatarProcessor()
{
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
[MenuItem("GameObject/ModularAvatar/Manual bake avatar", true, 100)]
static bool ValidateApplyToCurrentAvatarGameobject()
{
@ -82,311 +47,18 @@ namespace nadena.dev.modular_avatar.core.editor
[MenuItem("Tools/Modular Avatar/Manual bake avatar", true)]
private static bool ValidateApplyToCurrentAvatar()
{
var avatar = Selection.activeGameObject;
return (avatar != null && avatar.GetComponent<VRCAvatarDescriptor>() != null);
return ndmf.AvatarProcessor.CanProcessObject(Selection.activeGameObject);
}
[MenuItem("Tools/Modular Avatar/Manual bake avatar", false)]
private static void ApplyToCurrentAvatar()
{
var avatar = Selection.activeGameObject;
if (avatar == null || avatar.GetComponent<VRCAvatarDescriptor>() == null) return;
var basePath = "Assets/ModularAvatarOutput/" + avatar.name;
var savePath = basePath;
int extension = 0;
while (File.Exists(savePath) || Directory.Exists(savePath))
{
savePath = basePath + " " + (++extension);
}
string originalBasePath = RuntimeUtil.RelativePath(null, avatar);
avatar = Object.Instantiate(avatar);
string clonedBasePath = RuntimeUtil.RelativePath(null, avatar);
try
{
Util.OverridePath = savePath;
var original = avatar;
avatar.transform.position += Vector3.forward * 2;
BuildReport.Clear();
ProcessAvatar(avatar);
Selection.objects = new Object[] {avatar};
}
finally
{
Util.OverridePath = null;
BuildReport.RemapPaths(originalBasePath, clonedBasePath);
}
}
private static void OnPlayModeStateChanged(PlayModeStateChange obj)
{
if (obj == PlayModeStateChange.EnteredEditMode)
{
Util.DeleteTemporaryAssets();
}
}
public void OnPostprocessAvatar()
{
Util.DeleteTemporaryAssets();
}
public bool OnPreprocessAvatar(GameObject avatarGameObject)
{
try
{
BuildReport.Clear();
ProcessAvatar(avatarGameObject);
FixupAnimatorDebugData(avatarGameObject);
return true;
}
catch (Exception e)
{
Debug.LogError(e);
return false;
}
ndmf.AvatarProcessor.ProcessAvatarUI(Selection.activeGameObject);
}
public static void ProcessAvatar(GameObject avatarGameObject)
{
if (nowProcessing) return;
var vrcAvatarDescriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
Stopwatch sw = new Stopwatch();
sw.Start();
using (BuildReport.CurrentReport.ReportingOnAvatar(vrcAvatarDescriptor))
{
try
{
try
{
AssetDatabase.StartAssetEditing();
nowProcessing = true;
RemoveMissingScriptComponents(avatarGameObject);
ClearEditorOnlyTagComponents(avatarGameObject.transform);
BoneDatabase.ResetBones();
PathMappings.Init(vrcAvatarDescriptor.gameObject);
ClonedMenuMappings.Clear();
// Sometimes people like to nest one avatar in another, when transplanting clothing. To avoid issues
// with inconsistently determining the avatar root, we'll go ahead and remove the extra sub-avatars
// here.
foreach (Transform directChild in avatarGameObject.transform)
{
foreach (var component in directChild.GetComponentsInChildren<VRCAvatarDescriptor>(true))
{
Object.DestroyImmediate(component);
}
// Disable deprecation warning for reference to PipelineSaver
#pragma warning disable CS0618
foreach (var component in directChild.GetComponentsInChildren<PipelineSaver>(true))
#pragma warning restore CS0618
{
Object.DestroyImmediate(component);
}
}
var context = new BuildContext(vrcAvatarDescriptor);
new MeshSettingsPass(context).OnPreprocessAvatar();
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject, context);
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject, context);
context.AnimationDatabase.Bootstrap(vrcAvatarDescriptor);
new MenuInstallHook().OnPreprocessAvatar(avatarGameObject, context);
new MergeArmatureHook().OnPreprocessAvatar(context, avatarGameObject);
new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
new VisibleHeadAccessoryProcessor(vrcAvatarDescriptor).Process(context);
new ReplaceObjectPass(context).Process();
new RemapAnimationPass(vrcAvatarDescriptor).Process(context.AnimationDatabase);
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context);
PhysboneBlockerPass.Process(avatarGameObject);
context.CommitReferencedAssets();
AfterProcessing?.Invoke(avatarGameObject, context);
context.AnimationDatabase.Commit();
new GCGameObjectsPass(context, avatarGameObject).OnPreprocessAvatar();
context.CommitReferencedAssets();
}
finally
{
AssetDatabase.StopAssetEditing();
nowProcessing = false;
// Ensure that we clean up AvatarTagComponents after failed processing. This ensures we don't re-enter
// processing from the Awake method on the unprocessed AvatarTagComponents
var toDestroy = avatarGameObject.GetComponentsInChildren<AvatarTagComponent>(true).ToList();
var retryDestroy = new List<AvatarTagComponent>();
// Sometimes AvatarTagComponents have interdependencies and need to be deleted in the right order;
// retry until we purge them all.
bool madeProgress = true;
while (toDestroy.Count > 0)
{
if (!madeProgress)
{
throw new Exception("One or more components failed to destroy." +
RuntimeUtil.AvatarRootPath(toDestroy[0].gameObject));
}
foreach (var component in toDestroy)
{
try
{
if (component != null)
{
UnityEngine.Object.DestroyImmediate(component);
madeProgress = true;
}
}
catch (Exception)
{
retryDestroy.Add(component);
}
}
toDestroy = retryDestroy;
retryDestroy = new List<AvatarTagComponent>();
}
var activator = avatarGameObject.GetComponent<AvatarActivator>();
if (activator != null)
{
UnityEngine.Object.DestroyImmediate(activator);
}
ClonedMenuMappings.Clear();
AssetDatabase.SaveAssets();
Resources.UnloadUnusedAssets();
}
}
catch (Exception e)
{
BuildReport.LogException(e);
throw;
}
finally
{
ErrorReportUI.MaybeOpenErrorReportUI();
}
if (!BuildReport.CurrentReport.CurrentAvatar.successful)
{
throw new Exception("Fatal error reported during avatar processing.");
}
}
Debug.Log($"Processed avatar " + avatarGameObject.name + " in " + sw.ElapsedMilliseconds + "ms");
}
private static void RemoveMissingScriptComponents(GameObject avatarGameObject)
{
foreach (var child in avatarGameObject.GetComponentsInChildren<Transform>(true))
GameObjectUtility.RemoveMonoBehavioursWithMissingScript(child.gameObject);
}
private static void ClearEditorOnlyTagComponents(Transform obj)
{
// EditorOnly objects can be used for multiple purposes - users might want a camera rig to be available in
// play mode, for example. For now, we'll prune MA components from EditorOnly objects, but otherwise leave
// them in place when in play mode.
if (obj.CompareTag("EditorOnly"))
{
foreach (var component in obj.GetComponentsInChildren<AvatarTagComponent>(true))
{
UnityEngine.Object.DestroyImmediate(component);
}
}
else
{
foreach (Transform transform in obj)
{
ClearEditorOnlyTagComponents(transform);
}
}
}
[SuppressMessage("ReSharper", "PossibleNullReferenceException")]
private static void FixupAnimatorDebugData(GameObject avatarGameObject)
{
Object tempControlPanel = null;
try
{
// The VRCSDK captures some debug information about animators as part of the build process, prior to invoking
// hooks. For some reason this happens in the ValidateFeatures call on the SDK builder. Reinvoke it to
// refresh this debug info.
//
// All of these methods are public, but for compatibility with unitypackage-based SDKs, we need to use
// reflection to invoke everything here, as the asmdef structure is different between the two SDK variants.
// Bleh.
//
// Canny filed requesting that this processing move after build hooks:
// https://feedback.vrchat.com/sdk-bug-reports/p/animator-debug-information-needs-to-be-captured-after-invoking-preprocess-avatar
var ty_VRCSdkControlPanelAvatarBuilder3A = Util.FindType(
"VRC.SDK3.Editor.VRCSdkControlPanelAvatarBuilder3A"
);
var ty_AvatarPerformanceStats = Util.FindType(
"VRC.SDKBase.Validation.Performance.Stats.AvatarPerformanceStats"
);
var ty_VRCSdkControlPanel = Util.FindType("VRCSdkControlPanel");
if (ty_VRCSdkControlPanelAvatarBuilder3A == null || ty_AvatarPerformanceStats == null ||
ty_VRCSdkControlPanel == null)
{
return;
}
var avatar = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
var animator = avatarGameObject.GetComponent<Animator>();
var builder = ty_VRCSdkControlPanelAvatarBuilder3A.GetConstructor(Type.EmptyTypes)
?.Invoke(Array.Empty<object>());
var perfStats = ty_AvatarPerformanceStats.GetConstructor(new[] {typeof(bool)})
?.Invoke(new object[] {false});
if (builder == null || perfStats == null)
{
return;
}
tempControlPanel = ScriptableObject.CreateInstance(ty_VRCSdkControlPanel) as Object;
ty_VRCSdkControlPanelAvatarBuilder3A
.GetMethod("RegisterBuilder", BindingFlags.Public | BindingFlags.Instance)
.Invoke(builder, new object[] {tempControlPanel});
ty_VRCSdkControlPanelAvatarBuilder3A.GetMethod("ValidateFeatures").Invoke(
builder, new object[] {avatar, animator, perfStats}
);
}
catch (Exception e)
{
Debug.LogWarning(
"[ModularAvatar] Incompatible VRCSDK version; failed to regenerate animator debug data");
Debug.LogException(e);
}
finally
{
if (tempControlPanel != null) Object.DestroyImmediate(tempControlPanel);
}
ndmf.AvatarProcessor.ProcessAvatar(avatarGameObject);
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
@ -12,9 +11,11 @@ namespace nadena.dev.modular_avatar.core.editor
{
internal class BuildContext
{
internal readonly VRCAvatarDescriptor AvatarDescriptor;
internal readonly nadena.dev.ndmf.BuildContext PluginBuildContext;
internal VRCAvatarDescriptor AvatarDescriptor => PluginBuildContext.AvatarDescriptor;
internal readonly AnimationDatabase AnimationDatabase = new AnimationDatabase();
internal readonly UnityEngine.Object AssetContainer;
internal UnityEngine.Object AssetContainer => PluginBuildContext.AssetContainer;
private bool SaveImmediate = false;
@ -29,16 +30,14 @@ namespace nadena.dev.modular_avatar.core.editor
internal readonly Dictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>> PostProcessControls
= new Dictionary<ModularAvatarMenuInstaller, Action<VRCExpressionsMenu.Control>>();
public BuildContext(VRCAvatarDescriptor avatarDescriptor)
public BuildContext(nadena.dev.ndmf.BuildContext PluginBuildContext)
{
AvatarDescriptor = avatarDescriptor;
this.PluginBuildContext = PluginBuildContext;
}
// AssetDatabase.CreateAsset is super slow - so only do it once, and add everything else as sub-assets.
// This scriptable object exists for the sole purpose of providing a placeholder to dump everything we
// generate into. Note that we use a custom component here to force binary serialization; this saves both
// time as well as disk space (if you're using manual bake).
AssetContainer = ScriptableObject.CreateInstance<MAAssetBundle>();
AssetDatabase.CreateAsset(AssetContainer, Util.GenerateAssetPath());
public BuildContext(VRCAvatarDescriptor avatarDescriptor)
: this(new ndmf.BuildContext(avatarDescriptor, null))
{
}
public void SaveAsset(Object obj)
@ -124,95 +123,5 @@ namespace nadena.dev.modular_avatar.core.editor
return newMenu;
}
public void CommitReferencedAssets()
{
HashSet<UnityEngine.Object> referencedAssets = new HashSet<UnityEngine.Object>();
HashSet<UnityEngine.Object> sceneAssets = new HashSet<UnityEngine.Object>();
Walk(AvatarDescriptor.gameObject);
referencedAssets.RemoveWhere(sceneAssets.Contains);
referencedAssets.RemoveWhere(a => a is GameObject || a is Component);
referencedAssets.RemoveWhere(o => !string.IsNullOrEmpty(AssetDatabase.GetAssetPath(o)));
int index = 0;
foreach (var asset in referencedAssets)
{
if (asset.name == "")
{
asset.name = "Asset " + index++;
}
AssetDatabase.AddObjectToAsset(asset, AssetContainer);
}
SaveImmediate = true;
void Walk(GameObject root)
{
var components = AvatarDescriptor.gameObject.GetComponentsInChildren<Component>(true);
Queue<UnityEngine.Object> visitQueue = new Queue<UnityEngine.Object>(
components.Where(t => (!(t is Transform)))
);
while (visitQueue.Count > 0)
{
var current = visitQueue.Dequeue();
if (referencedAssets.Contains(current)) continue;
referencedAssets.Add(current);
// These assets have large internal arrays we don't want to walk through...
if (current is Mesh || current is AnimationClip || current is Texture) continue;
var so = new SerializedObject(current);
var sp = so.GetIterator();
bool enterChildren = true;
while (sp.Next(enterChildren))
{
enterChildren = true;
if (sp.name == "m_GameObject") continue;
if (sp.propertyType == SerializedPropertyType.String)
{
enterChildren = false;
continue;
}
if (sp.isArray && IsPrimitiveArray(sp))
{
enterChildren = false;
}
if (sp.propertyType != SerializedPropertyType.ObjectReference)
{
continue;
}
var obj = sp.objectReferenceValue;
if (obj != null && !referencedAssets.Contains(obj) && !(obj is Transform) &&
!(obj is GameObject))
{
visitQueue.Enqueue(sp.objectReferenceValue);
}
}
}
}
}
private bool IsPrimitiveArray(SerializedProperty prop)
{
if (prop.arraySize == 0) return false;
var propertyType = prop.GetArrayElementAtIndex(0).propertyType;
switch (propertyType)
{
case SerializedPropertyType.Generic:
case SerializedPropertyType.ObjectReference:
return false;
default:
return true;
}
}
}
}

View File

@ -1,51 +0,0 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using VRC.SDK3.Avatars.ScriptableObjects;
namespace nadena.dev.modular_avatar.core.editor
{
internal static class ClonedMenuMappings
{
/// <summary>
/// Map to link the cloned menu from the clone source.
/// If one menu is specified for multiple installers, they are replicated separately, so there is a one-to-many relationship.
/// </summary>
private static readonly Dictionary<VRCExpressionsMenu, ImmutableList<VRCExpressionsMenu>> ClonedMappings =
new Dictionary<VRCExpressionsMenu, ImmutableList<VRCExpressionsMenu>>();
/// <summary>
/// Map to link the clone source from the cloned menu.
/// Map is the opposite of ClonedMappings.
/// </summary>
private static readonly Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> OriginalMapping =
new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
public static void Clear()
{
ClonedMappings.Clear();
OriginalMapping.Clear();
}
public static void Add(VRCExpressionsMenu original, VRCExpressionsMenu clonedMenu)
{
if (!ClonedMappings.TryGetValue(original, out ImmutableList<VRCExpressionsMenu> clonedMenus))
{
clonedMenus = ImmutableList<VRCExpressionsMenu>.Empty;
}
ClonedMappings[original] = clonedMenus.Add(clonedMenu);
OriginalMapping[clonedMenu] = original;
}
public static bool TryGetClonedMenus(VRCExpressionsMenu original,
out ImmutableList<VRCExpressionsMenu> clonedMenus)
{
return ClonedMappings.TryGetValue(original, out clonedMenus);
}
public static VRCExpressionsMenu GetOriginal(VRCExpressionsMenu cloned)
{
return OriginalMapping.TryGetValue(cloned, out VRCExpressionsMenu original) ? original : null;
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: aeaeff9c3af44683bb2f8f5fe6c5791d
timeCreated: 1671016064

View File

@ -25,6 +25,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf.animation;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEngine;
@ -38,13 +39,19 @@ namespace nadena.dev.modular_avatar.core.editor
{
internal class MergeArmatureHook
{
private ndmf.BuildContext frameworkContext;
private BuildContext context;
private BoneDatabase BoneDatabase = new BoneDatabase();
private TrackObjectRenamesContext PathMappings => frameworkContext.Extension<TrackObjectRenamesContext>();
private HashSet<Transform> mergedObjects = new HashSet<Transform>();
private HashSet<Transform> thisPassAdded = new HashSet<Transform>();
internal void OnPreprocessAvatar(BuildContext context, GameObject avatarGameObject)
internal void OnPreprocessAvatar(ndmf.BuildContext context, GameObject avatarGameObject)
{
this.context = context;
this.frameworkContext = context;
this.context = context.Extension<ModularAvatarContext>().BuildContext;
var mergeArmatures =
avatarGameObject.transform.GetComponentsInChildren<ModularAvatarMergeArmature>(true);
@ -74,7 +81,7 @@ namespace nadena.dev.modular_avatar.core.editor
RetainBoneReferences(c as Component);
}
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, context);
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, PathMappings);
}
private void TopoProcessMergeArmatures(ModularAvatarMergeArmature[] mergeArmatures)

View File

@ -25,37 +25,38 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using nadena.dev.ndmf.animation;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
{
internal static class BoneDatabase
internal class BoneDatabase
{
private static Dictionary<Transform, bool> m_IsRetargetable = new Dictionary<Transform, bool>();
private Dictionary<Transform, bool> m_IsRetargetable = new Dictionary<Transform, bool>();
internal static void ResetBones()
internal void ResetBones()
{
m_IsRetargetable.Clear();
}
internal static bool IsRetargetable(Transform t)
internal bool IsRetargetable(Transform t)
{
return m_IsRetargetable.TryGetValue(t, out var result) && result;
}
internal static void AddMergedBone(Transform bone)
internal void AddMergedBone(Transform bone)
{
m_IsRetargetable[bone] = true;
}
internal static void RetainMergedBone(Transform bone)
internal void RetainMergedBone(Transform bone)
{
if (bone == null) return;
if (m_IsRetargetable.ContainsKey(bone)) m_IsRetargetable[bone] = false;
}
internal static Transform GetRetargetedBone(Transform bone)
internal Transform GetRetargetedBone(Transform bone)
{
if (bone == null || !m_IsRetargetable.ContainsKey(bone)) return null;
@ -65,14 +66,14 @@ namespace nadena.dev.modular_avatar.core.editor
return bone;
}
internal static IEnumerable<KeyValuePair<Transform, Transform>> GetRetargetedBones()
internal IEnumerable<KeyValuePair<Transform, Transform>> GetRetargetedBones()
{
return m_IsRetargetable.Where((kvp) => kvp.Value)
.Select(kvp => new KeyValuePair<Transform, Transform>(kvp.Key, GetRetargetedBone(kvp.Key)))
.Where(kvp => kvp.Value != null);
}
public static Transform GetRetargetedBone(Transform bone, bool fallbackToOriginal)
public Transform GetRetargetedBone(Transform bone, bool fallbackToOriginal)
{
Transform retargeted = GetRetargetedBone(bone);
@ -82,11 +83,14 @@ namespace nadena.dev.modular_avatar.core.editor
internal class RetargetMeshes
{
private BuildContext _context;
private BoneDatabase _boneDatabase;
private TrackObjectRenamesContext _pathTracker;
internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext context)
internal void OnPreprocessAvatar(GameObject avatarGameObject, BoneDatabase boneDatabase,
TrackObjectRenamesContext pathMappings)
{
_context = context;
this._boneDatabase = boneDatabase;
this._pathTracker = pathMappings;
foreach (var renderer in avatarGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
@ -95,19 +99,18 @@ namespace nadena.dev.modular_avatar.core.editor
bool isRetargetable = false;
foreach (var bone in renderer.bones)
{
if (BoneDatabase.GetRetargetedBone(bone) != null)
if (_boneDatabase.GetRetargetedBone(bone) != null)
{
isRetargetable = true;
break;
}
}
isRetargetable |= BoneDatabase.GetRetargetedBone(renderer.rootBone);
isRetargetable |= _boneDatabase.GetRetargetedBone(renderer.rootBone);
if (isRetargetable)
{
var newMesh = new MeshRetargeter(renderer).Retarget();
if (newMesh) _context.SaveAsset(newMesh);
new MeshRetargeter(renderer, _boneDatabase).Retarget();
}
});
}
@ -115,9 +118,9 @@ namespace nadena.dev.modular_avatar.core.editor
// Now remove retargeted bones
if (true)
{
foreach (var bonePair in BoneDatabase.GetRetargetedBones())
foreach (var bonePair in _boneDatabase.GetRetargetedBones())
{
if (BoneDatabase.GetRetargetedBone(bonePair.Key) == null) continue;
if (_boneDatabase.GetRetargetedBone(bonePair.Key) == null) continue;
var sourceBone = bonePair.Key;
var destBone = bonePair.Value;
@ -150,7 +153,7 @@ namespace nadena.dev.modular_avatar.core.editor
child.SetParent(destBone, true);
}
PathMappings.MarkRemoved(sourceBone.gameObject);
_pathTracker.MarkRemoved(sourceBone.gameObject);
UnityEngine.Object.DestroyImmediate(sourceBone.gameObject);
}
}
@ -164,11 +167,14 @@ namespace nadena.dev.modular_avatar.core.editor
internal class MeshRetargeter
{
private readonly SkinnedMeshRenderer renderer;
private readonly BoneDatabase _boneDatabase;
[CanBeNull] private Mesh src, dst;
public MeshRetargeter(SkinnedMeshRenderer renderer)
public MeshRetargeter(SkinnedMeshRenderer renderer, BoneDatabase boneDatabase)
{
this.renderer = renderer;
this._boneDatabase = boneDatabase;
}
[CanBeNull]
@ -218,7 +224,7 @@ namespace nadena.dev.modular_avatar.core.editor
for (int i = 0; i < originalBones.Length; i++)
{
Transform newBindTarget = BoneDatabase.GetRetargetedBone(originalBones[i]);
Transform newBindTarget = _boneDatabase.GetRetargetedBone(originalBones[i]);
if (newBindTarget == null) continue;
newBones[i] = newBindTarget;
@ -247,8 +253,8 @@ namespace nadena.dev.modular_avatar.core.editor
renderer.sharedMesh = dst;
}
var newRootBone = BoneDatabase.GetRetargetedBone(rootBone, true);
var newScaleBone = BoneDatabase.GetRetargetedBone(scaleBone, true);
var newRootBone = _boneDatabase.GetRetargetedBone(rootBone, true);
var newScaleBone = _boneDatabase.GetRetargetedBone(scaleBone, true);
var oldLossyScale = scaleBone.transform.lossyScale;
var newLossyScale = newScaleBone.transform.lossyScale;
@ -267,7 +273,7 @@ namespace nadena.dev.modular_avatar.core.editor
renderer.localBounds = bounds;
renderer.rootBone = newRootBone;
renderer.probeAnchor = BoneDatabase.GetRetargetedBone(renderer.probeAnchor, true);
renderer.probeAnchor = _boneDatabase.GetRetargetedBone(renderer.probeAnchor, true);
}
}
}

View File

@ -1,203 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 bd_
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
using System.Collections.Generic;
using System.Collections.Immutable;
using UnityEditor;
using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
{
internal static class PathMappings
{
private static Dictionary<GameObject, List<string>> _objectToOriginalPaths =
new Dictionary<GameObject, List<string>>();
private static ImmutableDictionary<string, string> _originalPathToMappedPath = null;
private static ImmutableDictionary<string, string> _transformOriginalPathToMappedPath = null;
private static HashSet<GameObject> _transformLookthroughObjects = new HashSet<GameObject>();
internal static void Init(GameObject root)
{
_objectToOriginalPaths.Clear();
_originalPathToMappedPath = null;
_transformLookthroughObjects.Clear();
foreach (var xform in root.GetComponentsInChildren<Transform>(true))
{
var path = RuntimeUtil.RelativePath(root, xform.gameObject);
_objectToOriginalPaths.Add(xform.gameObject, new List<string> {path});
}
ClearCache();
}
internal static void ClearCache()
{
_originalPathToMappedPath = _transformOriginalPathToMappedPath = null;
}
/// <summary>
/// Returns a path identifying a given object. This can include objects not originally present; in this case,
/// they will be assigned a randomly-generated internal ID which will be replaced during path remapping with
/// the true path.
/// </summary>
/// <param name="obj">Object to map</param>
/// <returns></returns>
internal static string GetObjectIdentifier(GameObject obj)
{
if (_objectToOriginalPaths.TryGetValue(obj, out var paths))
{
return paths[0];
}
else
{
var internalPath = "_ModularAvatarInternal/" + GUID.Generate();
_objectToOriginalPaths.Add(obj, new List<string> {internalPath});
return internalPath;
}
}
/// <summary>
/// When animating a transform component on a merged bone, we want to make sure we manipulate the original
/// avatar's bone, not a stub bone attached underneath. By making an object as transform lookthrough, any
/// queries for mapped paths on the transform component will walk up the tree to the next parent.
/// </summary>
/// <param name="obj">The object to mark transform lookthrough</param>
internal static void MarkTransformLookthrough(GameObject obj)
{
ClearCache();
_transformLookthroughObjects.Add(obj);
}
/// <summary>
/// Marks an object as having been removed. Its paths will be remapped to its parent.
/// </summary>
/// <param name="obj"></param>
internal static void MarkRemoved(GameObject obj)
{
ClearCache();
if (_objectToOriginalPaths.TryGetValue(obj, out var paths))
{
var parent = obj.transform.parent.gameObject;
if (_objectToOriginalPaths.TryGetValue(parent, out var parentPaths))
{
parentPaths.AddRange(paths);
}
_objectToOriginalPaths.Remove(obj);
_transformLookthroughObjects.Remove(obj);
}
}
/// <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)
{
if (cache != null) return cache;
ImmutableDictionary<string, string> dict = ImmutableDictionary<string, string>.Empty;
foreach (var kvp in _objectToOriginalPaths)
{
var obj = kvp.Key;
var paths = kvp.Value;
if (transformLookup)
{
while (_transformLookthroughObjects.Contains(obj))
{
obj = obj.transform.parent.gameObject;
}
}
var newPath = RuntimeUtil.AvatarRootPath(obj);
foreach (var origPath in paths)
{
if (!dict.ContainsKey(origPath))
{
dict = dict.Add(origPath, newPath);
}
}
}
cache = dict;
return cache;
}
internal static string MapPath(string path, bool isTransformMapping = false)
{
ImmutableDictionary<string, string> mappings;
if (isTransformMapping)
{
mappings = BuildMapping(ref _originalPathToMappedPath, true);
}
else
{
mappings = BuildMapping(ref _transformOriginalPathToMappedPath, false);
}
if (mappings.TryGetValue(path, out var mappedPath))
{
return mappedPath;
}
else
{
return path;
}
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 6a5a2ea7723848d1bfe793debcf298cc
timeCreated: 1661649007

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 06c2c2893c2741c0895e3b90ee8e33c0
timeCreated: 1691213499

View File

@ -0,0 +1,28 @@
using System;
using nadena.dev.ndmf;
using nadena.dev.modular_avatar.editor.ErrorReporting;
namespace nadena.dev.modular_avatar.core.editor
{
internal class ModularAvatarContext : IExtensionContext
{
private IDisposable toDispose;
internal BuildContext BuildContext { get; private set; }
public void OnActivate(ndmf.BuildContext context)
{
if (BuildContext == null)
{
BuildContext = new BuildContext(context);
}
toDispose = BuildReport.CurrentReport.ReportingOnAvatar(context.AvatarDescriptor);
}
public void OnDeactivate(ndmf.BuildContext context)
{
toDispose?.Dispose();
toDispose = null;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2be0ae3b99ac44c0a35522d7fd0c6f10
timeCreated: 1692614275

View File

@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using nadena.dev.ndmf;
using nadena.dev.ndmf.animation;
using UnityEngine;
[assembly: ExportsPlugin(
typeof(nadena.dev.modular_avatar.core.editor.plugin.PluginDefinition)
)]
namespace nadena.dev.modular_avatar.core.editor.plugin
{
class PluginDefinition : Plugin
{
public override string QualifiedName => "nadena.dev.modular-avatar";
public override ImmutableList<PluginPass> Passes => (new List<PluginPass>()
{
new ResolveObjectReferences(),
new ClearEditorOnlyTags(),
new MeshSettingsPluginPass(),
new RenameParametersPluginPass(),
new MergeAnimatorPluginPass(),
new MenuInstallPluginPass(),
new MergeArmaturePluginPass(),
new BoneProxyPluginPass(),
new VisibleHeadAccessoryPluginPass(),
new ReplaceObjectPluginPass(),
new BlendshapeSyncAnimationPluginPass(),
new PhysbonesBlockerPluginPass(),
new GCGameObjectsPluginPass(),
}).ToImmutableList();
}
/// <summary>
/// This plugin runs very early in order to resolve all AvatarObjectReferences to their
/// referent before any other plugins perform heirarchy manipulations.
/// </summary>
internal class ResolveObjectReferences : PluginPass
{
public override BuiltInPhase ExecutionPhase => BuiltInPhase.Resolving;
public override void Process(ndmf.BuildContext context)
{
foreach (var obj in context.AvatarRootObject.GetComponentsInChildren<AvatarTagComponent>())
{
obj.ResolveReferences();
}
}
}
abstract class MAPass : PluginPass
{
public override IImmutableSet<Type> RequiredContexts =>
ImmutableHashSet<Type>.Empty.Add(typeof(ModularAvatarContext));
public override IImmutableSet<object> CompatibleContexts =>
ImmutableHashSet<object>.Empty.Add(typeof(TrackObjectRenamesContext));
protected BuildContext MAContext(ndmf.BuildContext context)
{
return context.Extension<ModularAvatarContext>().BuildContext;
}
}
class ClearEditorOnlyTags : MAPass
{
public override void Process(ndmf.BuildContext context)
{
Traverse(context.AvatarRootTransform);
}
void Traverse(Transform obj)
{
// EditorOnly objects can be used for multiple purposes - users might want a camera rig to be available in
// play mode, for example. For now, we'll prune MA components from EditorOnly objects, but otherwise leave
// them in place when in play mode.
if (obj.CompareTag("EditorOnly"))
{
foreach (var component in obj.GetComponentsInChildren<AvatarTagComponent>(true))
{
UnityEngine.Object.DestroyImmediate(component);
}
}
else
{
foreach (Transform transform in obj)
{
Traverse(transform);
}
}
}
}
class MeshSettingsPluginPass : MAPass
{
public override void Process(ndmf.BuildContext context)
{
new MeshSettingsPass(MAContext(context)).OnPreprocessAvatar();
}
}
class RenameParametersPluginPass : MAPass
{
public override void Process(ndmf.BuildContext context)
{
new RenameParametersHook().OnPreprocessAvatar(context.AvatarRootObject, MAContext(context));
}
}
class MergeAnimatorPluginPass : MAPass
{
public override void Process(ndmf.BuildContext context)
{
new MergeAnimatorProcessor().OnPreprocessAvatar(context.AvatarRootObject, MAContext(context));
}
}
class MenuInstallPluginPass : MAPass
{
public override void Process(ndmf.BuildContext context)
{
new MenuInstallHook().OnPreprocessAvatar(context.AvatarRootObject, MAContext(context));
}
}
class MergeArmaturePluginPass : MAPass
{
public override IImmutableSet<Type> RequiredContexts =>
base.RequiredContexts.Add(typeof(TrackObjectRenamesContext));
public override void Process(ndmf.BuildContext context)
{
// The animation database is currently only used by the merge armature hook; it should probably become
// an extension context instead.
MAContext(context).AnimationDatabase.Bootstrap(context.AvatarDescriptor);
new MergeArmatureHook().OnPreprocessAvatar(context, context.AvatarRootObject);
MAContext(context).AnimationDatabase.Commit();
}
}
class BoneProxyPluginPass : MAPass
{
public override IImmutableSet<Type> RequiredContexts =>
base.RequiredContexts.Add(typeof(TrackObjectRenamesContext));
public override void Process(ndmf.BuildContext context)
{
new BoneProxyProcessor().OnPreprocessAvatar(context.AvatarRootObject);
}
}
class VisibleHeadAccessoryPluginPass : MAPass
{
public override IImmutableSet<Type> RequiredContexts =>
base.RequiredContexts.Add(typeof(TrackObjectRenamesContext));
public override void Process(ndmf.BuildContext context)
{
new VisibleHeadAccessoryProcessor(context.AvatarDescriptor).Process(MAContext(context));
}
}
class ReplaceObjectPluginPass : MAPass
{
public override IImmutableSet<Type> RequiredContexts =>
base.RequiredContexts.Add(typeof(TrackObjectRenamesContext));
public override void Process(ndmf.BuildContext context)
{
new ReplaceObjectPass(context).Process();
}
}
class BlendshapeSyncAnimationPluginPass : MAPass
{
// Flush animation path remappings, since we need an up-to-date path name while adjusting blendshape animations
public override IImmutableSet<object> CompatibleContexts =>
ImmutableHashSet<object>.Empty;
public override void Process(ndmf.BuildContext context)
{
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(context.AvatarRootObject, MAContext(context));
}
}
class PhysbonesBlockerPluginPass : MAPass
{
public override void Process(ndmf.BuildContext context)
{
PhysboneBlockerPass.Process(context.AvatarRootObject);
}
}
class GCGameObjectsPluginPass : MAPass
{
public override BuiltInPhase ExecutionPhase => BuiltInPhase.Optimization;
public override void Process(ndmf.BuildContext context)
{
new GCGameObjectsPass(MAContext(context), context.AvatarRootObject).OnPreprocessAvatar();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aed34bf4ee4045ff97b749ab1c36d845
timeCreated: 1691213504

View File

@ -1,77 +0,0 @@
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
namespace nadena.dev.modular_avatar.core.editor
{
/// <summary>
/// Remaps all animation path references based on PathMappings data.
/// </summary>
internal class RemapAnimationPass
{
private readonly VRCAvatarDescriptor _avatarDescriptor;
public RemapAnimationPass(VRCAvatarDescriptor avatarDescriptor)
{
_avatarDescriptor = avatarDescriptor;
}
public void Process(AnimationDatabase animDb)
{
PathMappings.ClearCache();
animDb.ForeachClip(clip =>
{
BuildReport.ReportingObject(clip.CurrentClip, () =>
{
if (clip.CurrentClip is AnimationClip anim && !clip.IsProxyAnimation)
{
clip.CurrentClip = MapMotion(anim);
}
});
});
}
private static string MapPath(EditorCurveBinding binding)
{
if (binding.type == typeof(Animator) && binding.path == "")
{
return "";
}
else
{
return PathMappings.MapPath(binding.path, binding.type == typeof(Transform));
}
}
private AnimationClip MapMotion(AnimationClip clip)
{
AnimationClip newClip = new AnimationClip();
newClip.name = "remapped " + clip.name;
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var newBinding = binding;
newBinding.path = MapPath(binding);
newClip.SetCurve(newBinding.path, newBinding.type, newBinding.propertyName,
AnimationUtility.GetEditorCurve(clip, binding));
}
foreach (var objBinding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
{
var newBinding = objBinding;
newBinding.path = MapPath(objBinding);
AnimationUtility.SetObjectReferenceCurve(newClip, newBinding,
AnimationUtility.GetObjectReferenceCurve(clip, objBinding));
}
newClip.wrapMode = clip.wrapMode;
newClip.legacy = clip.legacy;
newClip.frameRate = clip.frameRate;
newClip.localBounds = clip.localBounds;
AnimationUtility.SetAnimationClipSettings(newClip, AnimationUtility.GetAnimationClipSettings(clip));
return newClip;
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 13e4085d9ace44a680228680e5e1172e
timeCreated: 1671618477

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf.animation;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor;
using UnityEngine;
@ -13,9 +14,9 @@ namespace nadena.dev.modular_avatar.core.editor
internal class ReplaceObjectPass
{
private readonly BuildContext _buildContext;
private readonly ndmf.BuildContext _buildContext;
public ReplaceObjectPass(BuildContext context)
public ReplaceObjectPass(ndmf.BuildContext context)
{
_buildContext = context;
}
@ -122,7 +123,8 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
PathMappings.ReplaceObject(original, replacement);
_buildContext.Extension<TrackObjectRenamesContext>()
.ReplaceObject(original, replacement);
// Destroy original
UnityObject.DestroyImmediate(original);

View File

@ -193,8 +193,10 @@ namespace nadena.dev.modular_avatar.core.editor
public static bool IsTemporaryAsset(Object obj)
{
var path = AssetDatabase.GetAssetPath(obj);
var generatedAssetsFolder = OverridePath ?? generatedAssetsPath;
return string.IsNullOrEmpty(path) || path.StartsWith(GetGeneratedAssetsFolder() + "/");
return !EditorUtility.IsPersistent(obj) || string.IsNullOrEmpty(path) ||
path.StartsWith(generatedAssetsFolder + "/");
}
public static Type FindType(string typeName)

View File

@ -3,7 +3,8 @@
"references": [
"GUID:fc900867c0f47cd49b6e2ae4ef907300",
"GUID:5718fb738711cd34ea54e9553040911d",
"GUID:3456780c4fb2d324ab9c633d6f1b0ddb"
"GUID:3456780c4fb2d324ab9c633d6f1b0ddb",
"GUID:62ced99b048af7f4d8dfe4bed8373d76"
],
"includePlatforms": [
"Editor"
@ -27,6 +28,12 @@
],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"versionDefines": [
{
"name": "nadena.dev.ndmf",
"expression": "[0.0.1,999]",
"define": "AV3_BUILD_FRAMEWORK_PRESENT"
}
],
"noEngineReferences": false
}

View File

@ -1,15 +1,13 @@
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using VRC.SDK3.Avatars.Components;
using VRC.SDKBase;
namespace nadena.dev.modular_avatar.core
{
/// <summary>
/// This component is used to trigger MA processing upon entering play mode (prior to Av3Emu running).
/// This component was previously used to trigger avatar processing when entering play mode. This functionality has
/// moved to NDMF, so we leave here a stub to clean up detritus left behind from older versions of MA.
/// We create it on a hidden object via AvatarTagObject's OnValidate, and it will proceed to add MAAvatarActivator
/// components to all avatar roots which contain MA components. This MAAvatarActivator component then performs MA
/// processing on Awake.
@ -21,108 +19,19 @@ namespace nadena.dev.modular_avatar.core
[AddComponentMenu("")]
[ExecuteInEditMode]
[DefaultExecutionOrder(-9998)]
public class Activator : MonoBehaviour
public class Activator : MonoBehaviour, IEditorOnly
{
private const string TAG_OBJECT_NAME = "ModularAvatarInternal_Activator";
private void Awake()
private void Update()
{
if (!RuntimeUtil.isPlaying || this == null) return;
var scene = gameObject.scene;
foreach (var root in scene.GetRootGameObjects())
{
foreach (var avatar in root.GetComponentsInChildren<VRCAvatarDescriptor>())
{
if (avatar.GetComponentInChildren<AvatarTagComponent>(true) != null)
{
avatar.gameObject.GetOrAddComponent<AvatarActivator>().hideFlags = HideFlags.HideInInspector;
UnityEngine.Object.DestroyImmediate(gameObject);
}
}
}
}
private bool HasMAComponentsInScene()
{
var scene = gameObject.scene;
foreach (var root in scene.GetRootGameObjects())
{
if (root.GetComponentInChildren<AvatarTagComponent>(true) != null) return true;
}
return false;
}
private void OnValidate()
{
if (EditorApplication.isPlayingOrWillChangePlaymode) return;
EditorApplication.delayCall += () =>
{
if (this == null) return;
gameObject.hideFlags = HIDE_FLAGS;
if (!HasMAComponentsInScene())
{
var scene = gameObject.scene;
DestroyImmediate(gameObject);
EditorSceneManager.MarkSceneDirty(scene);
}
};
}
internal static void CreateIfNotPresent(Scene scene)
{
if (!scene.IsValid() || EditorSceneManager.IsPreviewScene(scene)) return;
if (EditorApplication.isPlayingOrWillChangePlaymode) return;
bool rootPresent = false;
foreach (var root in scene.GetRootGameObjects())
{
if (root.GetComponent<Activator>() != null)
{
root.hideFlags = HIDE_FLAGS;
if (rootPresent) DestroyImmediate(root);
rootPresent = true;
}
}
if (rootPresent) return;
var oldActiveScene = SceneManager.GetActiveScene();
try
{
SceneManager.SetActiveScene(scene);
var gameObject = new GameObject(TAG_OBJECT_NAME);
gameObject.AddComponent<Activator>();
gameObject.hideFlags = HIDE_FLAGS;
}
finally
{
SceneManager.SetActiveScene(oldActiveScene);
}
}
private const HideFlags HIDE_FLAGS = HideFlags.HideInHierarchy;
}
[AddComponentMenu("")]
[ExecuteInEditMode]
[DefaultExecutionOrder(-9997)]
public class AvatarActivator : MonoBehaviour
public class AvatarActivator : MonoBehaviour, IEditorOnly
{
private void Awake()
{
if (!RuntimeUtil.isPlaying || this == null) return;
RuntimeUtil.OnDemandProcessAvatar(RuntimeUtil.OnDemandSource.Awake, this);
}
private void Start()
{
if (!RuntimeUtil.isPlaying || this == null) return;
RuntimeUtil.OnDemandProcessAvatar(RuntimeUtil.OnDemandSource.Start, this);
}
private void Update()
{
DestroyImmediate(this);

View File

@ -7,6 +7,8 @@ namespace nadena.dev.modular_avatar.core
[Serializable]
public class AvatarObjectReference
{
private long ReferencesLockedAtFrame = long.MinValue;
public static string AVATAR_ROOT = "$$$AVATAR_ROOT$$$";
public string referencePath;
@ -16,7 +18,9 @@ namespace nadena.dev.modular_avatar.core
public GameObject Get(Component container)
{
if (_cacheValid && _cachedPath == referencePath && _cachedReference != null) return _cachedReference;
bool cacheValid = _cacheValid || ReferencesLockedAtFrame == Time.frameCount;
if (cacheValid && _cachedPath == referencePath && _cachedReference != null) return _cachedReference;
_cacheValid = true;
_cachedPath = referencePath;
@ -57,7 +61,8 @@ namespace nadena.dev.modular_avatar.core
referencePath = RuntimeUtil.AvatarRootPath(target);
}
_cacheValid = false;
_cachedReference = target;
_cacheValid = true;
}
private void InvalidateCache()

View File

@ -52,14 +52,6 @@ namespace nadena.dev.modular_avatar.core
{
if (RuntimeUtil.isPlaying) return;
RuntimeUtil.delayCall(() =>
{
if (this == null) return;
#if UNITY_EDITOR
Activator.CreateIfNotPresent(gameObject.scene);
#endif
});
OnChangeAction?.Invoke();
}
@ -67,5 +59,10 @@ namespace nadena.dev.modular_avatar.core
{
OnChangeAction?.Invoke();
}
/// <summary>
/// Eagerly resolve all AvatarTagReferences to their destinations.
/// </summary>
internal abstract void ResolveReferences();
}
}

View File

@ -12,5 +12,10 @@ namespace nadena.dev.modular_avatar.core
{
context.PushNode(new MenuNodesUnder(targetObject != null ? targetObject : gameObject));
}
internal override void ResolveReferences()
{
// no-op
}
}
}

View File

@ -22,5 +22,10 @@ namespace nadena.dev.modular_avatar.core
RuntimeUtil.InvalidateMenu();
}
internal override void ResolveReferences()
{
// no-op
}
}
}

View File

@ -64,6 +64,11 @@ namespace nadena.dev.modular_avatar.core
RuntimeUtil.OnHierarchyChanged -= Rebind;
}
internal override void ResolveReferences()
{
// no-op
}
private void Rebind()
{
if (this == null) return;

View File

@ -95,6 +95,11 @@ namespace nadena.dev.modular_avatar.core
public string subPath;
public BoneProxyAttachmentMode attachmentMode = BoneProxyAttachmentMode.Unset;
internal override void ResolveReferences()
{
_targetCache = UpdateDynamicMapping();
}
protected override void OnValidate()
{
base.OnValidate();
@ -102,8 +107,20 @@ namespace nadena.dev.modular_avatar.core
}
internal void ClearCache()
{
ClearCache(false);
}
internal void ClearCache(bool immediate)
{
if (immediate)
{
_targetCache = null;
} else if (_targetCache != null)
{
RuntimeUtil.delayCall(() => { _targetCache = null; });
}
RuntimeUtil.OnHierarchyChanged -= ClearCache;
}

View File

@ -22,5 +22,10 @@ namespace nadena.dev.modular_avatar.core
{
context.PushNode(installer);
}
internal override void ResolveReferences()
{
// no-op
}
}
}

View File

@ -37,6 +37,11 @@ namespace nadena.dev.modular_avatar.core
}
}
internal override void ResolveReferences()
{
// no-op
}
public void Visit(NodeContext context)
{
if (Control == null)

View File

@ -41,5 +41,10 @@ namespace nadena.dev.modular_avatar.core
public bool deleteAttachedAnimator;
public MergeAnimatorPathMode pathMode = MergeAnimatorPathMode.Relative;
public bool matchAvatarWriteDefaults;
internal override void ResolveReferences()
{
// no-op
}
}
}

View File

@ -89,6 +89,11 @@ namespace nadena.dev.modular_avatar.core
#endif
}
internal override void ResolveReferences()
{
mergeTarget?.Get(this);
}
void EditorUpdate()
{
if (this == null)

View File

@ -25,5 +25,11 @@ namespace nadena.dev.modular_avatar.core
public InheritMode InheritBounds = InheritMode.Inherit;
public AvatarObjectReference RootBone;
public Bounds Bounds = DEFAULT_BOUNDS;
internal override void ResolveReferences()
{
ProbeAnchor?.Get(this);
RootBone?.Get(this);
}
}
}

View File

@ -30,5 +30,9 @@ namespace nadena.dev.modular_avatar.core
[AddComponentMenu("Modular Avatar/MA PhysBone Blocker")]
public class ModularAvatarPBBlocker : AvatarTagComponent
{
internal override void ResolveReferences()
{
// no-op
}
}
}

View File

@ -33,5 +33,10 @@ namespace nadena.dev.modular_avatar.core
public class ModularAvatarParameters : AvatarTagComponent
{
public List<ParameterConfig> parameters = new List<ParameterConfig>();
internal override void ResolveReferences()
{
// no-op
}
}
}

View File

@ -7,5 +7,10 @@ namespace nadena.dev.modular_avatar.core
public class ModularAvatarReplaceObject : AvatarTagComponent
{
public AvatarObjectReference targetObject = new AvatarObjectReference();
internal override void ResolveReferences()
{
targetObject?.Get(this);
}
}
}

View File

@ -7,5 +7,9 @@ namespace nadena.dev.modular_avatar.core
public class ModularAvatarVisibleHeadAccessory : AvatarTagComponent
{
// no configuration needed
internal override void ResolveReferences()
{
// no-op
}
}
}

View File

@ -15,6 +15,7 @@
"com.unity.nuget.newtonsoft-json": "2.0.0"
},
"vpmDependencies": {
"com.vrchat.avatars": ">=3.2.0"
"com.vrchat.avatars": ">=3.2.0",
"nadena.dev.ndmf": "=0.1.0"
}
}

@ -0,0 +1 @@
Subproject commit 3a5f80dfabd90c27c1b5a9e216f1a9093c28e092

View File

@ -196,6 +196,12 @@
"com.unity.nuget.newtonsoft-json": "2.0.0"
}
},
"nadena.dev.ndmf": {
"version": "file:nadena.dev.ndmf",
"depth": 0,
"source": "embedded",
"dependencies": {}
},
"vrchat.blackstartx.gesture-manager": {
"version": "file:vrchat.blackstartx.gesture-manager",
"depth": 0,