针对移动设备修改着色器

我们在优化着色器这方面已经了解了较多的技术了,现在让我们来了解如何为移动设备编写高效,高质量的着色器代码。对于我们已经写好的着色器代码,通过一些小的修改让他们能在移动设备上高速运行好事比较简单的。这里包含了使用 approxview 或者 halfasview 光照函数变量等知识内容。我们可以减少所需的纹理数量并且对所用的纹理使用更好的压缩方式。这个知识点的最后,对于移动游戏,我们将会有一个优化很好的法线贴图,高光着色器。


  • 始前准备
    在开始前,我们先创建一个新的场景并且创建一些游戏对象用来使用我们的着色器:

    1. 创建一个新的场景并且添加一个默认球体和一个方向光。
    2. 创建一个新的材质球和着色器,并且把着色器应用到材质。
    3. 最后把材质应用到场景中的球体上。

    当完成上面的步骤后,你的场景看起来大概更下图差不多:
    diagram


  • 操作步骤
    在这个知识点中,我们会反复斟酌着色器中各种元素从而编写一个对移动平台友好着色器:
    • 1.首先根据所需的纹理修改着色器的 属性块(Properties block) 。在这个例子中,我们会使用一个alpha通道带有光滑纹理漫反射纹理,一张法线贴图,一个控制高光强度的滑动条。
    Properties
      {
          _Diffuse ("Base (RGB) Specular Amount (A)", 2D) = "white" {}
          _SpecIntensity ("Specular Width", Range(0.01, 1)) = 0.5
          _NormalMap ("Normal Map", 2D) = "bump" {}
      }
    
    • 2.下一个任务是设置 #pragma 申明。这些声明会打开或者关闭 表面着色器(Surface Shader) 的一些具体特性,并且最终影响着色器的性能消耗,是高成本还是低成本。
    CGPROGRAM
    #pragma surface surf MobileBlinnPhong exclude_path:prepass nolightmap noforwardadd halfasview
    
    • 3.接着我们在 CGPROGRAM 中定义与 属性块(Properties block) 中对应的变量。这次对于高光强度这个滑动条,我们将使用 fixed 类型的变量,从而减少着色器的内存使用:
    sampler2D _Diffuse;
    sampler2D _NormalMap;
    fixed _SpecIntensity;
    
    • 4.为了能将我们的纹理映射到游戏对象的表面,我们需要获取相应的UV。这个例子里,为了让着色器数据保持最小,我们将仅使用一个UV设置:
    struct Input
      {
          half2 uv_Diffuse;
      };
    
    • 5.这一步是要完成我们的光照函数,由于在 #pragma 声明中有了一些新的变量,所以这里我们就可以使用它们:
    inline fixed4 LightingMobileBlinnPhong(SurfaceOutput s, fixed3 lightDir, fixed3 halfDir, fixed atten)
      {
          fixed diff = max(0, dot(s.Normal, lightDir));
          fixed nh = max(0, dot(s.Normal, halfDir));
          fixed spec = pow(nh, s.Specular*128) * s.Gloss;
          fixed4 c;
          c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * (atten * 2);
          c.a = 0.0;
          return c;
      }
    
    • 6.最后,我们需要创建 surf() 函数并且处理表面的最终颜色:
    void surf (Input IN, inout SurfaceOutput o)
      {
          // Albedo comes from a texture tinted by color
          fixed4 diffuseTex = tex2D(_Diffuse, IN.uv_Diffuse);
          o.Albedo = diffuseTex.rgb;
          o.Gloss = diffuseTex.a;
          o.Alpha = 0.0;
          o.Specular = _SpecIntensity;
          o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_Diffuse));
      }
    

    当完成上面的步骤后,我们完成了这个知识点的部分代码,保存你的着色器代码并且返回Unity编辑等待着色器编译完。如果没有遇到什么错误,你会获得一个类似下图的结果:
    diagram

  • 原理介绍
    所以,让我们开始介绍这个着色器吧,看看它做了什么和没做什么。首先,它排除了后光线通道。也就是说如果你创建了一个连接 后渲染前通道(deferred renderer’s prepass) 的光线函数,那么它将不会使用那个特定的光线函数并且会去寻找默认的光线函数,比如我们这本书目前为止创建的那些默认的光线函数一样。
    这个特定的着色器并不受Unity内部的光照贴图系统的光照映射支持。这样的话对于使用了这个着色器的游戏对象来说,可以防止着色器试图去寻找法线贴图,从而可以让着色器有更好的性能表现,因为它不用再执行光线映射检测了。
    我们添加了 noforwardadd 声明,这样的话通过单个的方向光我们只要处理逐像素纹理即可。所有其他类型的光将会强制变成逐顶点光并且不会被在 surf() 函数中的任何逐像素操作所涉及。
    最后,我们使用 halfasview 声明告诉Unity,我们将不会使用普通光线函数中的 viewDir 参数。取而代之的是,我们将使用 半向量(half vector) 作为视野方向并且处理我们的高光。这样的话着色器的处理将会快很多,因为它是基于逐顶点操作来完成的。当用这个着色器来模拟真实世界中的高光时,其实它并不够精确,但是对于移动设备中的视效来说,它看起来已经很不错了并且着色器也优化的更好。
    这些技术可以让着色器更加高效和简洁【codewise 我不知道怎么翻译】。按照游戏的要求,根据你的游戏硬件和视觉质量要求来衡量你到底需要那些数据,最好确保只使用你需要的数据。最后,这些技术最终组成了游戏使用的那些着色器。