今回は、3Dゲームの見た目を一気にリッチにする「法線マップ(ノーマルマップ)」について解説していきます。
「法線マップって何?あの紫色の画像は何なの?」
「ポリゴンを増やさずに凹凸を出せるって、どういう仕組み?」
「あの紫の画像って、そもそもどうやって作るの?」
こんな悩みはありませんか?
法線マップは、ひとことで言うと「面の向きの情報を画像で持たせて、光の当たり方を騙す技術」です。実際のポリゴンは平らなままなのに、光の計算だけを凹凸があるかのように行うことで、立体的に見せます。
どれくらい変わるのか、自作のDirectXプロジェクトでまったく同じ床・同じテクスチャ・同じライトのまま、法線マップだけをON/OFFして描画してみました。


この記事では、法線マップの仕組み・あの紫色の正体・自分で法線マップを作る方法・HLSLでの実装・ハマりどころまで解説します。市販の素材を使うだけでなく「どう作られているか」まで踏み込むので、ぜひ最後まで読んでください。
仕組み|明るさは「面の向き」で決まる
まず前提として、3Dの明るさは面の向き(法線)と光の向きの関係だけで決まります。
- 法線が光の方を向いている → 明るい
- 法線が光に対して斜め・逆を向いている → 暗い
ということは、法線さえバラバラの向きにしてしまえば、実際の形が平らでも凹凸があるように見えるわけです。この「ピクセルごとに違う法線」を画像として保存したものが法線マップです。
ポリゴンを増やして本物の凹凸を作る方法と比べて、頂点数が増えない=軽いのが最大の利点です。
あの紫色の画像の正体
法線マップが紫っぽく見えるのには、はっきりした理由があります。RGBの各色が、法線のXYZに対応しているからです。
- R(赤) → 法線のX(左右の傾き)
- G(緑) → 法線のY(上下の傾き)
- B(青) → 法線のZ(面から手前に飛び出す向き)
傾きのない平らな部分の法線は (0, 0, 1) です。ところが画像はマイナスの値を保存できないので、-1〜1 を 0〜1 に変換して保存します。すると (0, 0, 1) は (0.5, 0.5, 1.0) になり、これはRGBで薄い紫です。法線マップ全体が紫がかって見えるのは、大部分が「平ら」だからなんですね。
法線マップを自分で作ってみる
ここが一番おもしろいところです。法線マップはハイトマップ(高さの白黒画像)から計算で作れます。今回使ったレンガも、既製の素材ではなく自分で生成したものです。

考え方はシンプルで、「隣のピクセルとの高さの差」=「傾き」です。左右のピクセルの高さを比べれば横方向の傾きが、上下を比べれば縦方向の傾きが分かります。
// ハイトマップ H から法線マップを作る
for (各ピクセル x, y) {
// 左右・上下の高さを取得
float left = H[y][x - 1];
float right = H[y][x + 1];
float up = H[y - 1][x];
float down = H[y + 1][x];
// 高さの差が、そのまま傾きになる
// strength を大きくすると凹凸が強調される
float nx = (left - right) * strength;
float ny = (up - down ) * strength;
float nz = 1.0;
// 正規化して長さ1のベクトルにする
正規化(nx, ny, nz);
// -1〜1 を 0〜1 に変換して色として保存
R = nx * 0.5 + 0.5;
G = ny * 0.5 + 0.5;
B = nz * 0.5 + 0.5;
}平らな部分は左右の高さが同じなので nx が0になり、(0.5, 0.5, 1.0) つまり紫になります。目地の縁のように高さが急に変わる場所だけ、色が青緑やピンクに寄ります。紫の画像の中で色がついている部分=傾いている部分だと分かると、法線マップが一気に読めるようになります。
HLSLでの実装
使う側はもっと簡単で、法線マップから色を読んで、それを法線として使うだけです。保存時に 0〜1 へ変換されているので、読むときは -1〜1 に戻します。
Texture2D g_Texture : register(t0); // カラーマップ
Texture2D g_NormalMap : register(t1); // 法線マップ
SamplerState g_Sampler : register(s0);
void main(in PS_IN In, out float4 outDiffuse : SV_Target) {
float4 texColor = g_Texture.Sample(g_Sampler, In.TexCoord);
// 法線マップの値は 0.0〜1.0 なので -1.0〜1.0 へ戻す
float3 normalMap = g_NormalMap.Sample(g_Sampler, In.TexCoord).xyz;
normalMap = normalMap * 2.0f - 1.0f;
// 使う座標系に合わせて成分を並べ替える(地面が水平な場合の例)
float3 normal = normalize(float3(normalMap.x, normalMap.z, normalMap.y));
// あとは普通のライティングと同じ。法線が入れ替わっただけ
float diffuse = saturate(dot(-Light.Direction.xyz, normal));
outDiffuse.rgb = texColor.rgb * diffuse;
outDiffuse.a = texColor.a;
}ポイントはライティングの計算式自体は何も変えていないことです。法線の取得元が「頂点」から「テクスチャ」に変わっただけで、あとは通常のピクセルライティングと同じです。
【重要】私が法線マップでハマった体験談
チーム制作で床に法線マップを入れたとき、凹凸は出たのに、光を当てている向きと影の向きが逆という気持ち悪い見た目になりました。レンガが凹んで見えたり、逆に膨らんで見えたりして安定しません。
原因は法線マップの成分の並べ替えを間違えていたことでした。法線マップはZが「面から飛び出す向き」ですが、床は上向き(Y)が飛び出す方向です。ここを (x, y, z) のまま使っていたため、上下と前後が入れ替わっていたんです。float3(normalMap.x, normalMap.z, normalMap.y) と並べ替えたら、一発で正しい凹凸になりました。
「凹凸が出るけど向きが変」なときは、計算式ではなく成分の順番を疑うのが早い、と学びました。
法線マップのよくある失敗例と対処法
①凹凸がまったく出ない
* 2.0f - 1.0f の変換を忘れていると、法線が全部プラス方向を向いてしまい変化が出ません。またテクスチャのスロット番号(register(t1) と PSSetShaderResources(1, ...))がずれていると、カラーマップを法線として読んでしまいます。
②DirectX用とOpenGL用を取り違える
配布されている法線マップには NormalDX と NormalGL の2種類があることがあります。緑(Y成分)の向きが逆で、取り違えると凹凸が反転して見えます。DirectXならDX版を使うか、シェーダー内で normalMap.y = -normalMap.y; と反転しましょう。
③真横から見ると平らなのがバレる
法線マップはあくまで光の当たり方を騙しているだけで、形状は平らなままです。そのためシルエットには凹凸が出ず、浅い角度から見ると平面だと分かります。輪郭にも凹凸が必要なら、パララックスマッピングや実際のポリゴンでの造形が必要です。
注意点
- 読み込んだ値は必ず
* 2 - 1で -1〜1 に戻す - 法線マップは色ではなく数値データ。sRGBではなくリニアで読む
- 形状は変わらないのでシルエットには効かない
まとめ
- 法線マップは面の向きを画像で持たせる技術
- 紫色に見えるのは、平らな部分の法線が(0.5, 0.5, 1.0) だから
- ハイトマップの高さの差から計算すれば、自分でも作れる
- ライティングの式は変えず、法線の取得元を変えるだけ
法線マップは、コストの割に見た目の向上が最も大きいテクニックのひとつです。仕組みが分かると既製の素材も読めるようになるので、まずは簡単なハイトマップから自分で作ってみるのがおすすめです。

