Cg Programming/Unity/Diffuse Reflection漫反射

    本教程涵盖了逐顶点漫反射。
    这是Unity中关于光照的一系列教程中的第一章。在本章中,我们从一个简单的方向光源开始漫反射,然后包括点光源和多个光源(使用多个通道)。延伸教程涵盖了这方面的扩展,特别是镜面反射、逐象素光照和双面光照。
这里写图片描述月球表面的光照反射(这是一个很好的近似)就只是漫反射。

漫反射

    月球表现出几乎完全地漫反射(也叫做兰伯特反射),也就是光是向四面八方反射的,没有镜面高光。其它有此材质的例子是粉笔和哑光笔;实际上,任何表面都表现出暗淡和不光滑。
    在完全漫反射的情况下,所观察到的反射光的强度取决于表面法向量和入射光光线的夹角的余弦值。如上图所示,我们很容易考虑从一个表面的点开始的归一化向量,这里光照会被计算:归一化的表面法向量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”}来标记着色器通道(pass),以保证这些统一值有正确的值。(下面我们会讨论这个标记的实际意义。)我们从带有语义NORMAL的顶点输入参数中获得物体坐标系中的表面法向量。既然我们是在世界空间中实现此方程的,那就必须像在章节“轮廓增强”中讨论的一样把表面法向量从物体空间转化到世界空间中去。
    着色器代码就像下面这样:
Shader "Cg per-vertex diffuse lighting" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
   }
   SubShader {
      Pass {    
         Tags { "LightMode" = "ForwardBase" } 
            // 确保所有的统一值被正确设置

         CGPROGRAM

         #pragma vertex vert  
         #pragma fragment frag 

         #include "UnityCG.cginc"

         //*译者注:uniform 变量在vertex和fragment两者之间声明方式完全一样,则它可以在vertex和fragment共享使用。(相当于一个被vertex和fragment shader共享的全局变量)*
         uniform float4 _LightColor0; 
            // 光源颜色 (from "Lighting.cginc")

         uniform float4 _Color; // 定义着色器属性

         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 = _Object2World;
            float4x4 modelMatrixInverse = _World2Object;

            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 = mul(UNITY_MATRIX_MVP, input.vertex);
            return output;
         }

         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }

         ENDCG
      }
   }
   Fallback "Diffuse"
}

    当你使用这个着色器时,确保场景中只有一个光源,而且必须是方向光。如果没有光源,你可以通过在菜单栏中选择Edit > Project Settings > Player来创建一个方向光源,然后在Inspector Window the Other Settings > Rendering > Rendering Path中设置为Forward。(以下会有更多关于“前向渲染路径”的细节。)

备用着色器

    着色器代码中的这行“Fallback Diffuse”定义了内置的备用着色器以防Unity没有找到合适的子着色器。对于我们的例子来说,如果没有使用“前向渲染路径”Unity将会使用备用着色器。通过为我们的着色器属性使用指定的名字“_Color”,我们要保证内置的备用着色器也能访问到它。在Unity的网站上可以看到内置着色器的源代码。查看源代码似乎是唯一的方法来决定合适的备用着色器和正在使用的属性名字。

多方向光照(像素)着色器代码

    目前我们只考虑到单个光源的情况。为了解决多光源,Unity会根据渲染和质量的设置来选择不同的技术方案。在本教程中,我们只是涵盖了“前向渲染路径”。为了选择它,选择Edit > Project Settings > Player然后在Inspector窗口中设置Other Settings > Rendering > Rendering Path为Forward。(此外,所有的摄像机应该被设置为使用player settings,目前它们是默认的。)
    在本教程中我们只考虑Unity所说的像素光照。对于第一个像素光照(通常是一个方向光),Unity会调用被标记为Tags { “LightMode” = “ForwardBase” }的着色器通道(如以上代码所述)。对于每个额外的像素光照,Unity会调用被标记为Tags { “LightMode” = “ForwardAdd” }的着色器通道。为了保证所有的光源作为像素光来渲染,你必须保证质量设置(quality settings)允许足够多的像素光:选择Edit > Project Settings > Quality然后增加任何你使用的quality settings中标记为Pixel Light Count的数量。如果场景中的光源数目超过了允许的最大像素光数目,Unity只渲染作为像素光的最重要的光。或者,为了把它们作为像素光来渲染,你可以设置所有的光源的渲染模式(Render Mode) 为重要(Important )。(对于不那么重要的顶点光照可以查阅章节“多光源”。)
    对于ForwardBase 通道我们的着色器代码目前是OK的。对于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" } 

         CGPROGRAM

         #pragma vertex vert  
         #pragma fragment frag 

         #include "UnityCG.cginc"

         //光源颜色(from "Lighting.cginc")
         uniform float4 _LightColor0; 

         uniform float4 _Color; // 定义着色器属性

         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 = _Object2World;
            float4x4 modelMatrixInverse = _World2Object; 

            //译者注:为什么是乘以modelMatrixInverse
            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 = mul(UNITY_MATRIX_MVP, input.vertex);
            return output;
         }

         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }

         ENDCG
      }

      Pass {    
         Tags { "LightMode" = "ForwardAdd" } 
            // 额外光源的通道
         Blend One One // 加性混合 

         CGPROGRAM

         #pragma vertex vert  
         #pragma fragment frag 

         #include "UnityCG.cginc"

         uniform float4 _LightColor0; 
            // color of light source (from "Lighting.cginc")

         uniform float4 _Color; // 定义着色器属性

         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 = _Object2World;
            float4x4 modelMatrixInverse = _World2Object; 

            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 = mul(UNITY_MATRIX_MVP, input.vertex);
            return output;
         }

         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }

         ENDCG
      }
   }
   Fallback "Diffuse"
}

    这似乎是一个相当长的着色器;但是,除了标记和ForwardAdd通道中的混合设置外两个通道都是等价的。

点光源的变化

    在方向光源的情况下,_WorldSpaceLightPos0指定了光入射的方向。但是,在点光源(或聚光源)的情况下,_WorldSpaceLightPos0指定了世界空间中光源的位置,我们必须计算光源的方向,即世界空间中点的位置与光源位置差的向量。既然一个点的第4个坐标是1,方向的第4个坐标是0,那么我们可以很轻易地区分以上两种情况:

            float3 lightDirection;

            if (0.0 == _WorldSpaceLightPos0.w) // 方向光
            {
               lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            } 
            else // 点光源(或聚光源)
            {
               lightDirection = normalize(_WorldSpaceLightPos0.xyz 
                  - mul(modelMatrix, input.vertex).xyz);
            }

    既然对于方向光来说没有光的衰减,我们应该用距离为点和聚光源加入一些衰减。当光从三维的一个点散开时,它在更大的距离上覆盖了更大的虚拟球体。因为这些球体表面随半径的增加成平方关系,并且每个球体光的总量是相同的,那么每块区域光的数量随着到点光源距离的增加而二次递减。因此,我们应该按到顶点距离的平方来划分光源的强度。
    既然二次衰减非常迅速,我们就使用距离的线性衰减,也就是我们使用距离代替距离的平方来划分强度。代码如下:

float3 lightDirection;
float attenuation;

if (0.0 == _WorldSpaceLightPos0.w) // 方向光
{
      attenuation = 1.0; //没有衰减
      lightDirection = normalize(_WorldSpaceLightPos0.xyz);
} 
else // 点或聚光源
{
        float3 vertexToLightSource = _WorldSpaceLightPos0.xyz 
                  - mul(modelMatrix, input.vertex).xyz;
        float distance = length(vertexToLightSource);
        attenuation = 1.0 / distance; // 线性衰减 
        lightDirection = normalize(vertexToLightSource);
}

    随后衰减因子应该乘以_LightColor0来计算入射光;查看如下的着色器代码。注意聚光源有额外的特征,不过已经超出了本教程的范围了。
    还要注意,这段代码不太可能给你最好的性能,因为任何if通常是相当消耗性能的。既然_WorldSpaceLightPos0的值要么是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; 
            // 光源颜色 (从内置的 "Lighting.cginc"获得)

         uniform float4 _Color; // 定义着色器属性

         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 = _Object2World;
            float4x4 modelMatrixInverse = _World2Object; 

            float3 normalDirection = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            float3 lightDirection;
            float attenuation;

            if (0.0 == _WorldSpaceLightPos0.w) // 方向光
            {
               attenuation = 1.0; // 无衰减
               lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            } 
            else // 点或聚光源
            {
               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 = mul(UNITY_MATRIX_MVP, input.vertex);
            return output;
         }

         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }

         ENDCG
      }

      Pass {    
         Tags { "LightMode" = "ForwardAdd" } 
            // 额外光源的通道
         Blend One One // 加性混合 

         CGPROGRAM

         #pragma vertex vert  
         #pragma fragment frag 

         #include "UnityCG.cginc"

         uniform float4 _LightColor0; 
            // color of light source (from "Lighting.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 = _Object2World;
            float4x4 modelMatrixInverse = _World2Object;

            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; // 线性衰减 
               lightDirection = normalize(vertexToLightSource);
            }

            float3 diffuseReflection = 
               attenuation * _LightColor0.rgb * _Color.rgb
               * max(0.0, dot(normalDirection, lightDirection));

            output.col = float4(diffuseReflection, 1.0);
            output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
            return output;
         }

         float4 frag(vertexOutput input) : COLOR
         {
            return input.col;
         }

         ENDCG
      }
   }
   Fallback "Diffuse"
}

注意,在ForwardBase通道中的光源通常是方向光;因此,第一个通道的代码实际上很简单。另一方面,对两个通道使用同样的Cg代码,可以更容易地从一个通道到另一个通道拷贝和粘贴代码,以防我们不得不编辑着色器代码。
如果这个着色器有问题,记得通过选择Edit > Project Settings > Player来激活“前向渲染路径“,随后在Inspector窗口设置Other Settings > Rendering > Rendering Path为Forward。

聚光灯的变化

Unity使用cookie纹理实现聚光灯,如”Cookies“部分所述;但是,这部分内容有点超前了。这里,我们把聚光灯看作点光源。

总结

恭喜你!你刚刚学完了Unity的逐像素光照是如何工作的。这对于下面有关更高级光照的教程是必不可少的。我们可看到了:

  • 漫反射是什么以及如何在数学上描述它。
  • 对于单方向光源如何在着色器中实现漫反射。
  • 如何用线性衰减扩展点光源的着色器。
  • 如何进一步扩展着色器解决多个逐像素光照。

扩展阅读

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值