WebGPU と TypeScript で始める、次世代のグラフィックス開発入門
はじめに
WebGPU を触ってみたいので開発環境の作り方を調べました。
そこで WebGPU Samples の Hello Triangle を vite で立てたローカルサーバ上で動かしてみました。
また、 TypeScript で開発できるようにしました。
開発環境構築
作業フォルダ作成
作業フォルダを作成しそのフォルダに移動します。
mkdir /path/to/work-dir
cd /path/to/work-dir
開発環境
開発環境は vite にします。
次の package.json をコピーしてください。
{
"type": "module"
}
その後 package.json を作成します。
pbpaste > package.json
必要な module を install します。
npm i -D typescript @webgpu/types vite
TypeScript
次の tsconfig.json をコピーしてください。
@webgpu/types
を指定することで TypeScript で WebGPU を認識できるようにしています。
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"types": ["@webgpu/types"]
},
"include": ["src"]
}
その後 tsconfig.json を作成します。
pbpaste > tsconfig.json
index.html
次の index.html をコピーしてください。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebGPU</title>
</head>
<body>
<canvas></canvas>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
その後 index.html を作成します。
pbpaste > index.html
src フォルダ
コードを配置するフォルダを作成します。
mkdir src
src/main.ts
次のコードをコピーしてください。
メモも残していますのでよろしければご確認ください。
// shader の import には ?raw が必要
import vertexShader from './vertex.wgsl?raw'
import fragmentShader from './fragment.wgsl?raw'
main()
// WebGPU の初期化には、非同期処理が必要
async function main() {
const canvas: HTMLCanvasElement = document.querySelector('canvas')!
// WebGPU が GPU ハードウェアにアクセスするために adapter をリクエストする
const adapter = await navigator.gpu.requestAdapter()
if (!adapter) {
// WebGPU 未対応環境の対応箇所A
throw new Error()
}
// WebGPU が GPUコマンドの実行や、メモリの割り当てを行うためにデバイスをリクエストする
// device が nullish になることはない(型定義を見る限り)
const device = await adapter.requestDevice()
// WebGPU コンテキストを取得
const context: GPUCanvasContext = canvas.getContext('webgpu')
if (!context) {
// WebGPU 未対応環境の対応箇所B
throw new Error()
}
const { devicePixelRatio } = window
canvas.width = devicePixelRatio * canvas.clientWidth
canvas.height = devicePixelRatio * canvas.clientHeight
// WebGPU が推奨する canvas のカラーフォーマットを取得
const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
// GPUデバイスと context を関連付ける
context.configure({
// 使用する GPU デバイスを指定
device,
// カラーフォーマットを指定
format: presentationFormat,
// アルファブレンディングのモードを指定
alphaMode: 'premultiplied',
})
// レンダーパイプラインを作成
// vertex|fragment shader が組み合わされ、描画処理が定義される
const pipeline = device.createRenderPipeline({
// パイプラインレイアウトを自動で設定する
layout: 'auto',
// vertex shader の設定
vertex: {
module: device.createShaderModule({
code: vertexShader,
}),
},
// fragment shader の設定
fragment: {
module: device.createShaderModule({
code: fragmentShader,
}),
// レンダーターゲットの設定
targets: [
{
// レンダーターゲットのカラーフォーマットを指定
format: presentationFormat,
},
],
},
primitive: {
// 描画する primitive のトポロジーを指定
topology: 'triangle-list',
},
})
function frame() {
// 複数のGPUコマンドをバッチ処理するために GPU command を生成
const commandEncoder = device.createCommandEncoder()
// レンダリング結果を表示するためのテクスチャを取得
const textureView = context.getCurrentTexture().createView()
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
{
view: textureView,
// 背景を指定
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
// ロード操作を指定
// 'load': 前のフレームからの描画結果が維持
// 'clear': 新しいフレームを描画する前に
// GPURenderPassDescriptor.colorAttachments.clearValue
// で指定された色で初期化
loadOp: 'clear',
// ストア操作を指定
// この場合レンダリング結果を保存するように設定している
storeOp: 'store',
},
],
}
// GPUコマンドの記録開始
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor)
// レンダーパイプラインを設定
// これにより、使用するシェーダーとレンダリング設定が指定される
passEncoder.setPipeline(pipeline)
// レンダリング開始
passEncoder.draw(3)
// GPUコマンドの記録終了
passEncoder.end()
// コマンドを GPUキューに送信し GPUコマンド を実行
device.queue.submit([commandEncoder.finish()])
requestAnimationFrame(frame)
}
requestAnimationFrame(frame)
}
その後 src/main.ts を作成します。
pbpaste > src/main.ts
src/vertex.wgsl
WebGPU Shading Language(WGSL) は、WebGPU 用のシェーディング言語です。
コードの雰囲気が Rust に似ていますね。
次のコードをコピーしてください。
メモも残していますのでよろしければご確認ください。
// @vertex 属性を付与することで
// この関数が頂点シェーダーであることを示す
@vertex
fn main(
// @builtin(vertex_index) は、頂点インデックスを表す組み込み変数
// これは、頂点バッファから頂点を識別するために使用される
@builtin(vertex_index) VertexIndex : u32
// クリップ空間座標を表す4次元ベクトルを返す
) -> @builtin(position) vec4f {
// pos は、3つの2次元ベクトルを含む配列
// これらのベクトルは、三角形の頂点座標を表す
var pos = array<vec2f, 3>(
vec2(0.0, 0.5),
vec2(-0.5, -0.5),
vec2(0.5, -0.5)
);
// 頂点インデックスを使用して、対応する頂点座標を配列から取得
// 取得した2次元ベクトルを4次元ベクトルに変換して返す
// z座標は0.0、w座標は1.0に設定
// これは、三角形をクリップ空間に配置するために必要
return vec4f(pos[VertexIndex], 0.0, 1.0);
}
その後 src/vertex.wgsl を作成します。
pbpaste > src/vertex.wgsl
src/fragment.wgsl
次のコードをコピーしてください。
メモも残していますのでよろしければご確認ください。
// @fragment 属性を付与することで、この関数がフラグメントシェーダーであることを示す
@fragment
// 出力カラーアタッチメントのインデックス0に書き込まれる4次元ベクトルを返す
fn main() -> @location(0) vec4f {
// 赤色
return vec4(1.0, 0.0, 0.0, 1.0);
}
その後 src/fragment.wgsl を作成します。
pbpaste > src/fragment.wgsl
実行
npx vite
web ブラウザに赤色の三角形が表示されます。
おわりに
簡単ではありますが、WebGPU を開発する環境が整いました。
Shadertoy のように shader で絵作りしながら WebGPU の理解を深めていこうと思います。
PR
株式会社 blue で開発に携わっている VOTE は、二択のお題に投票する Web アプリです。
技術的な話題から日常の選択など、多様なお題でお気軽に遊んでみてください。
株式会社blueの開発ブログです。新しいヒントや可能性を探しつつ検証にも熱く励んでおります。読んでいる方にも、弊社の記事から新しいキッカケや可能性を見つけてもらえたらと思います。開発者ブログ以外のblueの思いもこちらにて配信中→note.com/blueteam/
Discussion