法線・視差マッピング
ポリゴンを増やさずに凹凸を出す仕組みを、法線マップと接空間、視差オクルージョンマッピングの原理から理解し、破綻の少ない設定を選べるようになります。
- 1.法線マップは各テクセルに面の向き(法線ベクトル)を格納し、平坦なポリゴンでも陰影計算を欺いて擬似的な凹凸を作る。
- 2.格納される法線は接空間(タンジェント・従法線・法線が張る局所座標)基準なので、ライト方向を接空間へ変換して内積を取るか、TBN行列で世界座標へ戻す。
- 3.視差マッピングはハイトマップに沿って視線を進め UV をずらすことで立体視差を与え、視差オクルージョンマッピング(POM)はレイマーチで自己遮蔽まで再現する。
凹凸を「増やさずに」出すという発想
岩肌やレンガの目地のような細かな凹凸を、そのまま頂点で表現すると三角形が爆発的に増えます。そこで幾何形状は平坦なまま置き、陰影計算だけを凹凸があるかのように欺くのが法線・視差マッピングの根本発想です。ライティングの見た目を決めるのは、面の実際の形ではなく、拡散反射の N·L(法線とライト方向の内積)や鏡面反射に使われる法線ベクトル N です。ならば N をピクセルごとに差し替えれば、平面でも凹凸があるように光る、という理屈になります。N が陰影をどう左右するかは シェーディングと物理ベース描画 を前提とします。
バンプマッピングから法線マップへ
古典的なバンプマッピングは、グレースケールのハイトマップ(高さ場)の勾配から法線の摂動を実行時に計算しました。原理は正しいものの、毎ピクセルで微分を取る必要があり無駄が多い方式です。
現代の法線マッピングは、その摂動済みの法線をあらかじめテクスチャに焼き込んでおきます。RGB の各チャンネルに法線ベクトルの (x, y, z) 成分を入れるのです。ベクトル成分は -1〜+1 の範囲を取りますが、テクスチャは 0〜1 しか格納できないため、色 = 法線 × 0.5 + 0.5 という符号化で詰め込みます。法線マップが一様に青紫色(およそ RGB (128,128,255))に見えるのは、凹凸のない部分の法線が (0, 0, 1)、すなわち面の真上を向いているからです。読み出し側は 法線 = 色 × 2 - 1 で復号します。
法線マップの Z 成分(青)は、平坦部で +1 に近く、常に手前(面の外向き)を指します。X(赤)と Y(緑)は傾きに応じて 0.5 前後で上下するだけなので、全体として青が強く出ます。逆に言えば、青が極端に薄い・赤緑が飽和した法線マップは法線が横倒しになっており、光源によっては陰影が破綻します。
接空間 ── なぜ座標系を作り直すのか
ここが最大の勘所です。法線マップに焼かれた (0, 0, 1) の「1」は、どの方向を指す 1 なのか。ワールド座標の Z 軸ではありません。もしワールド基準で焼くと、そのテクスチャは1つの向きの面にしか貼れず、床と壁で別のマップが必要になり、モデルを回転させたら破綻します。
そこで法線は**接空間(タンジェントスペース)**という、面に貼り付いた局所座標系を基準に格納します。接空間は3つの直交ベクトルで張られます。
- 法線 N(Normal): 面の外向き。接空間の Z 軸に相当。
- 接線 T(Tangent): 面に沿い、テクスチャの U 方向(横)に一致させる。X 軸に相当。
- 従法線 B(Bitangent / Binormal): N と T の両方に直交。テクスチャの V 方向に対応し、
B = cross(N, T)で求まる。Y 軸に相当。
接線を UV の U 方向に合わせるのが要点で、これにより「テクスチャの右方向」と「面上の右方向」が一致し、どんな向きの面に貼っても法線マップの意味が保たれます。T と B は頂点の位置と UV の対応から前計算でき、その求め方は メッシュとジオメトリ表現 の接線属性で扱います。
TBN 行列による座標系の橋渡し
接空間で表された法線を、実際の照明計算が行われるワールド空間(あるいはビュー空間)へ持ち込むには、3本の基底ベクトルを列に並べた TBN 行列を使います。
// 頂点属性 N, T から接空間→ワールド空間の変換行列を組む
vec3 N = normalize(worldNormal);
vec3 T = normalize(worldTangent);
// グラム・シュミット直交化:T を N に直交させ、数値誤差を抑える
T = normalize(T - dot(T, N) * N);
vec3 B = cross(N, T); // handedness は後述
mat3 TBN = mat3(T, B, N); // 列ベクトルとして T,B,N を配置
// テクスチャから接空間法線を復号し、ワールドへ変換
vec3 nTex = texture(normalMap, uv).xyz * 2.0 - 1.0;
vec3 worldN = normalize(TBN * nTex); // これを N·L に使う
方向は双方向に使えます。TBN で接空間法線をワールドへ移すのが直感的ですが、逆にライト方向やビュー方向を **TBN の転置(=逆行列。正規直交なので転置で足りる)**で接空間へ引き込み、そこで内積を取る手もあります。後者はピクセルシェーダの行列積を減らせるため、頂点シェーダで一度だけライト方向を接空間へ変換して補間する最適化が定番です。座標変換で正規直交行列の逆が転置になる理屈は 座標変換 を参照してください。
接空間には右手系・左手系の二通りがあり、従法線の符号が逆になります。DirectX 系ツールと OpenGL 系ツールで 緑(Y)チャンネルの向きが逆なことが多く、これが原因で凹凸が反転して「へこみが出っ張って見える」不具合が頻発します。頂点データに接線の w 成分として handedness(±1)を持たせ B = cross(N, T) * tangent.w とするか、シェーダで nTex.g = -nTex.g して合わせます。凹凸の向きが逆なら、まず緑の反転を疑うのが定石です。
法線マップの限界 ── 視差が出ない
法線マッピングは陰影を欺くだけで、面そのものは平坦なままです。したがって視点を動かしても凹凸に**視差(パララックス)**が生じず、シルエットも真っ平らです。レンガを真横から見ると、目地の奥行きが消えてただの絵に見えてしまいます。この「動いても立体に見えない」問題を埋めるのが視差マッピングです。
視差マッピング ── UV を視線でずらす
視差マッピング(Parallax Mapping)は、ハイトマップの高さに応じて、サンプリングする UV を視線方向へずらす手法です。凸部は視点側に迫り出しているように、凹部は奥へ引っ込んでいるように UV を補正します。
単純な視差マッピング(接空間で計算):
V = 接空間での視線方向(面→カメラ)
h = texture(heightMap, uv).r // 0〜1 の高さ
s = h * scale // 迫り出し量。scale で強さ調整
// 高さに比例して、視線の水平成分の向きへ UV をずらす
offset = (V.xy / V.z) * s
uv' = uv - offset // ずらした UV で法線・色を読む
ずらした UV で改めて法線マップやカラーテクスチャを読むと、視点移動に追従して模様が動き、立体感が生まれます。ごく低コストですが、V.z(面に対する視線の傾き)で割るため浅い角度で破綻し、模様が大きく歪んで泳ぎます。高さ1点だけで近似する粗い方式である点が根本の弱みです。
視差オクルージョンマッピング(POM)
破綻を抑える本命が視差オクルージョンマッピング(Parallax Occlusion Mapping, POM)です。ハイトマップを厚みのある高さ場とみなし、接空間で視線をレイマーチ(一定ステップで前進)させて、視線が高さ場の表面と最初に交差する点を探します。
POM のレイマーチ(接空間、V は面→カメラ):
layers = ステップ数(角度が浅いほど増やすと良い)
dHeight = 1.0 / layers // 1ステップで下る高さ
dUV = (V.xy / V.z) * scale / layers // 1ステップの UV 移動量
rayHeight = 1.0 // 視線の高さ。上端(1)から降ろす
curUV = uv
// 視線を上端から降ろし、地形の高さを下回った点を探す
while ( texture(heightMap, curUV).r < rayHeight ):
curUV -= dUV
rayHeight -= dHeight
// ここで curUV は交差を跨いだ直後(視線が地形へ潜り込んだ)
交差を跨いだ直前と直後の2点が分かるので、各点での「視線の高さと地形の高さの差」を重みに UV を線形補間すると、階段状のアーティファクトが消えて滑らかな交差点が得られます(この後処理を parallax occlusion と呼びます)。得られた curUV で法線・アルベド・粗さを読み直せば、迫り出しが正しく効きます。
自己遮蔽(セルフオクルージョン)まで扱えるのが POM の真価です。交差点からライト方向へ同様のレイマーチを行い、途中で高さ場に遮られれば、その点は他の凸部の影に入っていると判定できます。これにより、法線マップだけでは決して出せない凹凸が落とす影が現れ、リアリティが一段上がります。
| 手法 | 追加コスト | できること | 主な破綻 |
|---|---|---|---|
| 法線マッピング | テクスチャ1枚+数演算 | 陰影の凹凸感 | 視差なし・シルエット平坦 |
| 視差マッピング | ハイト1サンプル | 軽い立体視差 | 浅い角度で模様が泳ぐ |
| POM | レイマーチ(多サンプル) | 正しい視差・自己遮蔽・影 | 縁の欠けとコスト増 |
| ディスプレイスメント | 頂点分割(テッセレーション) | 本物の凹凸・正しい輪郭 | 頂点数とメモリの増大 |
POM はレイマーチの各ステップでハイトマップをフェッチするため、テクスチャ帯域を大量に消費します。さらに、ずらした UV で法線・カラーも読むので、テクスチャマッピングとフィルタリング の LOD 計算が狂いやすく、ミップの選択を誤ると縮小時にちらつきます。加えて、面の縁(シルエット)は依然として元ポリゴンの平坦なままなので、強い迫り出しをかけると輪郭付近で高さ場が途切れ、隙間が見える「エッジの穴」が出ます。角度に応じてステップ数を動的に変える、縁で効果をフェードする、といった対策を併用します。
本物の凹凸が要るなら ── ディスプレイスメント
視差系はあくまで見た目の近似で、シルエットは変えられません。輪郭まで正しく凹凸を出すには、ディスプレイスメントマッピングでハイトマップに従い頂点自体を押し出します。近年はテッセレーションで動的に三角形を細分してから変位させるのが主流で、シルエットが正しく立体化する代わりに頂点数とメモリを消費します。近景はディスプレイスメント、中景は POM、遠景は法線マップと距離で切り替えるのが実務上のバランスの取り方です。
まとめ
- 法線マッピングは、各テクセルに符号化した法線ベクトル(
色 × 2 - 1で復号)でN·Lを欺き、平坦なポリゴンに擬似的な凹凸を与える。 - 法線は面に貼り付いた接空間(T・B・N)基準で格納し、接線を UV の U 方向へ合わせることで任意の向きの面に貼れる。ワールドとの橋渡しは TBN 行列(正規直交なので逆は転置)で行う。
- handedness と緑チャンネルの向きの不一致は凹凸反転の定番原因。まず緑の符号を疑う。
- 視差マッピングは高さに応じ UV をずらして軽い立体視差を出すが浅い角度で破綻し、POM はレイマーチで交差点を求めて自己遮蔽と影まで再現する。
- 視差系はシルエットを変えられないため、輪郭まで要るならディスプレイスメント(頂点変位)へ。距離に応じた使い分けが定石。
グラフィックス Article
法線・視差マッピングを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
グラフィックス
比較で見る軸
難易度: advanced / カテゴリ: グラフィックス / タグ数: 6
導入後に効く点
格納される法線は接空間(タンジェント・従法線・法線が張る局所座標)基準なので、ライト方向を接空間へ変換して内積を取るか、TBN行列で世界座標へ戻す。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- グラフィックス
- タグ数
- 6
判断チェックリスト
- 自社の用途が「グラフィックス / 法線マッピング」に近いか確認する。
- 強みである「法線マップは各テクセルに面の向き(法線ベクトル)を格納し、平坦なポリゴンでも陰影計算を欺いて擬似的な凹凸を作る。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。