這些內置渲染管線的著色器示例演示了編寫自定義著色器的基礎知識,并涵蓋了常見的用例。
有關編寫著色器的信息,請參閱編寫著色器。
設置場景
第一步是創建一些用于測試著色器的對象。在主菜單中選擇?Game Object?>?3D Object?>?Capsule。然后,調整攝像機位置,使其顯示膠囊體。在 Hierarchy 視圖中雙擊膠囊體 (Capsule) 以將 Scene 視圖聚焦在該膠囊體上,然后選擇主攝像機 (Main Camera) 對象,并單擊主菜單中的?Game object?>?Align with View。
在 Project 視圖中,從菜單中選擇?Create?>?Material,創建新的材質。Project 視圖中將顯示一個名為?New Material?的新材質。
創建著色器
現在以類似方式創建一個新的著色器資源。在 Project 視圖中,從菜單中選擇?Create?>?Shader?>?Unlit Shader。 隨后將創建一個基本著色器,該著色器僅顯示一個紋理,無任何光照。
Create?>?Shader?菜單中的其他條目將創建基本要素著色器 或其他類型,例如,基本表面著色器。
鏈接網格、材質和著色器
通過材質的檢視面板使材質使用著色器,或者在 Project 視圖中將著色器資源拖動到材質資源上。材質檢視面板在使用此著色器時將顯示白色球體。
現在將材質拖動到 Scene 或 Hierarchy 視圖中的網格對象上。或者,選擇對象,并在檢視面板中使其使用網格渲染器 (Mesh Renderer)?組件的 Materials 字段中的材質。
完整這些設置后,現在可以開始查看著色器代碼,并會在 Scene 視圖中看到對膠囊體上著色器所做的更改的結果。
著色器的主要部分
要開始檢查著色器的代碼,請在 Project 視圖中雙擊著色器資源。著色器代碼將在腳本編輯器(MonoDevelop 或 Visual Studio)中打開。
著色器最開始的代碼如下:
Shader "Unlit/NewUnlitShader"
{Properties{_MainTex ("Texture", 2D) = "white" {}}SubShader{Tags { "RenderType"="Opaque" }LOD 100Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag// 使霧生效#pragma multi_compile_fog#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;UNITY_FOG_COORDS(1)float4 vertex : SV_POSITION;};sampler2D _MainTex;float4 _MainTex_ST;v2f vert (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);UNITY_TRANSFER_FOG(o,o.vertex);return o;}fixed4 frag (v2f i) : SV_Target{// 對紋理進行采樣fixed4 col = tex2D(_MainTex, i.uv);// 應用霧UNITY_APPLY_FOG(i.fogCoord, col);return col;}ENDCG}}
}
這個初始著色器看上去不是那么簡單!但不要擔心, 我們將逐步介紹每個部分。
讓我們看看這個簡單著色器的主要部分。
Shader
Shader?命令包含一個帶有 著色器名稱的字符串。在材質檢視面板中選擇著色器時,可以使用正斜杠字符“/”將著色器放置在子菜單中。
屬性
Properties?代碼塊包含著色器變量 (紋理、顏色等),這些變量將保存為材質的一部分, 并顯示在材質檢視面板中。在我們的無光照著色器模板中, 聲明了單個紋理屬性。
SubShader
一個著色器 (Shader) 可以包含一個或多個子著色器 (SubShader),主要 用于實現不同 GPU 功能的著色器。 在本教程中,我們并不太關心這一點,所以我們 所有的著色器都只包含一個子著色器。
Pass
每個子著色器由多個通道組成,每個通道代表 為使用著色器材質渲染的同一對象執行 頂點和片元代碼。 許多簡單的著色器只使用一個通道,但與光照交互的 著色器可能需要更多通道(有關詳細信息,請參閱?光照管線)。通道內部的 命令通常設置固定函數狀態,例如 混合模式。
CGPROGRAM ..ENDCG
這些關鍵字包裹著頂點和片元著色器中的 HLSL 代碼 部分。通常,大多數相關代碼都包含在這里。有關詳細信息,請參閱?頂點和片元著色器。
簡單的無光照著色器
無光照著色器模板比顯示帶紋理的對象 絕對需要更多的代碼。例如, 它支持霧效,以及材質中的紋理平鋪/偏移字段。 讓我們最大程度簡化著色器,并添加更多注釋:
Shader "Unlit/SimpleUnlitTexturedShader"
{Properties{// 我們已刪除對紋理平鋪/偏移的支持,// 因此請讓它們不要顯示在材質檢視面板中[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}}SubShader{Pass{CGPROGRAM// 使用 "vert" 函數作為頂點著色器#pragma vertex vert// 使用 "frag" 函數作為像素(片元)著色器#pragma fragment frag// 頂點著色器輸入struct appdata{float4 vertex : POSITION; // 頂點位置float2 uv : TEXCOORD0; // 紋理坐標};// 頂點著色器輸出("頂點到片元")struct v2f{float2 uv : TEXCOORD0; // 紋理坐標float4 vertex : SV_POSITION; // 裁剪空間位置};// 頂點著色器v2f vert (appdata v){v2f o;// 將位置轉換為裁剪空間//(乘以模型*視圖*投影矩陣)o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);// 僅傳遞紋理坐標o.uv = v.uv;return o;}// 我們將進行采樣的紋理sampler2D _MainTex;// 像素著色器;返回低精度("fixed4" 類型)// 顏色("SV_Target" 語義)fixed4 frag (v2f i) : SV_Target{// 對紋理進行采樣并將其返回fixed4 col = tex2D(_MainTex, i.uv);return col;}ENDCG}}
}
頂點著色器是對 3D 模型的每個頂點運行的程序。通常情況下,它并不做任何特別有趣的事情。這里我們只是將頂點位置從對象空間轉換為所謂的“裁剪空間”(由 GPU 用于柵格化屏幕上的對象)。我們還傳遞未修改的輸入紋理坐標;我們需要該坐標來對片元著色器中的紋理進行采樣。
片元著色器是對屏幕上對象占據的每個像素運行的程序,通常用于計算和輸出每個像素的顏色。通常,屏幕上有數百萬個像素,并且片元著色器將 針對所有像素執行!優化片元著色器是整體游戲性能優化工作的重要組成部分。
一些變量或函數定義后跟一個語義指示符,例如?: POSITION?或?: SV_Target。這些語義指示符將這些變量的“含義”傳達給 GPU。有關詳細信息,請參閱著色器語義頁面。
用于具有良好紋理的良好模型時,我們的簡單著色器看起來非常好!
更簡單的單色著色器
讓我們進一步簡化著色器:我們將制作一個用單一顏色繪制整個對象的 著色器。這并不是很有用,但別忘了,我們是在學習。
Shader "Unlit/SingleColor"
{Properties{// 材質檢視面板的顏色屬性,默認為白色_Color ("Main Color", Color) = (1,1,1,1)}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag// 頂點著色器// 這次不使用 "appdata" 結構,僅手動拼寫輸入,// 并且不返回 v2f 結構,同樣僅返回單個輸出// float4 裁剪位置float4 vert (float4 vertex : POSITION) : SV_POSITION{return mul(UNITY_MATRIX_MVP, vertex);}// 來自材質的顏色fixed4 _Color;// 像素著色器,無需輸入fixed4 frag () : SV_Target{return _Color; // 僅將其返回}ENDCG}}
}
這次,著色器函數只是手動拼出輸入,而不是使用輸入結構 (appdata) 和輸出結構 (v2f)。兩種方式都有效,選擇使用哪種方式取決于您編寫代碼的風格和偏好。
使用網格法線,輕松獲利
讓我們繼續了解在世界空間中顯示網格法線的著色器。言歸正傳:
Shader "Unlit/WorldSpaceNormals"
{// 這次沒有 Properties 代碼塊!SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag// 包含 UnityObjectToWorldNormal helper 函數的 include 文件#include "UnityCG.cginc"struct v2f {// 我們將輸出世界空間法線作為常規 ("texcoord") 插值器之一half3 worldNormal : TEXCOORD0;float4 pos : SV_POSITION;};// 頂點著色器:將對象空間法線也作為輸入v2f vert (float4 vertex : POSITION, float3 normal : NORMAL){v2f o;o.pos = UnityObjectToClipPos(vertex);// UnityCG.cginc 文件包含將法線從對象變換到// 世界空間的函數,請使用該函數o.worldNormal = UnityObjectToWorldNormal(normal);return o;}fixed4 frag (v2f i) : SV_Target{fixed4 c = 0;// 法線是具有 xyz 分量的 3D 矢量;處于 -1..1// 范圍。要將其顯示為顏色,請將此范圍設置為 0..1// 并放入紅色、綠色、藍色分量c.rgb = i.worldNormal*0.5+0.5;return c;}ENDCG}}
}
除了產生漂亮的顏色外,法線還用于各種圖形效果:光照、反射、輪廓等。
在上面的著色器中,我們開始使用 Unity 的內置著色器 include 文件之一。 這里使用的?UnityCG.cginc?包含一個方便的函數?UnityObjectToWorldNormal。我們還使用了實用函數?UnityObjectToClipPos,該函數將頂點從對象空間轉換為屏幕。這使得代碼更易于閱讀,并且在某些情況下更有效。
在所謂的“插值器”(有時稱為“變化”)中,我們已經看到數據可從頂點傳入片元著色器。在 HLSL 著色語言中,它們通常用?TEXCOORDn?語義進行標記,其中每一個最多可以是一個 4 分量矢量(有關詳細信息,請參閱語義頁面)。
此外,我們學習了如何使用一種簡單的技術將標準化矢量(在 –1.0 到 +1.0 范圍內)可視化為顏色:只需將它們乘以二分之一并加二分之一。有關更多頂點數據可視化示例,請參閱可視化頂點數據。
使用世界空間法線的環境反射
如果在場景中使用天空盒作為反射源(請參閱?Lighting 窗口), 基本上會創建一個包含天空盒數據的“默認”反射探針。 反射探針內部是立方體貼圖紋理;我們將擴展上面的世界空間法線著色器來對其進行深入了解。
代碼現在開始變得有點復雜。當然,如果您希望著色器能夠自動處理光源、陰影、反射以及光照系統的其余部分,使用表面著色器會容易得多。此示例旨在向您展示如何以“手動”方式使用光照系統的各個部分。
Shader "Unlit/SkyReflection"
{SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f {half3 worldRefl : TEXCOORD0;float4 pos : SV_POSITION;};v2f vert (float4 vertex : POSITION, float3 normal : NORMAL){v2f o;o.pos = UnityObjectToClipPos(vertex);// 計算頂點的世界空間位置float3 worldPos = mul(_Object2World, vertex).xyz;// 計算世界空間視圖方向float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));// 世界空間法線float3 worldNormal = UnityObjectToWorldNormal(normal);// 世界空間反射矢量o.worldRefl = reflect(-worldViewDir, worldNormal);return o;}fixed4 frag (v2f i) : SV_Target{// 使用反射矢量對默認反射立方體貼圖進行采樣half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.worldRefl);// 將立方體貼圖數據解碼為實際顏色half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);// 將其輸出!fixed4 c = 0;c.rgb = skyColor;return c;}ENDCG}}
}
以上示例使用了內置著色器 include 文件中的部分內容:
- 來自內置著色器變量的?unity_SpecCube0、unity_SpecCube0_HDR、Object2World?和?UNITY_MATRIX_MVP。unity_SpecCube0?包含激活的反射探針的數據。
- UNITY_SAMPLE_TEXCUBE?是一個用于采樣立方體貼圖的內置宏。通常使用標準 HLSL 語法聲明和 使用大多數常規立方體貼圖(samplerCUBE?和?texCUBE),但 Unity 中的反射探針立方體貼圖以特殊方式聲明以節省采樣器字段。如果您不知道這是什么,請不要擔心,只需要知道:要使用?unity_SpecCube0?立方體貼圖,必須使用?UNITY_SAMPLE_TEXCUBE?宏。
- UnityCG.cginc?中的?UnityWorldSpaceViewDir?函數以及來自同一文件的?DecodeHDR?函數。后者用于從反射探針數據中獲取實際顏色(因為 Unity 以特殊編碼方式存儲反射探針立方體貼圖)。
- reflect?只是一個內置的 HLSL 函數,用于計算給定法線周圍的矢量反射。
使用法線貼圖的環境反射
通常,法線貼圖用于在對象上創建其他細節,而無需創建其他幾何體。讓我們看看如何使用法線貼圖紋理制作發射環境的著色器。
現在開始真正涉及到了數學知識,所以我們將分幾步完成。在上面的著色器中,反射 方向是按每個頂點計算的(在頂點著色器中),片元著色器僅執行反射探針 立方體貼圖查找。然而,一旦我們開始使用法線貼圖,表面法線本身需要按每個像素計算,這意味著我們還必須按每個像素計算環境的反射方式!
首先,讓我們重寫上面的著色器來實現相同效果,只是我們將一些計算移到片元著色器,從而按每個像素進行計算:
Shader "Unlit/SkyReflection Per Pixel"
{SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f {float3 worldPos : TEXCOORD0;half3 worldNormal : TEXCOORD1;float4 pos : SV_POSITION;};v2f vert (float4 vertex : POSITION, float3 normal : NORMAL){v2f o;o.pos = UnityObjectToClipPos(vertex);o.worldPos = mul(_Object2World, vertex).xyz;o.worldNormal = UnityObjectToWorldNormal(normal);return o;}fixed4 frag (v2f i) : SV_Target{// 計算視圖方向和反射矢量// 此處為每像素計算half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));half3 worldRefl = reflect(-worldViewDir, i.worldNormal);// 與在先前的著色器中相同half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);fixed4 c = 0;c.rgb = skyColor;return c;}ENDCG}}
}
這里并沒有帶給我們太多不同:著色器看起來完全相同,只是它現在運行得更慢,因為它對屏幕上的每個像素進行更多的計算,而不是僅對每個模型的頂點進行計算。但是,我們很快就會真正需要這些計算。更高的圖形保真度通常需要更復雜的著色器。
我們現在也要學習新東西,即所謂的“切線空間”。法線貼圖紋理通常在坐標空間中表示,可將其視為“跟隨模型表面”。在我們的著色器中,我們需要知道切線空間基矢量,從紋理中讀取法線矢量,將其轉換為世界空間,然后通過上面的著色器 進行所有數學運算。我們行動吧!
Shader "Unlit/SkyReflection Per Pixel"
{Properties {// 材質上的法線貼圖紋理,// 默認為虛擬的 "平面表面" 法線貼圖_BumpMap("Normal Map", 2D) = "bump" {}}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f {float3 worldPos : TEXCOORD0;// 這三個矢量將保持一個 3x3 旋轉矩陣,// 此矩陣進行從切線到世界空間的轉換half3 tspace0 : TEXCOORD1; // tangent.x, bitangent.x, normal.xhalf3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.yhalf3 tspace2 : TEXCOORD3; // tangent.z, bitangent.z, normal.z// 法線貼圖的紋理坐標float2 uv : TEXCOORD4;float4 pos : SV_POSITION;};// 頂點著色器現在還需要每頂點切線矢量。// 在 Unity 中,切線為 4D 矢量,其中使用 .w 分量// 指示雙切線矢量的方向。// 我們還需要紋理坐標。v2f vert (float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0){v2f o;o.pos = UnityObjectToClipPos(vertex);o.worldPos = mul(_Object2World, vertex).xyz;half3 wNormal = UnityObjectToWorldNormal(normal);half3 wTangent = UnityObjectToWorldDir(tangent.xyz);// 從法線和切線的交叉積計算雙切線half tangentSign = tangent.w * unity_WorldTransformParams.w;half3 wBitangent = cross(wNormal, wTangent) * tangentSign;// 輸出切線空間矩陣o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);o.uv = uv;return o;}// 來自著色器屬性的法線貼圖紋理sampler2D _BumpMap;fixed4 frag (v2f i) : SV_Target{// 對法線貼圖進行采樣,并根據 Unity 編碼進行解碼half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));// 將法線從切線變換到世界空間half3 worldNormal;worldNormal.x = dot(i.tspace0, tnormal);worldNormal.y = dot(i.tspace1, tnormal);worldNormal.z = dot(i.tspace2, tnormal);// 與在先前的著色器中處于相同的位置half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));half3 worldRefl = reflect(-worldViewDir, worldNormal);half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);fixed4 c = 0;c.rgb = skyColor;return c;}ENDCG}}
}
哎呦,這個非常復雜。但是,看看法線貼圖反射!
添加更多紋理
讓我們為上面的法線貼圖、天空反射著色器添加更多紋理。我們將添加基礎顏色紋理(如第一個無光照的示例中所示)和遮擋貼圖來使洞穴變暗。
Shader "Unlit/More Textures"
{Properties {// 我們將在材質中使用三種紋理_MainTex("Base texture", 2D) = "white" {}_OcclusionMap("Occlusion", 2D) = "white" {}_BumpMap("Normal Map", 2D) = "bump" {}}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"// 與在先前的著色器中完全相同struct v2f {float3 worldPos : TEXCOORD0;half3 tspace0 : TEXCOORD1;half3 tspace1 : TEXCOORD2;half3 tspace2 : TEXCOORD3;float2 uv : TEXCOORD4;float4 pos : SV_POSITION;};v2f vert (float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0){v2f o;o.pos = UnityObjectToClipPos(vertex);o.worldPos = mul(_Object2World, vertex).xyz;half3 wNormal = UnityObjectToWorldNormal(normal);half3 wTangent = UnityObjectToWorldDir(tangent.xyz);half tangentSign = tangent.w * unity_WorldTransformParams.w;half3 wBitangent = cross(wNormal, wTangent) * tangentSign;o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);o.uv = uv;return o;}// 來自著色器屬性的紋理sampler2D _MainTex;sampler2D _OcclusionMap;sampler2D _BumpMap;fixed4 frag (v2f i) : SV_Target{// 與在先前的著色器中相同...half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));half3 worldNormal;worldNormal.x = dot(i.tspace0, tnormal);worldNormal.y = dot(i.tspace1, tnormal);worldNormal.z = dot(i.tspace2, tnormal);half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));half3 worldRefl = reflect(-worldViewDir, worldNormal);half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR); fixed4 c = 0;c.rgb = skyColor;// 使用基本紋理和遮擋貼圖調制天空顏色fixed3 baseColor = tex2D(_MainTex, i.uv).rgb;fixed occlusion = tex2D(_OcclusionMap, i.uv).r;c.rgb *= baseColor;c.rgb *= occlusion;return c;}ENDCG}}
}
氣球貓看起來很不錯!
紋理化著色器示例
程序化棋盤圖案
下面的著色器根據網格的紋理坐標輸出棋盤圖案:
Shader "Unlit/Checkerboard"
{Properties{_Density ("Density", Range(2,50)) = 30}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{float2 uv : TEXCOORD0;float4 vertex : SV_POSITION;};float _Density;v2f vert (float4 pos : POSITION, float2 uv : TEXCOORD0){v2f o;o.vertex = UnityObjectToClipPos(pos);o.uv = uv * _Density;return o;}fixed4 frag (v2f i) : SV_Target{float2 c = i.uv;c = floor(c) / 2;float checker = frac(c.x + c.y) * 2;return checker;}ENDCG}}
}
Properties?代碼塊中的密度滑動條控制棋盤的密集程度。在頂點著色器中,網格 UV 與密度值相乘,使它們從 0 到 1 的范圍變為 0 到密度的范圍。假設密度設置為 30,這將使片元著色器中的?i.uv?輸入包含 0 到 30 的浮點值,對應于正在渲染的網格的各個位置。
然后,片元著色器代碼僅使用 HLSL 的內置?floor?函數獲取輸入坐標的整數部分,并將其除以 2。回想一下,輸入坐標是從 0 到 30 的數字;這使得它們都被“量化”為 0、0.5、1、1.5、2、2.5 等等的值。輸入坐標的 x 和 y 分量都完成了此操作。
接下來,我們將這些 x 和 y 坐標相加(每個坐標的可能值只有 0、0.5、1、1.5 等等),并且只使用另一個內置的 HLSL 函數?frac?來獲取小數部分。結果只能是 0.0 或 0.5。然后,我們將它乘以 2 使其為 0.0 或 1.0,并輸出為顏色(這分別產生黑色或白色)。
三面紋理
對于復雜網格或程序化網格,不使用常規 UV 坐標對它們進行紋理化,有時只需在三個主方向將紋理“投影”到對象上即可。這稱為“三面”紋理。這個想法是使用表面法線來加權三個紋理方向。下面是著色器:
Shader "Unlit/Triplanar"
{Properties{_MainTex ("Texture", 2D) = "white" {}_Tiling ("Tiling", Float) = 1.0_OcclusionMap("Occlusion", 2D) = "white" {}}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{half3 objNormal : TEXCOORD0;float3 coords : TEXCOORD1;float2 uv : TEXCOORD2;float4 pos : SV_POSITION;};float _Tiling;v2f vert (float4 pos : POSITION, float3 normal : NORMAL, float2 uv : TEXCOORD0){v2f o;o.pos = UnityObjectToClipPos(pos);o.coords = pos.xyz * _Tiling;o.objNormal = normal;o.uv = uv;return o;}sampler2D _MainTex;sampler2D _OcclusionMap;fixed4 frag (v2f i) : SV_Target{// 使用法線的絕對值作為紋理權重half3 blend = abs(i.objNormal);// 確保權重之和為 1(除以 x+y+z 之和)blend /= dot(blend,1.0);// 針對 x、y、z 軸讀取三個紋理投影fixed4 cx = tex2D(_MainTex, i.coords.yz);fixed4 cy = tex2D(_MainTex, i.coords.xz);fixed4 cz = tex2D(_MainTex, i.coords.xy);// 根據權重混合紋理fixed4 c = cx * blend.x + cy * blend.y + cz * blend.z;// 根據常規遮擋貼圖進行調制c *= tex2D(_OcclusionMap, i.uv);return c;}ENDCG}}
}
光照計算
通常,當您需要適用于 Unity 光照管線的著色器時, 可以編寫表面著色器。這樣會為您完成大部分“繁重的工作”, 您的著色器代碼只需要定義表面屬性。
但在某些情況下,您希望繞過標準表面著色器路徑; 或者是出于性能原因,您只想支持整個光照管線的某個有限子集, 或者您想要進行“標準光照”以外的自定義。以下示例 將說明如何從手動編寫的頂點和片元著色器獲取光照數據。 查看表面著色器生成的代碼(通過著色器檢視面板)也是一種 很好的學習資源。
簡單的漫射光照
我們需要做的第一件事就是指出我們的著色器確實需要傳遞給它的光照信息。Unity 的渲染管線支持各種渲染方式;這里我們將使用默認的前向渲染。
我們首先只支持一個方向光。Unity 中的前向渲染的工作方式是在一個名為?ForwardBase?的單個通道中渲染主方向光、環境光、光照貼圖和反射。在著色器中,通過添加以下通道標簽來指示這一情況:Tags {“LightMode”=“ForwardBase”}。這將使方向光數據通過一些內置變量傳入著色器。
以下著色器為每個頂點計算簡單漫射光照,并使用單個主紋理:
Shader "Lit/Simple Diffuse"
{Properties{[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}}SubShader{Pass{// 指示我們的通道是前向渲染管線中的// "基礎" 通道。它可設置環境光和主// 方向光數據;光線方向為 _WorldSpaceLightPos0// 而顏色為 _LightColor0Tags {"LightMode"="ForwardBase"}CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc" // 對于 UnityObjectToWorldNormal#include "UnityLightingCommon.cginc" // 對于 _LightColor0struct v2f{float2 uv : TEXCOORD0;fixed4 diff : COLOR0; // 漫射光照顏色float4 vertex : SV_POSITION;};v2f vert (appdata_base v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = v.texcoord;// 在世界空間中獲取頂點法線half3 worldNormal = UnityObjectToWorldNormal(v.normal);// 標準漫射(蘭伯特)光照的法線和// 光線方向之間的點積half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));// 考慮淺色因素o.diff = nl * _LightColor0;return o;}sampler2D _MainTex;fixed4 frag (v2f i) : SV_Target{// 樣本紋理fixed4 col = tex2D(_MainTex, i.uv);// 乘以光照col *= i.diff;return col;}ENDCG}}
}
這使得對象對光線方向作出反應:面向光源的部分獲得光照,而背向的部分完全沒有光照。
帶環境光的漫射光照
上面的示例不考慮任何環境光照或光照探針。我們來解決這個問題! 事實證明,我們可以通過添加一行代碼來實現這一目標。環境光和光照探針數據都以球諧函數形式傳遞給著色器,UnityCG.cginc?include 文件?中的?ShadeSH9?函數在給定世界空間法線的情況下完成所有估算工作。
Shader "Lit/Diffuse With Ambient"
{Properties{[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}}SubShader{Pass{Tags {"LightMode"="ForwardBase"}CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "UnityLightingCommon.cginc"struct v2f{float2 uv : TEXCOORD0;fixed4 diff : COLOR0;float4 vertex : SV_POSITION;};v2f vert (appdata_base v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = v.texcoord;half3 worldNormal = UnityObjectToWorldNormal(v.normal);half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));o.diff = nl * _LightColor0;// 與先前著色器的唯一區別:// 除了來自主光源的漫射光照,// 還可添加來自環境或光照探針的光照// 來自 UnityCG.cginc 的 ShadeSH9 函數使用世界空間法線// 對其進行估算o.diff.rgb += ShadeSH9(half4(worldNormal,1));return o;}sampler2D _MainTex;fixed4 frag (v2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv);col *= i.diff;return col;}ENDCG}}
}
事實上,這個著色器開始看起來非常類似于內置的舊版漫射著色器!
實現陰影投射
我們的著色器目前既不能接受也不能投射陰影。讓我們先實現陰影投射。
為了投射陰影,著色器必須在其任何子著色器或任何回退中具有?ShadowCaster?通道類型。ShadowCaster 通道用于將對象渲染到陰影貼圖中,通常它非常簡單:頂點著色器只需要估算頂點位置,片元著色器幾乎不執行任何操作。陰影貼圖只是深度緩沖區,因此即使是片元著色器輸出的顏色也無關緊要。
這意味著對于許多著色器,陰影投射物通道幾乎完全相同(除非對象具有基于自定義頂點著色器的變形,或者具有 Alpha 鏤空/半透明部分)。最簡單的捕捉方法是使用著色器命令?UsePass:
Pass
{// 常規光照通道
}
// 通過 VertexLit 內置著色器捕捉陰影投射物
UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"
但我們在這里是為了學習,所以讓我們通過“手動”方式實現相同效果。為了縮短代碼長度, 我們已經將光照通道 (“ForwardBase”) 替換為僅執行無紋理環境光的代碼。在它下面,有一個“ShadowCaster”通道,讓對象能夠支持陰影投射。
Shader "Lit/Shadow Casting"
{SubShader{// 非常簡單的光照通道,僅處理非紋理環境Pass{Tags {"LightMode"="ForwardBase"}CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{fixed4 diff : COLOR0;float4 vertex : SV_POSITION;};v2f vert (appdata_base v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);half3 worldNormal = UnityObjectToWorldNormal(v.normal);// 僅估算環境光o.diff.rgb = ShadeSH9(half4(worldNormal,1));o.diff.a = 1;return o;}fixed4 frag (v2f i) : SV_Target{return i.diff;}ENDCG}// 陰影投射物渲染通道,// 使用 UnityCG.cginc 中的宏手動實現Pass{Tags {"LightMode"="ShadowCaster"}CGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile_shadowcaster#include "UnityCG.cginc"struct v2f { V2F_SHADOW_CASTER;};v2f vert(appdata_base v){v2f o;TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)return o;}float4 frag(v2f i) : SV_Target{SHADOW_CASTER_FRAGMENT(i)}ENDCG}}
}
現在下方有一個平面,使用常規的內置漫射著色器,因此我們可以看到 我們的陰影生效(請記住,我們當前的著色器還不支持_接受_陰影!)。
我們使用了?#pragma multi_compile_shadowcaster?指令。這會導致著色器被編譯為多個變體,并為每個變體定義了不同的預處理器宏(有關詳細信息,請參閱?多個著色器變體頁面)。渲染到陰影貼圖時,點光源與其他光源類型需要著色器代碼略有不同,這就是需要此指令的原因。
接受陰影
Implementing support for receiving shadows will require compiling the base lighting pass into several variants, to handle cases of “directional light without shadows” and “directional light with shadows” properly.?#pragma multi_compile_fwdbase?directive does this (see?multiple shader variants?for details). In fact it does a lot more: it also compiles variants for the different lightmap types, Enlighten Realtime Global Illumination being on or off etc. Currently we don’t need all that, so we’ll explicitly skip these variants.
然后,為了獲得實際的陰影計算,我們將?#include “AutoLight.cginc”?著色器的?include 文件?并使用該文件中的 SHADOW_COORDS、TRANSFER_SHADOW 和 SHADOW_ATTENUATION 宏。
下面是著色器:
Shader "Lit/Diffuse With Shadows"
{Properties{[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}}SubShader{Pass{Tags {"LightMode"="ForwardBase"}CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "Lighting.cginc"// 將著色器編譯成多個有陰影和沒有陰影的變體//(我們還不關心任何光照貼圖,所以跳過這些變體)#pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight// 陰影 helper 函數和宏#include "AutoLight.cginc"struct v2f{float2 uv : TEXCOORD0;SHADOW_COORDS(1) // 將陰影數據放入 TEXCOORD1fixed3 diff : COLOR0;fixed3 ambient : COLOR1;float4 pos : SV_POSITION;};v2f vert (appdata_base v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv = v.texcoord;half3 worldNormal = UnityObjectToWorldNormal(v.normal);half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));o.diff = nl * _LightColor0.rgb;o.ambient = ShadeSH9(half4(worldNormal,1));// 計算陰影數據TRANSFER_SHADOW(o)return o;}sampler2D _MainTex;fixed4 frag (v2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv);// 計算陰影衰減(1.0 = 完全照亮,0.0 = 完全陰影)fixed shadow = SHADOW_ATTENUATION(i);// 用陰影使光照變暗,保持環境不變fixed3 lighting = i.diff * shadow + i.ambient;col.rgb *= lighting;return col;}ENDCG}// 陰影投射支持UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"}
}
看看,現在我們實現了陰影!
其他著色器示例
Fog
Shader "Custom/TextureCoordinates/Fog" {SubShader {Pass {CGPROGRAM#pragma vertex vert#pragma fragment frag//Needed for fog variation to be compiled.#pragma multi_compile_fog#include "UnityCG.cginc"struct vertexInput {float4 vertex : POSITION;float4 texcoord0 : TEXCOORD0;};struct fragmentInput{float4 position : SV_POSITION;float4 texcoord0 : TEXCOORD0;//Creates a variable that contains fog coordinates. The parameter must be a free TEXCOORD, for example 1 if TEXCOORD1 is free.UNITY_FOG_COORDS(1)};fragmentInput vert(vertexInput i){fragmentInput o;o.position = UnityObjectToClipPos(i.vertex);o.texcoord0 = i.texcoord0;//Compute fog amount from clip space position.UNITY_TRANSFER_FOG(o,o.position);return o;}fixed4 frag(fragmentInput i) : SV_Target {fixed4 color = fixed4(i.texcoord0.xy,0,0);//Apply fog (additive pass are automatically handled)UNITY_APPLY_FOG(i.fogCoord, color); //to handle custom fog color another option would have been //#ifdef UNITY_PASS_FORWARDADD// UNITY_APPLY_FOG_COLOR(i.fogCoord, color, float4(0,0,0,0));//#else// fixed4 myCustomColor = fixed4(0,0,1,0);// UNITY_APPLY_FOG_COLOR(i.fogCoord, color, myCustomColor);//#endifreturn color;}ENDCG}}
}