跳轉到內容

GLSL 程式設計/Unity/光線衰減

來自華夏公益教科書
倫勃朗·哈門松·凡·萊因的“富有的傻瓜”,1627 年。注意燭光隨著距離燭光的距離而衰減。

本教程涵蓋 **用於光線衰減的紋理** 或者更一般地說,**紋理作為查詢表**。

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

紋理貼圖作為查詢表

[編輯 | 編輯原始碼]

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

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

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

Unity 的紋理查詢用於光線衰減

[編輯 | 編輯原始碼]

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

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

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

完整的著色器程式碼

[編輯 | 編輯原始碼]

著色器程式碼基於 “Cookies” 部分 中的程式碼。ForwardBase 通道透過假設光源始終是方向性且沒有衰減而被略微簡化。ForwardAdd 通道的頂點著色器與 “Cookies” 部分 中的程式碼相同,但片段著色器包括用於光線衰減的紋理查詢,如上所述。但是,片段著色器缺少 cookie 衰減,以專注於距離衰減。重新包含 cookie 程式碼是直觀的(也是一個很好的練習)。

Shader "GLSL 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
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from Lighting.cginc)
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
 
         #ifdef VERTEX
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection = 
               normalize(vec3(_WorldSpaceLightPos0));
               // we assume that the light source in ForwardBase pass 
               // is a directional light source without attenuation
 
            vec3 ambientLighting = 
               vec3(gl_LightModel.ambient) * vec3(_Color);
 
            vec3 diffuseReflection = vec3(_LightColor0) * vec3(_Color) 
               * max(0.0, dot(normalDirection, lightDirection));
 
            vec3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            gl_FragColor = vec4(ambientLighting 
               + diffuseReflection + specularReflection, 1.0);
         }
 
         #endif
 
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from Lighting.cginc)
 
         uniform mat4 _LightMatrix0; // transformation 
            // from world to light space (from Autolight.cginc)
         uniform sampler2D _LightTextureB0; 
            // texture lookup (from Autolight.cginc)
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec4 positionInLightSpace; 
            // position of the vertex (and fragment) in light space
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
 
         #ifdef VERTEX
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            positionInLightSpace = _LightMatrix0 * position;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               lightDirection = normalize(vertexToLightSource);
               
               float distance = positionInLightSpace.z; 
                  // use z coordinate in light space as signed distance
               attenuation = 
                  texture2D(_LightTextureB0, vec2(distance)).a; 
                  // texture lookup for attenuation               
               // alternative with linear attenuation: 
               //    float distance = length(vertexToLightSource);
               //    attenuation = 1.0 / distance;
            }
 
            vec3 diffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_Color) 
               * max(0.0, dot(normalDirection, lightDirection));
 
            vec3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = attenuation * vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            gl_FragColor = 
               vec4(diffuseReflection + specularReflection, 1.0);
        }
 
         #endif
 
         ENDGLSL
      }
   } 
   // The definition of a fallback shader should be commented out 
   // during development:
   // Fallback "Specular"
}

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

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

另請注意,沒有 cookie 紋理的點光源在 _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;
      
      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
   }
}

在此程式碼中,ij 列舉紋理影像的紋理,而 xy 代表相應的紋理座標。紋理影像 alpha 分量的函式 (1.0-x)*(1.0-x) 恰好產生了與 Unity 查詢紋理相似的結果。

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

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

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

進一步閱讀

[編輯 | 編輯原始碼]

如果您想了解更多

  • 關於光源的光衰減,您應該閱讀 “漫反射”部分
  • 關於基本紋理對映,您應該閱讀 “紋理球體”部分
  • 關於光空間中的座標,您應該閱讀 “Cookies”部分
  • 關於 SECS 原則(簡單、明確、一致、最小範圍),您可以閱讀 David Straker 的“C 風格:標準和指南”一書的第 3 章,該書由 Prentice-Hall 於 1991 年出版,可透過 線上 獲得。


< GLSL 程式設計/Unity

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