mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-28 10:15:06 +08:00
feat: NDMF integration
This commit is contained in:
parent
b155202714
commit
99386fc756
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "Packages/nadena.dev.ndmf"]
|
||||
path = Packages/nadena.dev.ndmf
|
||||
url = https://github.com/bdunderscore/ndmf.git
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ namespace modular_avatar_tests
|
||||
Texture2D _iconTexture;
|
||||
|
||||
[SetUp]
|
||||
public virtual void Setup()
|
||||
public override void Setup()
|
||||
{
|
||||
base.Setup();
|
||||
_gameObject = new GameObject();
|
||||
|
@ -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");
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc087947fd98b2b43a853f93161cfe13
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
@ -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)),
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8035784f3364865a84cc938682be7a0
|
||||
timeCreated: 1690804771
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,9 @@
|
||||
"GUID:5718fb738711cd34ea54e9553040911d",
|
||||
"GUID:b906909fcc54f634db50f2cad0f988d9",
|
||||
"GUID:3456780c4fb2d324ab9c633d6f1b0ddb",
|
||||
"GUID:e9745f6a32442194c8dc5a43e9ab86f9"
|
||||
"GUID:e9745f6a32442194c8dc5a43e9ab86f9",
|
||||
"GUID:62ced99b048af7f4d8dfe4bed8373d76",
|
||||
"GUID:fe747755f7b44e048820525b07f9b956"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 246c8f6cb27c4758972ceac5e8700add
|
||||
timeCreated: 1661822272
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aeaeff9c3af44683bb2f8f5fe6c5791d
|
||||
timeCreated: 1671016064
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a5a2ea7723848d1bfe793debcf298cc
|
||||
timeCreated: 1661649007
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 06c2c2893c2741c0895e3b90ee8e33c0
|
||||
timeCreated: 1691213499
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2be0ae3b99ac44c0a35522d7fd0c6f10
|
||||
timeCreated: 1692614275
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aed34bf4ee4045ff97b749ab1c36d845
|
||||
timeCreated: 1691213504
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13e4085d9ace44a680228680e5e1172e
|
||||
timeCreated: 1671618477
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
@ -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);
|
||||
|
@ -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()
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -12,5 +12,10 @@ namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
context.PushNode(new MenuNodesUnder(targetObject != null ? targetObject : gameObject));
|
||||
}
|
||||
|
||||
internal override void ResolveReferences()
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
@ -22,5 +22,10 @@ namespace nadena.dev.modular_avatar.core
|
||||
|
||||
RuntimeUtil.InvalidateMenu();
|
||||
}
|
||||
|
||||
internal override void ResolveReferences()
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -22,5 +22,10 @@ namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
context.PushNode(installer);
|
||||
}
|
||||
|
||||
internal override void ResolveReferences()
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
@ -37,6 +37,11 @@ namespace nadena.dev.modular_avatar.core
|
||||
}
|
||||
}
|
||||
|
||||
internal override void ResolveReferences()
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
|
||||
public void Visit(NodeContext context)
|
||||
{
|
||||
if (Control == null)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -89,6 +89,11 @@ namespace nadena.dev.modular_avatar.core
|
||||
#endif
|
||||
}
|
||||
|
||||
internal override void ResolveReferences()
|
||||
{
|
||||
mergeTarget?.Get(this);
|
||||
}
|
||||
|
||||
void EditorUpdate()
|
||||
{
|
||||
if (this == null)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,5 +7,9 @@ namespace nadena.dev.modular_avatar.core
|
||||
public class ModularAvatarVisibleHeadAccessory : AvatarTagComponent
|
||||
{
|
||||
// no configuration needed
|
||||
internal override void ResolveReferences()
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
1
Packages/nadena.dev.ndmf
Submodule
1
Packages/nadena.dev.ndmf
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 3a5f80dfabd90c27c1b5a9e216f1a9093c28e092
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user