[忙しい人向け] 100行から始めるWebGPU（WGSL対応版）

この記事は[Web グラフィックス Advent Calendar 2020](https://qiita.com/advent-calendar/2020/webgraphics)の13日目の記事です。
※ 2023年1月7日、内容が古くなっていた為、記事の内容を最新化しました。
　 変更箇所は「[主な変更内容（2020年12月～2022年12月）](#主な変更内容2020年12月2022年12月)」を参照ください。

### はじめに

`WebGPU` は `WebGL` や `WebGL2` の後継とされているグラフィックス API です。
今のところリリース時期としては [2023年5月 Chrome 113 を目標](https://chromestatus.com/feature/6213121689518080) としているようです。
API の仕様は固まりつつありますが、まだ仕様変更等が入る可能性があることにご注意ください。

ここでは、主に、シェーダ機能「WGSL」（WebGPU Shading Language）について試してみようと思います。

[2019年時点](https://qiita.com/cx20/items/67f4e70b36e06ef77900)は WebGPU 用のシェーダの方針が決まっていなかった為、`glslang` と呼ばれるライブラリを用いて `GLSL` → `SPIR-V`（シェーダ中間言語）にコンパイルして使用する形が取られていました。
現在は、`Chrome Canary` で新しいシェーダ `WGSL` が実験的に使えるようになってきた為、それを使ってみようと思います。

### WGSL とは

`WGSL` は `WebGPU Shading Language` の略で、その名の通り `WebGPU` 用のシェーダです。
`GLSL` では C 言語ライクな構文でしたが `WGSL` では Rust に似た構文が採用されています。
2019年から2022年にかけて何回か構文の仕様変更がありました。
以下のサンプルコードは現時点の最新の仕様の内容で更新しています。

### GLSL と WGSL のコードの比較

|箇所|GLSL|WGSL|
|:-----:|:-----:|:-----:|
|VSコード|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26094/738f0801-1688-228b-1f17-9beda9e7a24b.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26094/25bd422d-3467-8cee-6cf7-128f00ff153f.png)|
|FSコード|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26094/03fb0b6f-b2dc-1da2-bc5c-0935520f5f44.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26094/da571c10-0e50-c99e-2c3f-8483efeb9c0b.png)|

### WebGPU の有効化方法

WebGPU は `Chrome Canary` や `Firefox Nightly` でフラグを有効化することで試すことができます。
ただし 2023年1月時点では、`WGSL` は `Chrome Canary` でしか動作しないようです。
なお、これらの機能は、実験的な機能である為、有効化したまま、信頼できない Web へのアクセスは行わないでください。

|ブラウザ                                                                           |対応するバックエンドのライブラリ          | WebGPU 有効化方法                         |
|:----------------------------------------------------------------------------------|:-----------------------------------------|:------------------------------------------|
|[Chrome Canary](https://www.google.com/chrome/canary/)                             |[Dawn](https://dawn.googlesource.com/dawn)|`chrome://flags/#enable-unsafe-web`        |
|[Firefox Nightly](https://www.mozilla.org/firefox/nightly)                         |[gfx-rs](https://github.com/gfx-rs/gfx)   |`dom.webgpu.enable`                        |
|[Safari Technology Preview](https://developer.apple.com/safari/technology-preview/)|[webkit](https://webkit.org/)             |Develop -> Experimental Features -> WebGPU |

### ポリゴンを１枚描画する WebGPU コード

以下、シェーダに `WGSL` を使用した場合のサンプルコードです。動作確認は `Chrome Canary 111` で行いました。他の環境では動作しない可能性がありますのでご注意ください。 

```html:index.html
<canvas id="c" width="512" height="512"></canvas>

<script id="vs" type="x-shader/x-vertex">
struct VertexOutput {
    @builtin(position) Position : vec4<f32>
}

@vertex
fn main(
    @location(0) position : vec3<f32>
) -> VertexOutput {
    var output : VertexOutput;
    output.Position = vec4<f32>(position, 1.0);
    return output;
}
</script>

<script id="fs" type="x-shader/x-fragment">
@fragment
fn main() -> @location(0) vec4<f32> {
    return vec4<f32>(0.0, 0.0, 1.0, 1.0);
}
</script>
```

```javascript:index.js
const vs = document.getElementById("vs").textContent;
const fs = document.getElementById("fs").textContent;
init();

async function init() {
    const gpu = navigator["gpu"];
    const adapter = await gpu.requestAdapter();
    const device = await adapter.requestDevice();

    const c = document.getElementById("c");
    c.width = window.innerWidth;
    c.height = window.innerHeight;
    const ctx = c.getContext("webgpu");
    const format = gpu.getPreferredCanvasFormat();
    ctx.configure({
        device: device,
        format: format,
        alphaMode: "opaque"
    });
    const pipeline = device.createRenderPipeline({
        layout: "auto",
        vertex: {
            module: device.createShaderModule({
                code: vs
            }),
            entryPoint: "main",
            buffers: [
                {
                    arrayStride: 3 * 4,
                    attributes: [
                        {
                            // position
                            shaderLocation: 0,
                            offset: 0,
                            format: "float32x3"
                        }
                    ]
                }
            ]
        },
        fragment: {
            module: device.createShaderModule({
                code:  fs
            }),
            entryPoint: "main",
            targets: [
                {
                    format: format
                }
            ]
        },
        primitive: {
            topology: "triangle-list"
        }
    });

    const positions = [ 
         0.0, 0.5, 0.0, // v0
        -0.5,-0.5, 0.0, // v1
         0.5,-0.5, 0.0  // v2
    ];
	  let vertexBuffer = makeVertexBuffer(device, new Float32Array(positions));

    const render =  function () {
        const commandEncoder = device.createCommandEncoder();
        const textureView = ctx.getCurrentTexture().createView();
        const renderPassDescriptor = {
            colorAttachments: [{
                view: textureView,
                loadOp: "clear",
                clearValue: {r: 1, g: 1, b: 1, a: 1},
                storeOp: "store"
            }]
        };
        const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
        passEncoder.setPipeline(pipeline);
        passEncoder.setVertexBuffer(0, vertexBuffer);
        passEncoder.draw(3, 1, 0, 0);
        passEncoder.end();
        device.queue.submit([commandEncoder.finish()]);
    }
    requestAnimationFrame(render);
}

function makeVertexBuffer(device, data) {
    const verticesBuffer = device.createBuffer({
        size: data.byteLength,
        usage: GPUBufferUsage.VERTEX,
        mappedAtCreation: true,
    });
    new Float32Array(verticesBuffer.getMappedRange()).set(data);
    verticesBuffer.unmap();
    return verticesBuffer;
}
```

実際に動作するコードは jsfiddle にアップしてありますので、ご利用ください。 ※ 実行には WebGPU 対応ブラウザが必要です。

[[jsfiddle] 100行で WebGPU 試してみるテスト(GLSL版)](https://jsfiddle.net/cx20/g1wmxL46/)
[[jsfiddle] 100行で WebGPU 試してみるテスト(WGSL版)](https://jsfiddle.net/cx20/mrwg2pck/)

### 主な変更内容（2020年12月～2022年12月）

■ WGSL 部分
変更内容：
・`GLSL` で言うところの `attribute` 属性はシェーダの `main` 関数の引き数で受け取れるようになりました。
・`struct`（構造体）が指定できるようになりました。
・属性を指定する書き方が Java のアノテーションのような書き方になりました。
　例）
|変更前|変更後|
|:-----|:-----|
|`[[location(0)]]`|`@location(0)`|
|`[[builtin(position)]]`|`@builtin(position)`|
|`[[stage(vertex)]]`|`@vertex`|
|`[[stage(fragment)]]`|`@fragment`|

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26094/bfb22d84-65e6-84a8-063d-56b43219f429.png)

■ WebGPU API 部分
変更内容：
・コンテキストを取得する際の名称が `gpupresent`→`webgpu` に変更されました。
・ピクセルフォーマットが `gpu.getPreferredCanvasFormat()` で取得できるようになりました。
・Render Pipline の設定で `colorStates:` の指定が不要になりました。
・Render Pass の `colorAttachments:` の指定方法が変更になりました。`loadOpe` / `storeOp` の指定が必要になりました。
・冗長だった名称が簡素化されました。
　例）
|変更前|変更後|
|:-----|:-----|
|`vertexStage:`|`vertex:`|
|`fragmentStage:`|`fragment:`|
|`colorStates:`|廃止|
|`vertexBuffers:`|`buffers:`|
|`primitiveTopology:`|`primitive:`|
|`frontFrace:`|`primitive:` に移動|
|`cullMode:`|`primitive:` に移動|
|`context.configureSwapChain()`|`context.configure()`|
|`passEncoder.endPass()`|`passEncoder.end()`|
|`device.defaultQueue`|`device.queue`|

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26094/00788a7e-b53e-cf08-c41d-e98887544949.png)

### その他のサンプル

手前味噌ですが、いくつかの基本的なサンプルについて GLSL 版と WGSL 版のサンプルを下記に置いてありますので興味ある方はどうぞ。

https://github.com/cx20/webgpu-test

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26094/df3edb8c-9428-3c62-5010-c6350bf3769b.png)

### おわりに

何故シェーダ言語が `GLSL` ベースではなく `Rust` ベースの新しい言語になったかは、少し闇が深そうなので、触れないでおきます。。
API やシェーダの仕様は固まりつつあるので、`WebGPU` の新しいシェーダ言語 `WGSL` に今のうちから慣れておくのも良いのではないでしょうか。

### 参考情報

■ WebGPU 仕様
https://www.w3.org/TR/webgpu/
■ WGSL 仕様
https://www.w3.org/TR/WGSL/
■ WebGPU Samples (WebGPU サンプル)
https://github.com/austinEng/webgpu-samples
■ WebGPU - Chrome Developers（開発ステータス）
https://developer.chrome.com/docs/web-platform/webgpu/
■ Raw WebGPU (WebGPU のチュートリアル）
https://alain.xyz/blog/raw-webgpu