【Unity】ノーマルマップを勉強してみた【NormalMap】

投稿者: | 2020年2月2日

はじめに

理屈はわかるけど実際組み込んでいくと失敗したのでメモ

このようなレンダリングをするまでの話です。

最終形

利用した法線マップがこちらです。

これを球に貼り付けているだけです。

混乱の原因

 普段は 頂点シェーダーで自分をグローバル座標に変換して、ピクセルシェーダーでワールド空間のライトで計算して色付しています。

 法線マップを使う場合は 逆で 頂点のローカル座標に ライトと視線をもってきて、ピクセルシェーダーでローカル座標で計算するようです。

 ライトをローカル座標にもってくるまではわかっていたのですが、視線はグローバル座標でピクセルシェーダーにもってきたのが混乱の原因でした。これでピクセルシェーダー内で法線をグローバル座標に変換し直して、再計算しようとしておかしな絵になってしまいました。
 視線も頂点シェーダー内でローカル座標に変換すればよかったんですね。

対応

頂点シェーダーで逆行列を作る

頂点シェーダー内で 法線と 接線 から逆行列を作り カメラとライトを頂点のローカル座標を基準に向きを変える。

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

疑問が解決できてスッキリ


参考させていただいたサイト様

この作品はユニティちゃんライセンス条項の元に提供されています

【Unity】ノーマルマップを勉強してみた【NormalMap】」への1件のフィードバック

  1. ピンバック: 【Unity】せっかくなのでプロジェクターでノーマルマップを投影してみた【Shader】 | | ぶろねこ -Blog on NEKOTEAM-

コメントを残す