こんにちはエンジニアの中島悟です。
前回は半年ほど前になりますがWebGPUで最小の手間で三角形を描いてみました。
しかしこの四角形ではフィルタやシェーダー芸くらいしか使い道がありません。
そこでもう少し拡張しようと思います。具体的には下のものです。
- 頂点バッファとインデックスバッファでポリゴンメッシュを描画する
- テクスチャを使う
- シェーダーにパラメーターを渡す
これだけあればモデルを描画することもできるでしょう。
サンプルコード
今回はコードの量もそれなりで、またテクスチャ用の画像もあるのでGithub Pagesに置きました。
WebGPUに対応したブラウザで開けばテクスチャの貼られた四面体が回転しているはずです。
またこの記事中ではコードを参照したい時にはGitHubへのリンクを貼っています。
なお↗のリンクはMDNのリファレンスへのリンクです。
それでは目的の部分を見ていきましょう。
頂点バッファとインデックスバッファでポリゴンメッシュを描画する
まずは設定した頂点データを描画できるようにします。
頂点バッファだけでも良いのですが、応用が効くようにインデックスバッファも使って描画します。
//----- vertex buffer -----
のコメントのあたりが頂点バッファの設定です。
WebGPUでは汎用に使えるGPUBuffer
があり、gpudevice.createBuffer(descriptor)
↗のdescriptor
にサイズや用途を指定して作成します。
頂点バッファとして使うためには用途usage
にGPUBufferUsage.VERTEX
を設定します。
またモデルデータは一度書き込んだら変更しないので、mappedAtCreation: true
を指定して作成後すぐに書き込めるようにしておきます。
gpubuffer.getMappedRange()
↗でCPUから書き込めるようにマップされたArrayBuffer
が取得できます。これからFloat32Array
を作成し、用意した頂点データをset
すれば完了です。
書き込みが終わったらgpubuffer.unmap()
↗しておきましょう。
今回は頂点データとして、位置(x,y,z)、色(r,g,b,a)、UV座標(u,v)を使うことにしました。 Cの構造体として書くと頂点1つあたりはこのようになります。
struct VertexInput { float position[3]; float color[4]; float uv[2]; }
しかしJavascriptでは構造体は定義できません。とはいえ今回は全てfloat
なので単純にFloat32Array
にメンバの順番に並べれば十分です。
頂点シェーダーではエントリーポイントの関数で、この構成と同じになるように引数を定義します。
@vertex fn vertex_main( @location(0) position : vec3f, @location(1) color : vec4f, @location(2) uv : vec2f ) -> VertexOut
そしてこのデータの構造をvertexBufferLayout
オブジェクトに定義します。これは後ほど使います。
次はインデックスバッファです。
//----- index buffer -----
のコメントの部分です。
インデックスバッファは値1つ当たりのバイト数と三角形の数からサイズを決めることができます。
ただ、バッファーのサイズが4の倍数Byteになるように切り上げないといけないようです。
これはWebGPUの仕様には書いてなさそうなので、現行のChrome固有の制限かもしれません。
このためインデックスデータの数とArrayのlengthは一致しない可能性があるので注意しましょう。
そしてインデックスバッファも頂点と同様に即座にマップされるようにして作成し、書き込んでおきます。
これでモデル周りの準備は完了です。
テクスチャを使う
テクスチャは//----- image texture -----
のあたりです。
下準備として画像などからCanvasのAPIのcreateImageBitmap(src)
メソッド↗でImageBitmap
オブジェクトを取得しておきます。
WebGPUではテクスチャはGPUTexture
で、gpudevice.createTexture(descriptor)
↗で作成します。
descriptor
にサイズ、フォーマット、用途などを記述します。
テクスチャとして使用するだけなのですが、画像データを描き込むためには用途としてGPUTextureUsage.COPY_DST
とGPUTextureUsage.RENDER_ATTACHMENT
を設定しないといけないようです。
データの書き込みはImageBitmap
からGPUで転送するGPUQueue
のメソッド、gpuqueue.copyExternalImageToTexture(source, destination, copySize)
↗を使います。
それからWebGPUでのテクスチャのサンプリング設定はGPUSampler
オブジェクトです。これもテクスチャのついでに作っておきます。gpudevice.createSampler(descriptor)
↗です。
また今回は3Dでレンダリングするので深度バッファが必要です。
WebGPUではこれにもテクスチャを使います。
フォーマットを深度バッファとして使えるdepth24plus
などにして、描画先と同じサイズで作成しておきます。
今回はテクスチャとしては使わないので、用途はGPUTextureUsage.RENDER_ATTACHMENT
だけにしておきました。
シェーダーにパラメーターを渡す
3Dで描画するにあたって、CPU側(Javascript)から設定したいシェーダーのパラメーターがいくつかあります。
そのためにWebGPUではUniform Buffer
という仕組みが用意されています。
これはシェーダー(GPU側)で定義したvar<uniform>
変数にGPUBuffer
を対応させてJavascriptから値を書き込むことができるというものです。
というわけでGPUBuffer
を用意します。用途にGPUBufferUsage.UNIFORM
を設定して作成します。
合わせてJavascript上で書き込み用に使うFloat32Array
を用意します。ここでは単純にUniform Bufferと同じ大きさのものを用意しておくことにしました。
Uniform Bufferに値を書き込むにはいくつか下準備が必要です。 GPU上のメモリには値の配置にいくつか制限があり、それを踏まえて対応した位置に値を書き込まなくてはいけません。
そのためにWebGPUではまずシェーダー側で、メモリレイアウトを明示的に設定することができます。
ただ今回はmat4x4f
(f32
が16個)が並んでいるだけの単純なものなので省略しています。
また色々な型のメンバが含まれている場合には、TypedArrayのコンストラクターにあるTypedArray(buffer, byteOffset, length)
でArrayBuffer
のビューを作成できるので、これを利用しても良さそうです。
例えばシェーダーで下のような構造体を定義したなら、
struct TypeMixedStruct { f32Value : f32, u32Value : u32 }
Javascript上では下のような感じでアクセスすることになるでしょう。
float32Array = new Float32Array(2); // f32換算のTypeMixedStruct全体のサイズ uint32Array = new Uint32Array(float32Array.buffer); // float32ArrayのbufferからUint32としてアクセスするビューを作成 float32Array[0] = 1.0; // TypeMixedStruct.f32Value へアクセス uint32Array[1] = 255; // TypeMixedStruct.u32Value へアクセス
この場合ではbyteOffset
を使っても良いかもしれません。
この辺りはwebブラウザ上という制約による面倒なところでしょうか。
シェーダーにパラメーターを渡す(改めて)
さて、テクスチャとUniform Bufferの準備ができたところで、改めてこれらにシェーダーからアクセスできるように設定します。
//----- uniform binding -----
に続く部分です。
リソースの対応付けはGPUBindGroupLayout
↗でシェーダー内のリソースの配置を定義し、GPUBindGroup
↗でレイアウトに対応するオブジェクトを設定します。
GPUBindGroupLayout
は定義を自分で書くこともできますが、layout: 'auto'
を設定してRenderPipelineを作成するとシェーダーで付けた@binding(i)
と@group(i)
属性から自動で生成してくれるのでそれを使うことにします。
gpurenderpipeline.getBindGroupLayout(index)
↗で使いたいGPUBindGroupLayout
を取得できます。index
は@group(index)
の数値です。
それから@binding(i)
の数値を考慮してGPUBindGroup
を作るためのオブジェクトを作成します。
const uniformBindGroupDescriptor = { layout: lauoutGroup, entry: [ {binding: 0, resource: resourceObject0}, {binding: 1, resource: resourceObject1}, ... ] }
binding: i
はもちろん@binding(i)
に対応します。
このオブジェクトをgpudevice.createBindGroup(descriptor)
↗に渡してやることで、所望のGPUBindGroup
が作成できます。
そして実際にBufferなどをバインドするのはGPURenderPassEncoder
のそれぞれのメソッドで行います。
当たり前ですがGPURenderPipeline
に設定したシェーダーと、使用するGPUBindGroup
のLayoutは一致していなくてはいけません。
そしてgpurenderpassencoder.setBindGroup(index, bindGroup)
↗のindex
は、GPURenderPipeline
が持っているGPUBindGroupLayout
のインデックスです。
前述の通り今回はgpurenderpipeline.getBindGroupLayout(index)
で取得しているので、これと同じインデックスの値を設定します。
似たような名前が並んでいて少しややこしいですが、これでUniform Bufferの準備は完了です。
シェーダーに渡すパラメーターを更新する
Uniform Bufferの準備もできましたが、中には毎フレーム更新したいパラメーターも存在します。
そのためにはまずUnifomr Buffer作成時に作ったGPUBuffer
と対になるFloat32Array
の値を更新します。
Uniform Bufferの中のメンバーのオフセットを取得できればスマートなのですが、これについては特に取得する手段などは無さそうなので自分で把握しておくしかないようです。
ここではuniformBufferOffsets
として定義しました。
これを使って、まずはFloat32Array
上の必要な部分を更新します。
今回はmat4x4f
が並んでいるだけなのでオフセットを間違えなければ大丈夫です。
そして更新したFloat32Array
は描画に合わせたタイミングでUniform Bufferに転送されて欲しいので、gpuqueue.writeBuffer(buffer, bufferOffset, data, dataOffset=0, size=undefined)
↗を使って転送コマンドをキューに積みます。
バッファーとデータ双方にoffsetとlengthが指定できるので部分的に更新することもできるのですが、今回は丸ごと全部を書き込んでいます。
なおdata
のサイズはバイト換算したサイズで4の倍数になっている必要がある↗そうです。
描画する
最後にGPUCommandEncoder
を作って描画コマンドをGPUQueue
に積みます。
今まで準備してきたPipelineとBindGroup、VertexBufferとIndexBufferをセットし、gpurenderpassencoder.drawIndexed(indexCount)
↗を使います。
これでポリゴンメッシュが描画されるはずです。
まとめ
結局のところWebGPUも他のコマンドバッファ形式のAPIと同様にそれなりに地道な準備が必要です。 Javascriptとブラウザが隠してくれていて楽なところもある一方で、Bufferの読み書きのように若干面倒になってしまっている部分もあります。 あちらを立てればこちらが立たずという感じですが、総合的には割ととっつきやすいのではないでしょうか。
さて次回はWebGPUでついに解禁されたComputeShaderを使ってみたいと思います。
それではまた。