概要
前回の記事ではWebGPUで実践的な開発環境を整える方法を紹介しました。
今回は、以前WebGLで実装したグラデーション表現をWebGPUで書き直します。
テクスチャ座標を使った様々な描画パターンを作成し、WebGLとの違いを確認します。
描画範囲を画面全体に拡大
前回までは中央に小さな矩形を描画していましたが、今回は画面全体に描画します。
頂点座標を[-0.5, 0.5]から[-1.0, 1.0]に変更します。
// 頂点データの定義(四角形の4つの頂点)
const vertices = new Float32Array([
- -0.5, -0.5, // 左下
- 0.5, -0.5, // 右下
- -0.5, 0.5, // 左上
- 0.5, 0.5, // 右上
+ -1.0, -1.0, // 左下
+ 1.0, -1.0, // 右下
+ -1.0, 1.0, // 左上
+ 1.0, 1.0, // 右上
]);
テクスチャ座標の準備
グラデーション表現のために、頂点シェーダーでテクスチャ座標を計算します。
座標を[-1, 1]から[0, 1]に正規化し、フラグメントシェーダーに渡します。
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) textureCoord: vec2f,
}
@vertex
fn vertexMain(@location(0) position: vec2f) -> VertexOutput {
var output: VertexOutput;
output.position = vec4f(position, 0.0, 1.0);
// 座標を [-1, 1] から [0, 1] に正規化
output.textureCoord = position * 0.5 + 0.5;
return output;
}
WGSLの主要な概念
ここで、初めて登場するWGSLの概念を説明します。
struct
複数の値をまとめて扱うためのデータ型です。
頂点シェーダーからフラグメントシェーダーに複数の値を渡す際に使用します。
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) textureCoord: vec2f,
}
@builtin
GPUの組み込み変数を指定する属性です。
@builtin(position)は頂点の画面上の位置を表す特別な変数で、必ず出力する必要があります。
@location
任意のデータをシェーダー間で受け渡しするための属性です。
入力側(関数の引数)では、頂点バッファのどの属性を受け取るかを指定します。
@vertex
fn vertexMain(@location(0) position: vec2f) -> VertexOutput {
// @location(0) は頂点バッファの0番目の属性(今回はposition)
}
出力側(構造体のフィールド)では、次のシェーダーステージに渡すデータの場所を指定します。
struct VertexOutput {
@location(0) textureCoord: vec2f, // フラグメントシェーダーに渡される
}
フラグメントシェーダーでは、同じ@location(0)で受け取ります。
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
// 頂点シェーダーから渡された textureCoord を受け取る
}
WebGLとの違い
WebGLでは、頂点シェーダーでoutキーワードを使って変数を宣言し、フラグメントシェーダーで同名のin変数として受け取っていました。
// WebGL版頂点シェーダー
out vec2 vTextureCoord;
void main() {
gl_Position = vec4(aPosition, 0.0, 1.0);
vTextureCoord = aPosition * 0.5 + 0.5;
}
// WebGL版フラグメントシェーダー
in vec2 vTextureCoord;
WebGPUでは、構造体を使って複数の値をまとめて返します。
@builtin(position)は組み込みの出力、@location(0)は任意のデータを渡すための出力です。
// WebGPU版
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) textureCoord: vec2f,
}
パターン1: 水平グラデーション
左から右に向かって黒→赤に変化するグラデーションを作ります。
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
// パターン1: 水平グラデーション
let r = textureCoord.x;
let g = 0.0;
let b = 0.0;
return vec4f(r, g, b, 1.0);
}
textureCoord.xは0から1の範囲で変化するため、赤色成分が左から右に向かって増えていきます。
WebGLとの違い
WebGLでは以下のように書いていました。
// WebGL版
precision mediump float;
in vec2 vTextureCoord;
out vec4 fragColor;
void main() {
float r = vTextureCoord.x;
float g = 0.0;
float b = 0.0;
fragColor = vec4(r, g, b, 1.0);
}
細かな違いはありますが、ここまでと比べればほぼ同じ考え方・書き方ができています。
パターン2: 垂直グラデーション
下から上に向かって黒→赤に変化するグラデーションを作ります。
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
// パターン2: 垂直グラデーション
let r = textureCoord.y;
let g = 0.0;
let b = 0.0;
return vec4f(r, g, b, 1.0);
}
textureCoord.xをtextureCoord.yに変えるだけで、縦方向のグラデーションになります。
パターン3: 斜めグラデーション
左下から右上に向かって黒→赤に変化するグラデーションを作ります。
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
// パターン3: 斜めグラデーション
let r = (textureCoord.x + textureCoord.y) / 2.0;
let g = 0.0;
let b = 0.0;
return vec4f(r, g, b, 1.0);
}
XとYの座標を平均することで、対角線方向のグラデーションが得られます。
パターン4: 波のようなパターン
ここまでの単なるグラデーションならCSSで書いた方が簡単です。
しかし、WebGPUの場合はより複雑な計算もできます。
例えば、斜めグラデーションに三角関数を組み合わせると、以下のようになります。
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
let pi = acos(-1.0);
let r = abs(sin((textureCoord.x + textureCoord.y) * pi * 3.0));
let g = 0.0;
let b = 0.0;
return vec4f(r, g, b, 1.0);
}
斜めのグラデーションにしても、波のような複雑なパターンができました。
さらに式に手を入れてみます。
パターン5: 千鳥格子風
XとYの三角関数を組み合わせて、千鳥格子風の複雑なパターンを作ります。
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
let pi = acos(-1.0);
// パターン5: 千鳥格子風
let r = sin(textureCoord.x * pi * 10.0) * cos(textureCoord.y * pi * 5.0) * 0.5 + 0.5;
let g = 0.0;
let b = 0.0;
return vec4f(r, g, b, 1.0);
}
円周率の扱い
WGSLには円周率の組み込み定数がないため、自分で計算する必要があります。
acos(-1.0)で円周率を求めることができます。
let pi = acos(-1.0);
WebGLとの違い
WebGLでも同様にacos(-1.0)で円周率を計算できました。
// WebGL版
float pi = acos(-1.0);
WGSLでも同じ方法が使えます。
// WebGPU版
let pi = acos(-1.0);
sin()やcos()などの三角関数は、WebGLと同じ名前で使用できます。
完全なコード
最終的なコードは以下のようになります。
import './style.css';
import vertexShaderCode from './shaders/plane.vert.wgsl?raw';
import fragmentShaderCode from './shaders/plane.frag.wgsl?raw';
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();
// 頂点データの定義(四角形の4つの頂点)
const vertices = new Float32Array([
-1.0, -1.0, // 左下
1.0, -1.0, // 右下
-1.0, 1.0, // 左上
1.0, 1.0, // 右上
]);
// 頂点バッファの作成
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: vertexShaderCode + fragmentShaderCode,
});
// レンダーパイプラインの構築
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',
},
});
// 描画処理
function render() {
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()]);
}
// キャンバスサイズの調整とコンテキスト設定
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
context.configure({
device: device,
format: canvasFormat,
});
// リサイズ後に再描画
render();
}
// 初期化時とリサイズ時にキャンバスサイズを調整
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
}
initWebGPU();
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) textureCoord: vec2f,
}
@vertex
fn vertexMain(@location(0) position: vec2f) -> VertexOutput {
var output: VertexOutput;
output.position = vec4f(position, 0.0, 1.0);
// 座標を [-1, 1] から [0, 1] に正規化
output.textureCoord = position * 0.5 + 0.5;
return output;
}
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
let pi = acos(-1.0);
// パターン1: 水平グラデーション
// let r = textureCoord.x;
// let g = 0.0;
// let b = 0.0;
// パターン2: 垂直グラデーション
// let r = textureCoord.y;
// let g = 0.0;
// let b = 0.0;
// パターン3: 斜めグラデーション
// let r = (textureCoord.x + textureCoord.y) / 2.0;
// let g = 0.0;
// let b = 0.0;
// パターン4: 波のようなパターン
// let r = abs(sin((textureCoord.x + textureCoord.y) * pi * 3.0));
// let g = 0.0;
// let b = 0.0;
// パターン5: 千鳥格子風
let r = sin(textureCoord.x * pi * 10.0) * cos(textureCoord.y * pi * 5.0) * 0.5 + 0.5;
let g = 0.0;
let b = 0.0;
return vec4f(r, g, b, 1.0);
}
まとめ
WebGPUでテクスチャ座標を使ったグラデーション表現を実装しました。
1つ目の記事では、WebGLとWebGPUの設計思想の違いが大きく、コード量もかなり違いました。
しかし今回は、シェーダーの記述方法に若干の違いはあるものの、基本的な考え方や処理の流れはほぼ同じでした。
構造体を使ったデータの受け渡しや、in/outの代わりに関数の引数と戻り値を使う点など、WGSL特有の書き方はありますが、グラデーションを作成するロジック自体(座標を使った計算や三角関数の使用)は似ていて、新しく学ぶにしても助かりました。



