【Unity】ノーマルマップを利用したシェーダー


はじめに

こちらのアセットを使用して解説しています。
https://assetstore.unity.com/packages/templates/tutorials/shader-calibration-scene-25422?locale=ja-JP

ノーマルマップとは

物体の凹凸を表現するために使われるテクスチャのことです。
モデルに直接凹凸をつけるよりもポリゴン数を抑えることができます。
また、ノーマルマップを使って、凹凸を表現することをノーマルマッピングと言います。
似たようなマップに ハイトマップ があります。

以下の画像はノーマルマップの例です。

凹凸を表現するマップ一覧

  • バンプマップ
  • ハイトマップ
  • ノーマルマップ
  • ディスプレイスメントマップ

各マップの詳細な説明に関しては下記の記事が参考になりました。
https://styly.cc/ja/tips/blender-mapping/

ノーマルマップを使って凹凸を表現する方法

ノーマルマップのカラーは「 接空間 でのベクトル」を示しています。
接空間とは、モデルの各頂点ごとに定義されるベクトル空間のことで、法線の方向をY軸としています。
X軸は 正接(Tangent)、Z軸は 従法線(Binormal) と呼ばれています。
下記の画像はそのイメージ図です。

ノーマルマップを考慮しない場合は、法線ベクトルとライトベクトルの内積で拡散反射光の強さを求められましたが、
ノーマルマップを考慮する場合は、ノーマルマップの色から求めた「接空間のベクトル」とライトベクトルの内積を行います。

ノーマルマップからベクトルに変換される仕組み

この仕組みはShaderLabでは自動的に行われるため、飛ばしても問題構いません。

以下の式は、ベクトルから色に変換する式です。

R=x2+0.5G=y2+0.5B=z2+0.5\begin{align} R &= \frac{x}{2}+0.5 \\\\ G &= \frac{y}{2}+0.5 \\\\ B &= \frac{z}{2}+0.5 \\\\ \end{align}

接空間では、Z方向(法線方向)が上になるため、ノーマルマップは全体的に青っぽくなります。

実際にこの式を使って、ベクトルからカラーに変換できる環境を以下のサンドボックスに用意しましたので、触ってみてください。

https://codesandbox.io/embed/github/went5/vector-to-normal-color-p5/tree/main/?fontsize=14&hidenavigation=1&theme=dark

ShaderLabでノーマルマッピングを実装する

NormalMap.shader

https://gist.github.com/went5/b0af4095df8d1cc2cc8b3db7fd5d406a

環境

  • Unity 2021.2.11f1
  • Build-in Rendering Pipline

法線ベクトルと正接ベクトルと従法線ベクトルを取得する

struct appdata
{
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 tangent:TANGENT;
    float2 uv:TEXCOORD0;
};

v2f vert(appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = v.uv;
    o.normal = UnityObjectToWorldNormal(v.normal);
    o.tangent = normalize(mul(unity_ObjectToWorld, v.tangent.xyz));
    o.binormal = normalize(cross(v.normal, v.tangent) * v.tangent.w);
    return o;
}

頂点シェーダーでは、接空間での各方向の軸を求めています。これらの軸をどう使うかについては、後ほど説明します。

tangent は構造体で定義しておくことで取得できます。このままだとローカル座標系のままなので、ワールド座標系にするための行列をかけます。

binormal は構造体で定義しても求められないため、計算して求めます。
binormalnormaltangent の外積で求められるため cross を使い求めます。
tangentのwにはビルド後のプラットフォームの違いを吸収するための値(1か-1)が入っています1

ノーマルマップから接空間のベクトルを取得する

float3x3 tangentTransform = float3x3(i.tangent, i.binormal, i.normal);
float3 localNormal = UnpackNormal(tex2D(_NormalMap, i.uv));
float3 worldNormal = normalize(mul(localNormal, tangentTransform));

UnpackNormal はノーマルマップから取得した色をベクトルに変換する関数です。
変換したベクトルはローカル座標系なので、ワールド座標系に変換する必要があります。

頂点シェーダーで求めた3つのベクトル( normal binormal tangent)
を元に、変換行列(tangentTransform)を作ります。
変換行列とノーマルマップから取得したベクトルをかけることで、ワールド座標系に変換できます。行列の掛け算は順番が違うと結果が変わるので注意してください。

これが、頂点シェーダーで接空間での各方向の軸を求めた理由です。

拡散反射光の強さを求める

float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
float NdotL = dot(worldNormal, lightDir);

法線ベクトルとライトベクトルの内積をすると拡散反射光の強さになるため、法線ベクトルを接空間でのベクトルに置き換えて内積をします。

以上で、冒頭の画像を実装できます。

参考

https://docs.unity3d.com/ja/2019.4/Manual/StandardShaderMaterialParameterNormalMap.html
https://esprog.hatenablog.com/entry/2016/05/01/025634
https://styly.cc/ja/tips/blender-mapping/

Footnotes

  1. UnityのシェーダーでBINORMAL入力セマンティクスが機能しない話と回避する方法の話

Table of contents


©️ 2026 went5. blog icon by

icons8