目次

webgpufundamentals.org

Fix, Fork, Contribute

WebGPU 頂点バッファ

この記事はGemini Code Assistによって自動翻訳されました。翻訳に問題がある場合は、お手数ですがこちらからPull Requestを送信してください。

前の記事では、頂点データをストレージバッファに入れ、組み込みの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"符号なし整数22vec2<u32>, vec2u
"uint8x4"符号なし整数44vec4<u32>, vec4u
"sint8x2"符号付き整数22vec2<i32>, vec2i
"sint8x4"符号付き整数44vec4<i32>, vec4i
"unorm8x2"符号なし正規化22vec2<f32>, vec2f
"unorm8x4"符号なし正規化44vec4<f32>, vec4f
"snorm8x2"符号付き正規化22vec2<f32>, vec2f
"snorm8x4"符号付き正規化44vec4<f32>, vec4f
"uint16x2"符号なし整数24vec2<u32>, vec2u
"uint16x4"符号なし整数48vec4<u32>, vec4u
"sint16x2"符号付き整数24vec2<i32>, vec2i
"sint16x4"符号付き整数48vec4<i32>, vec4i
"unorm16x2"符号なし正規化24vec2<f32>, vec2f
"unorm16x4"符号なし正規化48vec4<f32>, vec4f
"snorm16x2"符号付き正規化24vec2<f32>, vec2f
"snorm16x4"符号付き正規化48vec4<f32>, vec4f
"float16x2"浮動小数点数24vec2<f16>, vec2h
"float16x4"浮動小数点数48vec4<f16>, vec4h
"float32"浮動小数点数14f32
"float32x2"浮動小数点数28vec2<f32>, vec2f
"float32x3"浮動小数点数312vec3<f32>, vec3f
"float32x4"浮動小数点数416vec4<f32>, vec4f
"uint32"符号なし整数14u32
"uint32x2"符号なし整数28vec2<u32>, vec2u
"uint32x3"符号なし整数312vec3<u32>, vec3u
"uint32x4"符号なし整数416vec4<u32>, vec4u
"sint32"符号付き整数14i32
"sint32x2"符号付き整数28vec2<i32>, vec2i
"sint32x3"符号付き整数312vec3<i32>, vec3i
"sint32x4"符号付き整数416vec4<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つのバッファはインスタンスごとのcoloroffsetを保持し、もう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つの新しいエントリについて、stepModeinstanceに設定しました。これは、この属性がインスタンスごとに1回だけ次の値に進むことを意味します。デフォルトはstepMode: 'vertex'で、頂点ごとに1回進みます(そして、各インスタンスで最初からやり直します)。

2つのバッファがあります。scaleのみを保持するものは単純です。positionを保持する最初のバッファと同様に、頂点ごとに2つの32ビット浮動小数点数です。

もう1つのバッファはcoloroffsetを保持し、次のようにデータにインターリーブされます。

したがって、上記では、あるデータセットから次のデータセットに進むためのarrayStride6 * 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の属性はJavaScriptの属性と一致する必要はありません

上記では、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)の値は、インデックスバッファのインデックスと一致します。

次はテクスチャについて説明します。

問題点/バグ? githubでissueを作成.
comments powered by Disqus