2018-7-25
ノーマルマップに関して覚書
ノーマルマップを自前で実装する際に学んだことをメモ。
前提として、右手系で考える。
OpenGL と DirectX で法線マップ画像が異なる理由
OpenGL 用では緑が強いほど上方向へ傾いているが DirectX 用では下方向に傾いており、Y 軸に関して逆になっている。これは、OpenGL が右手系、DirectX が左手系と座標系が異なる事に起因している。
テクスチャに対して、手前を Z 軸正方向、右を X 軸正方向とすると、Y 軸正方向は右手系と左手系とでどうしても逆になってしまう。そのため、OpenGL と DirectX で法線マップ画像が異なっている。
接空間
法線マップは接空間上のベクトルを表現している。そのため右手系において、頂点に対して頂点法線を Z 軸、UV 座標の V 軸正方向を Y 軸、U 軸正方向を X 軸とする接空間を考える。さらに接空間の Z 軸を Normal、Y 軸を Binormal、X 軸を Tangent と定義する。
外積による Tangent、Binormal の計算
2 ベクトルの外積は、その 2 ベクトルが成す面に対して垂直なベクトルである。
そのため、説明記事によっては以下の方法で Tangent/Binormal を算出出来ると書いているものがある。
var tangent = cross(normal, vec3(0.0, 1.0, 0.0));
var binormal = cross(n, t);
しかし、外積は同じ向き or 逆方向の場合に結果が 0 ベクトルとなってしまう。そのためnormal = vec3(0.0, 1,0, 0.0)
の時等で結果がおかしくなってしまう。
さらに、この方法だと Tangent や Binormal が UV 座標の U 軸/V 軸と一致している保証がない。頂点(0, 1, 0)において UV(0, 0)で頂点(0, -1, 0)において UV(0, 1)となるような UV マップのように、ローカル座標 Y 軸負方向に行くほど V が大きくなるような UV マップもありえる。UV 座標の U 軸が Tangent、V 軸が Binormal と対応していると定義しているのに、その Tangent/Binormal の算出に UV 情報を用いないためこの方法では正確な法線情報が算出出来ない。
deltaPosition, deltaUV による Tangent, Binormal の計算
各三角面毎に頂点の Tangent/Binormal を、頂点座標と UV 座標の変化を元に算出する方法。
三角ポリゴンの頂点 v0,v1,v2 において、v0 の Tangent/Binormal は以下の式で算出出来る。(以下は擬似コード)
var deltaUV1 = v1.uv - v0.uv;
var deltaUV2 = v2.uv - v0.uv;
var deltaPos1 = v1.position - v0.position;
var deltaPos2 = v2.position - v0.position;
var r = 1.0 / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);
var tangent = (deltaPos1 * deltaUV2.y - deltaPos2 * deltaUV1.y) * r;
var binormal = (deltaPos2 * deltaUV1.x - deltaPos1 * deltaUV2.x) * r;
この計算式を利用して、モデルの頂点座標/UV データを元に算出しシェーダーへ uniform として送る。
ローカル空間における法線情報の算出
ローカル空間における Normal/Tangent/Binormal があれば、以下の式でローカル空間における法線情報を導く事が出来る。
var sampleNormal = texture2D(normalTex, uv);
sampleNormal = (sampleNormal * 2.0) - 1.0; // -1.0~1.0の範囲に収める
var localNormal = tangent * sampleNormal.x + binormal * (-1.0 * sampleNormal.y) + normal * sampleNormal.z;
(-1.0 * sampleNormal.y)
というように、Y に関して反転させているのは、テクスチャは UV 座標系において上下反転するから。