天狗会議録 Posts Pages About

シェーダの基礎を学習した

#cg2023/12/15

概要

2Dゲーム作成のためにしかグラフィックスAPIを使っていなかった都合上、3Dシェーディングの知識を欠いていた。

最近、諸事情あってPhong反射モデルを実装することになった。 そこで多くの曖昧な知識・知らない知識・勘違いしていた知識が発覚したため、学習した。

学習した内容を当記事にまとめる。 尚、グラフィックスAPIの仕様についてはOpenGL系のみを対象とする。

ソースコードはこちら

逆行列の取得

行列\(A\)の逆行列をGauss-Jordan法(掃き出し法)で計算するアルゴリズムは次のようである。

  1. \(A\)と同じサイズの単位行列\(E\)を作る
  2. すべての\(i\)行目について次を繰り返す
    1. \(A, E\)の\(i\)行目のすべて要素を\(A_{ii}\)で割る (\(A_{ii}\)が1になる)
    2. すべての\(i'(\neq i)\)行目について次を繰り返す
      1. \(A\)の\(i'\)行目から\(i\)行目の\(A_{i'i}\)倍を引く (\(A_{i'i}\)が0になる)
      2. \(E\)の\(i'\)行目から\(i\)行目の\(E_{i'i}\)倍を引く

上を実行し終わったときの\(E\)が求めたい逆行列となる。 また、\(A\)は単位行列となっている。

上の実装では、対角成分\(A_{ii}\)に0があると正確に計算を終えられない。 これを解決するために部分ピボット選択を行う。 これは\(i\)行目以降のすべて行の\(i\)列目の要素で最も大きな要素を見つけ、それが\(A_{ii}\)となるように行をスワップする行為である。 スワップ時に\(E\)も連動してスワップすることに注意する。

恐らく、どのようなアルゴリズムでも、逆行列を求める計算量は\(O(N^3)\)と重い。 できれば逆行列を求める状況は作らない方が良いだろう。

行列の列優先

OpenGL系では、列ベクトルを用いる。 従って、例えば平行移動は以下のように計算される。

\[ \begin{pmatrix} 1 & 0 & 0 & t_x\\ 0 & 1 & 0 & t_y\\ 0 & 0 & 1 & t_z\\ 0 & 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} x\\ y\\ z\\ 1 \end{pmatrix} = \begin{pmatrix} x + t_x\\ y + t_y\\ z + t_z\\ 1 \end{pmatrix} \]

しかし、OpenGL系では、行列は列優先でレイアウトされる。 GLSLのmat4はvec4[4]を意味し、vec4は前述の通り列ベクトルである。 つまり、行列の各要素にデータレイアウト上のインデックスを振ると次のようになる。

\[ \begin{pmatrix} 1 & & 5 & & 9 & & 13\\ 2 & & 6 & & 10 & & 14\\ 3 & & 7 & & 11 & & 15\\ 4 & & 8 & & 12 & & 16 \end{pmatrix} \]

シェーダ転送時の転置がサポートされていない限りは、プログラム側も列優先でデータを作らなければならない。 要は、常に数学的な行列を転置した行列を使うことになるため、演算の実装を誤ると期待した結果が得られなくなる。 これを回避するために、数学的な行列のまま操作できる機構を作るべきだろう。 サンプルコードのmath.jsがそれである。

ただ、なぜ列優先なのかわからなかった。 行列の乗算について、もし行ベクトルを用いるのであれば複数回の内積計算になるが、列ベクトルを用いると飛び飛びの要素を掛け合わせなければならない。 つまり、キャッシュ効率が悪そうに見える。 所詮32x16bit程度のデータを飛び飛びに選んでレジスタへ入れるオーバヘッドは、考慮すべきほど大きくないということなのだろうか。

ビュー行列の作成

ビュー行列は、カメラから見た座標系に物体を移すための変換行列である。 深くは考えていないが、恐らく、カメラには二種類の設定方式があり、それぞれに強いビュー行列作成手段がある。

まず簡単なのは、カメラの座標と回転角を設定する方式である。 物体を、カメラの座標だけ「逆に」平行移動し、カメラの回転角だけ「逆に」回転させれば良い。 従って、カメラの座標・回転角を\(C_{trs}, C_{rot}\)として、ビュー行列\(V\)は次のようになる。 最右辺の各逆行列は、パラメータを-1倍した各種変換行列でしかないので、逆行列を求める必要はない。

\[ V = (C_{trs}C_{rot})^{-1}=C_{rot}^{-1}C_{trs}^{-1} \]

もう一つは、カメラの座標・注視点・上方向を設定する方式である。 ビュー座標系の座標軸を求め、それに従って物体を移動する必要がある。 まず、座標軸を求める。 カメラの座標・注視点・上方向を\(v_{trs}, v_{lookat}, v_{up}\)として、それぞれの座標軸\(X, Y, Z\)は次のようになる。 外積の結果が右ねじの法則に従うことに留意する。

\[ Z = v_{lookat} - v_{trs}\\ X = Z \times v_{up}\\ Y = X \times Z \]

次に、座標軸とカメラ座標をもとにビュー行列を作る。

\[ \begin{pmatrix} X_x & X_y & X_z & -(v_{trs} \cdot X)\\ Y_x & Y_y & Y_z & -(v_{trs} \cdot Y)\\ Z_x & Z_y & Z_z & -(v_{trs} \cdot Z)\\ 0 & 0 & 0 & 1 \end{pmatrix} \]

サンプルでは後者の方式を採用した。

法線ベクトルの変換

ライティングのためには、法線ベクトルを適切に回転・拡縮しなければならない。 この回転は、物体に対するそれと同一である。 一方拡縮は、各パラメータが逆数となる。 従って、ワールド変換行列とは別に、そのような回転行列・拡縮行列をシェーダへ転送し、法線ベクトルに掛け合わせて・正規化すれば良い。

このような行列をワールド変換行列から作成できるらしい。 法線ベクトルを適切に変換するための行列\(W_{normal}\)は、ワールド変換行列を\(W\)として、次のように求める。

\[ W_{normal} = (W^{-1})^\mathrm{T} \]

Phong反射モデル

Phong反射モデルは、簡単で軽量なシェーディングモデルである。 環境光、拡散光、鏡面光の三要素をライトとマテリアルが持ち、その相互作用によって一表面の色(輝度)を決定する。 Direct3D 9やOpenGL 2.1までは標準実装されていたらしい。 モデル及び各変数の説明は次のようである。

\[ I_p = k_ai_a + k_d (L \cdot N) i_d + k_s (R \cdot V)^\alpha i_s \]
  • \(I_p\): 表面の色
  • \(k_a\): 物質の環境反射係数
  • \(k_d\): 物質の拡散反射係数
  • \(k_s\): 物質の鏡面反射係数
  • \(i_a\): 環境光色
  • \(i_d\): 拡散光色
  • \(i_s\): 鏡面光色
  • \(\alpha\): 物質の光沢度 (大きいほど反射光が小さく・強くなる)
  • \(L\): 表面からライトへの方向ベクトル
  • \(N\): 表面の法線ベクトル
  • \(R\): 反射光の方向ベクトル
  • \(V\): 表面からカメラへの方向ベクトル

ただし、\(R\)は次のように求められる。 尚、GLSLにはreflect関数という同じ計算をする関数がある。 が、サンプルではそのことを知らなかったため利用していない。

\[ R = -V + 2 (V \cdot N) N \]

また、各種反射係数はスカラーでもベクトルでもよい。 ベクトルである場合は、ベクトル同士の乗算が以下のような計算となることに注意する。 尚、反射係数がベクトルであるとは、各種光色の各要素に対する係数がベクトルを成すことを意味する。

\[ uv \overset{\mathrm{def}}{\Leftrightarrow} uv^\mathrm{T} \]

参考文献