GLSLで法線マッピング(Normal mapping)
dot3バンプマッピング
- 通常のテクスチャマッピング

- 法線マッピング

法線マップについて
法線マップを利用したバンプマッピングは、2000年ごろDirectX 8が発表されてプログラマブルシェーダが導入されたころに普及した技術のようです。
法線マップは、法線ベクトルのX,Y,Zが格納されたRGB画像です。
通常の画像ファイルのR,G,Bのところに、法線マップは接線T、従法線B、法線Nを格納します。
通常の画像ファイルのR,G,Bのところに、法線マップは接線T、従法線B、法線Nを格納します。

法線マップの作成方法は、ツールを使い作成します。
無償でも手に入る手ごろのものにGIMPのpluginがあります。
無償でも手に入る手ごろのものにGIMPのpluginがあります。
これは色(高さ)情報から法線マップを作成するものです。
使い方はほかのサイトを参考にしてください。
使い方はほかのサイトを参考にしてください。
法線の変換(メモ)
法線ベクトルの変換は、頂点や位置ベクトルとは異なり、数学的にはベクトルとしてではなくベクトルに垂直な平面と考えたほうがよいようです。なので、法線ベクトルの変換は垂直な平面の場合の変換ルールにより説明されます。
接線空間の法線をワールド空間に変換する場合は、逆転置行列を使います。
N = transpose( inverse( n ) );
ただし、法線マップはフラグメントシェーダで取り出すため、法線を座標変換すると計算コストがかかります。
よって、法線ベクトルを変換せずに済ませる方法を考えます。
よって、法線ベクトルを変換せずに済ませる方法を考えます。
解決方法としては、次の2つ。
- 法線マップにワールド空間に変換したベクトルを格納する
- 光線ベクトル、視線ベクトルを接線空間に変換する
1に関しては、動的に動くオブジェクトには適用できない問題があります。
ここでは、2の方法で実装を考えます。
ここでは、2の方法で実装を考えます。
シェーダプログラム
いまいち怪しいところが拭いきれていないのですが、とりあえずメモとして残しておきます。
間違いがあれば随時修正ということで。
間違いがあれば随時修正ということで。
法線は外部から与えていますが、接線と従法線は内部で適当に生成しています。
接線のルールとしては「法線と貼り付ける法線マップの+x方向を一致させる」らしいのですが、それっぽければいいかというかんじでここでは内部で生成しています。
接線の生成方法は単純で1つ基準となるベクトル(helpvec)を用意し法線との外積をとることで法線に90度のベクトルが生成されます。あとは、法線と接線の外積をとることで従法線が生成できます。
接線のルールとしては「法線と貼り付ける法線マップの+x方向を一致させる」らしいのですが、それっぽければいいかというかんじでここでは内部で生成しています。
接線の生成方法は単純で1つ基準となるベクトル(helpvec)を用意し法線との外積をとることで法線に90度のベクトルが生成されます。あとは、法線と接線の外積をとることで従法線が生成できます。
- 頂点シェーダ
#version 330
uniform mat4 modelMatrix; // モデル・マトリックス
uniform mat4 viewMatrix; // ビュー・マトリックス
uniform mat4 projectionMatrix; // 射影・マトリックス
uniform vec3 u_lightPos; // 光線の位置
uniform vec3 u_eyePos; // 視点の位置
in vec3 a_position;
in vec3 a_normal;
in vec2 a_texcoord0;
out vec3 v_viewDirection;
out vec3 v_lightDirection;
out vec2 v_texcoord;
void main(void)
{
mat4 modelViewMatrix = viewMatrix * modelMatrix;
mat3 n_mat = mat3( transpose( inverse(modelViewMatrix) ) ); // normal Matrix
vec3 eyePosWorld = (n_mat * u_eyePos);
vec3 viewDirWorld = normalize(eyePosWorld - a_position);
vec3 lightPosWorld = (n_mat * u_lightPos);
vec3 lightDirWorld = normalize(lightPosWorld - a_position);
vec3 helpVec = vec3(0.0, 0.0,1.0);
vec3 tangent = cross(a_normal, helpVec);
if (length(tangent) == 0.0) { tangent = vec3(0.0, 1.0, 0.0); }
vec3 binormal = cross(a_normal, tangent);
mat3 tangentMat = mat3(tangent, binormal, a_normal);
v_viewDirection = viewDirWorld * tangentMat;
v_lightDirection = lightDirWorld * tangentMat;
v_texcoord = a_texcoord0;
gl_Position = projectionMatrix * modelViewMatrix * vec4(a_position.xyz, 1.0);
}
- フラグメントシェーダ
法線マップによる光線反射の計算はフラグメントシェーダ側で行っている。
#version 330
uniform sampler2D s_baseMap; //ベースとなる通常のテクスチャ
uniform sampler2D s_bumpMap; //法線マップ
in vec3 v_viewDirection;
in vec3 v_lightDirection;
in vec2 v_texcoord;
out vec4 fragColor;
void main(void)
{
vec4 baseColor = texture2D(s_baseMap, v_texcoord);
vec3 normal = texture2D(s_bumpMap, v_texcoord).xyz;
normal = normalize(normal * 2.0 - 1.0); // -1.0~1.0
vec3 lightDirection = normalize(v_lightDirection);
vec3 viewDirection = normalize(v_viewDirection);
float nDotL = dot(normal, lightDirection);
vec3 reflection = (2.0*normal*nDotL) - lightDirection;
float rDotV = max(0.0, dot(reflection, viewDirection));
vec4 ambient = vec4(0.2, 0.2, 0.2, 1.0) * baseColor;
vec4 diffuse = vec4(0.8, 0.8, 0.8, 1.0) * nDotL * baseColor;
vec4 specular = vec4(1.0, 1.0, 1.0, 1.0) * pow(rDotV, 100.0);
fragColor = ambient + diffuse + specular;
//fragColor = ambient + diffuse;
}
ちなみに、以下はベースのテクスチャを単色にしたときの表示結果。

まとめ
今回、ようやく法線マッピングにとりかかることができました。
なかなか思うようにいかなくて試行錯誤しながらようやく形になった感じです。
正直正解というものをよく知らないので、本当にあっているかも微妙です。
まぁなんとなくそれっぽければ。。。
なかなか思うようにいかなくて試行錯誤しながらようやく形になった感じです。
正直正解というものをよく知らないので、本当にあっているかも微妙です。
まぁなんとなくそれっぽければ。。。
法線マッピングは、ピクセルの光線計算に注目した処理のようです。
なので単純なテクスチャマッピングに比べて立体感は表せるのですが何か物足りなさを感じます。
法線マップは初歩的な技術のようで、発展としては、
なので単純なテクスチャマッピングに比べて立体感は表せるのですが何か物足りなさを感じます。
法線マップは初歩的な技術のようで、発展としては、
- 視差マッピング(Parallax mapping)
- 変異マッピング(Displacement mapping)
というものがあるようです。
今後、勉強として手を付けてみたいところです。
今後、勉強として手を付けてみたいところです。
参考
- 「OpenGL ES2.0 プログラミングガイド」- 法線マップによるライティング