概要
前回の記事はWebGLとWebGPUでcanvasの背景を塗りつぶす処理を比較しました。
今回は、以前書いたこの記事の内容をWebGPUで書き直してみて、両者の違いを理解しようと思います。
完成物
見た目はこのようになります。
黒い背景の中心に赤い矩形を配置しているだけです。
WebGPU版の実装
まずは、WebGPUでの実装を見ていきます。
完全なコード
async function initWebGPU() {
// Canvas要素の取得
const canvas = document.getElementById('webgl-canvas') as HTMLCanvasElement;
// GPUアダプターとデバイスの取得
const adapter = await navigator.gpu.requestAdapter() as GPUAdapter;
const device = await adapter.requestDevice();
// Canvasコンテキストの取得と設定
const context = canvas.getContext('webgpu') as GPUCanvasContext;
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: canvasFormat,
});
// 頂点データの定義(四角形の4つの頂点)
const vertices = new Float32Array([
-0.5, -0.5, // 左下
0.5, -0.5, // 右下
-0.5, 0.5, // 左上
0.5, 0.5, // 右上
]);
// 頂点バッファの作成
const vertexBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertices);
// WGSLシェーダーの定義
const shaderModule = device.createShaderModule({
code: `
@vertex
fn vertexMain(@location(0) position: vec2f) -> @builtin(position) vec4f {
return vec4f(position, 0.0, 1.0);
}
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1.0, 0.0, 0.0, 1.0); // 赤色
}
`,
});
// レンダーパイプラインの構築
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shaderModule,
entryPoint: 'vertexMain',
buffers: [
{
arrayStride: 8, // 2 floats * 4 bytes
attributes: [
{
shaderLocation: 0,
offset: 0,
format: 'float32x2',
},
],
},
],
},
fragment: {
module: shaderModule,
entryPoint: 'fragmentMain',
targets: [
{
format: canvasFormat,
},
],
},
primitive: {
topology: 'triangle-strip',
},
});
// 描画コマンドの記録と実行
const encoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const renderPass = encoder.beginRenderPass({
colorAttachments: [
{
view: textureView,
loadOp: 'clear',
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, // 黒色
storeOp: 'store',
},
],
});
renderPass.setPipeline(pipeline);
renderPass.setVertexBuffer(0, vertexBuffer);
renderPass.draw(4); // 4つの頂点を描画
renderPass.end();
device.queue.submit([encoder.finish()]);
}
initWebGPU();
実装の流れ
WebGPUでの図形描画は、以下のステップで進みます。
- GPUデバイスの取得
- Canvasコンテキストの設定
- 頂点データの準備とバッファ作成
- シェーダーモジュールの作成
- レンダーパイプラインの構築
- コマンドエンコーダーでの描画命令の記録
- コマンドバッファの送信
重要なポイント
前回の記事で説明したコマンドバッファの仕組みに加え、今回は明示的なパイプライン設定が必要になります。
createRenderPipeline()では、頂点バッファのレイアウト、シェーダーのエントリーポイント、プリミティブのトポロジーなど、描画に必要な全ての情報を事前に設定します。
WebGL版の実装
次に、同じ処理をWebGLで実装した場合を見ていきます。
完全なコード
function initWebGL() {
const canvas = document.getElementById('webgl-canvas') as HTMLCanvasElement;
const gl = canvas.getContext('webgl2') as WebGL2RenderingContext;
// 頂点シェーダーのソースコード
const vertexShaderSource = `#version 300 es
in vec2 aPosition;
void main() {
gl_Position = vec4(aPosition, 0.0, 1.0);
}
`;
// フラグメントシェーダーのソースコード
const fragmentShaderSource = `#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
// シェーダーのコンパイル
const vertexShader = gl.createShader(gl.VERTEX_SHADER)!;
gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);
// プログラムの作成とシェーダーのリンク
const program = gl.createProgram()!;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// 頂点データ(四角形の4つの頂点)
const vertices = new Float32Array([
-0.5, -0.5, // 左下
0.5, -0.5, // 右下
-0.5, 0.5, // 左上
0.5, 0.5, // 右上
]);
// バッファの作成とデータの転送
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 属性の設定
const aPosition = gl.getAttribLocation(program, 'aPosition');
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
// 背景色の設定とクリア
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 描画
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
initWebGL();
実装の流れ
WebGLでの図形描画は、以下のステップで進みます。
- シェーダーの作成とコンパイル
- プログラムの作成とリンク
- 頂点データの準備
- バッファへのデータ転送
- 属性の設定
- 描画コマンドの実行
重要なポイント
前回の記事で説明した状態マシンモデルに従い、gl.bindBuffer()でバッファをバインドすると、その後の操作は全てそのバッファに対して実行されます。
シェーダーは個別にコンパイルし、プログラムとしてリンクする必要があります。
シェーダー言語の詳細な違い
今回の実装ではシェーダーを書きました。
WebGLとWebGPUでは異なるシェーダー言語を使用します。
WebGLはGLSL(OpenGL Shading Language)を使用します。
GLSLはOpenGLから派生したシェーダー言語で、C言語に似た構文を持ちます。
WebGPUはWGSL(WebGPU Shading Language)を使用します。
WGSLはWebGPU専用に設計された新しいシェーダー言語で、Rustに似た構文を持ちます。
バージョン宣言とエントリーポイント
GLSL(WebGL)では、ファイルの先頭でバージョンを宣言します。
#version 300 es
WGSL(WebGPU)では、バージョン宣言はなく、代わりに関数にアトリビュートを付けてエントリーポイントを指定します。
@vertex
fn vertexMain() { ... }
型の表記
GLSLでは型名と変数名の間にスペースを入れます。
vec2 aPosition
vec4 fragColor
WGSLでは変数名の後にコロンと型名を記述します。
position: vec2f
また、WGSLの型名には精度が含まれます。
vec2fは32ビット浮動小数点のvec2を意味し、正確にはvec2<f32>の簡略記法(エイリアス)です。
同様にvec4fはvec4<f32>のエイリアスになります。
入出力の指定
GLSLではinとoutキーワードで入出力を指定します。
in vec2 aPosition; // 頂点シェーダーの入力
out vec4 fragColor; // フラグメントシェーダーの出力
WGSLでは関数の引数と戻り値で表現し、アトリビュートで詳細を指定します。
fn vertexMain(@location(0) position: vec2f) -> @builtin(position) vec4f
fn fragmentMain() -> @location(0) vec4f
@location(0)は頂点バッファの0番目の属性から値を受け取ることを意味します。
@builtin(position)はGPUの組み込み変数(頂点の位置)に書き込むことを意味します。
ベクトルのコンストラクタ
GLSLでは型名をそのまま使います。
vec4(aPosition, 0.0, 1.0)
vec4(1.0, 0.0, 0.0, 1.0)
WGSLでは型名の後にfを付けます。
vec4f(position, 0.0, 1.0)
vec4f(1.0, 0.0, 0.0, 1.0)
精度修飾子
GLSLでは、フラグメントシェーダーで精度を明示的に指定する必要があります。
precision mediump float;
WGSLでは、型名自体に精度が含まれているため(vec2fのfは32ビット浮動小数点を表す)このような精度宣言は不要です。
main関数
GLSLでは必ずmain()という名前の関数がエントリーポイントになります。
void main() { ... }
WGSLでは任意の関数名を使用でき、パイプライン設定でentryPointとして指定します。
vertex: {
entryPoint: 'vertexMain',
...
}
この柔軟性により、1つのシェーダーモジュールに複数のエントリーポイントを定義できます。
その他の違い
コード量
WebGL版は約60行、WebGPU版は約100行と、WebGPUの方が多くのコードを必要とします。
これは、WebGPUが明示的な設定を重視する設計になっているためです。
バッファ管理
WebGLではbindBuffer()とbufferData()を使ってバッファを作成・転送します。
WebGPUではcreateBuffer()でバッファを作成し、writeBuffer()でデータを転送します。
WebGPUではバッファの用途(GPUBufferUsage.VERTEX、GPUBufferUsage.COPY_DST)を明示的に指定する必要があります。
特にGPUBufferUsage.COPY_DSTフラグは重要です。
writeBuffer()でCPUからデータを書き込む際は、転送先として使えるようにこのフラグを指定する必要があります。
WebGPUはこのような用途の指定が厳密で、フラグが不足しているとエラーになります。
属性の接続
WebGLではgetAttribLocation()で属性の場所を取得し、vertexAttribPointer()で接続します。
const aPosition = gl.getAttribLocation(program, 'aPosition');
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
WebGPUではパイプライン構築時に全て宣言的に記述します。
buffers: [
{
arrayStride: 8,
attributes: [
{
shaderLocation: 0,
offset: 0,
format: 'float32x2',
},
],
},
]
クリップ空間の違い
今回は2D描画(Z=0)なので影響しませんが、WebGLとWebGPUではクリップ空間のZ軸の範囲が異なります。
- WebGL: -1.0 ~ 1.0
- WebGPU: 0.0 ~ 1.0
3D描画を行う際は、この違いに注意が必要です。
特に既存のWebGLコードをWebGPUに移植する場合、深度計算やプロジェクション行列の調整が必要になります。
まとめ
赤い四角形を描画するというシンプルな内容でも、WebGLとWebGPUではコード
が大きく異なりました。
- シェーダー言語(GLSLとWGSL)は構文が異なり、WGSLの方が型安全性が高く現代的な設計
- WebGPUは明示的な設定が必要でコード量は多いが、最適化できる余地が大きい
- WebGLはステートマシン、WebGPUはコマンドバッファと設計思想が異なる
- WebGPU対応ブラウザが使える環境であれば、新規プロジェクトではWebGPUを選ぶメリットが大きい
- ただし、学習コストはWebGPUの方が高いため、シンプルな用途ならWebGLも十分選択肢になる
