この記事は グラフィックス全般 Advent Calendar 2023 の 18 日目の記事となります。
はじめに
今年の 5 月にリリースされた Chrome 113 でとうとう WebGPU が利用可能になりましたね。 まだ WebGPU が利用可能な環境は多くないですが、WebGL2 では使えなかったコンピュートシェーダーが使えるようになるなど、普及した未来が楽しみです。
今回は WebGPU を触ってみてハマったポイントを少し共有したいと思います。
texture_2d_array
WebGL2 では、一度に使用できるテクスチャの数の上限が厳しく、大量のテクスチャを扱うにはテクスチャ配列(TEXTURE_2D_ARRAY
)を使うなどの対策が必要でした。
WebGL2 の TEXTURE_2D_ARRAY
に関しては下記の記事が参考になりましたのでリンクを貼っておきます。
さて本題の 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 に聞いてみたところ、一旦バッファに読み込んで 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 バイトとなりエラーになります。
つまり、画像の横幅が 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
として扱われずエラーになります。
こちらも、苦肉の策として 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