【Unity】ノーマルマップを勉強してみた【NormalMap】
はじめに
理屈はわかるけど実際組み込んでいくと失敗したのでメモ
このようなレンダリングをするまでの話です。
利用した法線マップがこちらです。
これを球に貼り付けているだけです。
混乱の原因
普段は 頂点シェーダーで自分をグローバル座標に変換して、ピクセルシェーダーでワールド空間のライトで計算して色付しています。
法線マップを使う場合は 逆で 頂点のローカル座標に ライトと視線をもってきて、ピクセルシェーダーでローカル座標で計算するようです。
ライトをローカル座標にもってくるまではわかっていたのですが、視線はグローバル座標でピクセルシェーダーにもってきたのが混乱の原因でした。これでピクセルシェーダー内で法線をグローバル座標に変換し直して、再計算しようとしておかしな絵になってしまいました。
視線も頂点シェーダー内でローカル座標に変換すればよかったんですね。
対応
頂点シェーダーで逆行列を作る
頂点シェーダー内で 法線と 接線 から逆行列を作り カメラとライトを頂点のローカル座標を基準に向きを変える。
float3 n = normalize( v.normal);
float3 t = normalize( v.tangent.xyz);
float3 b = cross( n, t) * v.tangent.w;
// 逆行列
float3x3 invM = float3x3( t, b, n);
// ライトと視線をローカル座標内にもってくる
o.lightDir = mul( invM, ObjSpaceLightDir( v.vertex));
o.viewDir = mul( invM, ObjSpaceViewDir( v.vertex));
ピクセルシェーダー
ピクセルシェーダー内ではローカル座標として最後まで計算するようです。
ライトの計算
half4 frag( v2f i) : SV_Target
{
float3 L = normalize( i.lightDir);
float3 V = normalize( i.viewDir);
float3 halfDir = normalize( L + V);
float3 N = UnpackNormal( tex2D( _NormalMap, i.uv));
float3 diff = saturate( dot( N, L)) * _LightColor0;
float spec = pow( max( 0, dot( N, halfDir)), 50);
return float4( diff + spec, 1);
}
float3 N = UnpackNormal( tex2D( _NormalMap, i.uv));
の部分を表示してみると、このように正面を向いた法線になっています。確かにローカル座標です。
自分はこの法線をグローバルに変換しようとして失敗しました。
ノーマルマップでライトを計算しているだけのシンプルなシェーダー
Shader "Neko/Normal"
{
Properties
{
[Normal]_NormalMap("Normal Map", 2D) = "bump" {}
}//Properties
CGINCLUDE
#include "UnityCG.cginc"
#include "Lighting.cginc"
ENDCG
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
sampler2D _NormalMap;
float4 _NormalMap_ST;
float4 _NormalMap_TexelSize;
v2f vert( appdata v)
{
v2f o = (v2f)0;
o.vertex = UnityObjectToClipPos( v.vertex);
o.uv = float2( v.uv * _NormalMap_ST.xy + _NormalMap_ST.zw);
float3 n = normalize( v.normal);
float3 t = normalize( v.tangent.xyz);
float3 b = cross( n, t) * v.tangent.w;
// 逆行列
float3x3 invM = float3x3( t, b, n);
// ライトと視線をローカル座標内にもってくる
o.lightDir = mul( invM, ObjSpaceLightDir( v.vertex));
o.viewDir = mul( invM, ObjSpaceViewDir( v.vertex));
return o;
}
half4 frag( v2f i) : SV_Target
{
float3 L = normalize( i.lightDir);
float3 V = normalize( i.viewDir);
float3 halfDir = normalize( L + V);
float3 N = UnpackNormal( tex2D( _NormalMap, i.uv));
// アルベドとスペキュラーを計算
float3 diff = saturate( dot( N, L));
float spec = pow( max( 0, dot( N, halfDir)), 50);
return float4( diff + spec, 1);
}
ENDCG
}//Pass
}//SubShader
}//Shader
逆に
頂点シェーダーでグローバル行列を作って、ピクセルシェーダーで法線をグローバル座標に変換するのは可能なのだろうか?という疑問が出てきた。
やってみた。 できた。
逆行列の逆行列を作ってピクセルシェーダーに渡して、ピクセルシェーダー内で法線を回転させれば、同じように出るようです。
計算量が大幅に増えるし 利用価値がなさそうですが、どうしてもピクセル単位でグローバル座標で計算する場合は使えるかもしれないです。
ピクセルシェーダー内でグローバル座標でライト計算シェーダー
Shader "Neko/Normal"
{
Properties
{
[Normal]_NormalMap("Normal Map", 2D) = "bump" {}
}//Properties
CGINCLUDE
#include "UnityCG.cginc"
#include "Lighting.cginc"
ENDCG
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float4 vertexW : TEXCOORD1;
float3 m0 : TEXCOORD2;
float3 m1 : TEXCOORD3;
float3 m2 : TEXCOORD4;
};
sampler2D _NormalMap;
float4 _NormalMap_ST;
float4 _NormalMap_TexelSize;
v2f vert( appdata v)
{
v2f o = (v2f)0;
o.vertex = UnityObjectToClipPos( v.vertex);
o.uv = float2( v.uv * _NormalMap_ST.xy + _NormalMap_ST.zw);
float3 n = normalize( v.normal);
float3 t = normalize( v.tangent.xyz);
float3 b = cross( n, t) * v.tangent.w;
// 逆行列
float3x3 invM = float3x3( t, b, n);
// 更に逆行列変換する
float c00 = invM._m11 * invM._m22 - invM._m12 * invM._m21;
float c10 = invM._m12 * invM._m20 - invM._m10 * invM._m22;
float c20 = invM._m10 * invM._m21 - invM._m11 * invM._m20;
float det = invM._m00 * c00 + invM._m01 * c10 + invM._m02 * c20;
if( det != 0)
{
float invDet = 1.0 / det;
o.m0 = float3(
c00 * invDet,
( invM._m02 * invM._m21 - invM._m01 * invM._m22) * invDet,
( invM._m01 * invM._m12 - invM._m02 * invM._m11) * invDet
);
o.m1 = float3(
c10 * invDet,
(invM._m00 * invM._m22 - invM._m02 * invM._m20) * invDet,
(invM._m02 * invM._m10 - invM._m00 * invM._m12) * invDet
);
o.m2 = float3(
c20 * invDet,
(invM._m01 * invM._m20 - invM._m00 * invM._m21) * invDet,
(invM._m00 * invM._m11 - invM._m01 * invM._m10) * invDet
);
}
// ワールド座標
o.vertexW = mul( unity_ObjectToWorld, v.vertex);
return o;
}
half4 frag( v2f i) : SV_Target
{
float3 L = normalize( _WorldSpaceLightPos0.xyz); // ワールド差表のライト
float3 V = normalize( _WorldSpaceCameraPos - i.vertexW.xyz); // ワールド座標の視線
float3 halfDir = normalize( L + V);
// 法線を回転させる
float3 N = UnpackNormal( tex2D( _NormalMap, i.uv));
float3x3 m = float3x3( i.m0, i.m1, i.m2);
N = normalize( mul( m, N));
// アルベドとスペキュラーを計算
float3 diff = saturate( dot( N, L));
float spec = pow( max( 0, dot( N, halfDir)), 50);
return float4( diff + spec, 1);
}
ENDCG
}//Pass
}//SubShader
}//Shader
疑問が解決できてスッキリ
参考させていただいたサイト様