Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AvatarRoot API: Non-VRChat avatar support in VRCSDK projects #71

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Make Apply on Play non-persistent, as users seem to frequently have issues with it left turned off.
- Improve APIs for finding avatar roots, support non-VRChat avatars in VRCSDK projects (#71)

### Removed
- Removed a vestigial "Avatar Toolkit -> Apply on Play" menu item, which didn't do anything when selected. (#70)
Expand Down
27 changes: 20 additions & 7 deletions Runtime/RuntimeUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
#if NDMF_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
#endif
#if NDMF_VRM0
using VRM;
#endif
#if NDMF_VRM1
using UniVRM10;
#endif

namespace nadena.dev.ndmf.runtime
{
Expand Down Expand Up @@ -95,8 +101,17 @@ public static string AvatarRootPath(GameObject child)
public static bool IsAvatarRoot(Transform target)
{
#if NDMF_VRCSDK3_AVATARS
return target.GetComponent<VRCAvatarDescriptor>();
#else
if (target.GetComponent<VRCAvatarDescriptor>()) return true;
#endif
#if NDMF_VRM0
if (target.GetComponent<VRMMeta>()) return true;
#endif
#if NDMF_VRM1
if (target.GetComponent<Vrm10Instance>()) return true;
#endif
#if NDMF_VRCSDK3_AVATARS || NDMF_VRM0 || NDMF_VRM1
return false;
#else
var an = target.GetComponent<Animator>();
if (!an) return false;
var parent = target.transform.parent;
Expand Down Expand Up @@ -171,17 +186,15 @@ public static Transform FindAvatarInParents(Transform target)
/// <returns></returns>
internal static IEnumerable<Transform> FindAvatarsInScene(Scene scene)
{
var list = new List<Transform>();
foreach (var root in scene.GetRootGameObjects())
{
#if NDMF_VRCSDK3_AVATARS
foreach (var avatar in root.GetComponentsInChildren<VRCAvatarDescriptor>())
#else
foreach (var avatar in root.GetComponentsInChildren<Animator>())
#endif
{
if (IsAvatarRoot(avatar.transform)) yield return avatar.transform;
if (IsAvatarRoot(avatar.transform)) list.Add(avatar.transform);
}
}
return list;
}
}
}
14 changes: 13 additions & 1 deletion Runtime/nadena.dev.ndmf.runtime.asmdef
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"name": "nadena.dev.ndmf.runtime",
"references": [
"lyuma.av3emulator"
"lyuma.av3emulator",
"VRM",
"VRM10"
],
"includePlatforms": [],
"excludePlatforms": [],
Expand All @@ -20,6 +22,16 @@
"name": "lyuma.av3emulator",
"expression": "",
"define": "NDMF_LYUMA_AV3EMU"
},
{
"name": "com.vrmc.univrm",
"expression": "",
"define": "NDMF_VRM0"
},
{
"name": "com.vrmc.vrm",
"expression": "",
"define": "NDMF_VRM1"
}
],
"noEngineReferences": false
Expand Down
197 changes: 197 additions & 0 deletions UnitTests~/AvatarRootTests/AvatarRoot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
using nadena.dev.ndmf.runtime;
using NUnit.Framework;
using UnityEngine;

namespace UnitTests.AvatarRootTests
{
public class AvatarRoot : TestBase
{
private GameObject CreateGenericRoot(string name) => CreatePlatformRoot(name, isVRC: false, isVRM0: false, isVRM1: false);
private GameObject CreateVRCRoot(string name) => CreatePlatformRoot(name, isVRC: true, isVRM0: false, isVRM1: false);
private GameObject CreateVRM0Root(string name) => CreatePlatformRoot(name, isVRC: false, isVRM0: true, isVRM1: false);
private GameObject CreateVRM1Root(string name) => CreatePlatformRoot(name, isVRC: false, isVRM0: false, isVRM1: true);
private GameObject CreateHybridRoot(string name) => CreatePlatformRoot(name, isVRC: true, isVRM0: true, isVRM1: true);

private Transform parentAvatar;
private Transform childAvatar;

private void NoAvatars()
{
Assert.That(RuntimeUtil.IsAvatarRoot(parentAvatar), Is.False);
Assert.That(RuntimeUtil.IsAvatarRoot(childAvatar), Is.False);
Assert.That(RuntimeUtil.FindAvatarInParents(parentAvatar), Is.Null);
Assert.That(RuntimeUtil.FindAvatarInParents(childAvatar), Is.Null);
Assert.That(RuntimeUtil.FindAvatarsInScene(parentAvatar.gameObject.scene), Is.EquivalentTo(System.Array.Empty<Transform>()));
}

private void ParentIsAvatar()
{
Assert.That(RuntimeUtil.IsAvatarRoot(parentAvatar), Is.True);
Assert.That(RuntimeUtil.IsAvatarRoot(childAvatar), Is.False);
Assert.That(RuntimeUtil.FindAvatarInParents(parentAvatar), Is.EqualTo(parentAvatar));
Assert.That(RuntimeUtil.FindAvatarInParents(childAvatar), Is.EqualTo(parentAvatar));
Assert.That(RuntimeUtil.FindAvatarsInScene(parentAvatar.gameObject.scene), Is.EquivalentTo(new [] { parentAvatar }));
}

private void ChildIsAvatar()
{
Assert.That(RuntimeUtil.IsAvatarRoot(parentAvatar), Is.False);
Assert.That(RuntimeUtil.IsAvatarRoot(childAvatar), Is.True);
Assert.That(RuntimeUtil.FindAvatarInParents(parentAvatar), Is.EqualTo(null));
Assert.That(RuntimeUtil.FindAvatarInParents(childAvatar), Is.EqualTo(childAvatar));
Assert.That(RuntimeUtil.FindAvatarsInScene(parentAvatar.gameObject.scene), Is.EquivalentTo(new [] { childAvatar }));
}

private void ParentAndChildAreAvatars()
{
Assert.That(RuntimeUtil.IsAvatarRoot(parentAvatar), Is.True);
Assert.That(RuntimeUtil.IsAvatarRoot(childAvatar), Is.True);
Assert.That(RuntimeUtil.FindAvatarInParents(parentAvatar), Is.EqualTo(parentAvatar));
Assert.That(RuntimeUtil.FindAvatarInParents(childAvatar), Is.EqualTo(childAvatar));
Assert.That(RuntimeUtil.FindAvatarsInScene(parentAvatar.gameObject.scene), Is.EquivalentTo(new [] { parentAvatar, childAvatar }));
}

[Test]
public void TestGenericContainsGeneric()
{
parentAvatar = CreateGenericRoot("parent").transform;
childAvatar = CreateGenericRoot("child").transform;

childAvatar.parent = parentAvatar;

#if NDMF_VRCSDK3_AVATARS || NDMF_VRM0 || NDMF_VRM1
NoAvatars();
#else
// Use fallback heuristic
ParentIsAvatar();
#endif
}

#if NDMF_VRCSDK3_AVATARS
[Test]
public void TestGenericContainsVRC()
{
parentAvatar = CreateGenericRoot("parent").transform;
childAvatar = CreateVRCRoot("child").transform;

childAvatar.parent = parentAvatar;

ChildIsAvatar();
}

[Test]
public void TestVRCContainsGeneric()
{
parentAvatar = CreateVRCRoot("parent").transform;
childAvatar = CreateGenericRoot("child").transform;

childAvatar.parent = parentAvatar;

ParentIsAvatar();
}

[Test]
public void TestVRCContainsVRC()
{
parentAvatar = CreateVRCRoot("parent").transform;
childAvatar = CreateVRCRoot("child").transform;

childAvatar.parent = parentAvatar;

ParentAndChildAreAvatars();
}
#endif

#if NDMF_VRM0
[Test]
public void TestGenericContainsVRM0()
{
parentAvatar = CreateGenericRoot("parent").transform;
childAvatar = CreateVRM1Root("child").transform;

childAvatar.parent = parentAvatar;

ChildIsAvatar();
}

[Test]
public void TestVRM0ContainsGeneric()
{
parentAvatar = CreateVRM1Root("parent").transform;
childAvatar = CreateGenericRoot("child").transform;

childAvatar.parent = parentAvatar;

ParentIsAvatar();
}

[Test]
public void TestVRM0ContainsVRM0()
{
parentAvatar = CreateVRM1Root("parent").transform;
childAvatar = CreateVRM1Root("child").transform;

childAvatar.parent = parentAvatar;

ParentAndChildAreAvatars();
}
#endif

#if NDMF_VRCSDK3_AVATARS && NDMF_VRM0
[Test]
public void TestGenericContainsHybrid()
{
parentAvatar = CreateGenericRoot("parent").transform;
childAvatar = CreateHybridRoot("child").transform;

childAvatar.parent = parentAvatar;

ChildIsAvatar();
}

[Test]
public void TestHybridContainsGeneric()
{
parentAvatar = CreateHybridRoot("parent").transform;
childAvatar = CreateGenericRoot("child").transform;

childAvatar.parent = parentAvatar;

ParentIsAvatar();
}

[Test]
public void TestHybridContainsHybrid()
{
parentAvatar = CreateHybridRoot("parent").transform;
childAvatar = CreateHybridRoot("child").transform;

childAvatar.parent = parentAvatar;

ParentAndChildAreAvatars();
}

[Test]
public void TestVRCContainsVRM0()
{
parentAvatar = CreateVRCRoot("parent").transform;
childAvatar = CreateVRM0Root("child").transform;

childAvatar.parent = parentAvatar;

ParentAndChildAreAvatars();
}

[Test]
public void TestVRM0ContainsVRC()
{
parentAvatar = CreateVRM0Root("parent").transform;
childAvatar = CreateVRCRoot("child").transform;

childAvatar.parent = parentAvatar;

ParentAndChildAreAvatars();
}
#endif
}
}
3 changes: 3 additions & 0 deletions UnitTests~/AvatarRootTests/AvatarRoot.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 30 additions & 3 deletions UnitTests~/TestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
using VRC.SDK3.Avatars.Components;
#endif

#if NDMF_VRM0
using VRM;
#endif

#if NDMF_VRM1
using UniVRM10;
using UniHumanoid;
#endif

namespace UnitTests
{
public class TestBase
Expand Down Expand Up @@ -58,16 +67,34 @@ protected BuildContext CreateContext(GameObject root)
return new BuildContext(root, TEMP_ASSET_PATH); // TODO - cleanup
}

protected GameObject CreateRoot(string name)
protected GameObject CreateRoot(string name) => CreatePlatformRoot(name, isVRC: true, isVRM0: true, isVRM1: true);

protected GameObject CreatePlatformRoot(string name, bool isVRC, bool isVRM0, bool isVRM1)
{
//var path = AssetDatabase.GUIDToAssetPath(MinimalAvatarGuid);
//var go = GameObject.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(path));
var go = new GameObject();
go.name = name;
go.AddComponent<Animator>();
#if NDMF_VRCSDK3_AVATARS
go.AddComponent<VRCAvatarDescriptor>();
go.AddComponent<PipelineManager>();
if (isVRC)
{
go.AddComponent<VRCAvatarDescriptor>();
go.AddComponent<PipelineManager>();
}
#endif
#if NDMF_VRM0
if (isVRM0)
{
go.AddComponent<VRMMeta>();
}
#endif
#if NDMF_VRM1
if (isVRM1)
{
go.AddComponent<Vrm10Instance>();
go.AddComponent<Humanoid>();
}
#endif

objects.Add(go);
Expand Down
21 changes: 17 additions & 4 deletions UnitTests~/nadena.dev.ndmf.UnitTests.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
"name": "nadena.dev.ndmf.UnitTests",
"rootNamespace": "",
"references": [
"GUID:62ced99b048af7f4d8dfe4bed8373d76",
"GUID:b93f844d45cfcc44fa2b0eed5c9ec6bb",
"GUID:901e56b065a857d4483a77f8cae73588",
"GUID:fe747755f7b44e048820525b07f9b956"
"Unity.VisualStudio.Editor",
"nadena.dev.ndmf",
"VRM",
"VRM10",
"UniHumanoid",
"nadena.dev.ndmf.vrchat",
"nadena.dev.ndmf.runtime"
],
"includePlatforms": [
"Editor"
Expand All @@ -21,6 +24,16 @@
"name": "com.vrchat.avatars",
"expression": "(0,999)",
"define": "NDMF_VRCSDK3_AVATARS"
},
{
"name": "com.vrmc.univrm",
"expression": "",
"define": "NDMF_VRM0"
},
{
"name": "com.vrmc.vrm",
"expression": "",
"define": "NDMF_VRM1"
}
],
"noEngineReferences": false
Expand Down