【UnityShader】法線マップ #40
前回の成果
雪を降らせた。
今回やること
法線マップを勉強します。
事前準備
Scene上にPlaneを配置し、Cameraから見えるように回転させてください。
こちらの画像はShaderのプロパティのNormalMapにアタッチしてください。
ソースコード
Shader "Unlit/Normalmap" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _NormalMap ("Normal map", 2D) = "bump" {} _Shininess ("Shininess", Range(0.0, 1.0)) = 0.078125 } SubShader { Tags { "Queue"="Geometry" "RenderType"="Opaque"} Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #include "UnityCG.cginc" #pragma vertex vert #pragma fragment frag float4 _LightColor0; sampler2D _MainTex; sampler2D _NormalMap; half _Shininess; struct appdata { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct v2f { float4 pos : SV_POSITION; half2 uv : TEXCOORD0; half3 lightDir : TEXCOORD1; half3 viewDir : TEXCOORD2; }; v2f vert(appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord.xy; TANGENT_SPACE_ROTATION; o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)); o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)); return o; } float4 frag(v2f i) : COLOR { i.lightDir = normalize(i.lightDir); i.viewDir = normalize(i.viewDir); half3 halfDir = normalize(i.lightDir + i.viewDir); half4 tex = tex2D(_MainTex, i.uv); half3 normal = UnpackNormal(tex2D(_NormalMap, i.uv)); half4 diff = saturate(dot(normal, i.lightDir)) * _LightColor0; half3 spec = pow(max(0, dot(normal, halfDir)), _Shininess * 128.0) * _LightColor0.rgb * tex.rgb; fixed4 color; color.rgb = tex.rgb * diff + spec; return color; } ENDCG } } }
法線マップとは
テクスチャ画像の情報を使い、平板なモデルの表面に凹凸があるかのように見せる手法。陰影情報が記録されたRGB画像を用いて、モデル表面の法線(normal)を変化させることで凹凸を表現する。
とのことです。
法線マップは青紫色(下記画像右)のようなもので、適応すると(下記画像左)凹凸を表現できます。
では、Shaderの解説に移ります。
TANGENT_SPACE_ROTATION
これは接空間への3*3の行列を生成するマクロです。
定義は以下となっています。
// Declares 3x3 matrix 'rotation', filled with tangent space basis #define TANGENT_SPACE_ROTATION \ float3 binormal = cross( v.normal, v.tangent.xyz ) * v.tangent.w; \ float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
このマクロを解説するために必要な接空間から説明します。
接空間
これは3Dオブジェクトの各点で定義させるベクトル空間のことです。
下記の画像の灰色の半透明の平面を基準とした座標空間です。
そして、この平面の法線と該当の点の法線は一致します。
接ベクトル空間 - Wikipedia:より引用
接空間を使用する理由
下記の画像の青軸は、3Dオブジェクトの表面の法線を表しています。
一般的なシェーディングでは、この法線を使用して陰影をつけていきます。
しかし、これは各点毎に固定された値を持っているので、凹凸を表現することができません。
そこで、法線マップを使用して凹凸を表現するのですが、法線マップの存在する空間は接空間です。
ですので、接空間を使用します。
法線マップと接空間 - しゅみぷろ:より引用
空間の変換
接空間を使用してシェーディングを行うためには、オブジェクトの法線マップの値とライトの方向ベクトルを同じ空間に変換する必要があります。
やり方としては、
- 法線マップをライトやカメラの座標空間に変換
- ライトやカメラを接空間に変換
の2つがあります。
前者の場合、法線マップはピクセルシェーダーで取得することになり、座標の変換処理がピクセル処理時に毎回行われます。
後者の場合、頂点シェーダーでライトやカメラのベクトルを変換するだけで良いので、前者に比べて計算量が少ないです。
ですので、今回は後者を使用します。
従法線
従法線とは、
空間曲線上の1点で接触平面に垂直な直線
従法線(じゅうほうせん)とは - コトバンク:より引用 のことです。
なぜ従法線が必要なのかは、長くなってしまうので以下のサイト様を参考にしてください。
この従法線を求めるためには、法線と接ベクトルの外積で出すことができるのでこちらのソースコードで求められます。
float3 binormal = cross( v.normal, v.tangent.xyz ) * v.tangent.w;
接空間への変換
接空間は、
- 接ベクトル
- 従法線
- 法線
の3つのベクトルを行列にすることによって求めることができます。
各ベクトルのイメージ
名前 | 英語名 | 軸の色 |
---|---|---|
接ベクトル | Tangent | 赤色 |
従法線 | Binormal | 緑色 |
法線 | Normal | 青色 |
法線マップと接空間 - しゅみぷろ:より引用
以上から、TANGENT_SPACE_ROTATIONマクロの中身が理解できました。
接空間を理解するために参考にしたサイト様
ライトとカメラの方向ベクトルを接空間に変換
TANGENT_SPACE_ROTATIONマクロにより、rotation変数に3*3の行列が入っているので、以下のソースコードで接空間に変換できます。
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)); o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));
ObjSpaceLightDir
ライトのオブジェクト空間での方向ベクトルを取得できます。
ObjSpaceViewDir
カメラのオブジェクト空間での方向ベクトルを取得できます。
UnpackNormal
テクスチャに0~1で書き込まれている法線の情報を-1~1の値に変換するものです。
以下定義です。
inline fixed3 UnpackNormal(fixed4 packednormal) { #if defined(UNITY_NO_DXT5nm) return packednormal.xyz * 2 - 1; #else return UnpackNormalDXT5nm(packednormal); #endif }
diff,spec
この2つの変数は、拡散反射光(diffuse)と鏡面反射光(spacular)のことです。
詳しい解説は次回の記事で行います。
結果
Planeに凹凸が出れば完成です。
ソースコードにコメントを付与
Shader "Unlit/Normalmap" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _NormalMap ("Normal map", 2D) = "bump" {} _Shininess ("Shininess", Range(0.0, 1.0)) = 0.078125 } SubShader { Tags { "Queue"="Geometry" "RenderType"="Opaque"} Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #include "UnityCG.cginc" #pragma vertex vert #pragma fragment frag float4 _LightColor0; sampler2D _MainTex; sampler2D _NormalMap; half _Shininess; struct appdata { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct v2f { float4 pos : SV_POSITION; half2 uv : TEXCOORD0; half3 lightDir : TEXCOORD1; half3 viewDir : TEXCOORD2; }; v2f vert(appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord.xy; // 接空間の行列を取得 TANGENT_SPACE_ROTATION; // ライトの方向ベクトルを接空間に変換 o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)); // カメラの方向ベクトルを接空間に変換 o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)); return o; } float4 frag(v2f i) : COLOR { i.lightDir = normalize(i.lightDir); i.viewDir = normalize(i.viewDir); half3 halfDir = normalize(i.lightDir + i.viewDir); half4 tex = tex2D(_MainTex, i.uv); half3 normal = UnpackNormal(tex2D(_NormalMap, i.uv)); // 拡散反射光 half4 diff = saturate(dot(normal, i.lightDir)) * _LightColor0; // 鏡面反射光 half3 spec = pow(max(0, dot(normal, halfDir)), _Shininess * 128.0) * _LightColor0.rgb * tex.rgb; fixed4 color; color.rgb = tex.rgb * diff + spec; return color; } ENDCG } } }
今回はこれで以上となります。
ここまでご視聴ありがとうございました!