跳转到内容

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

除非另有说明,本页面上的所有示例源代码均归属公有领域。
华夏公益教科书