跳轉到內容

Cg 程式設計/Unity/漫反射

來自 Wikibooks,開放世界中的開放書籍
月球表面的光反射(在很好的近似下)只是漫反射。

本教程涵蓋 **逐頂點漫反射**。

它是 Unity 中關於基本光照的教程系列的第一部分。在本教程中,我們從單個方向光源的漫反射開始,然後包括點光源和多個光源(使用多次傳遞)。後面的教程涵蓋了其擴充套件,特別是鏡面反射、逐畫素光照和雙面光照。

漫反射可以使用表面法線向量 N 和光線向量 L 來計算,即指向光源的向量。

漫反射

[編輯 | 編輯原始碼]

月球幾乎完全表現出漫反射(也稱為朗伯反射),即光線向各個方向反射,沒有鏡面高光。其他此類材料的例子包括粉筆和啞光紙;實際上,任何看起來暗淡和啞光的表面都是如此。

在完美的漫反射情況下,觀察到的反射光的強度取決於表面法線向量和入射光線之間的夾角的餘弦。如左圖所示,通常考慮從表面一點開始的歸一化向量,在該點應該計算光照:歸一化表面法線向量 **N** 與表面正交,歸一化光線方向 **L** 指向光源。

對於觀察到的漫反射光 ,我們需要歸一化表面法線向量 **N** 和歸一化光源方向 **L** 之間的夾角的餘弦,即點積 **N**·**L**,因為任何兩個向量 **a** 和 **b** 的點積 **a**·**b** 是

.

在歸一化向量的情況下,長度 |**a**| 和 |**b**| 均為 1。

如果點積 **N**·**L** 為負,則光源位於表面的“錯誤”一側,我們應該將反射設定為 0。這可以透過使用 max(0, **N**·**L**)來實現,這確保了對於負點積,點積的值被鉗制為 0。此外,反射光取決於入射光的強度 和一個材料常數 用於漫反射:對於黑色表面,材料常數 為 0,對於白色表面,它為 1。則漫反射強度的方程為

對於彩色光,此方程適用於每個顏色分量(例如紅色、綠色和藍色)。因此,如果變數 ,和 表示顏色向量,乘法是按分量執行的(它們在 Cg 中是針對向量的),此方程也適用於彩色光。這就是我們實際在著色器程式碼中使用的。

一個方向光源的著色器程式碼

[編輯 | 編輯原始碼]

如果我們只有一個方向光源,則用於實現 方程的著色器程式碼相對較小。為了實現該方程,我們遵循關於實現方程的問題,這些問題在 “輪廓增強”部分 中進行了討論。

  • 該方程應該在頂點著色器還是片段著色器中實現?我們在這裡嘗試使用頂點著色器。在 “平滑鏡面高光”部分 中,我們將檢視在片段著色器中的實現。
  • 方程式應該在哪個座標系中實現?我們在 Unity 中預設嘗試使用世界空間。(事實證明這是一個不錯的選擇,因為 Unity 在世界空間中提供光線方向。)
  • 我們從哪裡獲取引數?這個問題的答案有點長

我們使用一個著色器屬性(參見 “世界空間中的著色”部分)讓使用者指定漫射材質顏色 。我們可以從 Unity 特定的統一變數 _WorldSpaceLightPos0 獲取世界空間中光源的方向,並從 Unity 特定的統一變數 _LightColor0 獲取光線顏色 。如 “世界空間中的著色”部分 所述,我們必須用 Tags {"LightMode" = "ForwardBase"} 標記著色器通道,以確保這些統一變數具有正確的值。(下面我們將討論此標籤的實際含義。)我們從帶有語義 NORMAL 的頂點輸入引數獲取物件座標中的表面法線向量。由於我們在世界空間中實現方程式,因此我們必須根據 “輪廓增強”部分 中的討論,將表面法線向量從物件空間轉換為世界空間。

然後著色器程式碼看起來像這樣

Shader "Cg per-vertex diffuse lighting" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
   }
   SubShader {
      Pass {	
         Tags { "LightMode" = "ForwardBase" } 
            // make sure that all uniforms are correctly set
 
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         #include "UnityCG.cginc"
 
         uniform float4 _LightColor0; 
            // color of light source (from "UnityLightingCommon.cginc")
 
         uniform float4 _Color; // define shader property for shaders
 
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 col : COLOR;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = unity_ObjectToWorld;
            float4x4 modelMatrixInverse = unity_WorldToObject;
 
            float3 normalDirection = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            // alternative: 
            // float3 normalDirection = UnityObjectToWorldNormal(input.normal);
            float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
 
            float3 diffuseReflection = _LightColor0.rgb * _Color.rgb
               * max(0.0, dot(normalDirection, lightDirection));
 
            output.col = float4(diffuseReflection, 1.0);
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }
 
         ENDCG
      }
   }
   Fallback "Diffuse"
}

使用此著色器時,請確保場景中只有一個光源,並且該光源必須是方向光。如果沒有光源,您可以透過從主選單中選擇遊戲物件>光>方向光來建立方向光源。

備用著色器

[edit | edit source]

著色器程式碼中的 Fallback "Diffuse" 行定義了一個內建的備用著色器,以防 Unity 找不到合適的子著色器。對於我們的示例,如果 Unity 不使用“前向渲染路徑”(見下文),則將使用備用著色器。透過為我們的著色器屬性選擇特定名稱“_Color”,我們確保此內建備用著色器也可以訪問它。內建著色器的原始碼可在 Unity 網站 上找到。檢查此原始碼似乎是確定合適的備用著色器及其使用的屬性名稱的唯一方法。

多個方向(畫素)光源的著色器程式碼

[edit | edit source]

到目前為止,我們只考慮了單個光源。為了處理多個光源,Unity 會根據渲染和質量設定選擇不同的技術。在本教程中,我們只介紹“前向渲染路徑”。(此外,所有攝像機都應配置為使用播放器設定,它們預設情況下就是這樣。)

在本教程中,我們只考慮 Unity 的所謂畫素光源。對於第一個畫素光源(始終是方向光),Unity 呼叫用 Tags { "LightMode" = "ForwardBase" }(如我們上面的程式碼中)標記的著色器通道。對於每個額外的畫素光源,Unity 呼叫用 Tags { "LightMode" = "ForwardAdd" } 標記的著色器通道。為了確保所有光源都作為畫素光源渲染,您必須確保質量設定允許足夠的畫素光源:選擇編輯>專案設定>質量,然後在您使用的任何質量設定中增加標記為畫素光源數量的數字。如果場景中的光源數量超過畫素光源數量的限制,則 Unity 僅將最重要的光源渲染為畫素光源。或者,您可以將所有光源的渲染模式設定為重要,以便將它們渲染為畫素光源。(有關不太重要的頂點光源的討論,請參閱 “多個光源”部分。)

到目前為止,我們的著色器程式碼對於 ForwardBase 通道來說是可以的。對於 ForwardAdd 通道,我們需要將反射光新增到已儲存在幀緩衝區中的光中。為此,我們只需要將混合配置為將新的片段輸出顏色新增到幀緩衝區中的顏色。正如 “透明度”部分 中所述,這是透過加性混合方程實現的,該方程由以下程式碼行指定

Blend One One

混合會自動將所有結果鉗制在 0 到 1 之間;因此,我們不必擔心顏色或 alpha 值大於 1。

總的來說,我們用於多個方向光源的新著色器變為

Shader "Cg per-vertex diffuse lighting" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
   }
   SubShader {
      Pass {	
         Tags { "LightMode" = "ForwardBase" } 
           // pass for first light source
 
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         #include "UnityCG.cginc"
 
         uniform float4 _LightColor0; 
            // color of light source (from "UnityLightingCommon.cginc")
 
         uniform float4 _Color; // define shader property for shaders
 
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 col : COLOR;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = unity_ObjectToWorld;
            float4x4 modelMatrixInverse = unity_WorldToObject; 
 
            float3 normalDirection = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
 
            float3 diffuseReflection = _LightColor0.rgb * _Color.rgb
               * max(0.0, dot(normalDirection, lightDirection));
 
            output.col = float4(diffuseReflection, 1.0);
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }
 
         ENDCG
      }
 
      Pass {	
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 
 
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         #include "UnityCG.cginc"
 
         uniform float4 _LightColor0; 
            // color of light source (from "UnityLightingCommon.cginc")
 
         uniform float4 _Color; // define shader property for shaders
 
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 col : COLOR;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = unity_ObjectToWorld;
            float4x4 modelMatrixInverse = unity_WorldToObject; 
 
            float3 normalDirection = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
 
            float3 diffuseReflection = _LightColor0.rgb * _Color.rgb
               * max(0.0, dot(normalDirection, lightDirection));
 
            output.col = float4(diffuseReflection, 1.0);
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }
 
         ENDCG
      }
   }
   Fallback "Diffuse"
}

這似乎是一個相當長的著色器;但是,除了標籤和 ForwardAdd 通道中的 Blend 設定外,這兩個通道是相同的。

點光源的更改

[edit | edit source]

在方向光源的情況下,_WorldSpaceLightPos0 指定光線照射的方向。但是,在點光源(或聚光燈源)的情況下,_WorldSpaceLightPos0 指定光源在世界空間中的位置,我們必須計算到光源的方向,即從頂點在世界空間中的位置到光源位置的差向量。由於點的第 4 個座標是 1,而方向的第 4 個座標是 0,因此我們可以輕鬆地區分這兩種情況

            float3 lightDirection;

            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            } 
            else // point or spot light
            {
               lightDirection = normalize(_WorldSpaceLightPos0.xyz 
                  - mul(modelMatrix, input.vertex).xyz);
            }

雖然方向光源沒有光線衰減,但我們應該對點光源和聚光燈源新增一些距離衰減。由於光線從三維空間中的一個點向外傳播,因此它在較遠距離處覆蓋越來越大的虛擬球體。由於這些球體的表面積隨著半徑的增加而二次增加,並且每個球體的總光量是相同的,因此每單位面積的光量隨著距離點光源的距離增加而二次減小。因此,我們應該將光源的強度除以到頂點的距離的平方。

由於二次衰減非常快,因此我們使用線性距離衰減,即我們使用距離而不是距離的平方來除以強度。程式碼可以是

            float3 lightDirection;
            float attenuation;

            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            } 
            else // point or spot light
            {
               float3 vertexToLightSource = _WorldSpaceLightPos0.xyz 
                  - mul(modelMatrix, input.vertex).xyz;
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }

然後應該將因子 attenuation_LightColor0 相乘以計算入射光;請參閱下面的著色器程式碼。請注意,聚光燈源具有其他功能,這些功能超出了本教程的範圍。

還要注意,此程式碼不太可能提供最佳效能,因為任何 if 通常成本都很高。由於 _WorldSpaceLightPos0.w 為 0 或 1,因此實際上重寫程式碼以避免使用 if 並進一步最佳化並不難

            float3 vertexToLightSource = 
               _WorldSpaceLightPos0.xyz - mul(modelMatrix, 
               input.vertex * _WorldSpaceLightPos0.w).xyz;
            float one_over_distance =  
               1.0 / length(vertexToLightSource);
            float attenuation = 
               lerp(1.0, one_over_distance, _WorldSpaceLightPos0.w); 
            float3 lightDirection = 
               vertexToLightSource * one_over_distance;

但是,為了清晰起見,我們將使用帶 if 的版本。(“保持簡單,傻瓜!”)

用於多個方向光和點光的完整著色器程式碼為

Shader "Cg per-vertex diffuse lighting" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
   }
   SubShader {
      Pass {	
         Tags { "LightMode" = "ForwardBase" } 
           // pass for first light source
 
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         #include "UnityCG.cginc"
 
         uniform float4 _LightColor0; 
            // color of light source (from "UnityLightingCommon.cginc")
 
         uniform float4 _Color; // define shader property for shaders
 
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 col : COLOR;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = unity_ObjectToWorld;
            float4x4 modelMatrixInverse = unity_WorldToObject; 
 
            float3 normalDirection = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            float3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            } 
            else // point or spot light
            {
               float3 vertexToLightSource = _WorldSpaceLightPos0.xyz 
                  - mul(modelMatrix, input.vertex).xyz;
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
 
            float3 diffuseReflection = 
               attenuation * _LightColor0.rgb * _Color.rgb
               * max(0.0, dot(normalDirection, lightDirection));
 
            output.col = float4(diffuseReflection, 1.0);
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }
 
         ENDCG
      }
 
      Pass {	
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 
 
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         #include "UnityCG.cginc"
 
         uniform float4 _LightColor0; 
            // color of light source (from "UnityLightingCommon.cginc")
 
         uniform float4 _Color; // define shader property for shaders
 
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 col : COLOR;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = unity_ObjectToWorld;
            float4x4 modelMatrixInverse = unity_WorldToObject;
 
            float3 normalDirection = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            float3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            } 
            else // point or spot light
            {
               float3 vertexToLightSource = _WorldSpaceLightPos0.xyz 
                  - mul(modelMatrix, input.vertex).xyz;
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
 
            float3 diffuseReflection = 
               attenuation * _LightColor0.rgb * _Color.rgb
               * max(0.0, dot(normalDirection, lightDirection));
  
            output.col = float4(diffuseReflection, 1.0);
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }
 
         ENDCG
      }
   }
   Fallback "Diffuse"
}

請注意,ForwardBase 通道中的光源始終是方向光;因此,第一個通道的程式碼實際上可以簡化。另一方面,對兩個通道使用相同的 Cg 程式碼可以使我們更容易在必須編輯著色器程式碼時將程式碼從一個通道複製貼上到另一個通道。

聚光燈的更改

[edit | edit source]

Unity 使用餅乾紋理實現聚光燈,如 “餅乾”部分 所述;但是,這有點高階。在這裡,我們將聚光燈視為點光源。

總結

[edit | edit source]

恭喜!您剛學會了 Unity 的每個畫素光源的工作原理。這對於以下有關更高階照明的教程至關重要。我們還看到了

  • 什麼是漫射反射,以及如何用數學方法描述它。
  • 如何在著色器中實現單個方向光源的漫射反射。
  • 如何將著色器擴充套件到具有線性衰減的點光源。
  • 如何進一步擴充套件著色器以處理多個每個畫素光源。

進一步閱讀

[edit | edit source]

如果您還想了解更多

< Cg 程式設計/Unity

除非另有說明,本頁面上的所有示例原始碼都屬於公有領域。
華夏公益教科書