凹みTips

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

Unity の CustomTextureUpdate で DXT1 を使ってみる

はじめに

以前、CustomTextureUpdate の記事を書きました。

tips.hecomi.com

この API は、ネイティブ(DLL 側)からバッファのポインタを渡してあげるだけでプラットフォーム間の差異(OpenGLDirectX 固有のテクスチャの更新方法)を気にせず且つ簡単にテクスチャの更新が可能となるものです。前回の記事では PNG をデコードして RGBARGBA... な 1 pixel - 32 bit なバッファにしてそれを指定しましたが、今回は DXT1 なバッファを使用する方法について書きたいと思います。

サンプルプロジェクト

github.com

DXTC

DXT1 は GPU DirectX Texture Compression(DXTC)という圧縮方式の規格の 1 つで、4x4 ピクセルのブロックで代表色を補間する方式で 1/6 まで容量を圧縮できるものです。DXTC で表現されたバッファは JPEGPNG といった他のフォーマットとは異なり、そのまま GPU に乗せることが出来るので、圧縮による品質低下が許容できる状況下ではとてもお得なフォーマットです。詳細は以下の記事でわかりやすくまとめられています。

www.webtech.co.jp

今回は簡単のために 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 ファイルのヘッダのフォーマットは以下をご参照ください。

docs.microsoft.com

dench.flatlib.jp

まずはロードする部分です。

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 側で Texture2DDXT1 フォーマットで作成されます。そしてコマンドバッファを介して次の 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 を指定する必要があります(以下参考):

github.com

こうすることで DXT1 なバッファを直接 Unity に渡すことが可能となりました。

f:id:hecomi:20190526160040p:plain

おわりに

Twitter で質問を頂いて試してみたもののうまくいかない...、となっていたのですが keijiro さんが返信をくださって解決しました、ありがとうございます: