法線をテクスチャに詰める
一言でいうと
照明は、法線だけを見て明るさを決めるので、法線をテクスチャから読み込んでくれば、平らな面も、凸凹に見えます。その法線を、どの座標系に入れるかが、接線空間の問題です。
なぜ必要なのか
レンガの壁の凹凸を、実際の形状で作ると、三角形が数十万個必要です。ところが、人が凹凸を見分ける手がかりの大部分は、明るさの変化です。出っ張った部分は、光を正面から受けて明るく、へこんだ部分は、斜めに受けて暗くなります。
前のコースで見たように、明るさは、法線と光の方向の内積で決まります。そうであれば、形状を変える代わりに、法線だけをピクセルごとに差し替えれば、同じ明るさの変化を作れます。三角形2つの平面に、レンガの凹凸が生じます。シルエットは相変わらず平らですが、正面から見ると、区別が難しいです。
どう動くのか
法線は、各成分が-1から1の間の単位ベクトルですが、テクスチャのチャンネルは、0から255の間です。そのため、移し替える規則が必要です。このコードブロックの韓国語は、保存するときと読み出すとき(そして、もう一度正規化すること)という意味です。
저장할 때 c = (n * 0.5 + 0.5) * 255
읽을 때 n = c / 255 * 2 - 1 (그리고 다시 정규화)
平らな部分の法線(0, 0, 1)は、(128, 128, 255)になります。ノーマルマップが全体的に薄紫色に見える理由が、これです。その色が、そのまま「ここは平らだ」という意味です。
どの座標系の法線を入れるかが、次の問題です。ワールド空間の法線をそのまま入れると、そのテクスチャは、その物体がその姿勢にあるときだけ合います。物体が回転すると、すべて間違います。
そのため、接線空間を使います。表面の各地点に、その表面を基準にした座標系を立てるのです。3つの軸は、次のとおりです。次のコードブロックの韓国語は、Nが表面に垂直な方向、Tが表面上でテクスチャのuが増える方向、Bが表面上でテクスチャのvが増える方向という意味です。
N (normal) 표면에 수직인 방향
T (tangent) 표면 위에서 텍스처의 u 가 늘어나는 방향
B (bitangent) 표면 위에서 텍스처의 v 가 늘어나는 방향
この座標系で、「平ら」は、常に(0, 0, 1)です。物体がどう置かれていても、同じです。そのため、同じノーマルマップを、複数の物体に貼れて、物体が動いても、そのまま合います。
TとBは、頂点の位置とUVから計算します。三角形の2つの辺と、それに対応するUVの変化量を、連立して解けば出てきます。
e1 = p1 - p0, e2 = p2 - p0
d1 = uv1 - uv0, d2 = uv2 - uv0
r = 1 / (d1.x * d2.y - d2.x * d1.y)
T = (e1 * d2.y - e2 * d1.y) * r
B = (e2 * d1.x - e1 * d2.x) * r
分母が0になる場合があります。三角形のUVが1点にまとまっているか、1本の直線の上に並んでいるときで、そうした三角形には、接線空間が定義されません。UVを展開するときに生じる、よくある問題なので、ツールが警告を出します。
ノーマルマップは、たいてい高解像度のモデルからベイクします。三角形数百万個のモデルを作っておき、低解像度のモデルの各地点で、高解像度の表面の法線を探して、接線空間に移して、テクスチャに書くのです。そのため、ゲームのキャラクターは、三角形が数万個なのに、表面のしわは、数百万個のモデルのものを、そのまま持っています。
関連する技法も、いくつかあります。ハイトマップは、法線の代わりに、高さ1つだけを保存して、シェーダーで傾きを計算して、法線を作ります。保存は3分の1ですが、計算がより必要です。パララックスマッピングは、見る方向によって、UVを少しずらして、奥行き感を真似るもので、ノーマルマップだけではできない「遮蔽」を、ある程度表現します。ディスプレースメントマッピングは、実際に頂点を押し出すので、シルエットまで変わりますが、形状がそのぶん必要です。
現場での姿
ノーマルマップを貼ったのに、照明が逆に見える問題の大部分は、緑のチャンネルの方向です。あるツールは、v軸が上を向くとみなしてベイクし、あるツールは、下を向くとみなします。2つの慣習が混ざると、凹凸が逆に見えます。解決法は、シェーダーで緑のチャンネルを反転させるか、テクスチャを焼き直すことです。
もう1つは、圧縮です。ノーマルマップに、一般的な画像圧縮をそのまま使うと、目に見えて壊れます。色の微細な誤差は、人が気づきませんが、法線の誤差は、明るさの誤差に増幅されるからです。そのため、ノーマルマップ専用の圧縮形式を使い、そのとき、z成分は保存せず、z = sqrt(1 - x² - y²)で復元することもあります。
そして、ノーマルマップは、シルエットを変えられません。物体の縁は、相変わらず平らに切れて見え、斜めから見ると、凹凸が消えます。そのため、シルエットが重要な所には、実際の形状や、パララックスマッピングのような、別の技法を使います。
最後に、ノーマルマップと照明の計算が出会う場所を、整理しておきます。方法は、2つあります。1つは、テクスチャから読んだ接線空間の法線を、TBN行列でワールド空間に移して、ワールド空間の光と内積すること、もう1つは、逆に、光の方向を接線空間に移して、そこで内積することです。結果は同じですが、コストが違います。光が1つだけなら、光を移すほうが安く、それも、頂点シェーダーで先に移して、補間できます。逆に、光が複数だったり、環境マップをあわせて使ったりするなら、法線をワールドに移すほうが良いです。
次のラボですること
法線を色に移し替える規則を作り、半球が格子状に並んだノーマルマップを、自分で生成します。次に、接線空間の3つの軸を、頂点の位置とUVから計算し、同じ平面を、定数の法線とノーマルマップで、それぞれ照らして、絵を比べます。最後に、光源を左から右へ移しながら6枚を取り出して、明るい側が光源についていくことを、数字で確認します。