跳轉到內容

Cg 程式設計/Unity/光衰減

來自 Wikibooks,開放世界中的開放書籍
TODO
待辦事項

編者注
顯然,Unity 已經改變了它計算光衰減的方式;因此,此程式碼似乎無法正常工作。暫時,此頁面將成為一個孤兒;也許我會找到時間在某個時候修復它。--Martin Kraus (討論貢獻) 2014 年 9 月 27 日 17:25(UTC)

倫勃朗·哈爾曼松·凡·萊茵的“富有的傻瓜”,1627 年。注意燭光隨著距離蠟燭的距離而衰減。

本教程介紹了光衰減的紋理,或者更一般地說,紋理作為查詢表。

它是基於 “Cookies” 部分。如果你還沒有閱讀本教程,你應該先閱讀它。

紋理貼圖作為查詢表

[編輯 | 編輯原始碼]

可以將紋理貼圖視為對二維函式的近似,該函式將紋理座標對映到 RGBA 顏色。如果將兩個紋理座標之一固定,紋理貼圖也可以表示一維函式。因此,通常可以使用紋理貼圖形式的查詢表來替換僅依賴於一個或兩個變數的數學表示式。(限制是紋理貼圖的解析度受紋理影像大小的限制,因此紋理查詢的精度可能不夠。)

使用這種紋理查詢的主要優點是潛在的效能提升:紋理查詢不依賴於數學表示式的複雜性,而僅依賴於紋理影像的大小(在一定程度上:紋理影像越小,快取效率越高,直到整個紋理適合快取)。但是,使用紋理查詢存在開銷;因此,替換簡單的數學表示式(包括內建函式)通常毫無意義。

哪些數學表示式應該用紋理查詢替換?不幸的是,沒有通用的答案,因為它取決於特定 GPU 是否特定查詢比評估特定數學表示式更快。但是,應該記住,紋理貼圖不太簡單(因為它需要程式碼來計算查詢表),不太顯式(因為數學函式被編碼在查詢表中),與其他數學表示式不一致,並且具有更廣泛的範圍(因為紋理在整個片段著色器中可用)。這些都是避免查詢表的充分理由。但是,效能的提升可能超過這些原因。在這種情況下,最好包含註釋來記錄如何在沒有查詢表的情況下實現相同的效果。

Unity 的光衰減紋理查詢

[編輯 | 編輯原始碼]

Unity 實際上在內部使用查詢紋理_LightTextureB0來進行點光和聚光燈的光衰減。(注意,在某些情況下,例如沒有餅乾紋理的點光,此查詢紋理設定為_LightTexture0,沒有B。此情況在此處忽略;因此,你應該使用聚光燈來測試程式碼。)在 “漫反射”部分中,描述瞭如何實現線性衰減:我們計算一個衰減因子,其中包括光源在世界空間中的位置與渲染的片段在世界空間中的位置之間的距離的倒數。為了表示此距離,Unity 使用光空間中的 座標。光空間座標已在 “Cookies” 部分 中討論過;這裡,重要的是我們可以使用 Unity 特定的統一矩陣_LightMatrix0將位置從世界空間轉換為光空間。類似於 “Cookies” 部分 中的程式碼,我們將光空間中的位置儲存在頂點輸出引數posLight 中。然後,我們可以在片段著色器中使用此引數的 座標在紋理_LightTextureB0 的 alpha 分量中查詢衰減因子

               float distance = input.posLight.z; 
                  // use z coordinate in light space as signed distance
               attenuation = 
                  tex2D(_LightTextureB0, float2(distance, distance)).a;
                  // texture lookup for attenuation               
               // alternative with linear attenuation: 
               //    float distance = length(vertexToLightSource);
               //    attenuation = 1.0 / distance;

使用紋理查詢,我們不必計算向量的長度(這涉及三個平方和一個平方根),也不必除以該長度。事實上,在查詢表中實現的實際衰減函式更復雜,以避免在短距離時出現飽和顏色。因此,與計算此實際衰減函式相比,我們節省了更多操作。

完整著色器程式碼

[編輯 | 編輯原始碼]

著色器程式碼基於 “Cookies” 部分 的程式碼。ForwardBase 透過假設光源始終是無衰減的定向光而略微簡化了傳遞。ForwardAdd 傳遞的頂點著色器與 “Cookies” 部分 中的程式碼相同,但片段著色器包含用於光衰減的紋理查詢,如上所述。但是,片段著色器缺少餅乾衰減,以便專注於距離衰減。重新包含餅乾程式碼(並且是一項很好的練習)是直觀的。

Shader "Cg light attenuation with texture lookup" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
      _SpecColor ("Specular Material Color", Color) = (1,1,1,1) 
      _Shininess ("Shininess", Float) = 10
   }
   SubShader {
      Pass {    
         Tags { "LightMode" = "ForwardBase" } 
            // pass for ambient light and 
            // first directional light source without attenuation
 
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         #include "UnityCG.cginc"
         uniform float4 _LightColor0; 
            // color of light source (from "Lighting.cginc")

         // User-specified properties
         uniform float4 _Color; 
         uniform float4 _SpecColor; 
         uniform float _Shininess;
 
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 posWorld : TEXCOORD0;
            float3 normalDir : TEXCOORD1;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = _Object2World;
            float4x4 modelMatrixInverse = _World2Object; 
 
            output.posWorld = mul(modelMatrix, input.vertex);
            output.normalDir = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            float3 normalDirection = normalize(input.normalDir);
 
            float3 viewDirection = normalize(
               _WorldSpaceCameraPos - input.posWorld.xyz);
            float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
 
            float3 ambientLighting = 
               UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
 
            float3 diffuseReflection = _LightColor0.rgb * _Color.rgb
               * max(0.0, dot(normalDirection, lightDirection));
 
            float3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = float3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = _LightColor0.rgb 
                  * _SpecColor.rgb * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            return float4(ambientLighting + diffuseReflection 
               + specularReflection, 1.0);
         }
 
         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 "Lighting.cginc")
         uniform float4x4 _LightMatrix0; // transformation 
            // from world to light space (from Autolight.cginc)
         uniform sampler2D _LightTextureB0; 
            // cookie alpha texture map (from Autolight.cginc)
 
        // User-specified properties
         uniform float4 _Color; 
         uniform float4 _SpecColor; 
         uniform float _Shininess;
 
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 posWorld : TEXCOORD0;
               // position of the vertex (and fragment) in world space 
            float4 posLight : TEXCOORD1;
               // position of the vertex (and fragment) in light space
            float3 normalDir : TEXCOORD2;
               // surface normal vector in world space
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = _Object2World;
            float4x4 modelMatrixInverse = _World2Object;
 
            output.posWorld = mul(modelMatrix, input.vertex);
            output.posLight = mul(_LightMatrix0, output.posWorld);
            output.normalDir = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            float3 normalDirection = normalize(input.normalDir);
 
            float3 viewDirection = normalize(
               _WorldSpaceCameraPos - input.posWorld.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 - input.posWorld.xyz;
               lightDirection = normalize(vertexToLightSource);
               
               float distance = input.posLight.z; 
                  // use z coordinate in light space as signed distance
               attenuation = 
                  tex2D(_LightTextureB0, float2(distance, distance)).a;
                  // texture lookup for attenuation               
               // alternative with linear attenuation: 
               //    float distance = length(vertexToLightSource);
               //    attenuation = 1.0 / distance;
            }
 
            float3 diffuseReflection = 
               attenuation * _LightColor0.rgb * _Color.rgb
               * max(0.0, dot(normalDirection, lightDirection));
 
            float3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = float3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = attenuation * _LightColor0.rgb 
                  * _SpecColor.rgb * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
            return float4(diffuseReflection + specularReflection, 1.0);
         }
         ENDCG
      }
   }
   Fallback "Specular"
}

如果你將此著色器計算的光照與內建著色器的光照進行比較,你會注意到強度相差約 2 到 4 倍。但是,這主要是由於內建著色器中的額外常數因子。在上面的程式碼中引入類似的常數因子是直觀的。

應該注意的是,光空間中的 座標不等於光源的距離;它甚至與該距離不成比例。事實上, 座標的含義取決於矩陣_LightMatrix0,它是 Unity 的一個未記錄的功能,因此隨時可能發生變化。但是,可以比較安全地假設值為 0 對應於非常近的位置,而值為 1 對應於更遠的位置。

還要注意的是,沒有餅乾紋理的點光在_LightTexture0 而不是_LightTextureB0 中指定衰減查詢紋理;因此,上面的程式碼不適用於它們。此外,程式碼沒有檢查 座標的符號,這對於聚光燈來說很好,但會導致點光源一側缺乏衰減。

計算查詢紋理

[編輯 | 編輯原始碼]

到目前為止,我們使用的是 Unity 提供的查詢紋理。如果 Unity 沒有在 `_LightTextureB0` 中提供紋理,我們必須自己計算該紋理。以下是一些用於計算類似查詢紋理的 JavaScript 程式碼。為了使用它,你必須將著色器程式碼中的 `_LightTextureB0` 名稱更改為 `_LookupTexture`,並將以下 JavaScript 附加到具有對應材質的任何遊戲物件。

@script ExecuteInEditMode()

public var upToDate : boolean = false;

function Start()
{
   upToDate = false;
}

function Update() 
{
   if (!upToDate) // is lookup texture not up to date? 
   {
      upToDate = true;
      var texture = new Texture2D(16, 16); 
         // width = 16 texels, height = 16 texels
      texture.filterMode = FilterMode.Bilinear;
      texture.wrapMode = TextureWrapMode.Clamp;
      
      GetComponent(Renderer).sharedMaterial.SetTexture("_LookupTexture", texture); 
         // "_LookupTexture" has to correspond to the name  
         // of the uniform sampler2D variable in the shader
      for (var j : int = 0; j < texture.height; j++)
      {
         for (var i : int = 0; i < texture.width; i++)
         {
            var x : float = (i + 0.5) / texture.width; 
               // first texture coordinate
            var y : float = (j + 0.5) / texture.height;  
               // second texture coordinate
            var color = Color(0.0, 0.0, 0.0, (1.0 - x) * (1.0 - x)); 
               // set RGBA of texels
            texture.SetPixel(i, j, color);
         }
      }
      texture.Apply(); // apply all the texture.SetPixel(...) commands
   }
}

在該程式碼中,`i` 和 `j` 列舉紋理影像的紋素,而 `x` 和 `y` 表示相應的紋理座標。紋理影像的 alpha 分量的函式 `(1.0-x)*(1.0-x)` 恰好產生了與 Unity 的查詢紋理相似的結果。

請注意,查詢紋理不應在每一幀都計算。相反,它只應在必要時計算。如果查詢紋理依賴於其他引數,則只有在任何引數發生改變時才應重新計算該紋理。這可以透過儲存計算查詢紋理的引數值並持續檢查任何新的引數是否與這些儲存值不同來實現。如果這是情況,則必須重新計算查詢紋理。

恭喜你,你已經完成了本教程。我們已經看到了

  • 如何使用內建紋理 `_LightTextureB0` 作為光衰減的查詢表。
  • 如何在 JavaScript 中計算自己的查詢紋理。

進一步閱讀

[編輯 | 編輯原始碼]

如果你仍然想要了解更多

  • 關於光源的光衰減,你應該閱讀 “漫反射” 部分。
  • 關於基本的紋理對映,你應該閱讀 “紋理球體” 部分。
  • 關於光空間中的座標,你應該閱讀 “Cookie” 部分。
  • 關於 SECS 原則(簡單、顯式、一致、最小範圍),你可以閱讀 David Straker 的著作“C Style: Standards and Guidelines”的第 3 章,該書由 Prentice-Hall 於 1991 年出版,可在 網上 獲得。

< Cg 程式設計/Unity

除非另有說明,本頁上的所有示例原始碼均授予公共領域。
華夏公益教科書