跳转到内容

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

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