第三章 理解光照模型

在前面的那些章节中,我们介绍了表面着色器并且还理解了如何修改一些物理属性(比如AlbedoSpecular)来模拟不同的材质。这些到底是如何工作的呢?每个表面着色器中最重要的部分一一光照模型lighting model。它的功能就是接受这些参数然后计算每一个像素点的最终着色。Unity通常会对开发者隐藏这部分,因为如果想要编写一个光照模型的话,你就必须要去理解光在物体表面是如何反射和折射的。这个章节中我们会毫无保留的向你展示光照模型是如何工作的,并且给你介绍一些你自己创建光照模型所需要的一些基础知识。

这一章中,我们会学习下面所列的知识点:

  • 创建一个自定义的漫反射光照模型
  • 创建一个Toon风格的着色器
  • 创建一个Phong类型类型的高光反射着色器
  • 创建 BlinnPhong 类型的高光反射着色器
  • 创建各向异性类型的高光反射着色器

  • 介绍

    想要模拟光的工作方式是一项非常具有挑战性的工作,同时也非常消耗计算资源。在之前的很早一段时间内,游戏中使用的都是一些非常简单的光照模型,效果看起来差到难以置信。尽管现在的3D游戏引擎已经使用了基于物理原理的渲染器,但是有些更简单的光照模型技术还是值得我们去探索的。但有时,我们不得不面对资源紧张的现实,没有办法在这些资源有限的设备上完整实现光照模型,比如我们的移动设备。所以你想在这上面实现自己的光照模型,那么你就很有必要了解这些简单的光照模型。


创建一个自定义的漫反射光照模型

如果你对Unity4很了解的话,你应该知道Unity提供的默认的着色器是基于一个叫Lambertian reflectance的光照模型。我们会在这个知识点向你展示如何创建一个自定义的光照模型,并且解释它后面的数学原理和实现方式。下面的两张图分别展示了标准着色器Standard Shader (右)diffuse Lambert着色器对同一个几何体进行渲染后,不同的显示效果:

基于Lambertian reflectance光照模型的着色器是典型的非真实渲染着色器;我们现实生活中没有物体会看起来像那样。然而Lambert 着色器依然能在一些低多边形风格的游戏中经常看到,因为跟一些复杂的几何体比起来,它们的三角面数量对比非常明显。用于计算Lambertian反射的光照模型非常高效,这特别适合移动端的游戏。

Unity其实已经提供了光照函数给我们,好让我们能在着色器中使用。它就是Lambertian光照模型。它是光反射模拟的一种更基础更有效率的形式,你能在当今的很多游戏中看到它的存在。 因为它们已经内建在了Unity的表面着色器语言中,所以我从这个开始和基于它开始构建也不失为一个好的选择。你也可以在Unity用户手册中找到一个例子,但我们还是会更深入学习,从而向你解释这些数据是从哪里来的以及为什么它是那样工作的。这些可以为设置光照模型打下一个很好的基础,这些知识将来也能在后面的章节中对我们有帮助。


  • 始前准备

    让我们从实现下面几个步骤开始:

    1. 创建一个新的着色器并且给它命名好。
    2. 创建一个新的材质球,命名好,并且把上一步新建的着色器应用于该材质。
    3. 接下来,创建一个球形对象,并且把它大致放在场景中间的位置。
    4. 最后,我们创建一个方向光源,让光找到游戏对象上。

    当你在Unity中设置好这些资源后, 你就会有一个类似于下图的场景:


    • 操作步骤

      Lambertian反射可以在着色器中修改下面的代码实现:

      1. 首先在着色器的属性Properties块中添加下面的属性:
      _MainTex("Texture", 2D) = "white"
      
      1. 修改着色器的**#pragma指示符,从而让着色器使用我们自定义的光照模型,而不是标准Standard**:
      #pragma surface surf SimpleLambert
      
      1. 使用一个非常简单的表面函数surface function,这个函数仅仅通过它的UV数据对纹理进行采样:
      void surf (Input IN, inout SurfaceOutput o)
      {
          o.Albedo = tex2D(_MainTex,IN.uv_MainTex).rgb;
      }
      
      1. 添加一个叫做LightingSimpleLambert()的函数,这个函数包含了下面实现Lambertian反射的代码:
      half4 LightingSimpleLambert(SurfaceOutput s,half3 lightDir,half atten)
      {
          half NdotL = dot(s.Normal,lightDir);
          half4 c;
          c.rgb = s.Albedo * _LightColor0.rgb * (NdotL*atten*1);
          c.a = s.Alpha;
          return c;
      }
      

  • 原理介绍

    如我们前面 第一章创建你的第一个着色器 所看到的那样,#pragma指示用于指定我们该用哪个表面函数。选择不同的光照模型都是按照下面同一种方法:SimpleLambert 会强制Cg 去寻找一个叫做LightingSimpleLambert()函数。注意开始前面的Lighting,只是在指示中被省略了。

    这个光照函数接收3个参数:表面输出结构体surface output(它包含了物理属性比如albedotransparency),光照照进来的方向direction和光的衰减attenuation

    根据Lambertian 反射原理, 表面反射光线的数量由表面法线跟入射光线的夹角决定。如果你玩过台球,那么你应该对这个概念比较熟悉;球的方向由球的入射方向跟球桌对应边缘的夹角决定。如果你以90度击球,这个球会垂直返回;如果你以一个非常小的角度击球,球的方向几乎改变很少。Lambertian光照模型跟这个很像;如果光以90度的方式照到一个三角面上,所有光线都会被反射回来。入射的角度越小,返回来的光线也会越少。下面的图片解释了这个概念:

    下图展示了一束阳光照射到了一个复杂的表面。L 指光的方向(就是着色器中的lightDir),N是表面的法线。光线以相同的角度反射,反射角跟入射角相同:

    I = N * L

    NL 平行的时候,光线会反射回来,从这个角度看这个几何图形会非常的亮。_LightColor0这个变量包含了对光进行计算后的到的颜色。

    注意

    Unity5之前的引擎,光强(intensity of the lights)是不一样的。如果你使用的是基于Lambertian反射模型的旧的漫反射着色器,你会发现NdotL乘了两个参数:是**(NdotL * atten * 2)而不是(NdotL * atten)**。如果你从Unity4中导入的自定义的着色器,需要你手动纠正这点。而Unity自带的一些老的着色器在设计的时候已经考虑到这点了,帮你纠正了。

    当点积是负数的时候,这个光是来自三角面的背面。 这对不透明的几何体没有问题,因为摄像机只会渲染朝前面向摄像机的三角面,否则就会被剔除,不去渲染。

    这个基本的Lambert光照模型很适合用于构建自己的着色器原型,因为它已经有了着色器中光照模型的核心功能。

    Unity已经为我们创建Lambert光照模型提供了原型。在你的Unity安装目录下的Editor/Data/CGIncludes文件夹内照到UnityCG.cginc文件,会发现其实已经有LamberBlinnPhong光照模型了。当你使用**#pragma surface surf Lambert指示编译着色器时,等于是告诉着色器请使用Unity提供的UnityCG.cginc文件中的Lambert光照模型,这样我们就不用重复造轮子。我们会在这章的稍后部分探索BlinnPhong**光照模型。