[转载]Unity 2018.2新功能:可编程着色器变体移除

发布于 2018-07-15  35 次阅读


由于着色器变体数量在不断的增加,播放器构建时间和数据大小会随着项目复杂度而增加。而通过允许开发人员控制Unity着色器编译器处理哪些着色器变体,并将其包含在播放器数据中,可大量减少播放器构建时间和数据大小。今天我们将为大家介绍Unity 2018.2 beta版中新功能可编程着色器变体移除功能。

可编程着色器变体移除功能允许开发人员移除所有包含无效代码路径的着色器变体、去除带有未使用功能的着色器变体,并创建着色器构建配置,例如“debug”(调试)和“release”(正式版),这些操作不会影响迭代时间或是复杂性的维护。

在本文中,首先会定义一些需要使用的术语。然后我们会关注于着色器变体的定义,解释为什么可以生成那么多变体。接下来,再介绍自动着色器变体移除功能,讲解可编程着色器变体移除功能在Unity着色器管线架构中的实现方法。之后,我们会介绍可编程着色器变体移除功能的API,讨论Fountainbleau演示项目中得到的结果,最后针对编写移除脚本提供一些技巧。

尽管学习可编程着色器变体移除功能并不简单,但这个功能却可以大幅提升团队效率。

下载示例项目


本文中的示例项目下载请访问:https://unity3d.com/unity/beta-download
注意:该项目需要使用Unity 2018.2.0b1。

基础概念与术语

为了理解可编程着色器变体移除功能,我们首先要准确理解一些相关概念。
着色器资源(Shader asset):着色器资源是个包含属性、子着色器、通道和HLSL代码的完整源代码文件。
着色器片段(Shader snippet):着色器片段是带有单个着色器阶段所有依赖的HLSL输入代码。
着色器阶段(Shader stage):着色器阶段是GPU渲染管线中的一个特定阶段,通常情况下是顶点着色器阶段(vertex shader stage)和片元着色器阶段(fragment shader stage)。
着色器关键字(Shader keyword):着色器关键字是个预处理标识符,用于在编译时跨着色器扩展分支。
着色器关键字集(Shader keyword set):着色器关键字的特定集合,用来识别特定代码路径。
着色器变体(Shader variant):着色器变体是通过Unity着色器编译器生成的平台专有着色器代码,变体会根据单个着色器阶段中的特定图形等级、通道、着色器关键字集等属性生成。
超着色器(Uber shader):超着色器是个可以生成多个着色器变体的着色器来源。

在Unity中,超着色器由多个部分管理,包括ShaderLab子着色器、通道、着色器类型,以及#pragma multi_compile和#pragma shader_feature这两个预处理指令。

计算生成的着色器变体的数量

要使用可编程着色器变体移除功能,我们需要对着色器变体这个概念有着清楚的认识,还要了解着色器变体是如何由着色器构建管线生成的。着色器变体的生成数量会与构建时间和播放器着色器变体数据量成正比。着色器变体是着色器构建管线的一个输出结果。

着色器关键字是生成着色器变体的一个重要因素。在使用着色器关键字时如果未经过仔细考虑,着色器变体计数可能会呈爆炸式增长,从而大大延长构建时间。

了解着色器变体的生成方法,可以查看下面这个简单的着色器代码,它会统计所生成的着色器变体数量。

Shader "ShaderVariantsStripping" {
    SubShader {
        Pass {
            Name "ShaderVariantsStripping/Pass"

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile COLOR_ORANGE COLOR_VIOLET COLOR_GREEN COLOR_GRAY
            #pragma multi_compile OP_ADD OP_MUL OP_SUB

            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 get_color() {
                #if defined(COLOR_ORANGE)
                    return fixed4(1.0, 0.5, 0.0, 1.0);
                #elif defined(COLOR_VIOLET)
                    return fixed4(0.8, 0.2, 0.8, 1.0);
                #elif defined(COLOR_GREEN)
                    return fixed4(0.5, 0.9, 0.3, 1.0);
                #elif defined(COLOR_GRAY)
                    return fixed4(0.5, 0.9, 0.3, 1.0);
                #else
                #error "Unknown 'color' keyword"
                #endif
            }

            fixed4 frag (v2f i) : SV_Target {
                fixed4 diffuse = tex2D(_MainTex, i.uv);
                fixed4 color = get_color();
                #if defined(OP_ADD)
                    return diffuse + color;
                #elif defined(OP_MUL)
                    return diffuse * color;
                #elif defined(OP_SUB)
                    return diffuse - color;
                #else
                #error "Unknown 'op' keyword"
                #endif
            }
        ENDCG
        }
    }
}

一个项目中的着色器变体总数是确定的,可以由下列公式计算得来:

$$ TotalShadersVariants = \sum{a = 1}^{ShaderAssets} \sum{s = 1}^{ShaderAssets} \sum{p = 1}^{ShaderAssets} \Bigg(Stages(a,s,p) \prod{d=1}^{Directives(a,s,p)} Keywords(a,s,p,d) \Bigg) $$

下面是个简单的ShaderVariantStripping示例,它可以帮助你更清楚地理解这个公式。这个示例中只有一个着色器,它简化了上面的公式,得出下面的公式:

$$ TotalShadersVariants = 1 \sum_{s = 1}^{ShaderAssets} \sum_{p = 1}^{Passes} \Bigg(Stages(s,p) \prod_{d=1}^{Directives(s,p)} Keywords(s,p,d) \Bigg) $$

类似地,这个着色器有一个子着色器和一个能够进一步简化公式的通道:

$$ PassVariants = Stages \prod_{d=1}^{Directives} Keywords(d) $$

公式中的关键字(Keywords )指的是平台和着色器关键字。图形等级是特定平台关键字集所组成的。

ShaderVariantStripping/Pass有两个多编译指令。第一个指令定义了四个关键字(即COLOR_ORANGE、COLOR_VIOLET、COLOR_GREEN和COLOR_GRAY),而第二个指令定义了三个关键字(即OP_ADD、OP_MUL和OP_SUB)。最后这个通道定义了二个着色器阶段:一个是顶点着色器阶段,另一个是片元着色器阶段。

$$ ColorPass = 2\times(4\times3) = 12 shader variants $$

这个着色器变体总数针对单个支持的图形API提供。然而,项目中的每个支持的图形API都需要有一个着色器变体的专用集合。例如:如果我们要构建一个支持OpenGL ES 3和Vulkan的Android播放器,我们需要二个着色器变体的集合。最后,播放器构建时间和着色器数据量是与所支持的图形API数量成正比的。

着色器构建管线

Unity中的着色器编译管线是个黑盒方法,每个项目中的着色器都会在里面被解析,从而提取着色器片段,收集变体的预处理指令。例如:multi_compile和shader_feature。这会产生一个编译参数表,每个着色器变体会有一个参数表。

这些编译参数包含着色器片段、图形等级、着色器类型、着色器关键字集、通道类型和名字等属性。每个集合编译参数都会用于制作单个着色器变体。

于是,Unity会通过二个启发式方法执行自动着色器变体移除通道。首先,该移除过程是基于项目设置进行的。例如:如果虚拟现实支持(Virtual Reality Supported)被禁用,则VR着色器变体会被系统地移除掉。第二,自动移除过程是基于图形设置(Graphics Settings)中着色器移除(Shader Stripping)部分的配置而定的。

[转载]Unity 2018.2新功能:可编程着色器变体移除插图
在GraphicsSettings中的自动着色器变体移除功能选项

自动着色器变体移除功能还会根据构建时间限制进行。Unity在构建时无法自动选取必要着色器变体,因为这些着色器变体是根据运行时C#的执行过程来确定是否必要的。例如:如果一个C#脚本加入了一个点光源,但却在构建时没有点光源,那么着色器构建管线就无法知道播放器是否需要一个着色器变体来执行点光源着色。

下面是个着色器变体列表,里面的已启用关键字都被自动移除:

Lightmap modes: LIGHTMAP_ON, DIRLIGHTMAP_COMBINED, DYNAMICLIGHTMAP_ON, LIGHTMAP_SHADOW_MIXING, SHADOWS_SHADOWMASK
Fog modes: FOG_LINEAR, FOG_EXP, FOG_EXP2
Instancing Variants: INSTANCING_ON

此外,当虚拟现实支持被禁用时,带有下列内置关键字的着色器变体都会被移除:

STEREO_INSTANCING_ON, STEREO_MULTIVIEW_ON, STEREO_CUBEMAP_RENDER_ON, UNITY_SINGLE_PASS_STEREO

当自动移除过程完成后,着色器构建管线会使用剩余编译参数集来安排并行着色器变体编译顺序,并根据平台所拥有的CPU核心线程尽可能多地启动多个同时编译过程。

下面是该过程的可视化展示。
[转载]Unity 2018.2新功能:可编程着色器变体移除插图1

在Unity 2018.2 beta中,着色器管线架构在着色器变体编译调度过程前加入了一个新阶段,它能让用户控制着色器变体编译过程。这个新阶段通过C#回调函数来暴露给用户代码,每个着色器片段都会执行一次回调函数。

可编程着色器变体移除的API

下列脚本能够移除所有带有“DEBUG”配置的着色器变体,这个配置会根据播放器构建开发中所使用“DEBUG”关键字识别。

using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;

class ShaderDebugBuildProcessor : IPreprocessShaders{
    ShaderKeyword m_KeywordDebug;
    public ShaderDebugBuildProcessor() {
        m_KeywordDebug = new ShaderKeyword("DEBUG");
    }

    public int callbackOrder { 
        get { return 0; } 
    }

    public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> shaderCompilerData) {
        if (EditorUserBuildSettings.development)
            return;
        for (int i = 0; i < shaderCompilerData.Count; ++i){
            if (shaderCompilerData[i].shaderKeywordSet.IsEnabled(m_KeywordDebug)){
                shaderCompilerData.RemoveAt(i);
                --i;
            }
        }
    }
}

OnProcessShader函数会在着色器变体编译调度过程之前调用。

Shader、ShaderSnippetData和ShaderCompilerData实例的每个组合都是个标识符,用于标识着色器编译器生成的单个着色器变体。为了移除那个着色器变体,我们只需要将它从ShaderCompilerData列表中去除即可。

着色器编译器所生成的每个着色器变体都会在这个回调函数中出现。当编写着色器变体移除脚本时,我们首先需要了解哪些变体需要被移除,因为这些变体在项目中不会使用。

结果

为渲染管线执行的着色器变体移除功能

可编程着色器变体移除功能的一个用例,是基于多个着色器关键字组合来系统地移除渲染管线中的无效着色器变体。

包含在HD渲染管线中的着色器变体移除脚本能让你通过使用HD渲染管线来系统地降低构建时间和项目大小。这个脚本会应用于以下着色器:
HDRenderPipeline/Lit
HDRenderPipeline/LitTessellation
HDRenderPipeline/LayeredLit
HDRenderPipeline/LayeredLitTessellation

这个脚本会产生以下结果:

未移除已移除
播放器着色器变体数据统计24350 (100%)12122 (49.8%)
播放器数据在硬盘上的大小511 MB151 MB
播放器构建时间4864 秒1356 秒

下图法国Fontainebleau的摄影制图法演示场景,使用了HD渲染管线渲染,在标准PlayStation 4 1920×1080分辨率下展示。
[转载]Unity 2018.2新功能:可编程着色器变体移除插图2

此外,Unity 2018.2中的轻量级渲染管线还有一个用于自动生成移除脚本的UI,它可以自动移除多达98%着色器变体,这个功能对移动端项目十分有用。

为项目执行着色器变体移除

另一个用例则是使用脚本来移除所有项目中未使用的渲染管线的渲染功能。通过使用轻量级渲染管线的内部测试演示,我们得到了项目的以下结果:

未移除已移除
播放器着色器变体数据统计310807056
播放器数据在硬盘上的大小121116
播放器构建时间839 秒286 秒

可以看出,使用可编程着色器变体移除功能能够带来显著的效果,如果我们进一步处理移除脚本,还能得到更好的效果。

[转载]Unity 2018.2新功能:可编程着色器变体移除插图3
轻量级渲染管线演示的截图

编写着色器变体移除代码的小技巧

提升着色器代码设计

项目通常会很容易陷入着色器变体计数的爆炸式增长问题,导致编译时间的暴涨和播放器数据大小的暴增。可编程着色器移除功能会解决这个问题,但在此之前,首先要重新评估要如何使用着色器关键字来生成更多相关着色器变体。

我们可以使用#pragma skip_variants来测试编辑器中未使用的关键字。例如:在ShaderStripping/Color Shader中,预处理指令会在如下代码中声明。这个方法将生成颜色关键字和运算符关键字的所有组合。

#pragma multi_compile COLOR_ORANGE COLOR_VIOLET COLOR_GREEN COLOR_GRAY // color keywords
#pragma multi_compile OP_ADD OP_MUL OP_SUB // operator keywords

例如:如果我们想要渲染下列场景。
[转载]Unity 2018.2新功能:可编程着色器变体移除插图4

COLOR_ORANGE + OP_ADD, COLOR_VIOLET + OP_MUL, COLOR_GREEN + OP_MUL.

首先,我们要确认每个关键字都是有用的。在这个场景中,COLOR_GRAY和OP_SUB从未被使用。如果我们可以确定这些关键字从未被使用,那么我们就应该把它们移除。

其次,我们会结合能够高效产生代码路径的关键字。本例中,“add”(添加)运算仅会和 “orange”颜色一同使用。所以,我们可以把它们结合为单个关键字并重构代码,如下所示。

#pragma multi_compile ADD_COLOR_ORANGE MUL_COLOR_VIOLET MUL_COLOR_GREEN
#if defined(ADD_COLOR_ORANGE)
#define COLOR_ORANGE
#define OP_ADD
#elif defined(MUL_COLOR_VIOLET)
#define COLOR_VIOLET
#define OP_MUL
#elif defined(MUL_COLOR_GREEN)
#define COLOR_GREEN
#define OP_MUL
#endif

当然关键字并不总能进行重构。在这些例子中,可编程着色器变体移除功能是个很有用的工具。

使用callbackOrder移除着色器变体的步骤

所有着色器变体移除脚本都会根据每个片段执行。我们可以通过使用callbackOrder成员函数来指定数值返回的顺序,从而确定脚本的执行顺序。这个着色器构建管线将按照callbackOrder的提升顺序执行回调函数,所以第一个是最低的,而最后一个是最高的。

使用多个着色器移除脚本的一个用例是将脚本按用途分开。例如:

  • 系统地移除所有含有无效代码路径的着色器变体。
  • 移除所有用于调试的着色器变体。
    [转载]Unity 2018.2新功能:可编程着色器变体移除插图5
  • 移除代码库内所有当前项目不必要的着色器变体。
  • 在移除脚本中,记录剩余着色器变体,然后将它们全部移除,以便在移除脚本中快速迭代。

编写着色器变体移除脚本的步骤

着色器变体移除功能十分强大,但是需要花些功夫才能得到较好的结果。
在项目视图中筛选出所有着色器。
选取一个着色器,然后在检视窗口中,点击“Show”打开该着色器的关键字/变体列表。这样会显示一个包含在构建版本中的关键字列表。
请确认你已经了解项目要使用哪些特定图形功能。
检查这些关键字是否在所有着色器阶段都被使用。对于不使用这些关键字的阶段来说,只需要一个变体。
移除脚本中的着色器变体。
验证构建版本的视觉效果是否正常。
为每个着色器重复第2步到第6步。


版权所有:unity中文官方社区 原文地址:Unity 2018.2新功能:可编程着色器变体移除