ARあるある言いたい

VR・ARについて書いていきます

Trilibを使ってFBXファイルをHoloLensで読み込む

Unity EditorおよびHoloLens(UWP)上で、FBXファイルをスクリプトから読み込みたい。そういうとき、ありますよね。

そんなときは、こちらの「TriLib - Model loader package」を使います。

assetstore.unity.com

環境

  • Unity 2018.3.14f1
  • HoloToolkit-Unity 2017.4.3.0
  • Visual Studio 2017 Version 15.9.13

使い方

基本的な使い方は開発者サイトで見られます。

ricardoreis.net

しかし、UWP用には少し工夫が必要です。マニュアルには「windows store用アプリ(つまりUWP)で動作させるにはzipファイルとして読み込む必要がある」とあるのですが、これはfbxをzipに圧縮するという意味ではありません。
LoadFromMemoryWithTexturesメソッドにファイルのバイト配列を渡す際に、そのファイルの拡張子がzipだと伝えればOKです。

UWP用の変更はUnity Editor上でも問題なく動くので、#UNITY_UWPディレクティブなどは使わず次のように実装します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using TriLib;
using System.IO;
using System;

public class FbxLoader : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        // FBXファイルはStreamingAssets以下に設置
        loadFbx(Application.streamingAssetsPath + "/crystal.fbx");
    }

    void loadFbx(string filePath)
    {
        // ファイルのバイト配列取得
        byte[] fileBytes = File.ReadAllBytes(filePath);

        using (var assetLoader = new AssetLoaderAsync())
        {
            try
            {
                // ロード時オプションを作成
                var assetLoaderOptions = AssetLoaderOptions.CreateInstance();
                // このオプションをtrueにしないとMixedRealityのプロジェクトが正しく動作しない
                assetLoaderOptions.UseOriginalPositionRotationAndScale = true;

                // バイト配列と拡張子がzipだと伝える
                var loadingThread = assetLoader.LoadFromMemoryWithTextures(fileBytes, "zip", assetLoaderOptions, null, delegate (GameObject loadedGameObject)
                {
                    // ロード完了後に行われる処理
                    // ロードされたfbxのモデルはloadedGameObject割り当てられる
                    Debug.Log("Load Finished");
                });
            }
            catch (Exception exception)
            {
                Debug.Log("exception");
            }

        }
    }
}

とりあえずUnityでこれを実行してみる。

f:id:mojo_nobu:20190623191303p:plain
出ました。なんかデカい

サイズが気になりますが(後述)、とりあえず先にHoloLensで出してみましょう。

プロジェクトをUWPへ書き出すとき、次のようがエラーが出ます。

Assets\TriLib\TriLibExtras\Scripts\AvatarLoader.cs(397,40): error CS0619: 'HumanDescription' is obsolete: 'HumanDescription is not supported on .NET scripting backend.'

Assets\TriLib\TriLibExtras\Scripts\AvatarLoader.cs(408,31): error CS0619: 'AvatarBuilder.BuildHumanAvatar(GameObject, HumanDescription)' is obsolete: 'BuildHumanAvatar is not supported on .NET scripting backend.'

ヒューマンまわりのエラーが出てます。今回はヒューマンモデルは対象外として、TrilibExtra以下のファイルも使わないのでフォルダごと削除してしまいましょう。

HoloLensで出してみました。

f:id:mojo_nobu:20190623211447j:plain
やはりデカイ

サイズ表示の問題、対策

基本的な使い方は以上です。ここからはサイズの問題について見ていきます。

上記の実行結果は想定していたよりも相当大きいサイズで表示されていました。これはtrilibではFBXファイル内の1単位(インチだったりメートルだったり)の長さを見ておらず、unityのスケール設定(メートル)を適用していることが原因です。

対策は2つ考えられます。

対策① モデルのスケール設定をメートルにしてから出力する

ファイルのほうをプロジェクトに合わせます。モデルの1単位当たりの大きさをメートルに設定してからfbxに出力すればOKです。
3ds Maxでは、メニューからCustomize/Units Setup.../System Unit Setupと進んでいき、System Unit Scaleの値を「1Unit = 1.0 meters」にすればOKです。

f:id:mojo_nobu:20190624103651p:plain
metersに設定

対策② FBXをascii形式で出力してUnits設定を取得する

FBXにはascii形式とbinary形式があります。ascii形式の場合その中身は文字列なので、そこから1単位当たりの大きさの値を取ってくることができます。

f:id:mojo_nobu:20190624104019p:plain
ここの"UnitScaleFactor"の値を取得する

取ってきた値をロードされたgameObjectに適用すればOKです。コードとしては以下になります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using TriLib;
using System.IO;
using System;

public class FbxLoader : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        loadFbx(Application.streamingAssetsPath + "/crystal_ascii.fbx");
    }

    void loadFbx(string filePath)
    {
        byte[] fileBytes = File.ReadAllBytes(filePath);

        float scaleFactor = 1f;
        // binaryかどうかの判定
       // (参考)https://www.slideshare.net/L1048576/fbx-1-1
        if (fileBytes[21] == 0x1a & fileBytes[22] == 0x00)
        {
            Debug.Log("binary");
        }
        else
        {
            Debug.Log("ascii");
            string asciiText = System.Text.Encoding.ASCII.GetString(fileBytes);
            foreach (string line in asciiText.Split('\n'))
            {
                if (line.Contains("UnitScaleFactor"))
                {
                    string[] tips = line.Split(',');
                    scaleFactor = float.Parse(tips[tips.Length - 1]) / 100f;
                    break;
                }
            }

        }

        using (var assetLoader = new AssetLoaderAsync())
        {
            try
            {
                var assetLoaderOptions = AssetLoaderOptions.CreateInstance();
                assetLoaderOptions.UseOriginalPositionRotationAndScale = true;

                var loadingThread = assetLoader.LoadFromMemoryWithTextures(fileBytes, "zip", assetLoaderOptions, null, delegate (GameObject loadedGameObject)
                {
                    // ロード完了後に行われる処理
                    // ロードされたfbxのモデルはloadedGameObject割り当てられる
                    Debug.Log("Load Finished");
                    // モデルにサイズを適用
                    loadedGameObject.transform.localScale = new Vector3(scaleFactor, scaleFactor, scaleFactor);
                });
            }
            catch (Exception exception)
            {
                Debug.Log("exception");
            }

        }
    }
}

ascii形式のFBXはサイズが大きくなりがちなので、どちらの方法を選択するかは検討の余地があるかと思います。