はじめに
前回は、GLSLでブラー処理を実装する方法について解説しました。
今回は、GLSLで簡易的な法線マップを生成する方法について解説します。
Warning
この記事では、簡易的な法線マップの生成について解説しているので、実際に法線マップを作成するさいには、既存の法線マップ作成ツールで作成してください。
コードは下記のGitHubのリポジトリのsrc/canvasで公開しています。
法線マップ(Normal Map)とは
法線マップとは、各ピクセルにおける法線ベクトルをRGB値として格納した画像です。RGBと法線の対応は次のようになります。
- R : x成分
- G : y成分
- B : z成分
今回のデモで画像を法線マップに変換すると次のような画像になります。

多くの法線マップにおいて、青っぽく見える理由は、z成分がほぼ1になるためです。
3DCGにおいては、法線マップを使用することで、表面の凹凸を表現することができます。
法線マップ生成の数式
法線ベクトルの数学的定義をみていき、GLSLのコードで実装してみます。
高さマップ
高さマップとは、画像の輝度を高さとして解釈するデータです。明るい部分は高く、暗い部分は低いとみなします。2D画像を次のような3D曲面として扱います。
ここで、は画像上の座標で、はその位置の高さ(輝度)になります。
これは3次元空間でという点の集合です。
GLSLのコードでは、取得した画像のRGB値からグレースケールに変換して輝度を計算するようにします。
// 高さマップ(輝度取得)
float height(vec2 uv) {
vec3 c = texture(uTexture, uv).rgb;
return dot(c, vec3(0.299, 0.587, 0.114));
}法線はこの高さ関数の勾配から求めることになります。
高さマップの接ベクトルを考える
曲面上の点を次のようにします。
x方向・y方向の接ベクトルは上記の式を偏微分することで得られます。
プログラムで偏微分するには、差分を求めて近似します。
GLSLで書くと、uvに対してpos.xやpos.yで足したり引いたりした値を、先ほどの輝度を求める関数heightに渡してあげてから引けばよいでしょう。
// 偏微分の近似を行うため差分を求める
float hL = height(uv - vec2(pos.x, 0.0));
float hR = height(uv + vec2(pos.x, 0.0));
float hD = height(uv - vec2(0.0, pos.y));
float hU = height(uv + vec2(0.0, pos.y));
// 偏微分
float dx = (hR - hL);
float dy = (hU - hD);接ベクトルの外積を求めて法線ベクトルを得る
曲面の法線は、2本の接ベクトルの外積で求めることができます。
この外積を計算すると
0で掛けている部分は消えるので整理すると
となります。
正規化
単位法線が必要なので、先ほどの法線ベクトルを正規化します。正規化するには、法線ベクトルの長さで割る必要があります。長さは次の式のようになります。
ですので、法線マップを生成する最終的な式は次のようになります。
これをGLSLで書くと次のようになります。
// 法線計算
vec3 normal = normalize(vec3(-dx, -dy, 1.0));
// [-1, 1] → [0, 1]に正規化
vec3 color = normal * 0.5 + 0.5;GLSLでは、normalize関数があるので、先ほどの偏微分のdxとdyをそのまま使えばよいです。
また、normalの値の範囲はになるので、の範囲にしてます。
法線マップを生成するGLSLコード
今回のデモの全コードは次のようになります。
#version 300 es
precision mediump float;
uniform sampler2D uTexture;
uniform vec2 uResolution;
uniform float strength; // 凸凹の強さ
in vec2 vUv;
out vec4 fragColor;
// 高さマップ(輝度取得)
float height(vec2 uv) {
vec3 c = texture(uTexture, uv).rgb;
return dot(c, vec3(0.299, 0.587, 0.114));
}
void main() {
vec2 uv = vUv;
vec2 pos = 1.0 / uResolution;
// 偏微分の近似を行うため差分を求める
float hL = height(uv - vec2(pos.x, 0.0));
float hR = height(uv + vec2(pos.x, 0.0));
float hD = height(uv - vec2(0.0, pos.y));
float hU = height(uv + vec2(0.0, pos.y));
// 偏微分
float dx = (hR - hL) * strength;
float dy = (hU - hD) * strength;
// 法線計算
vec3 normal = normalize(vec3(-dx, -dy, 1.0));
// [-1, 1] → [0, 1]に正規化
vec3 color = normal * 0.5 + 0.5;
fragColor = vec4(color, 1.0);
}デモでは、凸凹の強さを変化させられるようにstrengthをuniform変数で用意してあり、偏微分を求める箇所で掛けています。
まとめ
法線マップを作成するために必要な法線ベクトルの数式での導出方法を説明し、GLSLで実際に実装してみました。冒頭でも書いたとおり、このデモの法線マップの生成は簡易的なものになっているので、参考程度にみてください。
次回は、陰影付け処理を応用した特殊効果の画像処理をやっていきたいと思います!
画像処理のおすすめ本
下記は画像処理全般の基礎の勉強におすすめの書籍になります。