MRが楽しい

MRやVRについて学習したことを書き残す

MRTKのglTFインポータで陰影が表示されない問題を回避する

本日は MRTK の技術調査枠です。
MRTKのglTFインポータの動的読み込みで HoloLens 実行時に陰影が表示されない問題を回避する手順を記事にします。

前提条件

前回記事の続きです。
bluebirdofoz.hatenablog.com

MRTKのglTFインポータで陰影が表示されない問題

MRTKのglTFインポータを HoloLens2 上の動的読み込みで利用すると、以下のようにモデルの陰影が表示されない問題が発生します。
f:id:bluebirdofoz:20210108031527j:plain

この問題は以下の issue で管理されていますが、2021/01/08 現在、MRTK 2.5 で対応策は導入されていないようです。
github.com
github.com
github.com

原因

端的に言うとシェーダバリアントを最適化する仕組みにより、_DIRECTIONAL_LIGHT, _SPECULAR_HIGHLIGHTS の定義が無効化された状態でコンパイルされることが原因です。
microsoft.github.io

この問題は Editor 上では回避されるため、Editor のシーン再生でアプリを確認した場合は陰影が表示されます。
詳細は上記の issue に記述されています。
f:id:bluebirdofoz:20210108031949j:plain

回避策

_DIRECTIONAL_LIGHT, _SPECULAR_HIGHLIGHTS の定義が無効化されないシェーダに差し替えることで回避可能です。
一例として MRTKStandardShader を改変した以下のシェーダを利用してみました。
・MixedRealityStandardCullOffDefSpecular.shader

重要なのは 262 行目と 263 行目の以下の追記部分です。

            #define _DIRECTIONAL_LIGHT 1.0
            #define _SPECULAR_HIGHLIGHTS 1.0

_DIRECTIONAL_LIGHT と _SPECULAR_HIGHLIGHTS を有効な値で再定義しているため、シェーダバリアントの最適化で _DIRECTIONAL_LIGHT, _SPECULAR_HIGHLIGHTS の定義部分が無効化されなくなります。
再定義しているため、シェーダを Unity に取り込むとエラーが表示されますが、動作には問題ありません。
f:id:bluebirdofoz:20210108032321j:plain

後は読み込み完了後のモデルのシェーダをこのシェーダに差し替えるだけです。
前回記事のスクリプトを修正し、Renderer コンポーネントからマテリアルのシェーダを差し替える以下のスクリプトを作成しました。
・CustomGLBLoadTest.cs

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

// ファイル読み込み用
using System.IO;

// GLB読み込みライブラリの名前空間を追加
using Microsoft.MixedReality.Toolkit.Utilities.Gltf.Schema;
using Microsoft.MixedReality.Toolkit.Utilities.Gltf.Serialization;

// MRTKの名前空間を追加
using Microsoft.MixedReality.Toolkit.UI;
using Microsoft.MixedReality.Toolkit.Input;

// HoloLensフォルダ指定用
#if WINDOWS_UWP
using Windows.Storage;
#endif

public class CustomGLBLoadTest : MonoBehaviour
{
    /// <summary>
    /// 読み込みモデルファイル名
    /// </summary>
    [SerializeField, Tooltip("読み込みモデルファイル名")]
    private string p_LoadFileName;

    [SerializeField, Tooltip("再設定シェーダ")]
    private Shader p_ShaderOverride = null;

    /// <summary>
    /// 起動時処理
    /// </summary>
    void Start()
    {
        if (p_LoadFileName != null)
        {
            // ファイルパスを取得する
            string glbFilePath = Get3DObjectFilePath(p_LoadFileName);

            // glbファイルを読み込む
            AsyncLoadGLBByte(glbFilePath);
        }
    }

    /// <summary>
    /// 定期処理
    /// </summary>
    void Update()
    {
    }

    /// <summary>
    /// GLTF読み込みを実行する
    /// (バイト配列での読み込み例)
    /// </summary>
    /// <param name="filepath">ファイルパス</param>
    private async void AsyncLoadGLBByte(string filepath)
    {
        // Stopwatchの開始
        System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
        stopwatch.Start();

        // 読み込みファイルのバイト配列を取得する
        byte[] modelByteData = GetFileAsByteArray(filepath);

        // バイト配列を指定して読み込み
        GltfObject gltfObject = GltfUtility.GetGltfObjectFromGlb(modelByteData);

        Debug.Log("ElapsedMilliseconds : " + stopwatch.ElapsedMilliseconds + " ms");

        try
        {
            // 非同期の読み込み処理を完了まで待機する
            await gltfObject.ConstructAsync();
        }
        catch (Exception e)
        {
            Debug.LogError($"GlbLoad failed - {e.Message}\n{e.StackTrace}");
            return;
        }

        Debug.Log("ElapsedMilliseconds : " + stopwatch.ElapsedMilliseconds + " ms");

        stopwatch.Stop();

        if (gltfObject != null)
        {
            // ゲームオブジェクトの参照を取得する
            GameObject loadedGlbObject = null;
            loadedGlbObject = gltfObject.GameObjectReference;

            if (loadedGlbObject != null)
            {
                // 子オブジェクトに設定してローカルトランスフォームを初期化する
                loadedGlbObject.transform.parent = this.transform;
                loadedGlbObject.transform.localPosition = Vector3.zero;
                loadedGlbObject.transform.localEulerAngles = Vector3.zero;
                loadedGlbObject.transform.localScale = Vector3.one;

                // ゲームオブジェクトへの再帰的な設定
                RecursionSettingModel(loadedGlbObject);

                Debug.Log("Import successful");
            }
        }
    }

    /// <summary>
    /// ゲームオブジェクトへの再帰的な設定
    /// </summary>
    /// <param name="a_Object"></param>
    private void RecursionSettingModel(GameObject a_Object)
    {
        foreach (Transform child in a_Object.transform)
        {
            // Rendererコンポーネントの参照を取得する
            Renderer targetRenderer = child.gameObject.GetComponent<Renderer>();

            if (targetRenderer != null && p_ShaderOverride != null)
            {
                // Rendererコンポーネントが存在すればシェーダを再設定する
                targetRenderer.material.shader = p_ShaderOverride;
            }

            // 再帰処理
            RecursionSettingModel(child.gameObject);
        }
    }

    /// <summary>
    /// Returns the contents of the specified file as a byte array.
    /// 指定されたファイルの内容をバイト配列として返します
    /// </summary>
    static byte[] GetFileAsByteArray(string filePath)
    {
        FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);

        BinaryReader binaryReader = new BinaryReader(fileStream);

        return binaryReader.ReadBytes((int)fileStream.Length);
    }

    /// <summary>
    /// 3Dフォルダ直下にある3Dモデルのファイルパスを取得する
    /// UnityEditor : Assets/StreamingAssetsフォルダ
    /// HoloLens : Objects3Dフォルダ
    /// </summary>
    /// <param name="filename">ファイル名</param>
    /// <returns></returns>
    public string Get3DObjectFilePath(string filename)
    {
#if WINDOWS_UWP
            // HoloLens上での動作の場合、Objects3D.Pathフォルダを参照する
            string directorypath = KnownFolders.Objects3D.Path;
#else
        // Unity上での動作の場合、Assets/StreamingAssetsフォルダを参照する
        string directorypath = UnityEngine.Application.streamingAssetsPath;
#endif
        return Path.Combine(directorypath, filename);
    }
}

前回記事と同様に、glb ファイルを読み込むオブジェクトにスクリプトをアタッチします。
[p_ShaderOverride]にモデル読み込み後、差し替えを行いたいシェーダを設定します。
f:id:bluebirdofoz:20210108032358j:plain

これで完了です。プロジェクトをビルドしてアプリを HoloLens2 に展開します。

動作確認

前回記事と同様、HoloLens2 上でアプリを起動して Object3D フォルダの glb ファイルの動的読み込みを試します。
以下の通り、陰影が表示された状態でモデルが表示されます。
f:id:bluebirdofoz:20210108032409j:plain