WebGPUでレンダリング手法を学習するシリーズ、前回ディファードレンダリングではスポットライトで特定の方向にのみ影を投影していた。 それを点光源・全方位に拡張して、また環境マッピングもやってみることにした。
点光源で全方位に影を投影しようとした場合、単純に考えると角度が90度の四角錐を上下左右前後の6方向に行えばできるが、 点光源1つにつきシーンを6回レンダリングする必要があるというのは厳しいように思う。 しかし放物面の特性を使うと半球をカバーできるため2面でいけるということを知ったので、やってみた。
双放物面を使った全方位シャドウマッピング
放物面で半球状をマッピングできるのはなぜかというと、放物面の軸に沿って平行に入射した光が焦点の1点に集まる、という特性があるため(そのためパラボラアンテナとして利用されている)。 これを逆に、焦点からのあらゆる方向は反射されて平行に投影される、ということになる。 1方向で半球状がカバーできるので、2面で全方位カバーできることになる (細かいことは補足に)。
以下、実装に関して。
光源座標行列の変更
スポットライトでは影響範囲を放射状に広げるために、プロジェクション行列としてパースペクティブ変換の行列を与えていた。 双放物面ではその処理は必要ないので単に光源の座標系の(逆)行列を与えて、頂点シェーダーで放物面に投影する座標変換を行う。
ただし単に視点行列を与えるのではなく、ちょっと細工をする。 あらかじめカメラのファーが1.0となるように行列をスケールしてやる。 それによって頂点座標に行列をかけた結果の距離が自動的にファー以上の距離がクリップされるようになる (ニアは特に対処不要じゃない?と思って0.0という想定)。
また座標系が右手座標系なので、単純なスケール行列ではなく、ZZ成分を反転する必要があった (WebGPUは左手座標系とのことなんだけど、使用している行列モジュールwgpu-matrixが右手系として扱ってるから?)
全方位マッピングするので実際のところ光源の向きはほぼ必要ないんだけど、念のため残している。
シャドウマップ作成時
前回のシャドウマップ作成時には頂点シェーダーで出力した座標のデプス値によってデプスマップを更新するだけなので、フラグメントシェーダーはなにもする必要はなかった。 しかし今回はZクリップの対処が必要になる。
放物面への投影でZ座標が視点からの距離となって負の値にはならないため、別途変換前の座標をフラグメントシェーダーに渡す。
フラグメントで線形補間されたZ値がマイナス(視線の後方)ならdiscardで無効にする
(単純に0.0未満だと継ぎ目が見えるケースがあったので、適当なマージンを入れる)。
カリングを行うのであれば前方向と後ろ方向で反転させると思うが、光源が物体内にめり込むことがありうるとするとどちらもnoneでいいのかもしれない。
シャドウマップの適用
Deferred Lightingのフラグメントシェーダー内・ライティングの処理でシャドウマップを参照する処理も対応してやる。 フラグメントのワールド座標から光源座標系へ行列変換するところは同じ、でそのZ値によって光源向きの前方か後方かが判定できるので、2枚のシャドウマップのどちらを参照するかを選択する。 さらに座標を放物面変換をかけてやり、あとは同様にシャドウマップを参照して光源からのデプス値を取得して、影かどうかを判定する。
動的環境マッピング
上記で点光源・全方位に影を落とせるようになった。 追加で全方位の動的環境マッピングも試してみる。 キューブマッピングだと6面の描画が必要なところを、同様に双放物面で2面に抑えられる。
全体の処理手順
環境マップはその対象の原点を視点としたレンダリング結果をテクスチャに焼き込む、ということを行うので通常のシェーディングと同様の処理をすることになる。 なので通常と同様に、環境マップもディファードレンダリングで行う。
描画の処理手順としては
- 光源ごとにシャドウマップを描画
- 環境マップ構築(双放物面で、前後2枚)
- 環境マップ用のGバッファ描画
- 環境マップ描画(ディファード:シャドウマップを参照して光源処理)
- フレームバッファ用のGバッファ描画
- 最終的な描画でフレームバッファに反映(ディファード:マテリアルによっては環境マップを参照する)
となる。
- 最終的なディファードライティング後にはガンマ補正を入れるが、環境マップはリニア色空間のまま扱いたいのでかけないようにする
- ディファードライティング時にマテリアルの反射率を得られるようにするために、アルベドマップのa成分に入れておくようにした
座標系
環境マッピング用の座標系はどうするか。 歪みや継ぎ目を気にしてカメラ方向に合わすか、または環境マップの密度を考えて視線と直角にするか、とかセオリーはありそうだが、ライティングをワールド座標系で行なっているのでワールド座標系のまま扱うのが楽そう。
環境マップのシェーディング時の、ワールド座標復元法
で環境マップのライティング時に、通常と同様にワールド座標が必要になる。 しかし放物面反射で座標変換してしまっているので、スクリーン座標系XYと対応する環境マップ用Gバッファからサンプルしたデプス値だけでは復元できない(ハズ…)。
ワールド座標を格納するためのGバッファを1枚増やすしかないか…とも思ったが元のZ値があれば復元できるので、法線用のGバッファのaに入れておいて利用することで対処した。
コンピュートシェーダーで光源を更新
点光源の位置を三角関数で動かすようにしたんだけど、せっかくなので更新処理をコンピュートシェーダーにやらせてみた。
JavaScriptとから現在時刻をユニフォームで渡して、シェーダー側で位置を計算する。
行列も更新する必要があるが、wgpu-matrixのlookAtと同じものをシェーダー側で行えばよい。
現在の秒Date.now() / 1000をそのまま渡そうとするとfloat範囲を超えてしまっているので注意が必要
(JavaScriptの数値型はdoubleだけど、シェーダーに渡すにはf32なので)。
あとがき
そんなこんなでなんとか動くようになった(実際はもっといろいろつまずきながら)。 以下、こまごま:
- 点光源の位置がわかるようにするため、ディファードレンダリングの後にフォワードで描き込んでいる
- 放物面で非線型に変換されるので、モデルの頂点はある程度細かく割る必要がある
- まだ見た目が変に思える時があるのは、点光源がモデルにめり込むときとかZ値が近くなったとき?
- DeferredRendering時にガンマ補正をかけていたが、環境マップにもかかってしまって明るくなってしまっていた
- 光源を増やすともろに重くなるので、光源を増やし放題では全然ない。いまどきのエンジンがどうやってるのか、謎は深まった…
- 写実的なシーンでは点光源といっても全方向が必要なケースはほぼないだろうから、円筒マッピングで1面でできないかな?

Gバッファ、シャドウマップ、環境マップの内容
動画
補足
Game Programming - Dual Paraboloid Shadow Mappingや”View-independent Environment Maps”などの解説を見てすんなり理解できるほどかしこくないので、式の展開を逐一追う必要がある。
放物面のパラメータの導出
放物面は \(f(x, y) = \frac{1}{2} - \frac{1}{2} \left(x^2 + y^2 \right)\)というパラメータを用いる。 このパラメータはどうやって求めるのか?
ZXの2次元で考える。 放物線の定義によって\(x^2 = 4pz\)の焦点位置は\(p\)となる。 \(x = \pm 1\)で\(z\)が焦点と同じ値であれば、焦点を原点に合わせたときに\(x\)が-1〜+1で半球状から投影できることになる。 ということで方程式を解くと、
$$ \begin{align*} x^2 &= 4pz \\ 1^2 &= 4p \cdot p \\ \therefore p &= \pm \frac{1}{2} \end{align*} $$
上に凸にするために負の場合を取って\(z = -\frac{1}{2} x^2\)、そして焦点を原点に合わせることで説明通りの式になる。
放物面との交点座標の導出1
半球をマッピングするために、ワールド座標を放物面にマッピングするための座標変換をする必要がある。 どういう仕組みかよくわからないので追ってみる。
対象の座標への方向\(\vec{v}\)と放物面が交差する座標を求める。 ZX平面で考えて、Z軸からの角度を\(\theta\)とすると方向ベクトルは\(v_z = \cos \theta\)、\(v_x = \sin \theta\)。 これらの比と放物面の交点座標\((z, x)\)の比は同じになるので、
$$ \begin{align*} v_z : v_x &= z : x && \text{方向ベクトルと交点の比が等しい} \\ \cos \theta : \sin \theta &= \frac{1}{2} - \frac{1}{2} x^2 : x && \text{代入} \\ \cos \theta \cdot x &= \left( \frac{1}{2} - \frac{1}{2} x^2 \right) \cdot \sin \theta && \text{たすき掛け} \\ \cos \theta \cdot x &= \frac{1}{2} \sin \theta - \frac{1}{2} \sin \theta \cdot x^2 && \text{分配法則} \\ \sin \theta \cdot x^2 + 2 \cos \theta \cdot x - \sin \theta &= 0 && \text{両辺に2をかけて左辺に移項} \\ \end{align*} $$
\(x\)の2次方程式に対して解の公式を適用、プラスの方を考えて、
$$ \begin{align*} x &= \frac{-2 \cos \theta + \sqrt{(2 \cos \theta)^2 + 4 \cdot \sin \theta \cdot \sin \theta}}{2 \sin \theta} && \text{解の公式に代入} \\ &= \frac{-2 \cos \theta + 2 \sqrt{\cos^2 \theta + \sin^2 \theta}}{2 \sin \theta} && \text{ルートの中を展開して4を外に} \\ &= \frac{1 - \cos \theta}{\sin \theta} && \text{2を約分、ルートの中は1} \\ &= \frac{1 - \cos \theta}{\sin \theta} \cdot \frac{\sin \theta}{\sin \theta} = \frac{1 - \cos \theta}{\sin^2 \theta} \cdot \sin \theta && \sin \theta/\sin \theta \text{を乗算} \\ &= \frac{1 - \cos \theta}{1 - \cos^2 \theta} \cdot \sin \theta && \sin^2\theta=1-\cos^2\theta \\ &= \frac{1 - \cos \theta}{(1 + \cos \theta) (1 - \cos \theta)} \cdot \sin \theta && \text{分母を因数分解} \\ &= \frac{\sin \theta}{1 + \cos \theta} && \text{約分} \\ \end{align*} $$
\(\sin\theta, \cos\theta\)から元の方向ベクトルに戻すと
$$ x = \frac{v_x}{1 + v_z} $$
ZXの2次元で考えていたがY軸も考えるには、\(v_z\)と\(v_{xy}\)の角度を\(\theta\)とすれば、軸の取り方だけでどの向きでも同じなので結局、交点座標のxyは
$$ \left\lbrace \begin{array}{ll} x &= \frac{v_x}{1 + v_z} \\ y &= \frac{v_y}{1 + v_z} \end{array} \right . $$
となる。 求まった\((x, y)\)が放物面との交点なので、それをテクスチャ座標uvに変換するには-1〜+1を0~1に線形変換すればよい。
交点座標の導出方法2
“Game Programming - Dual Paraboloid Shadow Mapping”ではまたちょっと違う導出をしている。
放物面で反射したベクトルがZ軸に平行する(0, 0, 1)になることを利用して、法線は方向ベクトルと(0, 0, 1)を加算・正規化した値になる。
で法線は面の傾きに直角、傾きは偏微分ということから計算する。
放物面\((x, y, 0.5 - 0.5 (x^2 + y^2))^T\)の傾きは偏微分で、
$$ T_x = \begin{pmatrix} 1 \\ 0 \\ -x \end{pmatrix}, T_y = \begin{pmatrix} 0 \\ 1 \\ -y \end{pmatrix} $$
直角は外積で算出すると
$$ T_x \times T_y = \begin{pmatrix} 0 \cdot -y - 1 \cdot -x \\ -x \cdot 0 - (-y) \cdot 1 \\ 1 \cdot 1 - 0 \cdot 0 \end{pmatrix} = \begin{pmatrix} x \\ y \\ 1 \end{pmatrix} $$
これが方向ベクトル\(\vec{v}\)と\((0, 0, 1)^T\)の加算と同じ方向なので、
$$ \begin{pmatrix} v_x \\ v_y \\ v_z + 1 \end{pmatrix} // \begin{pmatrix} x \\ y \\ 1 \end{pmatrix} $$
z成分が1になるよう\(v_z + 1\)で割って見比べれば\(x = v_x / (v_z + 1)\)、\(y = v_y / (v_z + 1)\) が求まる(導出1と同じ)。
リンク
- Game Programming - Dual Paraboloid Shadow Mapping 反射の法線ベクトルから交点を算出
- t-pot『動的双放物面環境マッピング』 昔はシェーダーもアセンブリだったんだなぁ…
- もんしょの巣穴 - DirectXの話 第104回 全方位シャドウマップ
- 床井研究室 - 第22回 放物面マッピング
- View-independent Environment Maps, Wolfgang Heidrich and Hans-Peter Seidel
- 双放物面を使った環境マッピングの論文、実は環境マップの正面/背面方向が一番解像度(ピクセル密度)が低い(最大の1/4くらい)
- OpenGLでどうやるかの話、なんかほとんどの部分は行列計算でできるみたいだけど、よく理解してない…
- WebGPU、wgsl
- 3.3 Coordinate System
NOTE: WebGPU’s coordinate systems match DirectX’s coordinate systems in a graphics pipeline.
- WebGPUはDirectXの座標系と同じ
- 6.4.3. Reference and Pointer Types ポインタが使える(参照は内部扱い)
- シェーダーで関数呼出やポインタ、構造体の戻り値などを使った場合、どのようにコンパイルされるのか気になる…
- Pointers | Tour of WGSL エイリアスがどうとか
- 3.3 Coordinate System
直接は関係ないけど:
- Chapter 12. Omnidirectional Shadow Mapping | NVIDIA Developer GPU Gems、6面描画のはなし
- Reflection Mapping History
- 全然関係ないけどWebGPU関連で: Unity Web Player | BoatAttack | WebGPU いいね