Trilibを使った話

Trilibはランタイム中にFBX等の3Dデータを読み込むことができるアセットです。

assetstore.unity.com

色々ありTrilibを使う機会があったので、メモを記します。



UniTaskとの連携

UniTaskCompletionSourceを使って、連携をしました。

using System.Threading;
using Cysharp.Threading.Tasks;
using TriLibCore;
using UnityEngine;
using System;

public class FBX : IDisposable
{
    public GameObject Instance;

    private FBX(GameObject instance)
    {
        this.Instance = instance;

        // 必要なコンポーネントを参照したり
    }

    /// <summary>
    /// 指定されたPathのFBXをロードする
    /// </summary>
    /// <param name="path">fbxのpath</param>
    /// <param name="cancellationToken">cancellation</param>
    /// <returns>FBXのUniTaskCompletionSourceを返す</returns>
    public static UniTask<FBX> Load(string path, CancellationToken cancellationToken)
    {
        UniTaskCompletionSource<FBX> source = new UniTaskCompletionSource<FBX>();

        AssetLoader.LoadModelFromFile(
            path,
            null,
            (context) => source.TrySetResult(new FBX(context.RootGameObject)),
            null,
            null,
            null,
            null);

        return source.Task;
    }

    /// <summary>
    /// ロードしたら、FBXクラスのインスタンスを生成して、TrySetResultで返す
    /// cancelが発生していたら、FBXクラスのインスタンスをDisposeしてTrySetCanceledでStatusを更新する
    /// </summary>
    /// <param name="context">ロードした内容</param>
    /// <param name="source">fbxのUniTaskCompletionSource</param>
    /// <param name="cancellationToken">cancellation</param>
    private static void OnLoaded(AssetLoaderContext context, UniTaskCompletionSource<FBX> source, CancellationToken cancellationToken)
    {
        FBX fbx = new FBX(context.RootGameObject);

        if (cancellationToken.IsCancellationRequested)
        {
            fbx.Dispose();
            source.TrySetCanceled(cancellationToken);
            return;
        }

        source.TrySetResult(fbx);
    }

    public void Dispose()
    {
        if (this.Instance != null)
        {
            GameObject.Destroy(this.Instance);
        }
    }
}

FBX fbx = await FBX.Load("pathを指定",new CancellationTokenSource().Token);

Humanoid FBXを読み込む

HumanoidAvatarMapperをScriptableObjectとして生成して、対象となる3Dデータのスケルトン構造に合わせてファイルを編集する必要があります。 だいぶしんどいので、サンプルとして同封されているMixamo用のHumanoidAvatarMapperをDuplicateして編集することをお勧めします。

作成したHumanoidAvatarMapperをAssetLoaderOptionsに設定して、LoadModelFromFileの引数にして実行すると、Humanoid設定でFBXがロードされます。
またHumanoidAvatarMapperが読み込むHumanoid FBXに対して正常に作成できていない場合、例外を吐きます。

using TriLibCore;
using TriLibCore.Mappers;

[SerializeField]
private HumanoidAvatarMapper mapper;

AssetLoaderOptions assetLoaderOptions = AssetLoader.CreateDefaultLoaderOptions();

assetLoaderOptions.AnimationType = AnimationType.Humanoid;
assetLoaderOptions.HumanoidAvatarMapper = this.mapper;

AssetLoader.LoadModelFromFile("fbxのpath", null, (context) => {}, null, null, null, assetLoaderOptions);

Humanoid アニメーションが動いてるっぽくする

TrilibはHumnoaidアニメーションを読み込むことができません。ただ、AのFBXに含まれるアニメーションを、BのFBXにリターゲッティングする必要があったので、以下のような力技をしました。

Animationコンポーネントは、AssetLoaderOptionsを変更していなければ、読み込んだGameObjectからGetComponentすることができます。またHumanPoseHandlerを生成するために必要なAvatarは、 同じくGameObjectからAnimatorをGetComponentすることで参照することができます。
後は、Legacyアニメーションを実行した後に、FBX AのHumanPoseHandlerからHumanPoseを生成し、FBX BにSetHumanPoseからHumanPoseを流せばリターゲッティングが実現する。と思っていました。

一部のアニメーションが期待した通りに流れない

上記の方法だと、一部のアニメーションが正常に実行されない(なぜか回転値がおかしい)ことがありました。正確な原因は今もわかっていないのですが、Legacyアニメーションとして実行するところまでは正常、SetHumanPoseからリターゲッティングする際におかしくなっていました(TrilibのHumanoid読み込みがうまくいっていない?)。悩んだ結果、更なる力技で解決しました。

とりあえず、Legacyアニメーションは正常に実行できることが分かっていたので、リターゲッティングの元となるHumnaoid(図でいうとFBX A)には、Legacyアニメーションから更新した同じFBXのボーン座標をスクリプトから代入することで同期し、リターゲッティングを処理したいHumnaoid(図でいうとFBX B)には、従来通りHumanPoseHandlerから同期させました。

Muscle値を調整する

HumanPoseからmusclesを参照し、調整したいindex番目の要素に値を代入、最後にSetHumanPoseから調整したHumanPoseを反映すれば、実現します。

float strength = 0.5f;
HumanPoseHandler a;
HumanPoseHandler b;

HumanPose pose_a = defalut;
a.GetHumanPose(ref pose_a);

pose_a.muscles[index] += strength

b.SetHumanPose(ref pose_a);

どの身体部位がindexと対応しているかは、まとめてくれている方がいたので、参考にさせて頂きました。

gist.github.com

読み込んだMaterialにアクセスする

LoadModelFromFileのコールバックの引数から受け取れるAssetLoaderContextからLoadedMaterialsで参照できます。 LoadedMaterialsはConcurrentDictionary型で、ValuesでMaterial群を参照できます。

// contextはLoadModelFromFileのコールバックの引数として受け取ったAssetLoaderContext
ConcurrentDictionary<IMaterial, Material> loadedMaterials = context.LoadedMaterials;
Material[] materials = loadedMaterials.Values;

一部FBXが読み込めない時がある

FBXは読み込めない条件がいくつかある印象でした。
こちらで発生した特殊な事例としては、特定のバージョンのC4DからエクスポートしたFBXを読み込もうとした場合、非対応のFBX SDKのバージョンになるらしく、以下のようなメッセージを吐きました。

If your FBX file has been generated with the FBX SDK version 6 or previously, TriLib won't be able to load it.

Known Issues/Limitations - TriLib

ricardoreis.net

使ってみた感想

3Dデータをインポートすること自体は簡単な記述で実装することができる印象でした。また読み込んだデータを安全に破棄するための機能や、データのpathを参照するためのファイルブラウザ機能なども同封されており、良くできたアセットだなと思いました。
ただし、UnityEditorとは異なる仕組みで動作するImporterなため、「手動でUnityにインポートできたから、Trilibでも大丈夫だろう」と油断していると、何かしらが正常に動作しないみたいなことが起きて、途方にくれることが結構ありました。個人的には、UnityEditorに組み込まれているImporter機能をAPIとして叩けるようにしてくれないかなーと思いました。