記憶力が無い

プログラミングと室内園芸と何か

WebGPU の texture_2d_array を使う

この記事は グラフィックス全般 Advent Calendar 2023 の 18 日目の記事となります。

はじめに

今年の 5 月にリリースされた Chrome 113 でとうとう WebGPU が利用可能になりましたね。 まだ WebGPU が利用可能な環境は多くないですが、WebGL2 では使えなかったコンピュートシェーダーが使えるようになるなど、普及した未来が楽しみです。

今回は WebGPU を触ってみてハマったポイントを少し共有したいと思います。

texture_2d_array

WebGL2 では、一度に使用できるテクスチャの数の上限が厳しく、大量のテクスチャを扱うにはテクスチャ配列(TEXTURE_2D_ARRAY)を使うなどの対策が必要でした。

WebGL2 の TEXTURE_2D_ARRAY に関しては下記の記事が参考になりましたのでリンクを貼っておきます。

ics.media

さて本題の WebGPU についてですが、WebGPU でも texture_2d_array というテクスチャの配列の型が定義されています。

https://www.w3.org/TR/WGSL/#sampled-texture-type

調べた限りでは WebGPU でのテクスチャの数の上限の情報は見つかりませんでした。 しかし、仮に上限がかなり緩かったとしても、多数のテクスチャを扱う場合 texture_2d_array は有用だと思います。

今回は情報が少ない中手探りで texture_2d_array を使ってみてハマったポイントを 3 つ紹介します。

ハマりポイント1: テクスチャに直接データを送信できない

まず最初にぶち当たるのが、texture_2d_array のテクスチャへのデータの送信方法が分からないという問題です。

通常のテクスチャであれば、ググればすぐやり方が出てきて copyExternalImageToTexture()https://developer.mozilla.org/en-US/docs/Web/API/GPUQueue/copyExternalImageToTexture)を使えば良いことがわかります。 しかし、ドキュメントを見る限り texture_2d_array のテクスチャに複数枚の画像データを送信する方法がよくわかりません。

Exceptions のところに、

source.origin.z + the depthOrArrayLayers of the region to copy to is less than or equal to 1.

と書かれているところからも、複数枚(レイヤー)の画像データはこのメソッドでは扱えなさそうです。

ChatGPT に聞いた

ChatGPT に聞いてみたところ、一旦バッファに読み込んで copyBufferToTexture() メソッドでバッファからテクスチャにデータをコピーするとよいと言われました。 半信半疑で copyBufferToTexture() についていろいろ調べ、実際に試してみたところ、このやり方でうまくいきました。

ハマりポイント2: 画像データの横幅が 64 の倍数である必要がある

copyBufferToTexture() メソッドでは bytesPerRow が 256 の倍数でない場合エラーになります。

WebGPU の仕様でも次のように書かれています。

https://www.w3.org/TR/webgpu/#gpuimagecopybuffer

imageCopyBuffer.bytesPerRow must be a multiple of 256.

例えば、画像の横幅が 16 の場合、1 ピクセル当たり RGBA の 4 バイトで、1 行あたりのデータ(bytesPerRow)は 64 バイトとなりエラーになります。

画像の横幅が 16 の場合のエラー

つまり、画像の横幅が 64 の倍数でない場合 copyBufferToTexture() メソッドでデータを送信することができません。

苦肉の策としてキャンバスに画像を読み込む際に const width = Math.ceil(img.width / 64) * 64; のようにして無理やり画像の幅を 64 の倍数に変換して対応しました。

function fetchImageData(src) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => {
      const cvs = document.createElement('canvas');
      // 後でテクスチャを読みだす際に一行のバイト数が256の倍数になっている必要があるため
      // ここで画像データの幅を64の倍数にリサイズするしておく
      // ref: https://www.w3.org/TR/webgpu/#gpuimagecopybuffer
      const width = Math.ceil(img.width / 64) * 64;
      cvs.width = width;
      cvs.height = img.height;
      const ctx = cvs.getContext('2d');
      if (ctx == null) {
        throw new Error('画像の読み込みに失敗しました');
      }
      ctx.drawImage(img, 0, 0, width, img.height);
      resolve(ctx.getImageData(0, 0, width, img.height));
    };
    img.onerror = (e) => reject(e);
    img.src = src;
  });
}

ハマりポイント3: 画像が1枚のときは注意が必要

createTexture() メソッドでテクスチャを作成する際、size の 3 つ目の要素(depthOrArrayLayers)が 1 の時、テクスチャが texture_2d_array として扱われずエラーになります。

depthOrArrayLayers が 1 の時エラーになる

こちらも、苦肉の策として Math.max(textureCount, 2)depthOrArrayLayers が 1 にならないようにしました。

const texture = device.createTexture({
  size: [
    textureWidth,
    textureHeight,
    // layer が 1 だと texture_2d_array として扱われないので、
    // 2以上の値になるようにする
    Math.max(textureCount, 2)
  ],
  format: 'rgba8unorm',
  usage: GPUTextureUsage.TEXTURE_BINDING |
    GPUTextureUsage.COPY_DST |
    GPUTextureUsage.RENDER_ATTACHMENT
});

まとめ

情報が少ない中、どうにかこうにか texture_2d_array を使ってみました。

それぞれのハマりポイントの対策がこれでよいのかはよくわかってないです。 もしもっと良いやり方や、そもそもこのやり方間違ってるよという指摘などありましたら、コメントいただけると助かります。

今後このような WebGPU に関する知見が充実していくとよいですね。 WebGPU 楽しんでいきましょー!

--

texture_2d_array を使ったデモを作成したので置いておきます。 参考にしていただけたらと思います。 ttk1.github.io

ソースコードはこちらにあります。
※ ざっと作ったのでコード読みにくかったらすみません... github.com

Copyright © 2017 ttk1