Shader筆記十九——三種凹凸紋理實現
Normal Map
法線紋理是通過一張與漫反射紋理相對應的法線圖,儲存法線資訊,使用的時候對應紋理座標進行取樣,通過法線值影響光影計算的結果,從而產生凹凸效果。Normal Map可能是目前使用最為廣泛的一種凹凸貼圖技術了。之前的內容也有介紹過, https://zhuanlan.zhihu.com/p/31450857 這裡不詳述。貼一下Shader程式碼:
Shader "Bump/001_normal" { Properties{ _MainColor("MainColor",Color)=(1,1,1,1) _SpecularColor("SpecularColor",Color)=(1,1,1,1) _MainTex("MainTex",2D)="white"{} _BumpTex("BumpTex",2D)="bump"{} _BumpScale("BumpScale",Float)=1.0 _Gloss("Gloss",Range(8.0,256))=20 } SubShader{ Pass{ Tags{"RenderType"="Opaque" "LightMode"="ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" #define PI 3.14159265359 fixed4 _MainColor; fixed4 _SpecularColor; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpTex; float4 _BumpTex_ST; float _BumpScale; float _Gloss; struct a2v{ float4 vertex:POSITION; float4 texcoord:TEXCOORD0; float3 normal:NORMAL; float4 tangent:TANGENT; }; struct v2f{ float4 pos:SV_POSITION; float4 uv:TEXCOORD0; float3 lightDir:TEXCOORD1; float3 viewDir:TEXCOORD2; }; 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*_BumpTex_ST.xy+_BumpTex_ST.zw; //內建巨集,取得切線空間旋轉矩陣 TANGENT_SPACE_ROTATION; o.lightDir=mul(rotation,ObjSpaceLightDir(v.vertex).xyz); o.viewDir=mul(rotation,ObjSpaceViewDir(v.vertex).xyz); return o; } fixed4 frag(v2f i):SV_Target{ fixed3 tangentLightDir=normalize(i.lightDir); fixed3 tangentViewDir=normalize(i.viewDir); fixed3 tangentNormal=UnpackNormal(tex2D(_BumpTex,i.uv.zw)); tangentNormal.xy*=_BumpScale; tangentNormal.z=sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy))); fixed3 albedo=_MainColor.rgb*tex2D(_MainTex,i.uv.xy); fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo; //改進版 BRDF 函式 fixed3 diffuse=_LightColor0.rgb*albedo*max(0,saturate(dot(tangentNormal,tangentLightDir)))/PI; fixed3 halfDir=normalize(tangentLightDir+tangentViewDir); fixed3 specular=_LightColor0.rgb*_SpecularColor.rgb*pow(max(0,dot(tangentNormal,halfDir)),_Gloss)*max(0,saturate(dot(tangentNormal,tangentLightDir)))*(_Gloss+8)/(8*PI); return fixed4(ambient+diffuse+specular,1.0); } ENDCG } } }
Parallax Map
法線紋理在運用中有一個問題是,當視角發生變化時,並不會影響到凹凸的結果(漫反射計算與視角方向無關)。而實際上,當視角發生變化時,觀察到的凹凸不平表面的結果是不同的,為了儘量反映出凹凸效果與視角的相關性,有了Parallax Map和後面的Relief Map。Parallax Map 叫做視差貼圖,通過下圖(來自“Parallax Mapping with Offset Limiting: A PerPixel Approximation of Uneven")簡單瞭解下:
假如我們從eye所示方向觀察表面,由於表面的凹凸關係,我們實際看到的點應該是B點(即此時應該從B點處的紋理座標進行取樣),但由於紋理本身是一張平面圖,所以此時計算用的是A點的紋理座標取樣結果。為了糾正這一偏差結果,需要將取樣點的紋理座標進行適當偏移,使它靠近正確的B點,所以 Parallax Map 又叫做 Offset Map。但是想要正確的找到A點相對於B點的偏移量是比較麻煩的,大多是採用近似的偏移量來靠近B點(並不能精確到B點),這裡說一種:
//根據切線空間下的視角方向計算UV的取樣偏移 inline float2 CaculParallaxUVOffset(v2f i){ //高度圖高度取樣 float height=tex2D(_HeightTex,i.uv).r; float3 viewDir=normalize(i.viewDir); float2 offset=viewDir.xy/viewDir.z*height*_HeightScale; return offset; }
這裡用一個_HightScale係數外部控制偏移程度。Parallax Map的關鍵在於對切線空間的法線進行取樣並計算之前,通過視角方向上的偏移糾正取樣時的紋理座標,使取樣結果儘量靠近正確的取樣點。 完整Shader:
Shader "Bump/002_parallax"
{
Properties{
_MainColor("MainColor",Color)=(1,1,1,1)
_SpecularColor("SpecularColor",Color)=(1,1,1,1)
_MainTex("MainTex",2D)="white"{}
_BumpTex("BumpTex",2D)="bump"{}
_HeightTex("HeightTex",2D)="black"{}
_HeightScale("HeightScale",Range(0,0.2))=0.05
_Gloss("Gloss",Range(8,255))=20
}
SubShader{
Pass{
Tags{"RenderType"="Opaque" "LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#define PI 3.14159265359
fixed4 _MainColor;
fixed4 _SpecularColor;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
sampler2D _HeightTex;
float _HeightScale;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
float3 normal:NORMAL;
float4 tangent:TANGENT;
};
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 lightDir:TEXCOORD1;
float3 viewDir:TEXCOORD2;
};
//根據切線空間下的視角方向計算UV的取樣偏移
inline float2 CaculParallaxUVOffset(v2f i){
//高度圖高度取樣
float height=tex2D(_HeightTex,i.uv).r;
float3 viewDir=normalize(i.viewDir);
float2 offset=viewDir.xy/viewDir.z*height*_HeightScale;
return offset;
}
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
TANGENT_SPACE_ROTATION;
o.lightDir=mul(rotation,ObjSpaceLightDir(v.vertex).xyz);
o.viewDir=mul(rotation,ObjSpaceViewDir(v.vertex).xyz);
return o;
}
fixed4 frag(v2f i): SV_Target{
fixed3 albedo=_MainColor.rgb*tex2D(_MainTex,i.uv);
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb*albedo;
//在對法線進行取樣前,新進行UV偏移
i.uv+=CaculParallaxUVOffset(i);
fixed3 tangentNormalDir=UnpackNormal(tex2D(_BumpTex,i.uv));
fixed3 tangentLightDir=normalize(i.lightDir);
fixed3 tangentViewDir=normalize(i.viewDir);
fixed3 halfDir=normalize(tangentViewDir+tangentLightDir);
//改進版 BRDF
fixed3 diffuse=_LightColor0.rgb*ambient*max(0,saturate(dot(tangentNormalDir,tangentLightDir)))/PI;
fixed3 specular=_LightColor0.rgb*_SpecularColor.rgb*pow(max(0,dot(tangentNormalDir,halfDir)),_Gloss)*max(0,saturate(dot(tangentNormalDir,tangentLightDir)))*(_Gloss+8)/(8*PI);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
}
實現效果: 這裡將之前的Normal Map與Parallax Map進行對比(左:Normal 右:Parallax),在視角變化的情況下,Parallax的凹凸效果會發生變化。
Relief Map
Relief Map又叫 浮雕紋理 ,是對 Parallax的進一步精確。如果說Parallax只是根據視角方向在切線空間下的投影和高度圖作近似的偏移,那麼Relief Map則是以找到正確點B點進行取樣為目標,具體分為兩步:
- 通過步進法找到交點的大致範圍
- 通過二分法進一步找到交點
步進法找到交點大致範圍,其中一種思路: 根據切線空間下的視角方向的垂直分量,確定層級高度和UV每次偏移取樣的步進距離,視角方向的垂直分量越大,說明需要偏移的距離越小,因此劃分的密度越大。每次步進時,層高逐層增加,並沿著視角方向的UV偏移進行高度圖取樣,直到滿足 currentLayerDepth>currentDepthMapValue的條件,即圖中紅線和黃線所示,說明此時已經找到對應交點的大致範圍,進入二分法精確確定交點,這一步的具體演算法:
//根據 切線空間視角方向在垂直於紋理表面的分量,確定步進的層數,越接近垂直,層數越多,步進距離越小
float layerNum=lerp(_MinLayerNum,_MaxLayerNum,abs(dot(float3(0,0,1),tangent_viewDir)));
float layerDepth=1.0/layerNum;
float currentLayerDepth=0.0;
float2 deltaUV=tangent_viewDir.xy/tangent_viewDir.z*_HeightScale/layerNum;
float2 currentTexCoords=uv;
float currentDepthMapValue=tex2D(_DepthTex,currentTexCoords).r;
while(currentLayerDepth<currentDepthMapValue){
currentTexCoords-=deltaUV;
//在迴圈內需要加上unroll來限制迴圈次數或者改用tex2Dlod,直接使用tex2D取樣會出現報錯
currentDepthMapValue=tex2Dlod(_DepthTex,float4(currentTexCoords,0,0)).r;
currentLayerDepth+=layerDepth;
}
二分法精確求交點: 在確定大致範圍後,步進距離每次減半,直到逼近目標值,一般是進行五次二分逼近能得到比較接近的結果:
Relief Map與Parallax Map的區別在於一個是精確求點,一個是向正確方向大致偏移,後續的處理結果類似。 完整Shader:
Shader "Bump/003_relief"
{
Properties
{
_MainColor("MaincColor",Color)=(1,1,1,1)
_SpecularColor("SpecualrColor",Color)=(1,1,1,1)
_MainTex("MainTex",2D)="white"{}
_BumpTex("BumpTex",2D)="bump"{}
_DepthTex("DepthTex",2D)="black"{}
_Gloss("Gloss",Range(8,256))=20
_HeightScale("HightScale",Range(-1.0,1.0))=0.1
_MinLayerNum("MinlayerNum",Range(0,100))=30
_MaxLayerNum("MaxLayerNum",Range(0,200))=50
}
SubShader{
Tags{"RenderType"="Opaque"}
Pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#define PI 3.14159265359
fixed4 _MainColor;
fixed4 _SpecularColor;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
sampler2D _DepthTex;
float _Gloss;
float _HeightScale;
float _MinLayerNum;
float _MaxLayerNum;
struct a2v{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
float3 normal:NORMAL;
float4 tangent:TANGENT;
};
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 lightDir:TEXCOORD1;
float3 viewDir:TEXCOORD2;
};
//通過步進方式找到 視角方向 與紋理交點 的實際高度值
float2 ReliefMappingUV(float2 uv,float3 tangent_viewDir){
//根據 切線空間視角方向在垂直於紋理表面的分量,確定步進的層數,越接近垂直,層數越多,步進距離越小
float layerNum=lerp(_MinLayerNum,_MaxLayerNum,abs(dot(float3(0,0,1),tangent_viewDir)));
float layerDepth=1.0/layerNum;
float currentLayerDepth=0.0;
float2 deltaUV=tangent_viewDir.xy/tangent_viewDir.z*_HeightScale/layerNum;
float2 currentTexCoords=uv;
float currentDepthMapValue=tex2D(_DepthTex,currentTexCoords).r;
while(currentLayerDepth<currentDepthMapValue){
currentTexCoords-=deltaUV;
//在迴圈內需要加上unroll來限制迴圈次數或者改用tex2Dlod,直接使用tex2D取樣會出現報錯
currentDepthMapValue=tex2Dlod(_DepthTex,float4(currentTexCoords,0,0)).r;
currentLayerDepth+=layerDepth;
}
//進行二分法查詢
float2 halfDeltaUV=deltaUV/2.0;
float halfLayerDepth=layerDepth/2.0;
currentTexCoords+=halfDeltaUV;
currentLayerDepth+=halfLayerDepth;
int searchesNum=5;
for(int i=0;i<searchesNum;i++){
halfDeltaUV=halfDeltaUV/2.0;
halfLayerDepth=halfLayerDepth/2.0;
currentDepthMapValue=tex2Dlod(_DepthTex,float4(currentTexCoords,0,0)).r;
if(currentLayerDepth<currentDepthMapValue){
currentTexCoords-=halfDeltaUV;
currentLayerDepth+=halfLayerDepth;
}
else{
currentTexCoords+=halfDeltaUV;
currentLayerDepth-=halfLayerDepth;
}
}
return currentTexCoords;
}
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
TANGENT_SPACE_ROTATION;
o.lightDir=normalize(mul(rotation,ObjSpaceLightDir(v.vertex).xyz));
o.viewDir=normalize(mul(rotation,ObjSpaceViewDir(v.vertex).xyz));
return o;
}
fixed4 frag(v2f i):SV_Target{
float3 tangent_lightDir=normalize(i.lightDir);
float3 tangent_viewDir=normalize(i.viewDir);
float2 uv=ReliefMappingUV(i.uv,tangent_viewDir);
//去掉邊緣越界造成的紋理取樣異常
if(uv.x>1.0||uv.y>1.0||uv.x<0.0||uv.y<0.0)
discard;
float3 albedo=_MainColor.rgb*tex2D(_MainTex,uv).rgb;
float3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb*albedo;
float3 tangent_normal=normalize(UnpackNormal(tex2D(_BumpTex,uv)));
//改進版 BRDF
float3 diffuse=_LightColor0.rgb*albedo*max(0,saturate(dot(tangent_normal,tangent_lightDir)))/PI;
float3 halfDir=normalize(tangent_viewDir+tangent_lightDir);
float3 specular=_LightColor0.rgb*_SpecularColor.rgb*pow(saturate(dot(halfDir,tangent_normal)),_Gloss)*(8+_Gloss)/(8*PI);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
}
實際效果: (左:Normal 中:Parallax 右:Relief) Relief由於在Shader中進行了迴圈操作,比較費效能。