📐

『リアルタイムグラフィックスの数学』をGLSLじゃなくてWGSLではじめてみたときのメモ

2022/09/08に公開

シェーダープログラミングに入門したいと思い続けて幾星霜、ついに待っていた本が出た。この本はGLSLの本だけど(副題に「GLSLではじめる」ってばっちり書いてあるし)、せっかくなのでWebGPU & WGSLでやってみようと思ってやってみた時のメモ。ぜんぜんわかってないので、変なところあればご指摘いただけるとありがたいです。

https://gihyo.jp/book/2022/978-4-297-13034-3

なぜ WGSL なのか

なぜなのかというと、

  • GLSL より WebGPU & WGSLを主に使うので(といっても超初心者です...)、GLSL力よりもWGSL力を高めたい
  • 単に本に書かれたコードを写経するより、別の言語でやった方が理解が深まりそう

という個人的な理由。WGSL は、今のところ(着実に開発は進んでるとはいえ)まだまだ開発中という感じで、いろいろ動かなかったり仕様がちょこちょこ変わったりするので、単にシェーダープログラミングをやりたいだけであればあまりおすすめしません。念のため。

読み始める前に

まず直面する GLSL との違いとして、WGSL には shadertoy も glsl-canvas もない。shadertoy 的な存在としてこのオンラインエディタがあって便利だったけど、いつの間にか動かなくなっていた...(まだまだ仕様策定中なので仕様がどんどん変わっていくだけで、誰も悪くない)

https://github.com/takahirox/online-wgsl-editor

ので、Learn wgpu(wgpu は Rust の WebGPU API実装)を読み返しながら、fragment shader だけ書けば実行してくれる CLI をつくった。

https://github.com/yutannihilation/math_of_realtime_graphics_wgsl_version

0章

座標系

WGSL、というか WebGPU の座標系は fragment shader ではY軸は下向き(つまり (x, y) = (0, 0) となるのは左上)になっている。なので、shadertoy で書かれた例とは上下逆になるので注意。仕様はこのあたり: https://gpuweb.github.io/gpuweb/#coordinate-systems

あとは、vertex shaderを使うならZ軸が[0, 1]になっている、という違いもあるけど、今回はfragment shderだけなので忘れてOK。

constが動かない

WGSL の仕様にはちゃんとあるけど、なぜかシンタックスエラーになる。これは単に naga のバグなのかも。とりあえず let を使えば動く。constlet の違いをそもそも理解できていない...

   ┌─ wgsl:13:1
   │
13 │ const PI = 3.1415926;
   │ ^^^^^ expected global item ('struct', 'let', 'var', 'type', ';', 'fn') or the end of the file


    expected global item ('struct', 'let', 'var', 'type', ';', 'fn') or the end of the file, found 'const'

調べてみるとこれは思ったより複雑で、WGSL の仕様にはかつて const があったけど、それが let にリネームされたのが去年の話で、 そこから一部のスコープに限って const が再導入されたのが今年の4月、ということでまだ実装側も混乱してるっぽい。naga(wgpu の裏側でshader言語をパース・翻訳してくれているcrate)に関してはこのissueを追っておくといいらしい。

https://github.com/gfx-rs/naga/issues/1829

1章

step()

GLSL の step() は、

step(0.5, vec2(1.1, 1.2));

という感じで動くけど、WGSL の step() はシグネチャが、

@const fn step(edge: T, x: T) -> T

となっていて、引数の次元を合わせる必要がある。具体的には、

step(vec2(0.5, 0.5), vec2(1.1, 1.2))

という感じにする必要がある。ここはそのうち仕様が変わるかも、という気もする(例えば、昔はたしか vec2(a) とは書けなくて vec2(a, a) と書く必要があったけど、今はできるようになってる)

atan()

GLSL の atan()x = 0 のときは未定義動作になっている。一方、WGSL の atan2()(WGSL では一変数版は atan()、二変数版は atan2() と別の関数に分かれている) は特に気にしなくても動いてそう?、と思ったのでどうなってるのか見てみると、どうやら実装によって違うっぽく、HLSL(DirectX)だけは x = 0 でも問題なく動く。手元のパソコンが Windows だったので命拾いしただけっぽい。

https://github.com/gpuweb/gpuweb/issues/2765

ただ、x = 0 もサポートする方向で進んでいるっぽい。

Looks like MSL doesn't handle this, but we should.
(https://github.com/gfx-rs/naga/issues/604)

swizzling

GLSL だと stpq でも swizzling できるけど、WGSL は xyzwrgba しかサポートしていない。

https://www.w3.org/TR/WGSL/#syntax-swizzle_name

あと、ここはルールがまだよく理解できてないけど、component が複数のとき write access できない? つまり、これはいけるけど、

var v: vec3<f32>;
v.x = 1.0;

これはエラーになる。

v.xy = vec2(1.0);
43 │       var v: vec3<f32>;
   │ ╭─────────────────────^
44 │ │     v.xy = vec2(1.0);
   │ ╰────────^ expression is not a reference


    the left-hand side of an assignment must be a reference

じゃあどうすればいいかというと、とりあえずこうすればいいので実用上そこまで困らなくはある。でもやっぱりルールを理解したい。。

vec3(vec2(1.0), v.z)

mod()

WGSL には mod() はない。ふつうに % を使ってくれ、ということらしい。

ただし、mod(x, y)x - y * floor(x / y) なのに対して、 %x - y * trunc(x / y) だという微妙な違いがある。具体的には、xもしくはyが負の値の時に違いがある。これは GLSL の mod() と HLSL の fmod() との違いとして知られる?ものらしい(参考:https://www.natsuneko.blog/entry/2021/09/19/glsl-mod-and-hlsl-fmod-are-not-equivalent

clamp()

これも step() と同じで引数の次元を揃える必要がある。

var v = vec2(1.1);

# ダメ
v = clamp(v, 0.0, 1.0);

# 動く
v = clamp(v, vec2(0.0), vec2(1.0));

めんどくさ...、と思ったけど、いちばんよくあるであろう [0, 1] の区間なら saturate() という関数が用意されているのでこれを使うのが楽。

v = saturate(v);

2章

<<

仕様によれば << の両辺は型が揃っている必要があるらしい。たとえばこれはエラーになる。なぜかわかります?

var n = 0u;
n = (n << 1);

nu32なのに対して、1i32だから、ということらしい。こうすれば通る。

n = (n << 1u);

floatBitsToUInt()

WGSL には bitcast() という operation がある(参考: https://www.w3.org/TR/WGSL/#bitcast-expr)。以下のようにすれば f32u32 に変換できるらしい。

let x = 1.0;
let x2 = bitcast<u32>(x);

型推論はしてくれないっぽく、let x2: u32 = bitcast(x); だとエラーになる。

3章

forループ

これは、for は普通に使えるのであまり気にする必要はないけど、WGSL だと loop という文法があって、for はそのシンタックスシュガーらしい。

4章

三項演算子

WGSL に三項演算子はない。じゃあどうするかというと、 select() という関数があるのでこれを使う。問題は、引数の順序が覚えづらいこと...

select(条件がfalseのとき, 条件がtrueのとき, 条件)

具体的には、三項演算子 A ? B : C は以下と同じになるはず。頭がこんがらがる...

select(C, B,  A)
select(B, C, !A)

まあ、「順番かえちゃう?」という議論を追ってると、似たような関数である mix() が以下のようになってるのを考えると、これはこれで整合性があるのかなーと思った。

mix(割合が0のとき, 割合が1のとき, 割合)

個人的には三項演算子がある言語をほとんど使わないので(Rust とか R とか)そんなに困らないけど、このへんは人によって体験が違いそうですね。。

6章

++

WGSL でも for ループに f32 は使えるが、++i32u32 にしか定義されていないらしい。+= 1.0 にすれば問題なく動く。

Shader 'Shader' parsing error: increment/decrement operation requires reference type to be one of i32 or u32
    ┌─ wgsl:200:32
    │
200 │     for (var j = -2.0; j < 2.0; j++) {
    │                                ^^ must be a reference type of i32 or u32


    increment/decrement operation requires reference type to be one of i32 or u32

Discussion