ノーマルマップのあれこれ

この記事

もう大分前ですが3Dモデルを読み込んで OpenGL で表示するようなプログラムを書いていて ノーマルマップを適用するあたりがなかなか難しかったのでまとめておこうという内容です。 ノーマルマップを使ったシェーダーの計算の話などです。

法線ベクトルの影響

Blender でフラットシェーディングとスムーズシェーディングを切り替えて法線ベクトル表示してみます。

f:id:tkaaad97:20210301002758p:plain
フラット

f:id:tkaaad97:20210301002818p:plain
スムーズ

結構見た目が変わります、さらにノーマルマップを付けると違ってきます。

シェーダーで使う式は色々ありますが多くのものは拡散光と反射光それから環境光を足し合わせるような形になっています。 拡散光と反射光に法線ベクトルが影響します。 拡散光の場合は光の方向ベクトルと法線ベクトルが影響し、反射光の場合はカメラの向きも影響します。

このあたりが参考になりそうです。

www.opengl-tutorial.org

あと glTF の仕様では式やレファレンス実装がわかるので勉強しやすい気がします。

github.com

github.com

頂点属性

ベースカラーマップで必要な頂点属性は uv 座標のみですが、 ノーマルマップの場合は uv 座標、法線ベクトル、接ベクトルが必要になります。

ノーマルマップのテクスチャから uv 座標で取得した法線ベクトルは、接ベクトル空間でのベクトルなのでこれをグローバル座標系に変換する必要があります。 接ベクトル空間の座標軸は接ベクトル (tangent) 、従法線ベクトル (bitangent) 、法線ベクトル (normal) と呼ばれます。 従法線ベクトルは接ベクトルと法線ベクトルの外積から求められるので頂点属性には入ってません。

頂点属性から接ベクトルを省略して uv 座標から求める方法が使われることがあります。

https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/2e6f9f1cfef04239cc8c8c403a5c49a242b1dc3f/src/shaders/pbr.frag#L142-L143

    vec3 t_ = (uv_dy.t * dFdx(v_Position) - uv_dx.t * dFdy(v_Position)) /
        (uv_dx.s * uv_dy.t - uv_dy.s * uv_dx.t);

このあたりの計算がそれなんですがこれがどうやって出てきたかすぐは分かりませんでした。 色々式をいじってみたところ次のような感じだと思われます。

接ベクトルを $\textbf{t} = ( t _ {x}, t _ {y}, t _ {z} )^{T} $ 、 従法線ベクトルを $\textbf{b} = (b _ {x}, b _ {y}, b _ {z}) ^ {T} $ とします。

uv 座標系の $u$ 軸と接ベクトルが平行、$v$ 軸と従法線ベクトルが平行になるように $\textbf{t}$ と $\textbf{b}$ を求めます。

$\textbf{t}$ と $\textbf{b}$ で作られる平面上の点 $(t, b)$ のグローバル座標系での位置座標は下のようにあらわせます。

$$ \begin{pmatrix} t_x & b_x \\ t_y & b_y \\ t_z & b_z \end{pmatrix} \begin{pmatrix} t \\ b \end{pmatrix}= \begin{pmatrix} x \\ y \\ z \end{pmatrix} $$

$u$ で偏微分すると

$$ \begin{pmatrix} t_x & b_x \\ t_y & b_y \\ t_z & b_z \end{pmatrix} \begin{pmatrix} 1 \\ 0 \end{pmatrix}= \begin{pmatrix} \frac{\partial x}{\partial u} \\ \frac{\partial y}{\partial u} \\ \frac{\partial z}{\partial u} \end{pmatrix} $$

$v$ で偏微分すると

$$ \begin{pmatrix} t_x & b_x \\ t_y & b_y \\ t_z & b_z \end{pmatrix} \begin{pmatrix} 0 \\ 1 \end{pmatrix}= \begin{pmatrix} \frac{\partial x}{\partial v} \\ \frac{\partial y}{\partial v} \\ \frac{\partial z}{\partial v} \end{pmatrix} $$

$\textbf{t}$ と $\textbf{b}$ を偏微分であらあわせました。

$$ \begin{pmatrix} t_x & b_x \\ t_y & b_y \\ t_z & b_z \end{pmatrix} =\begin{pmatrix} \frac{\partial x}{\partial u} & \frac{\partial x}{\partial v} \\ \frac{\partial y}{\partial u} & \frac{\partial y}{\partial v} \\ \frac{\partial z}{\partial u} & \frac{\partial z}{\partial v} \end{pmatrix} $$

glsl には dFdx, dFdy というスクリーン座標での偏微分を計算する関数があるのでこれを使う形に書き換えます。

$$ \begin{pmatrix} t_x & b_x \\ t_y & b_y \\ t_z & b_z \end{pmatrix} \begin{pmatrix} \frac{\partial u}{\partial X} & \frac{\partial u}{\partial Y} \\ \frac{\partial v}{\partial X} & \frac{\partial v}{\partial Y} \end{pmatrix}= \begin{pmatrix} \frac{\partial x}{\partial u} & \frac{\partial x}{\partial v} \\ \frac{\partial y}{\partial u} & \frac{\partial y}{\partial v} \\ \frac{\partial z}{\partial u} & \frac{\partial z}{\partial v} \end{pmatrix} \begin{pmatrix} \frac{\partial u}{\partial X} & \frac{\partial u}{\partial Y} \\ \frac{\partial v}{\partial X} & \frac{\partial v}{\partial Y} \end{pmatrix} $$

$$ \begin{pmatrix} t_x & b_x \\ t_y & b_y \\ t_z & b_z \end{pmatrix} \begin{pmatrix} \frac{\partial u}{\partial X} & \frac{\partial u}{\partial Y} \\ \frac{\partial v}{\partial X} & \frac{\partial v}{\partial Y} \end{pmatrix}= \begin{pmatrix} \frac{\partial x}{\partial X} & \frac{\partial x}{\partial Y} \\ \frac{\partial y}{\partial X} & \frac{\partial y}{\partial Y} \\ \frac{\partial z}{\partial X} & \frac{\partial z}{\partial Y} \end{pmatrix} $$

$$ \begin{pmatrix} t_x & b_x \\ t_y & b_y \\ t_z & b_z \end{pmatrix}= \begin{pmatrix} \frac{\partial x}{\partial X} & \frac{\partial x}{\partial Y} \\ \frac{\partial y}{\partial X} & \frac{\partial y}{\partial Y} \\ \frac{\partial z}{\partial X} & \frac{\partial z}{\partial Y} \end{pmatrix} \begin{pmatrix} \frac{\partial u}{\partial X} & \frac{\partial u}{\partial Y} \\ \frac{\partial v}{\partial X} & \frac{\partial v}{\partial Y} \end{pmatrix}^{-1} $$

$$ \begin{pmatrix} t_x & b_x \\ t_y & b_y \\ t_z & b_z \end{pmatrix}= \frac{1}{\frac{\partial u}{\partial X} \frac{\partial v}{\partial Y} - \frac{\partial u}{\partial Y} \frac{\partial v}{\partial X}} \begin{pmatrix} \frac{\partial x}{\partial X} & \frac{\partial x}{\partial Y} \\ \frac{\partial y}{\partial X} & \frac{\partial y}{\partial Y} \\ \frac{\partial z}{\partial X} & \frac{\partial z}{\partial Y} \end{pmatrix} \begin{pmatrix} \frac{\partial v}{\partial X} & - \frac{\partial v}{\partial Y} \\ -\frac{\partial u}{\partial X} & \frac{\partial u}{\partial Y} \end{pmatrix} $$

シェーダーのコードでは $\textbf{b}$ の方が使われてないですが $\textbf{t}$ は同じ形に変形できました。

dFdx, dFdy は使わずに似た計算で求める場合もあるようです。

marupeke296.com

www.opengl-tutorial.org

ノーマルマップありのシェーダーでの計算の流れ

頂点シェーダー

頂点シェーダーからピクセルシェーダーに接ベクトル、従法線ベクトル、法線ベクトルが渡されます。 頂点シェーダーでは接ベクトル、従法線ベクトル、法線ベクトルに変形行列をかけて回転させます。 変形行列をかけるとき平行移動は考慮しないように注意します。

ピクセルシェーダ

頂点シェーダーからピクセルシェーダーに渡されるときに線形補間されてベクトルの長さや直交関係がずれてしまうので補正します。 接ベクトル、従法線ベクトル、法線ベクトルとノーマルマップから取得したベクトルから実際の法線ベクトルを求めます。 ライトとカメラの情報はユニフォームで渡して拡散光や反射光の計算を行い結果の色を出力します。

どの座標系で計算するか

接ベクトル空間で計算を行うようにして、頂点シェーダーでライトとカメラのベクトルを接ベクトル空間に変換することで計算量を減らすという方法があります。

ただライトを複数渡したい場合にはこのやり方ではちょっと書きにくくなってしまいます。ということでグローバル座標系で計算するというやり方をする場合もあります。

参考

marupeke296.com

www.opengl-tutorial.org

wgld.org