『リアルタイムグラフィックスの数学』をGLSLじゃなくてWGSLではじめてみたときのメモ
シェーダープログラミングに入門したいと思い続けて幾星霜、ついに待っていた本が出た。この本はGLSLの本だけど(副題に「GLSLではじめる」ってばっちり書いてあるし)、せっかくなのでWebGPU & WGSLでやってみようと思ってやってみた時のメモ。ぜんぜんわかってないので、変なところあればご指摘いただけるとありがたいです。
なぜ WGSL なのか
なぜなのかというと、
- GLSL より WebGPU & WGSLを主に使うので(といっても超初心者です...)、GLSL力よりもWGSL力を高めたい
- 単に本に書かれたコードを写経するより、別の言語でやった方が理解が深まりそう
という個人的な理由。WGSL は、今のところ(着実に開発は進んでるとはいえ)まだまだ開発中という感じで、いろいろ動かなかったり仕様がちょこちょこ変わったりするので、単にシェーダープログラミングをやりたいだけであればあまりおすすめしません。念のため。
読み始める前に
まず直面する GLSL との違いとして、WGSL には shadertoy も glsl-canvas もない。shadertoy 的な存在としてこのオンラインエディタがあって便利だったけど、いつの間にか動かなくなっていた...(まだまだ仕様策定中なので仕様がどんどん変わっていくだけで、誰も悪くない)
ので、Learn wgpu(wgpu は Rust の WebGPU API実装)を読み返しながら、fragment shader だけ書けば実行してくれる CLI をつくった。
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
を使えば動く。const
と let
の違いをそもそも理解できていない...
┌─ 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を追っておくといいらしい。
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 だったので命拾いしただけっぽい。
ただ、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 は xyzw
と rgba
しかサポートしていない。
あと、ここはルールがまだよく理解できてないけど、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);
n
はu32
なのに対して、1
はi32
だから、ということらしい。こうすれば通る。
n = (n << 1u);
floatBitsToUInt()
WGSL には bitcast()
という operation がある(参考: https://www.w3.org/TR/WGSL/#bitcast-expr)。以下のようにすれば f32
を u32
に変換できるらしい。
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
は使えるが、++
は i32
と u32
にしか定義されていないらしい。+= 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