跳转到内容

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

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