はじめに
以前、CustomTextureUpdate の記事を書きました。
この API は、ネイティブ(DLL 側)からバッファのポインタを渡してあげるだけでプラットフォーム間の差異(OpenGL や DirectX 固有のテクスチャの更新方法)を気にせず且つ簡単にテクスチャの更新が可能となるものです。前回の記事では PNG をデコードして RGBARGBA... な 1 pixel - 32 bit なバッファにしてそれを指定しましたが、今回は DXT1 なバッファを使用する方法について書きたいと思います。
サンプルプロジェクト
DXTC
DXT1 は GPU DirectX Texture Compression(DXTC)という圧縮方式の規格の 1 つで、4x4 ピクセルのブロックで代表色を補間する方式で 1/6 まで容量を圧縮できるものです。DXTC で表現されたバッファは JPEG や PNG といった他のフォーマットとは異なり、そのまま GPU に乗せることが出来るので、圧縮による品質低下が許容できる状況下ではとてもお得なフォーマットです。詳細は以下の記事でわかりやすくまとめられています。
今回は簡単のために DXT1 のみを扱います。
準備
Unity(C#)側
CustomTextureUpdate を Unity(C#)から呼び出す方法は前回と同じですので、CustomTextureUpdate そのものにつきましてはこちらに目を通して見てください。差分としては、IssuePluginCustomTextureUpdate
が obsolete になっているので、IssuePluginCustomTextureUpdateV2
を代わりに使います。また、テクスチャのフォーマットを TextureFormat.RGBA32
ではなく TextureFormat.DXT1
を使用します。
[DllImport("CustomTextureUpdate")] public static extern uint Load(IntPtr data, int size); [DllImport("CustomTextureUpdate")] public static extern int GetWidth(uint id); [DllImport("CustomTextureUpdate")] public static extern int GetHeight(uint id); [DllImport("CustomTextureUpdate")] public static extern IntPtr GetTextureUpdateCallback(); IEnumerator LoadImage(string url) { // データのロード using (var www = UnityWebRequest.Get(url)) { yield return www.SendWebRequest(); Load(www.downloadHandler.data); } } 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.DXT1, false); var renderer = GetComponent<Renderer>(); renderer.material.mainTexture = texture; // テクスチャ更新用のネイティブ側のコールバックを取得 var callback = GetTextureUpdateCallback(); // コマンドバッファの生成 var command = new CommandBuffer(); command.name = "CustomTextureUpdeate"; // テクスチャの更新イベントをコマンドバッファに登録 // 引数はネイティブ側のコールバックとテクスチャおよび int で与えられる任意のデータ command.IssuePluginCustomTextureUpdateV2(callback, texture, id); // コマンドバッファを即時実行 Graphics.ExecuteCommandBuffer(command); }
DLL(C++)側
.dds ファイルのヘッダのフォーマットは以下をご参照ください。
まずはロードする部分です。
struct DdsHeader { DWORD dwMagic; DWORD dwSize; DWORD dwFlags; DWORD dwHeight; DWORD dwWidth; DWORD dwPitchOrLinearSize; DWORD dwDepth; DWORD dwMipMapCount; DWORD dwReserved1[11]; DWORD dwPfSize; DWORD dwPfFlags; DWORD dwFourCC; DWORD dwRGBBitCount; DWORD dwRBitMask; DWORD dwGBitMask; DWORD dwBBitMask; DWORD dwRGBAlphaBitMask; DWORD dwCaps; DWORD dwCaps2; DWORD dwReservedCaps[2]; DWORD dwReserved2; }; void DdsLoader::Load(const void *pData, size_t dataSize) { const auto *pHeader = reinterpret_cast<const DdsHeader*>(pData); // DDS ファイルかどうかチェック if (pHeader->dwMagic != 0x20534444 /* " SDD" */) return; // 今回は簡単のために DXT1 のみを扱う if (pHeader->dwFourCC != 0x31545844 /* "1TXD" */) return; width_ = pHeader->dwWidth; height_ = pHeader->dwHeight; constexpr size_t headerSize = sizeof(DdsHeader); const size_t bufferSize = dataSize - headerSize; const auto *pBuffer = reinterpret_cast<const char*>(pData) + headerSize; data_ = std::make_unique<char[]>(bufferSize); std::memcpy(data_.get(), pBuffer, bufferSize); hasLoaded_ = true; }
ヘッダを読み込んで横幅・縦幅を取得、またヘッダを除く領域をコピーして格納しておきます。流れとしてはこの横幅・縦幅を使って Unity 側で Texture2D
が DXT1
フォーマットで作成されます。そしてコマンドバッファを介して次の OnTextureUpdate()
が呼ばれます。
void UNITY_INTERFACE_API OnTextureUpdate(int eventId, void *pData) { const auto event = static_cast<UnityRenderingExtEventType>(eventId); if (event == kUnityRenderingExtEventUpdateTextureBeginV2) { auto *pParams = reinterpret_cast<UnityRenderingExtTextureUpdateParamsV2*>(pData); const auto id = pParams->userData; if (auto *pLoader = GetLoader(id)) { if (pLoader->GetWidth() == pParams->width && pLoader->GetHeight() == pParams->height) { pParams->texData = pLoader->GetData(); pParams->bpp = 2; } else { // Error } } } else if (event == kUnityRenderingExtEventUpdateTextureEndV2) { auto *pParams = reinterpret_cast<UnityRenderingExtTextureUpdateParamsV2*>(pData); pParams->texData = nullptr; } } UNITY_INTERFACE_EXPORT UnityRenderingEventAndData UNITY_INTERFACE_API GetTextureUpdateCallback() { return OnTextureUpdate; }
UnityRenderingExtTextureUpdateParamsV2::texData
に先程コピーしたデータを渡しています。しかしこれだけだと正常にテクスチャが表示されません。現在、Unity ではピクセル毎のバイト数を正しく扱っていない模様で、ワークアラウンドで UnityRenderingExtTextureUpdateParamsV2::bpp
(bytes-per-pixel)を書き換えて上げる必要があります。DXT5 では 2 ではなく 4 を指定する必要があります(以下参考):
こうすることで DXT1 なバッファを直接 Unity に渡すことが可能となりました。
おわりに
Twitter で質問を頂いて試してみたもののうまくいかない...、となっていたのですが keijiro さんが返信をくださって解決しました、ありがとうございます:
今思い出しましたが DX11 だけは邪悪なテクニックによって運用が可能でした。 https://t.co/c20fvVzG50
— Keijiro Takahashi (@_kzr) 2019年5月26日