1、基礎光照
(1)看世界的光
模擬真實的光照環境來生成一張圖像,需要考慮3種物理現象。
- 光線從光源中被發射出來。
- 光線和場景中的一些物體相交:一些光線被物體吸收了,而另一些光線被散射到其他方向
- 攝像機吸收了一些光,產生了一張圖像。
【量化光】
使用輻照度來量化光。
對于平行光來說,它的輻照度可通過計算在垂直于I(光照方向)的單位面積上單位時間內穿過的能量來得到的。
當物體表面和I不垂直時,可以使用光源方向I和表面法線n之間的夾角的余弦值來得到。
因為輻照度是和照射到物體表面時光線之間的距離成反比的,因此輻照度和
成正比。
可以使用光源方向I和表面法線n的點積來得到。
【吸收和散射】
散射只改變光線的方向,但不改變光線的密度和顏色。
吸收只改變光線的密度和顏色,但不改變光線的方向。
光線在物體表面經過散射后,有兩種方向:一種將會散射到物體內部,這個叫折射或透射;另一種將會散射到外部,這種叫反射。
對于不透明物體,折射的光線會在物體內部繼續傳播,最終有一部分光線會重新從物體表面被發射出去。
為了區分兩種不同的散射方向,在光照模型中使用了不同部分來計算它們:
1)高光發射(specular)部分表示物體表面是如何反射光線的
2)漫發射(diffuse)部分則表示有多少光線被折射、吸收和散射出表面。
根據入射光線的數量和方向,可以計算出射光線的數量和方向,通常使用出射度(exitance)來描述它。輻照度和出射度之間是滿足線性關系的,而它們之間的比值就是材質的漫反射和高光反射屬性。
【著色】
著色:根據材質屬性(如漫反射屬性等)、光源信息(如光源方向、輻照度等),使用一個等式去計算沿某個觀察方向的出射度的過程。
這個等式稱為光照模型。
不同的光照模型有不同的目的,例如一些用于描述粗糙的物體表面,一些用于描述金屬表面等。
(2)標準光照模型
基本方法:把進入攝像機內的光線分為4個部分,每個部分使用一種方法來計算它的貢獻度。
1)自發光(emissive):當給定一個方向時,一個表面本身會向該方向發射多少輻射量。
2)高光發射(specular):當光線從光源照射到模型表面時,該表面會在完全鏡面反射方向散射多少輻射量
3)漫反射(diffuse):當光線從光源照射到模型表面時,該表面會向每個方向散射多少輻射量。
4)環境光(ambient):其他所有的間接光照。
【逐像素還是逐頂點】
在哪里計算光照模型?
在片元著色器中計算,被稱為逐像素光照(per-pixel lighting)。
在頂點著色器中計算,被稱為逐頂點光照(per-vertex lighting)。
【saturate(x)函數】
把x截取在[0,1]范圍內,如果x是一個矢量,那么會對它的每一個分量進行這樣的操作。
(3)漫反射光照模型
基本光照中漫反射部分的計算公式:
LightMode標簽是Pass標簽中的一種,用于定義該Pass在Unity的光照流水線中的角色。只有定義了正確的LightMode,才能得到一些Unity的內置光照變量。
內置變量:
_Diffuse:材質的漫反射顏色
_LightColor0:光源的顏色和強度信息(想要得到正確的值需要定義合適的LightMode標簽)
_WorldSpaceLightPos0:光源方向(假設場景中只有一個光源且該光源的類型是平行光)
? ? ? ? ? ? ? ? // Transform the normal from object space to world space
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
在3D圖形中,頂點位置使用模型到世界矩陣進行變換即可,但是法線向量不能直接用這個矩陣來變換。法線是方向向量,代表的是垂直于表面的方向。當模型發生非均勻縮放時(比如在X軸縮放2倍,Y軸不變),直接用_Object2World變換法線會導致它不再垂直于表面。
正確的做法:使用模型到世界變換矩陣的逆轉置矩陣來變換法線。
(float3x3)_World2Object:因為法線是方向向量(沒有位置信息),我們只需要變換方向,不需要平移部分,所以取_World2Object的3*3子矩陣(去掉最后一行一列的平移項),只保留旋轉和縮放部分。
逐頂點代碼:
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "Custom/DiffuseVertexLevel"
{Properties{_Diffuse("Diffuse", Color) = (1, 1, 1, 1)}SubShader{Pass{Tags{ "LightMode" = "ForwardBase"}CGPROGRAM#pragma vertex vert#pragma fragment frag#include "Lighting.cginc"fixed4 _Diffuse;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;};struct v2f{float4 pos: SV_POSITION;fixed3 color: COLOR;};v2f vert(a2v v){v2f o;// Transform the vertex from object space to projection spaceo.pos = UnityObjectToClipPos(v.vertex);// Get ambient termfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;// Transform the normal from object space to world spacefixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));// Get the light direction in world spacefixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);// Compute diffuse termfixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));o.color = ambient + diffuse;return o;}fixed4 frag(v2f i): SV_Target{return fixed4(i.color, 1.0);}ENDCG}}Fallback "Diffuse"
}
逐像素代碼:
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'Shader "Custom/DiffusePixelLevel"
{Properties{_Diffuse("Diffuse", Color) = (1, 1, 1, 1)}SubShader{Pass{Tags{ "LightMode" = "ForwardBase"}CGPROGRAM#pragma vertex vert#pragma fragment frag#include "Lighting.cginc"fixed4 _Diffuse;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;};struct v2f{float4 pos: SV_POSITION;float3 worldNormal: TEXCOORD0;};v2f vert(a2v v){v2f o;// Transform the vertex from object space to projection spaceo.pos = UnityObjectToClipPos(v.vertex);// Transform the normal from object space to world spaceo.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);return o;}fixed4 frag(v2f i): SV_Target{// Get ambient termfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;// Get the normal in world spacefixed3 worldNormal = normalize(i.worldNormal);// Get the light direction in world spacefixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);// Compute diffuse termfixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));fixed3 color = ambient + diffuse;return fixed4(color, 1.0);}ENDCG}}Fallback "Diffuse"
}
效果:
上面的漫反射光照模型也被稱為蘭伯特光照模型,因為它符合蘭伯特定律:在平面某點漫反射光線的光強與該反射點的法向量和入射光角度的余弦值成正比。
存在一個問題:在光照無法到達的區域,模型的外觀通常是全黑的,沒有任何明暗變化。
廣義的半蘭伯特光照模型公式:
絕大多數情況下和
的值均為0.5。
我們可以把的結果范圍從[-1,1]映射到[0,1]范圍內。
對于模型的背光面,原蘭伯特光照模型中點積結果將映射到同一個值,即0值處。而在半蘭伯特模型中,背光面也可以有明暗變化,不同的點積結果會映射到不同的值上。
半蘭伯特代碼:
Shader "Custom/HalfLambertMat"
{Properties{_Diffuse("Diffuse", Color) = (1, 1, 1, 1)}SubShader{Pass{Tags{ "LightMode" = "ForwardBase"}CGPROGRAM#pragma vertex vert#pragma fragment frag#include "Lighting.cginc"fixed4 _Diffuse;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;};struct v2f{float4 pos: SV_POSITION;float3 worldNormal: TEXCOORD0;};v2f vert(a2v v){v2f o;// Transform the vertex from object space to projection spaceo.pos = UnityObjectToClipPos(v.vertex);// Transform the normal from object space to world spaceo.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);return o;}fixed4 frag(v2f i): SV_Target{// Get ambient termfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;// Get the normal in world spacefixed3 worldNormal = normalize(i.worldNormal);// Get the light direction in world spacefixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);// Compute diffuse termfixed halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;fixed3 color = ambient + diffuse;return fixed4(color, 1.0);}ENDCG}}Fallback "Diffuse"
}
(4)高光反射光照模型
高光反射的計算公式:
4個參數:
入射光線的顏色和強度,材質的高光反射系數
,視角方向
以及反射方向
。
反射方向可以由表面法線和光源方向計算而得。
CG提供了計算反射方向的函數reflect(i, n),i是入射方向,n是法線方向。
【逐頂點方法】
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "Custom/SpecularVertexLevel"
{Properties{// 材質的漫反射顏色_Diffuse("Diffuse", Color) = (1,1,1,1) // 材質的高光反射顏色_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 _Diffuse;fixed4 _Specular;float _Gloss;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;};struct v2f{float4 pos: SV_POSITION;fixed3 color: COLOR;};v2f vert(a2v v){v2f o;// Transform the vertex from object space to projection spaceo.pos = UnityObjectToClipPos(v.vertex);// Get ambient termfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;// Transform the normal from object space to world spacefixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));// Get the light direction in world spacefixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);// Compute diffuse termfixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));// Get the reflect direction in world spacefixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));// Get the view direction in world spacefixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);// Compute Specular termfixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);o.color = ambient + diffuse + specular;return o;}fixed4 frag(v2f i): SV_Target{return fixed4(i.color, 1.0);}ENDCG}}FallBack "Diffuse"
}
_WorldSpaceLightPos0:表示從表面指向光源(常用光照方向),當光源在上方時,_WorldSpaceLightPos0=(0,1,0),表示光從上往下照。
由于CG的reflect函數的入射方向要求由光源指向交點處,因此我們需要對worldLightDir取反后再傳給reflect函數。
效果:
問題:高光部分明顯不平滑。因為高光反射部分的計算是非線性的,而在頂點著色器中計算光照再進行插值的過程是線性的,破壞了原計算的非線性關系,就會出現較大的視覺問題。因此需要使用逐像素的方法來計算高光反射。
【逐像素方法】
代碼:
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'Shader "Custom/SpecularPixelLevel"
{Properties{// 材質的漫反射顏色_Diffuse("Diffuse", Color) = (1,1,1,1) // 材質的高光反射顏色_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 _Diffuse;fixed4 _Specular;float _Gloss;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;};struct v2f{float4 pos: SV_POSITION;float3 worldNormal: TEXCOORD0;float3 worldPos : TEXCOORD1;};v2f vert(a2v v){v2f o;// Transform the vertex from object space to projection spaceo.pos = UnityObjectToClipPos(v.vertex);// Transform the normal from object space to world spaceo.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);// Transform the vertex from object space to world spaceo.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;return o;}fixed4 frag(v2f i): SV_Target{// Get ambient termfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;fixed3 worldNormal = normalize(i.worldNormal);// Get the light direction in world spacefixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);// Compute diffuse termfixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));// Get the reflect direction in world spacefixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));// Get the view direction in world spacefixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);// Compute Specular termfixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);return fixed4(ambient + diffuse + specular, 1.0);}ENDCG}}FallBack "Diffuse"
}
效果:
(5)Unity內置的函數
之前計算光源方向、視角方向的方法只適用于平行光,如果需要處理更復雜的光照類型(如點光源或聚光燈),之前計算光源方向的方法就是錯誤的。
Unity提供了一些內置函數來幫助我們計算這些信息,再UnityCG.cginc文件中。
上面的幫助函數使得我們不需要跟各種變換矩陣、內置變量打交道,也不需要考慮各種不同的情況(例如使用了哪種光源),而僅僅調用一個函數就可以得到需要的信息。
注意:上面的函數都沒有保證得到的方向矢量是單位矢量,因此,需要在使用前把它們歸一化。
通過內置函數,上面的代碼優化為:
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'Shader "Custom/SpecularPixelLevel"
{Properties{// 材質的漫反射顏色_Diffuse("Diffuse", Color) = (1,1,1,1) // 材質的高光反射顏色_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"#include "UnityCG.cginc"fixed4 _Diffuse;fixed4 _Specular;float _Gloss;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;};struct v2f{float4 pos: SV_POSITION;float3 worldNormal: TEXCOORD0;float3 worldPos : TEXCOORD1;};v2f vert(a2v v){v2f o;// Transform the vertex from object space to projection spaceo.pos = UnityObjectToClipPos(v.vertex);// Transform the normal from object space to world spaceo.worldNormal = UnityObjectToWorldNormal(v.normal);// Transform the vertex from object space to world spaceo.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;return o;}fixed4 frag(v2f i): SV_Target{// Get ambient termfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;fixed3 worldNormal = normalize(i.worldNormal);// Get the light direction in world spacefixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));// Compute diffuse termfixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));// Get the reflect direction in world spacefixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));// Get the view direction in world spacefixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));// Compute Specular termfixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);return fixed4(ambient + diffuse + specular, 1.0);}ENDCG}}FallBack "Diffuse"
}
2、基礎紋理
紋理最初的目的就是使用一張圖片來控制模型的外觀。使用紋理映射(texture mapping)技術,把一張圖“粘”在模型表面,逐紋素(texel)地控制模型的顏色。
紋理映射坐標定義了該頂點在紋理對應的2D坐標。通常,這些坐標使用一個二維變量(u,v)來表示,其中u是橫向坐標,而v是縱向坐標。因此,紋理映射坐標也被稱為UV坐標。
(1)單張紋理
目標:使用一張紋理來代替物體的漫反射顏色。
代碼如下:
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "Custom/SingleTexture"
{Properties{_Color("Color Tint", Color) = (1,1,1,1)_MainTex("Main Tex", 2D) = "whilte"{}_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;fixed4 _Specular;float _Gloss;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;float4 texcoord: TEXCOORD0;};struct v2f{float4 pos: SV_POSITION;float3 worldNormal: TEXCOORD0;float3 worldPos: TEXCOORD1;float2 uv: TEXCOORD2;};v2f vert(a2v v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;// Or just call the built-in function // o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);return o;}fixed4 frag(v2f i): SV_Target{fixed3 worldNormal = normalize(i.worldNormal);fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));// Use the texture to sample the diffuse colorfixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));fixed3 halfDir = normalize(worldLightDir + viewDir);fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);return fixed4(ambient + diffuse + specular, 1.0);}ENDCG}}FallBack "Specular"
}
_MainTex("Main Tex", 2D) = "white" {}
聲明了一個名為_MainTex的紋理,2D是紋理屬性的聲明方式。
使用一個字符串后跟一個花括號作為它的初始值,“white”是內置紋理的名字,也就是一個全白的紋理。
在CG代碼片中聲明紋理類型相匹配的變量,以便和材質面板中的屬性建立聯系:
_MainTex對應兩個:
- sampler2D _MainTex;
- float4 _MainTex_ST;
_MainTex_ST的名字不是任意起的,在Unity中,我們需要使用 紋理名_ST 的方式來聲明某個紋理的屬性。其中,ST是縮放(scale)和平移(translation)的縮寫。_MainTex_ST可以讓我們得到該紋理的縮放和平移(偏移)的值,_MainTex_ST.xy存儲的是縮放值,而_MainTex_ST.zw存儲的是偏移值。
在頂點著色器中:
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
計算過程:首先使用縮放屬性_MainTex_ST.xy對頂點紋理坐標進行縮放,然后再使用偏移屬性_MainTex_ST.zw對結果進行偏移。也可以使用內置宏TRANSFORM_TEX來幫助計算上述過程,第一個參數是頂點紋理坐標,第二個參數是紋理名。
在片元著色器中:
?fixed3 albedo = tex2D(_MainTex, i.uv).rgb *?_Color.rgb;
使用CG的tex2D函數對紋理進行采樣,它的第一個參數是需要被采樣的紋理,第二個參數是一個float2類型的紋理坐標,它將返回計算得到的紋素值。使用采樣結果和顏色屬性_Color的乘積來作為材質的反射率albedo,并把它和環境光照相乘得到環境光部分。
效果:
(2)紋理的屬性
紋理映射的簡單描述:聲明一個紋理變量,再使用tex2D函數采樣。
紋理的類型有:Texture類型、Normalmap類型等。
(導入素材后的界面)
之所以要為導入的紋理選擇合適的類型,是因為只有這樣才能讓Unity知道我們的意圖,為Unity Shader傳遞正確的紋理,并在一些情況下可以讓Unity對該紋理進行優化。
Wrap Mode:決定了當紋理坐標超過[0,1]范圍后將會如何被平鋪。
Filter Mode:決定了當紋理由于變換而產生拉伸時將會采用哪種濾波模式,支持Point、Bilinear、Trilinear模式。3種方式效果依次提升,但需要耗費的性能也依次增大。紋理濾波會影響放大或縮小紋理是得到的圖片質量。例如,當把一張64*64大小的紋理貼在一個512*512大小的平面上時,就需要放大紋理。
紋理縮放更加復雜的原因在于我們往往需要處理抗鋸齒的問題。
(3)凹凸映射
凹凸映射的目的:使用一張紋理來修改模型表面的法線,以便為模型提供更多的細節。這種方法不會真的改變模型的頂點位置,只是讓模型看起來好像是"凹凸不平"的樣子。
兩種方法:
- 高度映射(height mapping):使用一張高度紋理(height map)來模擬表面位移(displacement),然后得到一個修改后的法線值。
- 法線映射(normal mapping):使用一張法線紋理(normal map)來直接存儲表面法線。
1)高度映射
高度圖存儲的是強度值(intensity),用于表示模型表面局部的海拔高度。因此,顏色越淺表明該位置的表面越向外凸起,而顏色越深表明該位置越向里凹。
這種方法的好處是非常直觀,缺點是計算更加復雜。
高度圖通常會和法線映射一起使用,用于給出表面凹凸的額外信息。也就是說,我們通常會使用法線映射來修改光照。
2)法線紋理
法線紋理種存儲的是表面的法線方向。
由于法線方向的分量范圍在[-1,1],而像素分量范圍為[0,1],因此我們需要做一個映射:
這就要求在shader中對法線紋理進行紋理采樣后,還需要對結果進行一次反映射的過程,以得到原先的法線方向。
反映射的過程就是使用上面映射函數的逆函數:
3)白話說明
想象你有一個3D模型,比如一個石頭。這個石頭表面不是完全光滑的,有凹凸不平的細節,比如小坑、裂縫、凸起等。
在計算機圖形里,為了讓這個石頭看起來有這些細節,我們有兩種辦法:
- 真的把模型做得更復雜:加很多小三角形來做出凹凸(但這樣太費性能)。
- 假裝它有凹凸:用一張“貼圖”告訴計算機:“這里看起來應該是凸的,那里看起來是凹的”,但實際上模型還是平的——這就是**法線貼圖(Normal Map)**的作用。
法線貼圖怎么工作:
其實是一張記錄了“每個點表面朝向”的圖。每個像素的顏色代表了那個位置的“表面法線方向”(你可以理解為“表面是朝哪個方向凸出來的”)。
但方向是相對的,必須有個參考系——就像你說“前面”“左邊”,得先知道你臉朝哪。
所以關鍵問題是:這個方向是相對于誰來說的?
這就引出了兩種不同的“參考系”(也就是坐標空間):
1. 模型空間法線貼圖(Object-Space Normal Map)
- 所有點都用同一個參考系,就是整個模型自己的坐標系。
- 比如:整個石頭的“上”是Y軸,“右”是X軸,“前”是Z軸。
- 每個點的法線方向都是相對于這個統一坐標系來記錄的。
- 結果就是:不同方向的顏色五顏六色(因為有的地方朝上,有的朝左,有的斜著……顏色各不相同)。
- 缺點:這張貼圖只能給這一個特定模型用。換個模型或旋轉一下,就不對了。
2.切線空間法線貼圖(Tangent-Space Normal Map)
它的思路是:每個點都有自己的“局部坐標系”。
這個局部坐標系是怎么定的呢?三個方向:
- Z軸:垂直于表面 → 就是原來的法線方向(n)
- X軸:沿著表面的“切線”方向(t),比如紋理拉伸的方向
- Y軸:垂直于X和Z → 叫做副切線(b)
這三個軸合起來叫“TBN坐標系”,每個頂點都有自己的一套。
類比:你在地球上的某個點,你的“上”是頭頂(Z),你的“前”是鼻子方向(Y),你的“右”是右手方向(X)。雖然地球上每個人的方向都不同,但在自己看來都很自然。
現在,法線貼圖記錄的是:在這個點自己的坐標系下,新的法線往哪兒偏了?
- 如果沒變化(還是垂直于表面),那就是 (0, 0, 1) —— Z軸方向。
- 計算機把 (0,0,1) 轉換成顏色就是?RGB(0.5, 0.5, 1)?—— 淺藍色(利用前面法線轉像素的公式)
所以你看切線空間法線貼圖,一大片都是淺藍色,說明大多數地方沒有凹凸變化。
只有真正凹下去或凸出來的地方,顏色才會偏紅、偏綠、偏紫……
為什么切線空間更常用?
- 同一張法線貼圖可以用在不同模型上(比如磚墻貼圖用在墻上、地上、柱子上都行)
- 模型變形或動畫時也能正確工作(比如角色手臂彎曲,法線還能跟著變)
- ?貼圖看起來“整齊”,藍色為主,容易檢查錯誤
而這一切的前提,是我們用了“每個點自己看自己”的方式——也就是切線空間,才讓這種“藍色為主”的貼圖變得有意義又實用。
就像給每個小格子發了一個指南針,告訴它:“你是朝天的”,那就涂成藍色;“你往左歪了”,那就加點紅。
4)UV坐標
UV坐標就是給3D模型“貼地圖”時用的“經緯度”。
舉例:想象你有一個地球儀(3D模型),你想給它貼一張世界地圖(這就是你的紋理,比如顏色、凹凸圖等)。
問題是:地圖是一張flat的紙,地球是一個round的球,怎么貼才不歪、不皺、不錯位?
這時候就需要一個貼圖指南告訴計算機貼的位置,這個貼圖指南就是UV坐標。
UV怎么工作?
每個3D模型的頂點(就是模型上的一個點),都會有一個對應的(U,V)值。
比如:
- 一個頂點的 UV 是 (0.5, 0.5) → 就對應貼圖正中間的像素
- UV 是 (0, 0) → 貼圖左下角
- UV 是 (1, 1) → 貼圖右上角
這樣,計算機就知道:這個點該顯示貼圖哪個位置的顏色。
UV坐標用途:
1. 上顏色(漫反射貼圖)
你想讓一個角色臉上有痣、衣服有花紋,就得靠UV把“顏色圖”準確貼上去。
?2. 做凹凸(法線貼圖)
前面說的法線貼圖,也是靠UV找到每個點該用哪個法線方向。
3. 控制材質(金屬度、粗糙度等)
哪些地方亮?哪些地方糙?也都靠UV來定位。
?4. 動畫效果(比如水流、發光移動)
移動UV坐標,就能讓紋理“滑動”,看起來像水在流、光在跑。
5)在切線空間下計算
切線空間是由頂點法線和切線構建出的一個坐標空間。
在切線空間下計算光照模型。基本思路:在偏遠著色器中通過紋理采樣得到切線空間下的法線,然后再與切線空間下的視角方向、光照方向等進行計算,得到最終的光照結果。
代碼如下:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "Custom/NormalMapTangentSpace"
{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;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 * _BumpMap_ST.xy + _BumpMap_ST.zw;// Compute the binormalfloat3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;// construct a matrix which transform vectors from object space to rangent spacefloat3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);// transform the light direction from object space to teangent spaceo.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;// transform the view direction from object space to tangent spaceo.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);// Get the texel in the normal Mapfixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);fixed3 tangentNormal = UnpackNormal(packedNormal);tangentNormal.xy *= _BumpScale;tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);return fixed4(ambient + diffuse + specular, 1.0);}ENDCG}}FallBack "Specular"
}
對于法線紋理_BumpMap,使用“bump”作為它的默認值。
“bump"是Unity內置的法線紋理,當沒有提供任何法線紋理時,”bump“就對應了模型自帶的法線信息。_BumpScale則是用于控制凹凸程度的,當它為0時,意味著該法線紋理不會對光照產生任何影響。
為了的該紋理的屬性(平鋪和偏移系數),我們為_MainTex和_BumpMap定義了_MainTex_ST和_BuumpMap_ST變量。
使用TANGENT語義來描述float4類型的tangent變量,和法線方向normal不同,tangent是float4而非float3,是因為我們需要使用rangent.w分量來決定切線空間中的第三個坐標軸-副切線的方向性。
白話解釋:
每個頂點都有自己的“局部坐標系”,叫切線空間,由三個方向組成:
- T(Tangent):切線方向 → x軸(通常是紋理U方向)
- B(Bitangent):副切線方向 → y軸(通常是紋理V方向)
- N(Normal):法線方向 → z軸(垂直于表面)
這三個方向要構成一個“坐標系”,必須滿足手性規則(就像左右手)。
問題來了:T 和 N 我都有了,B 怎么算?
你可能會說:“用叉積不就行了?B = N × T”
沒錯!但叉積只能告訴你“垂直方向是哪個”,不能告訴你“正負”!
tangent.w
?的作用:副切線方向的“開關”:+1 表示正常,-1 表示反向。
由于使用了兩張紋理,因此需要存儲兩個紋理坐標。為此把v2f中的uv變量的類型定義為float4類型,其中xy分量存儲了_MainTex的紋理坐標,而zw分量存儲了_BumpMap的紋理坐標。
tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
法線紋理中存儲的是法線經過映射后得到的像素值,因此需要把它們反映射回來,首先把packedNormal的xy分量按之前提到的公式映射回法線方向,然后乘以_BumpScale(控制凹凸程度)來得到tangentNormal的xy分量。由于法線都是單位矢量,因此tangentNormal.z分量可以由tangentNormal.xy計算得到。
效果:
需要上傳2張圖片,一張是普通的紋理圖(Texture Type為Default),另一張是紋理法線圖(Texture Type設置為Normal map)。
(4)漸變紋理
例子:
使用這種方式可以自由地控制物體的漫反射光照。
代碼:
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "Custom/RampTexture"
{Properties{_Color("Color Tint", Color) = (1,1,1,1)_RampTex("Ramp Tex", 2D) = "white"{}_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 _RampTex;float4 _RampTex_ST;fixed4 _Specular;float _Gloss;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;float4 texcoord: TEXCOORD0;};struct v2f{float4 pos: SV_POSITION;float3 worldNormal: TEXCOORD0;float3 worldPos: TEXCOORD1;float2 uv: TEXCOORD2;};v2f vert(a2v v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);return o;}fixed4 frag(v2f i): SV_Target{fixed3 worldNormal = normalize(i.worldNormal);fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;// Use the texture to sample the diffuse Colorfixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb;fixed3 diffuse = _LightColor0.rgb * diffuseColor;fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));fixed3 halfDir = normalize(worldLightDir + viewDir);fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);return fixed4(ambient + diffuse + specular, 1.0);}ENDCG}}FallBack "Specular"
}
代碼解讀:
1)整體干啥:不用傳統的光照公式算顏色,而是用一張“顏色條”貼圖(即RampTex對應的貼圖)來決定物體亮還是暗。
2)_RampTex("Ramp Tex", 2D) = "white"{}:一張2D貼圖,默認是白色,這張圖就是“明暗對照表”。
3)SubShader:子著色器,真正干活的地方
4)Pass{}:一次渲染通道
5)float4 _RampTex_ST; 貼圖的平鋪(Scale)和偏移(Translate),Unity自動生成
6)struct a2v中是每個頂點都會帶來的信息,vertex:頂點在模型里的位置,normal:這個點的法線方向(表面朝哪),texcoord:UV坐標,用來查貼圖(只有橫豎坐標值,對應平鋪的貼圖)
7)float4 pos: SV_POSITION; 這個點最終在屏幕上的位置
8)TRANSFORM_TEX:是Unity的宏,等價于:
v.texcoord.xy * _RampTex_ST.xy + _RampTex_ST.zw
就是說:你可以縮放/移動貼圖位置。
9)fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
這個不是普通光照,而是半蘭伯特光照。
dot(worldNormal, worldLightDir):兩個方向的夾角點積,結果在[-1,1]。面對光,接近1;背對光,接近-1。 0.5* ... + 0.5:把[-1,1]映射到[0,1]。半蘭伯特就是按暗部也有點光,過渡更平滑。
10)fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb;
tex2D(_RampTex, ...):在_RampTex這張圖上取顏色
fixed2(halfLambert, halfLambert):用halfLambert當作UV坐標,比如halfLambert=0.8,就查貼圖的(0.8,0.8)的位置。假設_RampTex是一條從黑到白的橫條,亮的地方取白色,暗的地方取黑色,中間過渡去灰色或你設定的顏色,這樣就控制了明暗的風格。
最后乘以_Color,整體潤色。
11)fixed3 diffuse = _LightColor0.rgb * diffuseColor;
真正的漫反射=光源顏色 * 查表得到的顏色
12)fixed3 halfDir = normalize(worldLightDir + viewDir);
半角向量:光照方向和視線方向的中間方向,高光是否可見,要看法線是否接近這個方向
(5)遮罩紋理
作用:遮罩允許我們可以保護某些區域,使它們免于某些修改。
舉例:在之前的實現中,我們都是把高光反射應用到模型表面的所有地方,即所有的像素都使用同樣大小的高光強度和高光指數。但有時,我們希望模型表面某些區域的反光強烈一些,而某些區域弱一些。為了得到更加細膩的效果,我們就可以使用一張遮罩紋理來控制光照。
使用遮罩紋理的流程:通過采樣得到遮罩紋理的紋素值,然后使用其中某個(或某幾個)通道的值(例如texel.r)來與某種表面屬性進行相乘,這樣,當該通道的值為0時,可以保護表面不受該屬性的影響。
代碼:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "Custom/MaskTexture"
{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_SpecularMask("Specular Mask", 2D) = "white"{}_SpecularScale("Specular 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;float _BumpScale;sampler2D _SpecularMask;float _SpecularScale;fixed4 _Specular;float _Gloss;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;float4 tangent: TANGENT;float4 texcoord: TEXCOORD0;};struct v2f{float4 pos: SV_POSITION;float2 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;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(_BumpMap, i.uv));tangentNormal.xy *= _BumpScale;tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);// Get the mask valuefixed specularMask = tex2D(_SpecularMask, i.uv).rgb * _SpecularScale;// Compute specular term with the specular maskfixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss) * specularMask;return fixed4(ambient + diffuse + specular, 1.0);}ENDCG}}FallBack "Specular"
}
_SpecularMask即是我們需要使用的高光反射遮罩紋理,_SpecularScale則是用于控制遮罩影響度的系數。
我們為主紋理_MainTex、法線紋理_BumpMap和遮罩紋理_SpecularMask定義了它們共同使用的紋理屬性_MainTex_ST。這意味著,在材質面板中修改主紋理的平鋪系數和偏移系數會同時影響3個紋理的采樣。
在頂點著色器中,我們對光照方向和視角方向進行了坐標空間的變換,把它們從模型空間變換到了切線空間中,以便在片元著色器中和法線進行光照運算。
白話解釋:
1)o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
計算貼圖要用的UV坐標。
v.texcoord.xy:模型自帶的UV(比如0~1)
_MainTex_ST.xy:你在材質面板上設的縮放(scale)
_MainTex_ST.zw:
2)TANGENT_SPACE_ROTATION;
宏函數,它自動完成2件事情:
- 計算出當前頂點的副切線(binormal)
- 和切線(tangent)、法線(normal)一起構造一個從模型空間到切線空間的旋轉矩陣叫rotation
這個rotation是Unity自動生成的變量。
作用:讓光照計算可以在每個頂點自己的小坐標系里進行,法線切圖才有效。
3)normalize的好處:
情況 | 向量長度 | 光照計算是否準確 | 為什么 |
---|---|---|---|
沒歸一化 | 可能 >1 或 <1 | ? 不準 | 點積結果被放大/縮小 |
歸一化后 | 一定是 1 | ? 準確 | 點積只反映角度關系 |
4)fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv));
tex2D():在_BumpMap這張圖上查顏色
法線貼圖的顏色不是普通顏色,而是把方向存成了RGB,
UnpackNormal:Unity提供的函數,把RGB顏色還原成方向向量,比如紅色多-> 表面往右凸,綠色多->往上凸。
5)tangentNormal.xy *= _BumpScale;
控制凸凹程度
6)tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
重新計算z分量,保證法線長度為1
7)fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
獲取這個像素的基礎顏色。
albedo:反照率,表示這個材質本來是什么顏色。
8)fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
環境光受材質的影響。
9)fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss) * specularMask;
_LightColor0:光源顏色
_Specular:高光顏色
pow(..., _Gloss):指數越大,高光越小越亮
specularMask:乘上遮罩,控制哪里亮哪里不亮
沒有遮罩時,全模型統一高光;有了遮罩,可以局部控制。
10)?Tags{"LightMode"="ForwardBase"}
只有定義了正確的LightMode,我們才能正確得到一些Unity內置光照變量,例如:_LightColor0。
3、透明效果
透明效果的2種實現方法:
- 透明度測試(Alpha Test):只要一個片元的透明度不滿足條件(通常是小于某個閾值),那么它對應的片元就會被舍棄。它的效果很極端,要么完全透明,即看不到,要么完全不透明,就像不透明物體那樣。
- 透明度混合(Alpha Blending):使用當前片元的透明度作為混合因子,與已經存儲在顏色緩沖中的顏色值進行混合,得到新的顏色。
(1)通用渲染順序
對于不透明物體,不考慮它們的渲染順序也能得到正確的排序效果,這是由于強大的深度緩沖(depth-buffer,也被稱為z-buffer)的存在。
它的基本思想:根據深度緩存中的值來判斷該片元距離攝像機的距離,當渲染一個片元時,需要把它的深度值和已經存在于深度緩沖中的值進行比較(如果開啟了深度測試),如果它的值距離攝像機更遠,那么說明這個片元不應該被渲染到屏幕上(有物體擋住了它);否則,這個片元應該覆蓋掉此時顏色緩沖中的像素值,并把它的深度值更新到深度緩沖中(如果開啟了深度寫入)。
【深度緩沖技術】
它像一個小本本,記錄每個像素距離攝像機有多遠。
比如從近到遠的3個物體:車、房子、樹。
先畫樹:每個像素記下距離=10
再畫房子:發現“我比樹近(距離=5)”,于是覆蓋樹,更新距離為5
再畫車:發現“我比房子還近(距離=1)”,覆蓋房子,更新為1
結果:車在最前面,房子在中間,樹在后面。
這個過程叫:
- 深度測試:比較“我要畫的點”和“已經畫的點”,誰更近
- 深度寫入:把新的距離寫進“小本本”
渲染引擎一般都會先對物體進行排序,再渲染。常用的方式是:
1)先渲染所有不透明物體,并開啟它們的深度測試和深度寫入
2)把半透明物體按它們距離攝像機的遠近進行排序,然后按照從后往前的順序渲染這些半透明物體,并開啟它們的深度測試,但關閉深度寫入。
(2)Shader的渲染順序
Unity為了解決渲染順序的問題提供了渲染 隊列這一解決方案。
Unity在內部使用一系列整數索引來表示每個渲染隊列,且索引號越小表示越早被渲染。
1)透明度測試代碼
SubShader {Tags { "Queue"="AlphaTest" }Pass {...}}
2)透明度混合代碼
SubShader {Tags { "Queue"="Transparent" }Pass {ZWrite Off...}}
ZWrite Off用于關閉深度寫入,在這里我們選擇把它寫在Pass中,也可以把它寫在SubShader中,這意味著該SubShader下的所有Pass都會關閉深度寫入。
(3)透明度測試
在片元著色器中是使用clip函數進行透明度測試。
void clip(floatx x);
x: 裁剪時使用的標量或矢量條件
描述:如果給定參數的任何一個分量是負數,就會舍棄當前像素的輸出顏色。
等同于下面的代碼:
void clip(float4 x){if (any(x < 0))discard;}
透明度測試完整代碼:
Shader "Custom/AlphaTest"
{Properties{_Color("Main Tint", Color) = (1,1,1,1)_MainTex("Main Tex", 2D) = "white"{}_Cutoff("Alpha Cutoff", Range(0,1)) = 0.5}SubShader{Tags{"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}Pass{Tags{"LightMode"="ForwardBase"}CGPROGRAM#pragma vertex vert #pragma fragment frag#include "Lighting.cginc"fixed4 _Color;sampler2D _MainTex;float4 _MainTex_ST;fixed _Cutoff;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;float4 texcoord: TEXCOORD0;};struct v2f{float4 pos: SV_POSITION;float3 worldNormal: TEXCOORD0;float3 worldPos: TEXCOORD1;float2 uv: TEXCOORD2;};v2f vert(a2v v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);return o;}fixed4 frag(v2f i): SV_Target{fixed3 worldNormal = normalize(i.worldNormal);fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));fixed4 texColor = tex2D(_MainTex, i.uv);// Alpha testclip(texColor.a - _Cutoff);fixed3 albedo = texColor.rgb * _Color.rgb;fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));return fixed4(ambient + diffuse, 1.0);}ENDCG}}FallBack "Transparent/Cutout/VertexLit"
}
代碼解釋:
1)面板參數
參數名 | 中文名 | 默認值 | 作用 |
---|---|---|---|
_Color | 主色調 | 白色?(1,1,1,1) | 整體調色,比如想變紅就把這里調紅 |
_MainTex | 主紋理 | 白色貼圖 | 你的圖片(比如樹葉、鐵絲網) |
_Cutoff | 透明度裁剪閾值 | 0.5 | 控制“多透明才算透明” |
如果貼圖某個像素的透明度是0.3,而_Cutoff=0.5, 0.3<0.5,這個像素被舍棄了,看不見。
2)SubShader標簽
標簽 | 含義 |
---|---|
"Queue"="AlphaTest" | “我是透明裁剪物體,請在不透明物體之后、透明混合物體之前渲染我” |
"IgnoreProjector"="True" | “不要把我投影到其他物體上(比如不要讓這個樹葉影子打到墻上)” |
"RenderType"="TransparentCutout" | “我是‘有透明有不透明’的類型”,Unity 其他系統(比如光照)會特殊處理 |
3)Shader的工作流程:
1. 拿到模型的每個頂點(位置、法線、UV)↓
2. 頂點著色器:轉到世界空間,計算光照方向,處理 UV↓
3. GPU 插值:把頂點之間的像素都算出對應的法線、位置、UV↓
4. 像素著色器:采樣紋理 → 判斷透明度 → 太透明就剪掉↓
5. 不剪的像素:計算光照(環境光 + 漫反射)→ 輸出顏色
效果:
隨著Alpha cutoff參數的增大,更多的像素由于不滿足透明度測試條件而被剔除。
(4)透明度混合
這種方法可以得到真正的半透明效果。它會使用當前片元的透明度作為混合因子,與已經存儲在顏色緩沖中的顏色值進行混合,得到新的顏色。但是,透明度混合需要關閉深度寫入,這使得我們要非常小心物體的渲染順序。
為了進行混合,我們需要使用Unity提供的混合命令——Blend。Blend是Unity提供的設置混合模式的命令。想要實現半透明的效果就需要把當前自身的顏色和已經存在于顏色緩沖中的顏色值進行混合,混合時使用的函數就是由該指令決定的。Blend命令的語義:
這個命令在設置混合因子的同時也開啟了混合模式,只有開啟了混合之后,設置片元的透明通道才有意義。
代碼:
Shader "Custom/AlphaBlend"
{Properties{_Color("Main Tint", Color) = (1,1,1,1)_MainTex("Main Tex", 2D) = "white"{}_AlphaScale("Alpha Scale", Range(0,1)) = 1}SubShader{Tags{"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}Pass{Tags{"LightMode"="ForwardBase"}ZWrite OffBlend SrcAlpha OneMinusSrcAlphaCGPROGRAM#pragma vertex vert #pragma fragment frag#include "Lighting.cginc"fixed4 _Color;sampler2D _MainTex;float4 _MainTex_ST;fixed _AlphaScale;struct a2v{float4 vertex: POSITION;float3 normal: NORMAL;float4 texcoord: TEXCOORD0;};struct v2f{float4 pos: SV_POSITION;float3 worldNormal: TEXCOORD0;float3 worldPos: TEXCOORD1;float2 uv: TEXCOORD2;};v2f vert(a2v v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);return o;}fixed4 frag(v2f i): SV_Target{fixed3 worldNormal = normalize(i.worldNormal);fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));fixed4 texColor = tex2D(_MainTex, i.uv);fixed3 albedo = texColor.rgb * _Color.rgb;fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));return fixed4(ambient + diffuse, texColor.a * _AlphaScale);}ENDCG}}FallBack "Transparent/VertexLit"
}
指令 | 作用 |
---|---|
ZWrite Off | 關掉“占位置”功能?→ 讓后面的物體還能畫上來 |
Blend SrcAlpha OneMinusSrcAlpha | 開啟“混合”模式?→ 當前顏色和后面的顏色“疊在一起” |
Blend SrcAlpha OneMinusSrcAlpha
翻譯成數據公式如下:
最終顏色 = 當前顏色 × 當前透明度 + 背后顏色 × (1 - 當前透明度)
術語 | 含義 |
---|---|
SrcAlpha | Source Alpha?→ 當前要畫的顏色的透明度(texColor.a ) |
OneMinusSrcAlpha | 1 - SrcAlpha ?→ 當前顏色不占的比例 |
效果:
存在問題:
當模型網格之間有互相交叉的結構時,往往會得到錯誤的半透明效果。
(5)開啟深度寫入的半透明效果
針對方法:使用兩個Pass來渲染模型,第一個Pass開啟深度寫入,但不輸出顏色,它的目的僅僅是為了把該模型的深度值寫入深度緩沖中。第二個Pass進行正常的透明度混合,由于上一個Pass已經得到了逐像素的正確的深度信息,該Pass就可以按照像素級別的深度排序結果進行透明渲染。
示例:
這個新添加的Pass的目的僅僅是為了把模型的深度信息寫入深度緩沖中,從而剔除模型中被自身遮擋的片元。
ColorMask設為0,意味著該Pass不寫入任何顏色通道,即不會輸出任何顏色。
(6)ShaderLab的混合命令
混合的實現流程:當片元著色器產生一個顏色的時候,可以選擇與顏色緩存中的顏色進行混合。這樣一來,混合就和兩個操作有關:源顏色(source color)和目標顏色(destiination color)。源顏色,用S表示,指的是由片元著色器產生的顏色值;目標顏色,用D表示,指的是從顏色緩沖中讀取到的顏色值。對它們進行混合后得到的輸出顏色,用O表示,它會重新寫入到顏色緩沖中。
混合中的源顏色、目標顏色和輸出顏色,都包含RGBA四個通道的值,而并非僅僅是RGB通道。
1)混合等式和參數
混合是一個逐片元的操作,而且它不是可編程的,但卻是高度可配置的。
我們可以設置混合時使用的運算操作、混合因子等影響混合。
現在,我們已知兩個操作數:源顏色S和目標顏色D,想要得到輸出顏色O就必須使用一個燈飾來計算。
我們把這個等式稱為混合等式。當進行混合時,我們需要使用兩個混合等式:一個用于混合RGB通道,一個用于混合A通道。
當設置混合狀態時,實際上設置的就是混合等式中的操作和因子。
默認情況下,混合等式使用的操作都是加操作,我們只需要再設置一下混合因子即可。
由于需要兩個等式(分別用于混合RGB通道和A通道),每個等式有兩個因子(一個用于和源顏色相乘,一個用于和目標顏色相乘),因此一共需要4個因子。
ShaderLab中設置混合因子的命令:
可以發現,第一個命令只提供了兩個因子,這意味著將使用同樣的混合因子來混合RGB通道和A通道,即此時SrcFactorA將等于SrcFactor, DstFactorA將等于DstFactor。下面就是使用這些因子進行加法混合時使用的混合公式:
Orgb=SrcFactor×Srgb+DstFactor×Drgb
Oa=SrcFactorA×Sa+DstFactorA×Da
ShaderLab中的混合因子:
假如我們想要在混合后,輸出顏色的透明度值就是源顏色的透明度,可以使用下面的命令:
Blend srcAlpha OneMinusSrcAlpha, One Zero
2)混合操作