「UnityShader筆記」07. 在世界空間下基於法線貼圖實現凹凸紋理效果
阿新 • • 發佈:2022-06-06
Part1.程式碼逐段解析
struct a2v{
float4 vertex : POSITION; //頂點模型空間位置
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0; //紋理uv
};
在頂點著色器的輸入結構中,我們需要獲取模型空間下頂點的位置、法向量、切向量、紋理uv
struct v2f{ float4 pos : SV_POSITION; //頂點在裁剪空間的位置 float4 uv : TEXCOORD0; //紋理uv float4 TtoW0 : TEXCOORD1; float4 TtoW1 : TEXCOORD2; float4 TtoW2 : TEXCOORD3; };
在從頂點著色器到片元著色器的通訊結構體中,需要獲取頂點在裁剪空間的位置,經過尺度、偏移變換後的紋理uv座標,以及三組float4型別的值,用來儲存從切線空間到世界空間的變換矩陣,以及頂點在世界空間的位置
v2f vert(a2v v){ v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; //計算世界空間的頂點座標 float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; //計算世界空間下的法向量 fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); //計算世界空間下的切向量 fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); //法向量與切向量叉乘,再乘以方向因子,得到副切向量 fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; //將代表x軸的切向量,代表y軸的副切向量,代表z軸的法線方向按列排列,即可得到從切線空間到世界空間的變換矩陣 //這裡需要將變換矩陣儲存在插值暫存器中,為了節省空間,使用閒置的w分量來儲存世界座標 o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y); o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z); return o; }
在頂點著色器中,我們的主要任務是:
- 獲取頂點在裁剪空間下的座標,用於片元著色
- 獲取頂點在世界空間下的座標,用於光照計算
- 計算紋理uv的尺度變換與偏移
- 計算從切線空間到世界空間的變換矩陣,用於在片元著色器中將切線空間的法向量變換到世界空間,從而在世界空間下進行光照計算
由於是從切線空間到世界空間的變換矩陣,我們不用做取逆操作,直接將三個座標軸向量按列拼接即可
為了最大化利用插值暫存器的空間,我們將頂點的世界座標的三個分量分別儲存在三個float4型別的w分量中
fixed4 frag(v2f i) : SV_Target{ //從w分量中提取出世界座標 float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w); //使用內建函式獲取世界空間下的光照方向 fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos)); //使用內建函式獲取世界空間下的觀察方向 fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos)); //通過對法線紋理進行取樣,獲取切線空間下的法線 fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw)); //對法線的x、y分量乘上凹凸程度變換因子 bump.xy *= _BumpScale; //利用法向量是單位向量的特性直接求解出z分量 bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy))); //使用頂點著色器中構造的變換矩陣,將法線從切線空間變換到世界空間 bump = normalize(half3(dot(i.TtoW0.xyz, bump),dot(i.TtoW1.xyz, bump),dot(i.TtoW2.xyz, bump))); //通過世界空間的法向量,進行光照計算,註釋從略(可參照之前的筆記) fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir)); fixed3 halfDir = normalize(lightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss); return fixed4(ambient + diffuse + specular, 1.0); }
在片元著色器中,我們的主要任務是:
- 通過世界空間的頂點座標,獲取世界空間下的光照方向和觀察方向
- 對法線紋理進行取樣,得到切線空間下的法向量
- 通過頂點著色器中構建的變換矩陣,將法向量變換到世界空間
- 通過法向量進行光照計算
Part2.一些問題
Q1.為什麼要在片元著色器進行法線變換?
因為法線紋理的取樣是必須在片元著色器才能完成的,因此只有在片元著色器才能獲得法線,而必須先獲取法線才能進行法線變換
Q2.為什麼在切線空間實現凹凸貼圖時,沒有通過插值暫存器傳輸變換矩陣?
因為在切線空間中,需要的變換是將光照方向、觀察方向從模型空間變換到切線空間,法線本身就在切線空間,是不需要變換的。而光照方向、觀察方向在頂點著色器就能獲取,因此在頂點著色器構造出變換矩陣後,直接就地對兩向量進行變換
Part3.完整程式碼
Shader "Chapter7/bumpText_World"
{
Properties
{
_Color ("Color Tint", Color) = (1,1,1,1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap("Normal Map",2D) = "bump"{}
_BumpScale("Bump Scale",Float) = 1.0
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20
}
SubShader
{
Pass{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
fixed4 frag(v2f i) : SV_Target{
float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
bump = normalize(half3(dot(i.TtoW0.xyz, bump),dot(i.TtoW1.xyz, bump),dot(i.TtoW2.xyz, bump)));
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
Part4.效果圖
在切線空間和世界空間下實現的凹凸貼圖,視覺效果是完全一樣的