第七章 移动设备着色器适配

在接下来的两章,我们将着手于让我们写的着色器对不同的平台都有较好的性能表现。我们不会讨论任何一个特殊的平台,我们将会分解着色器内的元素,这样的话我们就可以对它们进行调整,从而让它们对于移动平台有更好的优化并且通常来说对其他任何平台来说也更高效。这些技术涵盖了从 了解Unity提供的一些可以减少着色器内存溢出方面的内建变量学习可以让我们的着色器代码更加高效的方法。这一章将会包含下面的这些知识点:

  • 什么是低成本着色器
  • 着色器的性能分析
  • 针对移动设备修改着色器

介绍

学习如何优化着色器的艺术将会出现在你参与的任何游戏项目中。在任何产品中总有需要优化着色器的时候,或者需要用更少的纹理来产生相同的效果。作为一个技术美术或者着色器编程人员,你必须要理解这些核心的基本原理来优化你的着色器代码从而让你的游戏在提升性能表现的同时又能达到相同的视觉表现。有了这些知识也可以为你自己开始写着色器代码进行铺垫。比如,你知道使用你着色器的游戏将会运行在移动设备中,我们可以自动的设置所有的光照函数使用 half vector 作为视野方向,或者把所有的 浮点型变量类型(float variable types) 都设置成 fixed 类型 或 half 类型。前面提到的这些技术或者很多的其他技术,都可以让你的着色器在目标硬件上更加高效的运行。开始我们的着色器优化学习之旅吧。


什么是低成本着色器

我们首先问一个问题,什么是低成本的着色器,它回答起来可能有点困难因为有太多的元素可以可以让一个着色器变得更加高效了。它可以是你的变量使用的内存的大小。可以是你的着色器使用的纹理的大小。也可是一个工作良好的着色器,但是我们却只使用了相较之前一半的代码或者数据就获得了相同的视觉效果。我们将会在这个知识点中探索一些这样的技术并且会展示如何将这些技术结合起来从而让你的着色器更快更高效,并且不管是在移动设备还是在PC上都生成当今游戏中每个人都期望的高质量的视觉效果。


  • 始前准备
    在开始这个知识点之前,我们需要准备一些资源并且把它们放一块。所以让我们按照下面的几个任务来:

    • 1.创建一个新的场景,并且在场景中添加一个球体和一个方向光。
    • 2.创建一个新的着色器和材质球,并且把着色器应用到材质上。
    • 3.然后把材质球应用到我们刚刚创建的球体。
    • 4.最后,我们修改我们之前创建的着色器让它能使用漫反射纹理和法线贴图,并且创建一个自定义的光线函数。下面的代码展示的是修改后的着色器代码:
    Shader "Custom/MSA"
    {
        Properties
        {
            _MainTex ("Albedo (RGB)", 2D) = "white" {}
            _NormalMap ("Normal Map", 2D) = "bump" {}
        }
        SubShader
        {
            Tags { "RenderType"="Opaque" }
            LOD 200
    
            CGPROGRAM
    
            sampler2D _MainTex;
            sampler2D _NormalMap;
            #pragma surface surf SimpleLambert
    
            struct Input
            {
                float2 uv_MainTex;
                float2 uv_NormalMap;
            };
    
            inline float4 LightingSimpleLambert(SurfaceOutput s, float3 lightDir, float atten)
            {
                float diff = max(0, dot(s.Normal, lightDir));
                float4 c;
                c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2);
                c.a = s.Alpha;
                return c;
            }
    
            void surf (Input IN, inout SurfaceOutput o)
            {
                // Albedo comes from a texture tinted by color
                fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
                o.Albedo = c.rgb;
                o.Alpha = c.a;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }
    

    现在你应该有如下图所示的一个设置。下面的这个设置将让我们初步了解一些在Unity中使用表面着色器进行优化的基本概念:
    diagram


  • 操作步骤
    我们将构建一个简单的漫反射着色器用来了解几种常用的优化着色器的方法。
    首先,我们将会优化变量类型从而可以让它们在处理数据的时候使用更少的内存:
    1. 让我们从着色器的 输入结构体(struct Input) 着手。当前我们的UV数据是保存在一个 float2 类型的变量中的。我们需要将它改成 half2
    struct Input
    {
      half2 uv_MainTex;
      half2 uv_NormalMap;
    };
    
    1. 接下来我们去修改光照函数,通过如下的变量类型的修改从而减少了变量的内存占用:
    inline fixed4 LightingSimpleLambert(SurfaceOutput s, fixed3 lightDir, fixed atten)
    {
        fixed diff = max(0, dot(s.Normal, lightDir));
        fixed4 c;
        c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2);
        c.a = s.Alpha;
        return c;
    }
    
    1. 最后,我们来修改 surf() 函数中的变量类型,这样就完成了这次的优化。代码如下所示:
    void surf (Input IN, inout SurfaceOutput o)
    {
        // Albedo comes from a texture tinted by color
        fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
        o.Albedo = c.rgb;
        o.Alpha = c.a;
        o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap));
    }
    
    现在我们优化了我们的变量,接下来我们将利用Unity内建的 光线函数变量(lighting function variable),这样我们就可以控制光线该如何被着色器处理。通过这样做,我们极大的减少着色器需要处理的光线数量。按照下面的代码修改着色器中的 #pragma 声明:
    #pragma surface surf SimpleLambert noforwardadd

    我们之后还会通过让法线贴图和漫反射纹理共享UV来继续优化着色器。为了这个优化,我们简单的通过在 UnpackNormal() 函数中用 _MainTex 的UV替换掉 _NormalMap 的UV,从而修改着色器的 UV查找(UV lookup)
    void surf (Input IN, inout SurfaceOutput o)
    {
        // Albedo comes from a texture tinted by color
        fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
        o.Albedo = c.rgb;
        o.Alpha = c.a;
        o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
    }
    
    1. 由于我们已经不再需要法线贴图的UV了,所以我们需要确保在 输入结构体(Input struct) 中移除掉跟法线贴图UV相关的代码:
    struct Input
    {
        half2 uv_MainTex;
    };
    
    1. 最后,我们通过告诉着色器只需要工作在一些特定的渲染器中从而进一步优化我们的着色器:
      #pragma surface surf SimpleLambert exclude_path:prepass noforwardadd

      优化的最终结果显示我们几乎注意不到视效质量上的不同,但我们却减少了这个着色器在屏幕上绘制的次数。我们在下一个知识点将会学习如何确定着色器的绘制次数,这里我们关心的是我们通过更少的数据消耗而取得了相同的效果。所以当你在创建自己的着色器的时候也要留意这一点。下图向我们展示了我们的着色器的最终效果:
      diagram

  • 原理介绍
    现在我们了解了一些能优化我们着色器的方法,现在我们更进一步的探索,去了解这些技术为什么会起作用,并且看一看一些其他的你自己可以尝试的技术。

    我们留意一下每一个变量,当我们定义它们的时候,保存在它们每一个中的数据的大小。如果你熟悉编程,你应该理解你可以定义不同类型大小的值和变量。也就是说一个浮点类型事实上占用了最大的内存消耗。下面的描述向我们更多的陈述了这些变量的细节:

    • Float:一个浮点类型是一个32位精度的值并且是我们看到的三种数据类型中最慢的。它还有与之对应的其他浮点类型值 float2float3float4
    • Half:half 变量类型是一个缩减了位数的16位浮点值,它适合用来保存UV和颜色值,相比于float类型它要快很多。跟上面的浮点类型类似,half类型也有与之对应的其他类型,它们是 half2half3,和 half4
    • Fixed:fixed 类型是三种类型中大小最小的,但它可以用于光线计算和颜色计算,它也有与之对应其他fixed类型:fixed2fixed3fixed4