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

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,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
30 changes: 22 additions & 8 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 @@ -94,14 +100,23 @@ public static string AvatarRootPath(GameObject child)
/// <returns></returns>
public static bool IsAvatarRoot(Transform target)
Copy link
Owner

@bdunderscore bdunderscore Sep 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらも要更新?(仮実装です)

#if NDMF_VRCSDK3_AVATARS
if (context.GetComponent<VRCAvatarDescriptor>(elem.gameObject) != null)
{
candidate = elem.gameObject;
break;
}
#else
if (context.GetComponent<Animator>(elem.gameObject) != null)
{
candidate = elem.gameObject;
}
#endif

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そうかもしれません・・・
NDMF 1.5.0 リリース後に作業を再開する予定でしたが、この機会に全体を見直しておきます。

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

了解です。

{
// First, look for platform specific avatar descriptors
// TODO: ignore nested avatar descriptors?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

たぶん以下の扱いをちゃんと考えないといけない

  • VRCAvatarDescriptor の子に Vrm10Instance
    • VRCAvatarDescriptor をアバターとみなすべき
  • Vrm10Instance の子に VRCAvatarDescriptor
    • Vrm10Instance をアバターとみなすべき
  • Animator の子に VRCAvatarDescriptor / Vrm10Instance
    • バニラの Animator はアバターではないかもしれない
      • VRChatプロジェクトだと VRCAvatarDescriptor がアバターっぽく見える
      • Resoniteアバターがメインターゲットになるなら Animator がアバターになるべきケースもある
        • その時はたぶん VRCAvatarDescriptor を外してもらった方が良い
      • FindAvatarsInScene() みたいに全てのアバターかもしれない Animator をリストアップするとエラー UI などが不便になるかもしれない
      • FindAvatarRoots() は少し賢い
    • Animator が実は CVRAvatar だったケース
      • CVRAvatar をアバターとみなしたいけど、上との区別が無理
      • VRCAvatarDescriptor を外すか、 ndmf が CVRAvatar に対応するか、 AvatarRoot マーカーコンポーネントを定義してつけてもらうか・・・

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

考えました #432

#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

// Then, look for Animators, which is the generic avatar root as long as there are no Animators in its parents
var an = target.GetComponent<Animator>();
if (!an) return false;
var parent = target.transform.parent;
return !(parent && parent.GetComponentInParent<Animator>());
#endif
}

/// <summary>
Expand Down Expand Up @@ -130,6 +145,7 @@ public static IEnumerable<GameObject> FindAvatarRoots(GameObject root = null)
else
{
GameObject priorRoot = null;
// TODO: allow generic avatars in VRChat projects?
#if NDMF_VRCSDK3_AVATARS
var candidates = root.GetComponentsInChildren<VRCAvatarDescriptor>();
#else
Expand Down Expand Up @@ -171,17 +187,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
8 changes: 8 additions & 0 deletions UnitTests~/AvatarRootTests.meta

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

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
Loading
Loading