法线贴图

3D模型的每一个三角面都有一个 朝向facing direction ,这个朝向就是它向前的指向。它通常用一个垂直并且放置在三角面正中心的一个箭头来表示。这个朝向对于光在表面反射中来说非常的重要。如果相邻的两个三角面朝向不同的方向,那么它们对光的反射也会朝向不同的角度,也就是在着色器中它们的处理方式会不一样。对于曲面物体来说,这里有个疑问:很显然这些拥有曲面的几何体仍然是由平面三角形构成的,那光线改如何处理?

为了避免这个问题,对应三角面的光的反射计算方式此时不根据它的朝向计算,而是根据它的 法线方向normal direction 方向计算。前面 向着色器添加纹理 那个知识点讲到,顶点是保存有数据的。法线方向信息也是继UV信息之后,保存在顶点中最有用的信息。这是一个单位长度的向量,并且它表示了顶点的朝向。不考虑朝向的话,那么三角面内的每一个顶点都有它自己的法线方向,只不过这个法线方向是一个存储在顶点中的线性插值。这给了我们用低模模拟高模的能力。下面示例图展示了同一个几何形状在不同的顶点插值密度下的表现。在左边,法线垂直于由顶点表示的面;很明显每个面之间有明显的的割裂感。再看看最右边的几何体,它的法线是通过他的面线性插值得到的,可以看出来的是,尽管它的表面看起来还是很粗糙,但是光线的反射看起来却似乎很光滑。很容易看出来尽管这三个物体的几何体都相同,但是它们的光线反射却不一样。尽管都是由平面三角形构成,但是右边物体的光线反射似乎看起来像曲面反射。

diagram

一个有着粗糙的边的光滑物体很明显的表示单位顶点的法线肯定进行了线性插值。如果我们对保存在每个顶点的法线按其方向进行绘制,我们就能够看到它们,正如下图所展示的那样。你应该注意的是每个三角形仅有三条法线,但是相连的三角形有相同的顶点,会看到不止有一条法线从中绘制出来。 diagram

法线贴图在计算3D模型的法线技术中脱颖而出。跟纹理贴图类似,法线方向也可以用一张额外的纹理表示,我们把它们叫做 法线贴图normal map 或者 凹凸贴图bump map 。 法线贴图通常是一张RGB图片,里面的RGB通常分别用来表示法线方向中的X,Y,Z方向。现在有很多种技术方法来创建一张法线贴图。比如这些应用程序,CrazyBump (http://www.crazybump.com/)跟 NDO Painter (http://www.crazybump.com/)可以把2D数据转换成法线数据。其他的应用程序比如 Zbrush 4R7 (http://www.pixologic.com/)和 AUTODESK (http://usa.autodesk.com)可以把雕刻数据转换成法线贴图。如何创建法线贴图完全超出了本书的范畴,但上面的内容对你了解相关相应的知识还是有好处的。

在Unity中向着色器添加法线的过程很简单,因为在表面着色器中有着 UnpackNormals() 这样的方法给你调用。就让我们看看这是怎样的一个过程。


  • 始前准备

    分别创建一个新的材质和着色器,并且把它设置到 场景视图Scene view 中的游戏对象中去。这样的话,我们的项目非常简单,好让我们仅仅是观察法线贴图这项技术。

    这个知识点中你需要一张法线贴图,但是我们这本书附带的Unity工程中包含了一张。[当然,你也可以从我这里把这张图片下载下来,如下图]

    diagram


  • 操作步骤

    下面就是创建法线贴图着色器的步骤了:

    1. 让我们设置好我们的属性块,从而可以获得颜色和贴图:

      Properties
      {
      	_MainTint ("Diffuse Tint", Color) = (1,1,1,1)
      	_NormalTex ("Normal Map", 2D) = "bump" {}
      }
      

      注意

      因为用的是 bump 来初始化了属性的贴图类型,这等于是告诉了Unity _NormalTex 包含了法线贴图。如果这个贴图没有被设置, 那么会默认给它设置一张灰色的贴图。颜色值会用 (0.5,0.5,0.5,1) ,然后看不出一点凹凸感。

    2. CGPROGRAM 下面的 SubShader{} 块中声明下面两个变量,让这两个变量跟属性块中的两个属性关联起来:

      CPROGRAM
      #pragma surface surf Lambert
      // Link the property to the CG program
      sampler2D _NormalTex;
      float4 _MainTint;
      
    3. 我们需要修改 输入结构体Input struct 的名字,从而让我们可以让我们通过模型的UV来访问法线贴图:

      // Make sure you get the UVs for the texture in the struct
      struct Input
      {
      	float2 uv_NormalTex;
      }
      
    4. 最后,我们通过内建的 UnpackNormal() 函数从法线贴图中提取出我们需要的法线信息。接着,你只要把这些新的法线应用到表面着色器的输出上即可:

      // Get the normal data out of the normal map texture
      // using the UnpackNormal function
      float3 normalMap = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex));
      // Apply the new normal to the lighting model
      o.Normal = normalMap.rgb;
      

    下图展示了我们的法线贴图着色器的最终效果:

    diagram 注意

    着色器可以同时拥有纹理贴图和法线贴图。用UV数据同时来关联这两种贴图并不少见。然而,也可以在特有的顶点数据(UV2)中提供第二个UV的参数设置来设置法线贴图。


  • 原理介绍

    法线贴图效果背后的数学原理完全超出了本书的范畴,不过Unity已经为我们做了这一切。由于Unity为我们创建了各种方法所以我们不用一遍又一遍的做重复劳动。这也是为什么说表面着色器是编写着色器代码的高效方式的另一个原因。

    如果你在Unity的安装文件夹下的 Data 文件夹下的 UnityCG.cginc 文件中查找的话,你可以找到 UnpackNormal() 这个函数的定义。当你在表面着色器声明这个函数的时候,Unity会通过提供给它的法线贴图进行处理并且返回给你正确类型的数据,这样你就可以逐像素光照函数中使用它们。这为你节约了大量的时间!当对一张纹理[法线贴图]进行采样时,你可以获得从0到1取值范围的RGB值;然而,法线方向向量的取值范围确实-1到1。通过 UnpackNormal() 函数就可以给法线方向向量获取合理的值。

    当你通过 UnpackNormal() 函数对法线贴图进行了处理后,你把处理后的结果返回给了 SurfaceOutput 结构体,这样你才能在光照函数中使用它们。这是通过着色器中 o.Normal = normalMap.rgb; 这条语句实现的。我们会在**第三章理解光照模型**这一章中会看,法线究竟是怎么用于计算每一个像素点最终颜色的。


  • 额外内容

    你也可以在你的法线贴图着色器中添加一些控制从而可以让用户修改法线贴图的强度。这一点很容易通过改变法线贴图变量的 xy 元素然后把它们全部返回来办到。在法线贴图着色器中的属性块中添加另一个属性,名称为NormalMapIntensity

    _NormalMapIntensity("Normal intensity", Range(0,1)) = 1
    

    把解包出来的法线贴图数据的 xy 都乘以这个属性然后把这个得到的新的值返回给法线贴图变量:

    fixed3 n = UnpackNormal(tex2D(_BumpTex, IN.uv_ uv_MainTex)).rgb;
    n.x *= _NormalMapIntensity;
    n.y *= _NormalMapIntensity;
    o.Normal = normalize(n);
    

    注意

    法线向量的长度最好是等于1。当它们乘以 _NormalMapIntensity 后会改变它们的长度,所以对它进行标准化[归一化]是很有必要的。

    现在你可以让使用者在材质的 检查器面板Inspector tab 修改法线贴图的强度了。 下图为我们展示了法线贴图在不同强度参数下的不同表现:

    diagram