凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity でテクスチャに使う画像をネイティブ側で読み込んでみた(CustomTextureUpdate 編)

はじめに

前回は以下の記事で低レベルネイティブプラグインインターフェースを使ったテクスチャのアップデートについての解説を行いました:

tips.hecomi.com

しかし前回のプロジェクトでは以下の点で複数プラットフォーム対応が面倒という問題がありました:

  1. グラフィクス API 毎のコードを書く
  2. 各プラットフォーム向けのライブラリが必要
  3. 複数のプラットフォーム向けにビルド

この内、ネイティブプラグインを使っている関係で 2 と 3 は仕方ないのですが、1 に関しては 2017.2b10 から追加された CustomTextureUpdate を利用することで意識しなくても良くなります。今回の記事では、これを使って前回同様テクスチャ更新を行う解説を行いたいと思います。

サンプルプロジェクト

github.com

結果は前回と同じくテクスチャが反映されます:

f:id:hecomi:20180716005626p:plain

API 概要

Unity 2017.2.0b10 より、以下のようなコマンドバッファ経由で操作できる新しい API(低レベルネイティブプラグインレンダリング拡張)がいくつか追加されました:

IssuePluginCustomTextureUpdate() はこの 1 つです。API そのものの解説については以下の keijiro さんのリポジトリにとても分かりやすくまとまっているのでこちらをご参照ください:

github.com

プロジェクト解説

ビルド方法については前回の記事をご参照ください。ここではサンプルプロジェクトの解説をします。テクスチャの更新の流れとしては以下のようになります。

  1. Unity 側でテクスチャを用意
  2. Unity 側でコマンドバッファを用意
  3. ネイティブ側の更新用コールバック関数ポインタを取得
  4. Unity 側で IssuePluginCustomTextureUpdate() にテクスチャと更新用関数を渡す
  5. Graphics.ExecuteCommandBuffer() してコマンドバッファを実行
  6. ネイティブプラグイン側で引数で与えられたポインタに画像データ(配列)を格納

コードで概要を見てみます。実際のサンプルプロジェクトのコードとだいたい一緒ですが細かいところは分かりやすいように修正しています。まずは C# 側です。

const string dllName = "CustomTextureUpdate";

[DllImport(dllName)]
public static extern uint Load(IntPtr data, int size);

[DllImport(dllName)]
public static extern int GetWidth(uint id);

[DllImport(dllName)]
public static extern int GetHeight(uint id);

[DllImport(dllName)]
public static extern IntPtr GetTextureUpdateCallback();

IEnumerator LoadImage(string url)
{
    // データのロード
    using (var www = new WWW(url)) 
    {
        yield return www;
        Load(www.bytes);
    }
}

async void Load(byte[] data)
{
    // テクスチャのデコード
    var handle = GCHandle.Alloc(data, GCHandleType.Pinned);
    var pointer = handle.AddrOfPinnedObject();
    await Task.Run(() => 
    {
        var id = Load(pointer, data.Length);
    });
    handle.Free();

    // テクスチャサイズ取得
    var width = GetWidth(id);
    var height = GetHeight(id);

    // テクスチャ生成
    var texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
    var renderer = GetComponent<Renderer>();
    renderer.material.mainTexture = texture;

    // テクスチャ更新用のネイティブ側のコールバックを取得
    var callback = GetTextureUpdateCallback();

    // コマンドバッファの生成
    var command = new CommandBuffer();
    command.name = "CustomTextureUpdeate";

    // テクスチャの更新イベントをコマンドバッファに登録
    // 引数はネイティブ側のコールバックとテクスチャおよび int で与えられる任意のデータ
    command.IssuePluginCustomTextureUpdate(callback, texture, id);

    // コマンドバッファを即時実行
    Graphics.ExecuteCommandBuffer(command); 
}

ネイティブとのやり取りは以下の 3 点です:

  • 画像データのデコード
  • ネイティブ側のコールバックの取得
  • コールバックを指定して更新処理を発行

デコードは前回と同じです。コールバックの取得は低レベルネイティブプラグインインターフェースと同じように関数ポインタを取得しています。そしてネイティブ側でそれを呼び出すのも低レベルネイティブプラグインインターフェースの GL.IssuePluginEvent()と同様にイベントを発行するのですが、ここでは CommandBuffer を作成して IssuePluginCustomTextureUpdate() をそれ経由で呼び出します。なぜこのようなデザイン差があるのかはわかっていないです...(レンダリングスレッドで同じタイミングで更新する GL.IssuePluginEvent() に対して開発者が好きなタイミングで更新処理を行えるようにするための柔軟性担保、みたいな感じでしょうか)。

さて、ではネイティブ側を見ていきます。ネイティブ側ではいくつかのヘッダファイルが必要になります。これらは Unity のディレクトリに含まれており、例えば Mac ならば /Applications/Unity/Unity.app/Contents/PluginAPI にあります。今回はその中で以下の 3 つのヘッダファイルを利用します。

  • IUnityInterface.h
    • UNITY_INTERFACE_APIUNITY_INTERFACE_EXPORT といった DLL を書く上で必要なキーワードなど
  • IUnityGraphics.h
    • 低レベルネイティブプラグインインターフェースを書く上で必要な型など
    • IUnityInterface.h をインクルードしている
  • IUnityRenderingExtension.h
    • 今回の CustomTextureUpdate のようなレンダリング拡張に必要な型など
    • IUnityGraphics.h をインクルードしている

画像ロードの部分は前回と大体同じなので省略します。コードは次のようになります。

#include "Unity/IUnityRenderingExtensions.h"

...

void OnTextureUpdate(int eventId, void *pData)
{
    // イベントの種類を取得
    const auto event = static_cast<UnityRenderingExtEventType>(eventId);

    // テクスチャ更新の直前に呼ばれる
    // texData にテクスチャデータを格納する
    if (event == kUnityRenderingExtEventUpdateTextureBegin)
    {
        // 諸々の情報は pData に入っているので型をもとに戻す
        auto *pParams = reinterpret_cast<UnityRenderingExtTextureUpdateParams*>(pData);

        // C# 側から IssuePluginCustomTextureUpdate の引数で貰った値を取得
        const auto id = pParams->userData;

        // デコード済みのデータを取得
        auto *pLoader = GetLoader(id)
        if (!pLoader) return;

        // デコード済みのデータと与えられたテクスチャのサイズを比較
        if (pLoader->GetWidth() != pParams->width ||
            pLoader->GetHeight() != pParams->height)
        {
            return;
        }

        // texData に画像データを与えることでテクスチャに反映される
        pParams->texData = pLoader->GetData();
    }
    // テクスチャの更新直後に呼ばれる
    else if (event == kUnityRenderingExtEventUpdateTextureEnd)
    {
        // 何もしない...
    }
}

UNITY_INTERFACE_EXPORT UnityRenderingEventAndData UNITY_INTERFACE_API GetTextureUpdateCallback()
{
    return OnTextureUpdate;
}

更新用の関数を返して、その中で UnityRenderingExtTextureUpdateParamstexData に画像を詰めるのが流れになります。UnityRenderingExtTextureUpdateParamsIUnityRenderingExtension.h で定義されている型で、テクスチャの更新に必要な情報がまとめられています。

struct UnityRenderingExtTextureUpdateParams
{
    // テクスチャの更新用データ、プラグインでセットする
    // nullptr を指定すると何もしない
    void* texData;

    // ユーザ定義のデータ、C# から渡される
    unsigned int userData;

    // テクスチャの ID
    unsigned int textureID;

    // テクスチャのフォーマット
    UnityRenderingExtTextureFormat format;

    // テクスチャの横幅
    unsigned int width;

    // テクスチャの縦幅
    unsigned int height;

    // テクスチャのピクセル単位のバイト (bytes per pixel)
    unsigned int bpp;
};

今回は横着して横幅、縦幅しかチェックしていませんが、bpp でチェックしたほうが安全だと思います。

これでビルド・実行すれば、画像がテクスチャへと反映されるようになります。

パフォーマンス

コマンドバッファ部の処理は 400x400 のテクスチャで以下くらいです。

Profiler.BeginSample("Loader.cs - CommandBuffer");
{
    command_.IssuePluginCustomTextureUpdate(callback, texture, id);
    Graphics.ExecuteCommandBuffer(command); 
}
Profiler.EndSample();

f:id:hecomi:20180715232512p:plain

問題点

Graphics.ExecuteCommandBufferAsync() を使って非同期でネイティブ側のコードを実行したかったのですが、IssuePluginCustomTextureUpdate() は非同期対応してないようで駄目でした。レンダリングスレッドで実行できる低レベルネイティブプラグインインターフェースに軍配が上がるケースもあるかもしれません。

また、前回同様 1 テクスチャあたり 1 タスクが回ってしまう手抜きな実装になっているので、最適化についても前回の記事でコメントしたときと同様に必要です。

おわりに

各プラットフォームごとの最適化などは難しくなってしまいますが、用途によっては同じコードを使えて且つテクスチャの更新周りは Unity 側が担保してくれるため、メンテ負荷がとても軽減される便利な機能だと思います。今回は画像ロードというユースケースに絞っての解説になりましたが、カメラ画像や特殊なフォーマットの動画の読み込みなど、使えるユースケースは色々あると思います。