はじめに
おひさしぶりです。
プライベートPCの不調でVScodeすらまともに動かなくなって、数か月ほど個人的な開発ができていませんでした……
先週に新しいPCを自作して快適な開発環境とゲーム環境を手に入れれました!
さて今回はVertexBufferについての記事を書いていこうと思います。
VertexBufferとは
VertexBufferとは日本語で頂点バッファーというもので、VertexShaderで描画する頂点データをShaderに渡すためのものです。
前回は頂点データを直接Shaderファイルに定義しましたが、通常ではShaderファイルにVertexBufferを渡して計算を行います。
今回はVertexBufferをShaderファイルへ渡す方法について書いていきます。
VertexBufferの渡し方
まずはShaderに渡す頂点データの作成を行います。
今回はShaderでよく使われるfloate型の配列であるFloate32Arrayを使って頂点データを用意します。
const vertices: Float32Array = new Float32Array(
[
// x, y, r, g, b
0.0, 0.5, 1.0, 0.0, 0.0,
-0.5, -0.5, 0.0, 1.0, 0.0,
0.5, -0.5, 0.0, 0.0, 1.0,
]
);
Shaderには頂点座標と色のデータを1つの配列で渡します。
ちょっと不思議な使い方をしていると感じる人もるかもしれません。解説しておくと1行が1つの頂点データに対応しており、1列目がx座標、2列目がy座標、3列目以降が色データのRGBに対応しています。
このようなデータ構造で正しく渡せているのかを思う方がいるかもしれませんが、後々配列についてのフォーマットをWebGPUに登録して認識させるので安心してください。
次にVertexBufferを作成します。上で定義した配列は頂点データです。VertexBufferはShaderにデータを渡すための入れ物だと思ってくれるといいと思います。
const verticesBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX,
mappedAtCreation: true,
});
new Float32Array(verticesBuffer.getMappedRange()).set(vertices);
verticesBuffer.unmap();
device.createBufferでVertexBufferを作成します。
sizeには頂点データのバイトサイズ、usageにはBufferの種類(今回はVertexBuffer)、mappedAtCreationは作成後すぐにデータをセットできるかどうかを設定できます。今回はすぐにデータをセットするのでtureを設定します。
次に作成したVertexBufferにデータをセットします。verticesBuffer.getMappedRange()でデータをセットするメモリを取得します。
そこにnew Float32Array(verticesBuffer.getMappedRange())を行うことでFloat32Arrayの配列をセットできます。
set(vertices)で先ほどのメモリに頂点データをセットします。
これ以降VertexBufferにデータをセットすることはないのでverticesBuffer.unmap()でVertexBufferのメモリをクローズしてShaderに送る準備をします。
VertexBufferをShaderに送るにはpiplineのBuffersに記述する必要があります。
const pipeline = device.createRenderPipeline({
vertex:{
module: shaderModule,
entryPoint: "vs_main",
buffers:[
{
arrayStride: 20,
attributes: [
{
shaderLocation:0,
format:"float32x2",
offset:0,
},
{
shaderLocation:1,
format:"float32x3",
offset:8,
},
]
}
]
},
fragment:{
module: shaderModule,
entryPoint: "fs_main",
targets: [{
format: format
}]
},
layout: pipelineLayout
});
前回のpiplineのvertexにbuffersを追加しましょう.
buffersには2つの要素がありarryStrideには頂点データの1行分のバイトサイズを設定します。今回は4バイトのfloate型が5つなので20を設定します。
次にattributesを設定します。attributesには3つの要素があり、shaderLocationにはShaderのLocation番号を設定します。今回は頂点と色のデータをShaderに渡しているので0と1をそれぞれ設定します。
次にformatを設定します。これは渡す配列のformatを設定します。頂点データはfloat32の2列の配列なのでfloat32x2
を設定します。色データはfloat32の3列なのでfloat32x3
を設定します。
最後にoffsetは頂点データの1行の頂点座標のデータと色データの識別のための要素です。それぞれの先頭のバイト番号を設定します。
頂点データは1行目の1番目から頂点データなので0を設定します。次に色データは3番目からなのでバイト番号的には8番目からなので8を設定します。
最後にrenderpassにVertexBufferを設定します。
renderpass.setVertexBuffer(0, verticesBuffer);
これでTypeScript側のコーディングは終わりです。
次にShaderを書いていきます。
Shader
今回はShaderのvs_mainのみ修正を行います。
@vertex
fn vs_main(
@location(0) position: vec2<f32>,
@location(1) color: vec3<f32>,
@builtin(vertex_index) v_id: u32
) -> Fragment {
var output : Fragment;
output.Position = vec4<f32>(position, 0.0, 1.0);
output.Color = vec4<f32>(color, 1.0);
return output;
}
前回は関数内に頂点座標と色データを定義していましたが、今回はTypeScript から送られてくるVertexBufferを使っているので定義していません。
代わりにvs_mainの引数に@location(0) position: vec2<f32>
と @location(1) color: vec3<f32>
があります。これはpiplineのbuffers内で定義したshaderLocationに対応する番号で値が渡ってきています。これをそのまま出力用のoutputにそれぞれ代入してあげるだけで終了です。
かなりスマートに書けましたね。基本的にShader内でデータを定義するのはよくないので、このようにTypeScriptから渡ってくるデータを使うのがいいです。VertexBuffer以外にTypeScriptでもShaderでも読み書きできるBufferが存在しているのでそれらを使いのが良いです。
出力結果では実行してみましょう。
出力結果
終わり
お疲れさまでした。今回はShaderにデータを共有する方法でした。
今後どんな記事を書こうか迷っている状態です。自分も全然理解度が足りてないので手探りで試してる状態です。
何か面白いことができたら記事を書いてみようと思ってます。
今回の完全なコードはここにあります。
ではまたまた~