前の記事では、頂点データをストレージバッファに入れ、組み込みのvertex_index
を使用してインデックス付けしました。その手法は人気が高まっていますが、頂点シェーダーに頂点データを提供するための従来の方法は、頂点バッファと属性を使用することです。
頂点バッファは、他のWebGPUバッファと同様に、データを保持します。違いは、頂点シェーダーから直接アクセスしないことです。代わりに、WebGPUにバッファ内のデータの種類と編成方法を伝え、データをバッファから取り出して提供してもらいます。
前の記事の最後の例を取り上げ、ストレージバッファの使用から頂点バッファの使用に変更しましょう。
最初に行うことは、シェーダーを変更して、頂点データを頂点バッファから取得するようにすることです。
struct OurStruct { color: vec4f, offset: vec2f, }; struct OtherStruct { scale: vec2f, }; struct Vertex { - position: vec2f, + @location(0) position: vec2f, }; struct VSOutput { @builtin(position) position: vec4f, @location(0) color: vec4f, }; @group(0) @binding(0) var<storage, read> ourStructs: array<OurStruct>; @group(0) @binding(1) var<storage, read> otherStructs: array<OtherStruct>; -@group(0) @binding(2) var<storage, read> pos: array<Vertex>; @vertex fn vs( - @builtin(vertex_index) vertexIndex : u32, + vert: Vertex, @builtin(instance_index) instanceIndex: u32 ) -> VSOutput { let otherStruct = otherStructs[instanceIndex]; let ourStruct = ourStructs[instanceIndex]; var vsOut: VSOutput; vsOut.position = vec4f( - pos[vertexIndex].position * otherStruct.scale + ourStruct.offset, 0.0, 1.0); + vert.position * otherStruct.scale + ourStruct.offset, 0.0, 1.0); vsOut.color = ourStruct.color; return vsOut; } ...
ご覧のとおり、これは小さな変更です。重要な部分は、@location(0)
で位置フィールドを宣言することです。
次に、@location(0)
のデータを供給する方法をWebGPUに伝える必要があります。そのためには、レンダーパイプラインを使用します。
const pipeline = device.createRenderPipeline({ label: 'vertex buffer pipeline', layout: 'auto', vertex: { module, + buffers: [ + { + arrayStride: 2 * 4, // 2 floats, 4 bytes each + attributes: [ + {shaderLocation: 0, offset: 0, format: 'float32x2'}, // position + ], + }, + ], }, fragment: { module, targets: [{ format: presentationFormat }], }, });
pipeline
記述子のvertex
エントリに、1つ以上の頂点バッファからデータを取得する方法を記述するために使用されるbuffers
配列を追加しました。最初の、そして唯一のバッファについて、バイト単位でarrayStride
を設定しました。この場合のストライドは、バッファ内の1つの頂点のデータから、バッファ内の次の頂点のデータに進むために取得するバイト数です。
データはvec2f
であり、2つのfloat32数値なので、arrayStride
を8に設定しました。
次に、属性の配列を定義します。1つしかありません。shaderLocation: 0
は、Vertex
構造体のlocation(0)
に対応します。offset: 0
は、この属性のデータが頂点バッファのバイト0から始まることを示します。最後に、format: 'float32x2'
は、WebGPUにバッファからデータを2つの32ビット浮動小数点数として取得するように指示します。(注:attributes
プロパティは、最初の記事の単純化された描画図に示されています)。
頂点データを保持するバッファの使用法をSTORAGE
からVERTEX
に変更し、バインドグループから削除する必要があります。
- const vertexStorageBuffer = device.createBuffer({ - label: 'storage buffer vertices', - size: vertexData.byteLength, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - }); + const vertexBuffer = device.createBuffer({ + label: 'vertex buffer vertices', + size: vertexData.byteLength, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, + }); + device.queue.writeBuffer(vertexBuffer, 0, vertexData); const bindGroup = device.createBindGroup({ label: 'bind group for objects', layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: staticStorageBuffer }}, { binding: 1, resource: { buffer: changingStorageBuffer }}, - { binding: 2, resource: { buffer: vertexStorageBuffer }}, ], });
次に、描画時に、使用する頂点バッファをWebGPUに伝える必要があります。
pass.setPipeline(pipeline); + pass.setVertexBuffer(0, vertexBuffer);
ここでの0
は、上記で指定したレンダーパイプラインbuffers
配列の最初の要素に対応します。
これで、頂点にストレージバッファを使用する代わりに、頂点バッファを使用するように切り替えました。
描画コマンドが実行されるときの状態は、次のようになります。
属性format
フィールドは、次のいずれかの型になります。
頂点形式 | データ型 | コンポーネント | バイトサイズ | WGSL型の例 |
---|---|---|---|---|
"uint8x2" | 符号なし整数 | 2 | 2 | vec2<u32> , vec2u |
"uint8x4" | 符号なし整数 | 4 | 4 | vec4<u32> , vec4u |
"sint8x2" | 符号付き整数 | 2 | 2 | vec2<i32> , vec2i |
"sint8x4" | 符号付き整数 | 4 | 4 | vec4<i32> , vec4i |
"unorm8x2" | 符号なし正規化 | 2 | 2 | vec2<f32> , vec2f |
"unorm8x4" | 符号なし正規化 | 4 | 4 | vec4<f32> , vec4f |
"snorm8x2" | 符号付き正規化 | 2 | 2 | vec2<f32> , vec2f |
"snorm8x4" | 符号付き正規化 | 4 | 4 | vec4<f32> , vec4f |
"uint16x2" | 符号なし整数 | 2 | 4 | vec2<u32> , vec2u |
"uint16x4" | 符号なし整数 | 4 | 8 | vec4<u32> , vec4u |
"sint16x2" | 符号付き整数 | 2 | 4 | vec2<i32> , vec2i |
"sint16x4" | 符号付き整数 | 4 | 8 | vec4<i32> , vec4i |
"unorm16x2" | 符号なし正規化 | 2 | 4 | vec2<f32> , vec2f |
"unorm16x4" | 符号なし正規化 | 4 | 8 | vec4<f32> , vec4f |
"snorm16x2" | 符号付き正規化 | 2 | 4 | vec2<f32> , vec2f |
"snorm16x4" | 符号付き正規化 | 4 | 8 | vec4<f32> , vec4f |
"float16x2" | 浮動小数点数 | 2 | 4 | vec2<f16> , vec2h |
"float16x4" | 浮動小数点数 | 4 | 8 | vec4<f16> , vec4h |
"float32" | 浮動小数点数 | 1 | 4 | f32 |
"float32x2" | 浮動小数点数 | 2 | 8 | vec2<f32> , vec2f |
"float32x3" | 浮動小数点数 | 3 | 12 | vec3<f32> , vec3f |
"float32x4" | 浮動小数点数 | 4 | 16 | vec4<f32> , vec4f |
"uint32" | 符号なし整数 | 1 | 4 | u32 |
"uint32x2" | 符号なし整数 | 2 | 8 | vec2<u32> , vec2u |
"uint32x3" | 符号なし整数 | 3 | 12 | vec3<u32> , vec3u |
"uint32x4" | 符号なし整数 | 4 | 16 | vec4<u32> , vec4u |
"sint32" | 符号付き整数 | 1 | 4 | i32 |
"sint32x2" | 符号付き整数 | 2 | 8 | vec2<i32> , vec2i |
"sint32x3" | 符号付き整数 | 3 | 12 | vec3<i32> , vec3i |
"sint32x4" | 符号付き整数 | 4 | 16 | vec4<i32> , vec4i |
属性は、頂点ごとまたはインスタンスごとに進めることができます。インスタンスごとに進めることは、instanceIndex
が@builtin(instance_index)
から値を取得するotherStructs[instanceIndex]
とourStructs[instanceIndex]
をインデックス付けするときに行っていることと事実上同じです。
ストレージバッファを削除し、頂点バッファを使用して同じことを実現しましょう。まず、シェーダーを変更して、ストレージバッファの代わりに頂点属性を使用するようにします。
-struct OurStruct { - color: vec4f, - offset: vec2f, -}; - -struct OtherStruct { - scale: vec2f, -}; struct Vertex { @location(0) position: vec2f, + @location(1) color: vec4f, + @location(2) offset: vec2f, + @location(3) scale: vec2f, }; struct VSOutput { @builtin(position) position: vec4f, @location(0) color: vec4f, }; -@group(0) @binding(0) var<storage, read> ourStructs: array<OurStruct>; -@group(0) @binding(1) var<storage, read> otherStructs: array<OtherStruct>; @vertex fn vs( vert: Vertex, - @builtin(instance_index) instanceIndex: u32 ) -> VSOutput { - let otherStruct = otherStructs[instanceIndex]; - let ourStruct = ourStructs[instanceIndex]; var vsOut: VSOutput; - vsOut.position = vec4f( - vert.position * otherStruct.scale + ourStruct.offset, 0.0, 1.0); - vsOut.color = ourStruct.color; + vsOut.position = vec4f( + vert.position * vert.scale + vert.offset, 0.0, 1.0); + vsOut.color = vert.color; return vsOut; } @fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { return vsOut.color; }
次に、レンダーパイプラインを更新して、それらの属性にデータを供給する方法を伝える必要があります。変更を最小限に抑えるために、ストレージバッファ用に作成したデータをほぼそのまま使用します。2つのバッファを使用します。1つのバッファはインスタンスごとのcolor
とoffset
を保持し、もう1つはscale
を保持します。
const pipeline = device.createRenderPipeline({ label: 'flat colors', layout: 'auto', vertex: { module, buffers: [ { arrayStride: 2 * 4, // 2 floats, 4 bytes each attributes: [ {shaderLocation: 0, offset: 0, format: 'float32x2'}, // position ], }, + { + arrayStride: 6 * 4, // 6 floats, 4 bytes each + stepMode: 'instance', + attributes: [ + {shaderLocation: 1, offset: 0, format: 'float32x4'}, // color + {shaderLocation: 2, offset: 16, format: 'float32x2'}, // offset + ], + }, + { + arrayStride: 2 * 4, // 2 floats, 4 bytes each + stepMode: 'instance', + attributes: [ + {shaderLocation: 3, offset: 0, format: 'float32x2'}, // scale + ], + }, ], }, fragment: { module, targets: [{ format: presentationFormat }], }, });
上記では、パイプライン記述のbuffers
配列に2つのエントリを追加したので、これで3つのバッファエントリになり、3つのバッファでデータを供給することをWebGPUに伝えています。
2つの新しいエントリについて、stepMode
をinstance
に設定しました。これは、この属性がインスタンスごとに1回だけ次の値に進むことを意味します。デフォルトはstepMode: 'vertex'
で、頂点ごとに1回進みます(そして、各インスタンスで最初からやり直します)。
2つのバッファがあります。scale
のみを保持するものは単純です。position
を保持する最初のバッファと同様に、頂点ごとに2つの32ビット浮動小数点数です。
もう1つのバッファはcolor
とoffset
を保持し、次のようにデータにインターリーブされます。
したがって、上記では、あるデータセットから次のデータセットに進むためのarrayStride
を6 * 4
、つまり6つの32ビット浮動小数点数(それぞれ4バイト、合計24バイト)に設定しました。color
はオフセット0から始まりますが、offset
は16バイトから始まります。
次に、バッファを設定するコードを変更できます。
// 2つの頂点バッファを作成します const staticUnitSize = - 4 * 4 + // colorは4つの32ビット浮動小数点数(各4バイト)です - 2 * 4 + // offsetは2つの32ビット浮動小数点数(各4バイト)です - 2 * 4; // パディング + 4; // colorは4バイトです const changingUnitSize = - 2 * 4; // scaleは2つの32ビット浮動小数点数(各4バイト)です + 2 * 4 + // offsetは2つの32ビット浮動小数点数(各4バイト)です + 2 * 4; // scaleは2つの32ビット浮動小数点数(各4バイト)です * const staticVertexBufferSize = staticUnitSize * kNumObjects; * const changingVertexBufferSize = changingUnitSize * kNumObjects; * const staticVertexBuffer = device.createBuffer({ * label: 'static vertex for objects', * size: staticVertexBufferSize, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, }); * const changingVertexBuffer = device.createBuffer({ * label: 'changing vertex for objects', * size: changingVertexBufferSize, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, });
頂点属性には、ストレージバッファの構造体と同じパディング制限がないため、パディングはもう必要ありません。それ以外は、使用法をSTORAGE
からVERTEX
に変更しただけです(そして、すべての変数を「ストレージ」から「頂点」に名前変更しました)。
ストレージバッファを使用しなくなったため、バインドグループはもう必要ありません。
- const bindGroup = device.createBindGroup({ - label: 'bind group for objects', - layout: pipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: staticStorageBuffer }}, - { binding: 1, resource: { buffer: changingStorageBuffer }}, - ], - });
最後に、バインドグループを設定する必要はありませんが、頂点バッファを設定する必要があります。
const encoder = device.createCommandEncoder(); const pass = encoder.beginRenderPass(renderPassDescriptor); pass.setPipeline(pipeline); pass.setVertexBuffer(0, vertexBuffer); + pass.setVertexBuffer(1, staticVertexBuffer); + pass.setVertexBuffer(2, changingVertexBuffer); ... - pass.setBindGroup(0, bindGroup); pass.draw(numVertices, kNumObjects); pass.end();
ここでのsetVertexBuffer
の最初のパラメータは、上記で作成したパイプラインのbuffers
配列の要素に対応します。
これで、以前と同じものができましたが、すべての頂点バッファを使用し、ストレージバッファは使用していません。
楽しみのために、頂点ごとに色を付ける属性を追加しましょう。まず、シェーダーを変更しましょう。
struct Vertex { @location(0) position: vec2f, @location(1) color: vec4f, @location(2) offset: vec2f, @location(3) scale: vec2f, + @location(4) perVertexColor: vec3f, }; struct VSOutput { @builtin(position) position: vec4f, @location(0) color: vec4f, }; @vertex fn vs( vert: Vertex, ) -> VSOutput { var vsOut: VSOutput; vsOut.position = vec4f( vert.position * vert.scale + vert.offset, 0.0, 1.0); - vsOut.color = vert.color; + vsOut.color = vert.color * vec4f(vert.perVertexColor, 1); return vsOut; } @fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { return vsOut.color; }
次に、データを供給する方法を記述するためにパイプラインを更新する必要があります。perVertexColor
データをposition
と次のようにインターリーブします。
したがって、arrayStride
を新しいデータをカバーするように変更し、新しい属性を追加する必要があります。2つの32ビット浮動小数点数の後に始まるため、バッファへのoffset
は8バイトです。
const pipeline = device.createRenderPipeline({ label: 'per vertex color', layout: 'auto', vertex: { module, buffers: [ { - arrayStride: 2 * 4, // 2 floats, 4 bytes each + arrayStride: 5 * 4, // 5 floats, 4 bytes each attributes: [ {shaderLocation: 0, offset: 0, format: 'float32x2'}, // position + {shaderLocation: 4, offset: 8, format: 'float32x3'}, // perVertexColor ], }, { arrayStride: 6 * 4, // 6 floats, 4 bytes each stepMode: 'instance', attributes: [ {shaderLocation: 1, offset: 0, format: 'float32x4'}, // color {shaderLocation: 2, offset: 16, format: 'float32x2'}, // offset ], }, { arrayStride: 2 * 4, // 2 floats, 4 bytes each stepMode: 'instance', attributes: [ {shaderLocation: 3, offset: 0, format: 'float32x2'}, // scale ], }, ], }, fragment: { module, targets: [{ format: presentationFormat }], }, });
円の頂点生成コードを更新して、円の外側の端にある頂点に暗い色を、内側の頂点に明るい色を提供します。
function createCircleVertices({ radius = 1, numSubdivisions = 24, innerRadius = 0, startAngle = 0, endAngle = Math.PI * 2, } = {}) { - // 1つのサブディビジョンあたり2つの三角形、1つの三角形あたり3つの頂点、それぞれ5つの値(xyrgb)。 + // 1つのサブディビジョンあたり2つの三角形、1つの三角形あたり3つの頂点 const numVertices = numSubdivisions * 3 * 2; - const vertexData = new Float32Array(numVertices * (2 + 3)); + // 位置(xy)に2つの32ビット値、色(rgb)に1つの32ビット値 + // 32ビットの色値は、4つの8ビット値として書き込み/読み取りされます + const vertexData = new Float32Array(numVertices * (2 + 1)); + const colorData = new Uint8Array(vertexData.buffer); let offset = 0; + let colorOffset = 8; const addVertex = (x, y, r, g, b) => { vertexData[offset++] = x; vertexData[offset++] = y; - vertexData[offset++] = r; - vertexData[offset++] = g; - vertexData[offset++] = b; + offset += 1; // 色をスキップします + colorData[colorOffset++] = r * 255; + colorData[colorOffset++] = g * 255; + colorData[colorOffset++] = b * 255; + colorOffset += 9; // 余分なバイトと位置をスキップします }; + const innerColor = [1, 1, 1]; + const outerColor = [0.1, 0.1, 0.1]; // 1つのサブディビジョンあたり2つの三角形 // // 0--1 4 // | / /| // |/ / | // 2 3--5 for (let i = 0; i < numSubdivisions; ++i) { const angle1 = startAngle + (i + 0) * (endAngle - startAngle) / numSubdivisions; const angle2 = startAngle + (i + 1) * (endAngle - startAngle) / numSubdivisions; const c1 = Math.cos(angle1); const s1 = Math.sin(angle1); const c2 = Math.cos(angle2); const s2 = Math.sin(angle2); // 最初の三角形 - addVertex(c1 * radius, s1 * radius); - addVertex(c2 * radius, s2 * radius); - addVertex(c1 * innerRadius, s1 * innerRadius); + addVertex(c1 * radius, s1 * radius, ...outerColor); + addVertex(c2 * radius, s2 * radius, ...outerColor); + addVertex(c1 * innerRadius, s1 * innerRadius, ...innerColor); // 2番目の三角形 - addVertex(c1 * innerRadius, s1 * innerRadius); - addVertex(c2 * radius, s2 * radius); - addVertex(c2 * innerRadius, s2 * innerRadius); + addVertex(c1 * innerRadius, s1 * innerRadius, ...innerColor); + addVertex(c2 * radius, s2 * radius, ...outerColor); + addVertex(c2 * innerRadius, s2 * innerRadius, ...innerColor); } return { vertexData, numVertices, }; }
そして、シェーディングされた円が得られます。
上記では、WGSLでperVertexColor
属性を次のようにvec3f
として宣言しました。
struct Vertex { @location(0) position: vec2f, @location(1) color: vec4f, @location(2) offset: vec2f, @location(3) scale: vec2f, * @location(4) perVertexColor: vec3f, };
そして、次のように使用しました。
@vertex fn vs( vert: Vertex, ) -> VSOutput { var vsOut: VSOutput; vsOut.position = vec4f( vert.position * vert.scale + vert.offset, 0.0, 1.0); * vsOut.color = vert.color * vec4f(vert.perVertexColor, 1); return vsOut; }
vec4f
として宣言し、次のように使用することもできます。
struct Vertex { @location(0) position: vec2f, @location(1) color: vec4f, @location(2) offset: vec2f, @location(3) scale: vec2f, - @location(4) perVertexColor: vec3f, + @location(4) perVertexColor: vec4f, }; ... @vertex fn vs( vert: Vertex, ) -> VSOutput { var vsOut: VSOutput; vsOut.position = vec4f( vert.position * vert.scale + vert.offset, 0.0, 1.0); - vsOut.color = vert.color * vec4f(vert.perVertexColor, 1); + vsOut.color = vert.color * vert.perVertexColor; return vsOut; }
そして、他に何も変更しません。JavaScriptでは、まだ頂点ごとに3つの浮動小数点数としてデータを提供しています。
{ arrayStride: 5 * 4, // 5 floats, 4 bytes each attributes: [ {shaderLocation: 0, offset: 0, format: 'float32x2'}, // position * {shaderLocation: 4, offset: 8, format: 'float32x3'}, // perVertexColor ], },
これは、属性がシェーダーで常に4つの値を利用できるため機能します。デフォルトは0, 0, 0, 1
なので、提供しない値はこれらのデフォルトになります。
色のために32ビット浮動小数点値を使用しています。各perVertexColor
には3つの値があり、頂点ごとの色ごとに合計12バイトになります。各color
には4つの値があり、インスタンスごとの色ごとに合計16バイトになります。
8ビット値を使用し、WebGPUに0↔255から0.0↔1.0に正規化するように指示することで、これを最適化できます。
有効な属性形式のリストを見ると、3値の8ビット形式はありませんが、'unorm8x4'
があるので、それを使用しましょう。
まず、頂点を生成するコードを変更して、正規化される8ビット値として色を格納するようにします。
function createCircleVertices({ radius = 1, numSubdivisions = 24, innerRadius = 0, startAngle = 0, endAngle = Math.PI * 2, } = {}) { - // 1つのサブディビジョンあたり2つの三角形、1つの三角形あたり3つの頂点、それぞれ5つの値(xyrgb)。 + // 1つのサブディビジョンあたり2つの三角形、1つの三角形あたり3つの頂点 const numVertices = numSubdivisions * 3 * 2; - const vertexData = new Float32Array(numVertices * (2 + 3)); + // 位置(xy)に2つの32ビット値、色(rgb_)に1つの32ビット値 + // 32ビットの色値は、4つの8ビット値として書き込み/読み取りされます + const vertexData = new Float32Array(numVertices * (2 + 1)); + const colorData = new Uint8Array(vertexData.buffer); let offset = 0; + let colorOffset = 8; const addVertex = (x, y, r, g, b) => { vertexData[offset++] = x; vertexData[offset++] = y; - vertexData[offset++] = r; - vertexData[offset++] = g; - vertexData[offset++] = b; + offset += 1; // 色をスキップします + colorData[colorOffset++] = r * 255; + colorData[colorOffset++] = g * 255; + colorData[colorOffset++] = b * 255; + colorOffset += 9; // 余分なバイトと位置をスキップします };
上記では、vertexData
と同じデータのUint8Array
ビューであるcolorData
を作成します。これが不明な場合は、データメモリレイアウトに関する記事を確認してください。
次に、colorData
を使用して色を挿入し、0↔1から0↔255に展開します。
この(頂点ごとの)データのメモリレイアウトは次のようになります。
また、インスタンスごとのデータを更新する必要があります。
const kNumObjects = 100; const objectInfos = []; // 2つの頂点バッファを作成します const staticUnitSize = - 4 * 4 + // colorは4つの32ビット浮動小数点数(各4バイト)です + 4 + // colorは4バイトです 2 * 4; // offsetは2つの32ビット浮動小数点数(各4バイト)です const changingUnitSize = 2 * 4; // scaleは2つの32ビット浮動小数点数(各4バイト)です * const staticVertexBufferSize = staticUnitSize * kNumObjects; * const changingVertexBufferSize = changingUnitSize * kNumObjects; * const staticVertexBuffer = device.createBuffer({ * label: 'static vertex for objects', * size: staticVertexBufferSize, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, }); * const changingVertexBuffer = device.createBuffer({ * label: 'changing vertex for objects', * size: changingVertexBufferSize, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, }); // float32インデックスでのさまざまなユニフォーム値へのオフセット const kColorOffset = 0; - const kOffsetOffset = 4; + const kOffsetOffset = 1; const kScaleOffset = 0; { - const staticVertexValues = new Float32Array(staticVertexBufferSize / 4); + const staticVertexValuesU8 = new Uint8Array(staticVertexBufferSize); + const staticVertexValuesF32 = new Float32Array(staticVertexValuesU8.buffer); for (let i = 0; i < kNumObjects; ++i) { - const staticOffset = i * (staticUnitSize / 4); + const staticOffsetU8 = i * staticUnitSize; + const staticOffsetF32 = staticOffsetU8 / 4; // これらは一度だけ設定されるので、今すぐ設定します - staticVertexValues.set([rand(), rand(), rand(), 1], staticOffset + kColorOffset); // 色を設定します - staticVertexValues.set([rand(-0.9, 0.9), rand(-0.9, 0.9)], staticOffset + kOffsetOffset); // オフセットを設定します + staticVertexValuesU8.set( // 色を設定します + [rand() * 255, rand() * 255, rand() * 255, 255], + staticOffsetU8 + kColorOffset); + + staticVertexValuesF32.set( // オフセットを設定します + [rand(-0.9, 0.9), rand(-0.9, 0.9)], + staticOffsetF32 + kOffsetOffset); objectInfos.push({ scale: rand(0.2, 0.5), }); } - device.queue.writeBuffer(staticVertexBuffer, 0, staticVertexValues); + device.queue.writeBuffer(staticVertexBuffer, 0, staticVertexValuesF32); }
インスタンスごとのデータのレイアウトは次のようになります。
次に、パイプラインを変更して、データを8ビット符号なし値として取得し、それらを0↔1に正規化し、オフセットを更新し、ストライドを新しいサイズに更新する必要があります。
const pipeline = device.createRenderPipeline({ label: 'per vertex color', layout: 'auto', vertex: { module, buffers: [ { - arrayStride: 5 * 4, // 5 floats, 4 bytes each + arrayStride: 2 * 4 + 4, // 2 floats, 4 bytes each + 4 bytes attributes: [ {shaderLocation: 0, offset: 0, format: 'float32x2'}, // position - {shaderLocation: 4, offset: 8, format: 'float32x3'}, // perVertexColor + {shaderLocation: 4, offset: 8, format: 'unorm8x4'}, // perVertexColor ], }, { - arrayStride: 6 * 4, // 6 floats, 4 bytes each + arrayStride: 4 + 2 * 4, // 4 bytes + 2 floats, 4 bytes each stepMode: 'instance', attributes: [ - {shaderLocation: 1, offset: 0, format: 'float32x4'}, // color - {shaderLocation: 2, offset: 16, format: 'float32x2'}, // offset + {shaderLocation: 1, offset: 0, format: 'unorm8x4'}, // color + {shaderLocation: 2, offset: 4, format: 'float32x2'}, // offset ], }, { arrayStride: 2 * 4, // 2 floats, 4 bytes each stepMode: 'instance', attributes: [ {shaderLocation: 3, offset: 0, format: 'float32x2'}, // scale ], }, ], }, fragment: { module, targets: [{ format: presentationFormat }], }, });
そして、それで少しスペースを節約しました。頂点ごとに20バイトを使用していましたが、現在は頂点ごとに12バイトを使用しており、40%の節約になります。そして、インスタンスごとに24バイトを使用していましたが、現在は12バイトを使用しており、50%の節約になります。
構造体を使用する必要はないことに注意してください。これも同様に機能します。
@vertex fn vs( - vert: Vertex, + @location(0) position: vec2f, + @location(1) color: vec4f, + @location(2) offset: vec2f, + @location(3) scale: vec2f, + @location(4) perVertexColor: vec3f, ) -> VSOutput { var vsOut: VSOutput; - vsOut.position = vec4f( - vert.position * vert.scale + vert.offset, 0.0, 1.0); - vsOut.color = vert.color * vec4f(vert.perVertexColor, 1); + vsOut.position = vec4f( + position * scale + offset, 0.0, 1.0); + vsOut.color = color * vec4f(perVertexColor, 1); return vsOut; }
繰り返しになりますが、WebGPUが気にするのは、シェーダーでlocations
を定義し、APIを介してそれらの場所にデータを提供することだけです。
ここで説明する最後のことは、インデックスバッファです。インデックスバッファは、頂点を処理して使用する順序を記述します。
draw
は、頂点を順番に処理すると考えることができます。
0, 1, 2, 3, 4, 5, .....
インデックスバッファを使用すると、その順序を変更できます。
円のサブディビジョンごとに6つの頂点を作成していましたが、そのうち2つは同一でした。
代わりに、4つだけ作成し、インデックスを使用して、WebGPUに次の順序でインデックスを描画するように指示することで、それらの4つの頂点を6回使用します。
0, 1, 2, 2, 1, 3, ...
function createCircleVertices({ radius = 1, numSubdivisions = 24, innerRadius = 0, startAngle = 0, endAngle = Math.PI * 2, } = {}) { - // 1つのサブディビジョンあたり2つの三角形、1つの三角形あたり3つの頂点 - const numVertices = numSubdivisions * 3 * 2; + // 各サブディビジョンに2つの頂点、+円を一周するための1つ。 + const numVertices = (numSubdivisions + 1) * 2; // 位置(xy)に2つの32ビット値、色(rgb)に1つの32ビット値 // 32ビットの色値は、4つの8ビット値として書き込み/読み取りされます const vertexData = new Float32Array(numVertices * (2 + 1)); const colorData = new Uint8Array(vertexData.buffer); let offset = 0; + let colorOffset = 8; const addVertex = (x, y, r, g, b) => { vertexData[offset++] = x; vertexData[offset++] = y; + offset += 1; // 色をスキップします + colorData[colorOffset++] = r * 255; + colorData[colorOffset++] = g * 255; + colorData[colorOffset++] = b * 255; + colorOffset += 9; // 余分なバイトと位置をスキップします }; + const innerColor = [1, 1, 1]; + const outerColor = [0.1, 0.1, 0.1]; - // 1つのサブディビジョンあたり2つの三角形 - // - // 0--1 4 - // | / /| - // |/ / | - // 2 3--5 - for (let i = 0; i < numSubdivisions; ++i) { - const angle1 = startAngle + (i + 0) * (endAngle - startAngle) / numSubdivisions; - const angle2 = startAngle + (i + 1) * (endAngle - startAngle) / numSubdivisions; - - const c1 = Math.cos(angle1); - const s1 = Math.sin(angle1); - const c2 = Math.cos(angle2); - const s2 = Math.sin(angle2); - - // 最初の三角形 - addVertex(c1 * radius, s1 * radius); - addVertex(c2 * radius, s2 * radius); - addVertex(c1 * innerRadius, s1 * innerRadius); - - // 2番目の三角形 - addVertex(c1 * innerRadius, s1 * innerRadius); - addVertex(c2 * radius, s2 * radius); - addVertex(c2 * innerRadius, s2 * innerRadius); - } + // 1つのサブディビジョンあたり2つの三角形 + // + // 0 2 4 6 8 ... + // + // 1 3 5 7 9 ... + for (let i = 0; i <= numSubdivisions; ++i) { + const angle = startAngle + (i + 0) * (endAngle - startAngle) / numSubdivisions; + + const c1 = Math.cos(angle); + const s1 = Math.sin(angle); + + addVertex(c1 * radius, s1 * radius, ...outerColor); + addVertex(c1 * innerRadius, s1 * innerRadius, ...innerColor); + } + const indexData = new Uint32Array(numSubdivisions * 6); + let ndx = 0; + + // 1番目の三角形 2番目の三角形 3番目の三角形 4番目の三角形 + // 0 1 2 2 1 3 2 3 4 4 3 5 + // + // 0--2 2 2--4 4 ..... + // | / /| | / /| + // |/ / | |/ / | + // 1 1--3 3 3--5 ..... + for (let i = 0; i < numSubdivisions; ++i) { + const ndxOffset = i * 2; + + // 最初の三角形 + indexData[ndx++] = ndxOffset; + indexData[ndx++] = ndxOffset + 1; + indexData[ndx++] = ndxOffset + 2; + + // 2番目の三角形 + indexData[ndx++] = ndxOffset + 2; + indexData[ndx++] = ndxOffset + 1; + indexData[ndx++] = ndxOffset + 3; + } return { vertexData, + indexData, - numVertices, + numVertices: indexData.length, }; }
次に、インデックスバッファを作成する必要があります。
- const { vertexData, numVertices } = createCircleVertices({ + const { vertexData, indexData, numVertices } = createCircleVertices({ radius: 0.5, innerRadius: 0.25, }); const vertexBuffer = device.createBuffer({ label: 'vertex buffer', size: vertexData.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, }); device.queue.writeBuffer(vertexBuffer, 0, vertexData); + const indexBuffer = device.createBuffer({ + label: 'index buffer', + size: indexData.byteLength, + usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST, + }); + device.queue.writeBuffer(indexBuffer, 0, indexData);
使用法をINDEX
に設定したことに注意してください。
最後に、描画時にインデックスバッファを指定する必要があります。
pass.setPipeline(pipeline); pass.setVertexBuffer(0, vertexBuffer); pass.setVertexBuffer(1, staticVertexBuffer); pass.setVertexBuffer(2, changingVertexBuffer); + pass.setIndexBuffer(indexBuffer, 'uint32');
バッファには32ビット符号なし整数インデックスが含まれているため、ここで'uint32'
を渡す必要があります。16ビット符号なしインデックスを使用することもでき、その場合は'uint16'
を渡します。
そして、draw
の代わりにdrawIndexed
を呼び出す必要があります。
- pass.draw(numVertices, kNumObjects); + pass.drawIndexed(numVertices, kNumObjects);
これで、スペースを少し節約し(33%)、頂点シェーダーで頂点を計算するときの処理も同様に節約できる可能性があります。GPUがすでに計算した頂点を再利用できる可能性があるためです。
前の記事のストレージバッファの例で、インデックスバッファを使用することもできたことに注意してください。その場合、渡される@builtin(vertex_index)
の値は、インデックスバッファのインデックスと一致します。
次はテクスチャについて説明します。