合并Shader系列 | 如何合并渲染状态

合并Shader系列 | 如何合并渲染状态

《合并Shader》系列旨在介绍一些在保证功能不打折的情况下精简Shader数量的方法,其遵循的原理就是把相似功能的Shader文件合并在一个文件里。在掌握了这些技能后,研发团队能在极致情况下做到把所有的Shader文件合并成一个,如Unity 5.x的Standard着色器。本系列会从简到繁的方式来依次遍历林林总总的合并方法,希望大家有所收获。


在数学中我们学习过:把多项式中的同类项合并成一项叫做合并同类项。同理,提取Shader的相似部分,把多个Shader合并成一个就叫做Shader的合并,也叫合并Shader,偶尔也会引用数学的名词来称呼他为Shader的合并同类项。

Shader的合并方式有很多,根据不同的合并技能和方法可以划分为不同的派系。今天优先介绍一种不太常见、但又很实用的派系,往下看。

在Shader的合并方法中,MaterialPropertyDrawer(属性定义)可以说是自成一派,但又与其他派系有着千丝万缕的关系,今天我们就先拿它来开刀。

在此之前我想补充一点:对于Shader的合并,首先让人想到的应该是宏定义,相信宏定义也是大家应用最广、最先接触的。(毕竟由于GPU的特殊性,Shader里常常通篇都充满了各种宏定义。)当然,该系列会对宏定义有所介绍,它可是Shader合并里功高盖世的重要角色,很多地方都会有它的身影。但对它的介绍不在这一篇,也许会是下一篇。因为我认为在合并Shader的众多方法中,最简单的不是它,而是使用Unity已经预先定制好的几种MaterialPropertyDrawer的方式。只用修改两行代码,就可以搞定一类Shader的合并,该方法主要用来合并那些只是渲染状态不一样的Shader。


◆◆
初次简单使用

一个完整的Shader,它的渲染状态变量有很多种,由于不是每一种状态的改变都能很明显地看到结果。那么作为初次使用,我们就优先选择一种最明显的、最易懂的状态作为测试用例—ZTest,即深度比较。对ZTest不太了解的朋友,可以看看官方的学习文档或者查看一下相关技术书籍,我就不在这里具体介绍这个状态了。

1.1 首先我们选用的是Unity官方提供的一个最常用Shader

Normal-Diffuse.shader(Legacy Shaders/Diffuse)

1.2 在属性列表(Properties)中添加一行

[Enum(UnityEngine.Rendering.CompareFunction)] _ZTest ("ZTest", Float) = 2

1.3 在SubShader中添加一行

ZTest [_ZTest]

1.4 完整的Shader

Shader "ShaderCombine/01.ShaderCombineSimpleZTest"
{
    Properties {
        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        [Enum(UnityEngine.Rendering.CompareFunction)] _ZTest ("ZTest", Float) = 2
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        ZTest [_ZTest]

        CGPROGRAM
        #pragma surface surf Lambert

        sampler2D _MainTex;
        fixed4 _Color;

        struct Input {
            float2 uv_MainTex;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }

    Fallback "Legacy Shaders/VertexLit"
}

只用添加这两行代码,我们就可以在Inspector面板中控制使用该Shader物体的深度测试方法。

1.5 直观展示
在Unity中的测试示例是这样的:
请输入图片描述
当然大家也可以通过这个示例试验下每一种深度测试的方法是否与你心中所想、或之前所学的有所冲突。


◆◆
再次深入使用

在上面的例子中,我们只使用了深度测试。但单单一个深度测试肯定满足不了我的需求,我们还需要更多、更多的状态,比如背面剔除、混合模式等等。

在这一节中,我列举出了一些常用的状态控制量,对于一些不常用的模板等,就不在这里列举。大家可以依葫芦画瓢,因为大部分从理论上来说都是可行的。

2.1 一个大而全的简单示例Shader如下:

Shader "ShaderCombine/02.ShaderCombineCommonState"
{
    Properties {
        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        [Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull Mode", Float) = 1
        [Enum(Off,0,On,1)] _ZWrite ("ZWrite", Float) = 1
        [Enum(UnityEngine.Rendering.CompareFunction)] _ZTest ("ZTest", Float) = 1
        [Enum(UnityEngine.Rendering.BlendMode)] _SourceBlend ("Source Blend Mode", Float) = 2        
        [Enum(UnityEngine.Rendering.BlendMode)] _DestBlend ("Dest Blend Mode", Float) = 2
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        ZTest [_ZTest]
        Cull [_Cull]
        ZWrite [_ZWrite]
        ZTest [_ZTest]
        Blend [_SourceBlend] [_DestBlend]

        CGPROGRAM
        #pragma surface surf Lambert

        sampler2D _MainTex;
        fixed4 _Color;

        struct Input {
            float2 uv_MainTex;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }

    Fallback "Legacy Shaders/VertexLit"
}

2.2 直观展示
应用效果:
请输入图片描述


◆◆◆
有限自定义

在上面的示例中,我们都是使用Unity预先定义好的一些枚举类型,比如UnityEngine.Rendering.CompareFunction、UnityEngine.Rendering.BlendMode等。这些定义好的类型把每个状态可能的选项都一一列举了,但有时候我们并不需要这么多选项,或者说我们并不希望给美术列举出所有的可选项,毕竟很多选项我们可能做完整个项目或者几个项目都不会使用到,而选项过多也会带来很的麻烦。或者换一个说法,我希望我们的功能使用起来简单易懂、不易出错并且可控,那就需要我们开发做更多的工作,去掉那些"无用"的选项。其实说到底无非就是:我们能否自定义每个状态的选项呢?答案当然是可以的。

在给出自定义方式前,我们先来熟悉一下Unity给我们提供的这几个枚举类型。

3.1 剔除模式

UnityEngine.Rendering.CullMode:
public enum CullMode
{       
    Off   = 0,    //Disable culling.       
    Front = 1,    //Cull front-facing geometry.       
    Back  = 2     //Cull back-facing geometry.
}

3.2 比较方式
该比较方式通用与深度比较和模板比较


UnityEngine.Rendering.CompareFunction
//Depth or stencil comparison function.
public enum CompareFunction
{
    Disabled     = 0,   //Depth or stencil test is disabled.
    Never        = 1,   //Never pass depth or stencil test.
    Less         = 2,   //Pass depth or stencil test when new value is less than old one.
    Equal        = 3,   //Pass depth or stencil test when values are equal.
    LessEqual    = 4,   //Pass depth or stencil test when new value is less or equal than old one.
    Greater      = 5,   //Pass depth or stencil test when new value is greater than old one.
    NotEqual     = 6,   //Pass depth or stencil test when values are different.
    GreaterEqual = 7,   //Pass depth or stencil test when new value is greater or equal than old one.
    Always       = 8    //Always pass depth or stencil test.
}

3.3 混合模式

UnityEngine.Rendering.BlendMode
//Blend mode for controlling the blending.
public enum BlendMode
{
    Zero             = 0,   //Blend factor is (0, 0, 0, 0).
    One              = 1,   //Blend factor is (1, 1, 1, 1).
    DstColor         = 2,   //Blend factor is (Rd, Gd, Bd, Ad).
    SrcColor         = 3,   //Blend factor is (Rs, Gs, Bs, As).
    OneMinusDstColor = 4,   //Blend factor is (1 - Rd, 1 - Gd, 1 - Bd, 1 - Ad).
    SrcAlpha         = 5,   //Blend factor is (As, As, As, As).
    OneMinusSrcColor = 6,   //Blend factor is (1 - Rs, 1 - Gs, 1 - Bs, 1 - As).
    DstAlpha         = 7,   //Blend factor is (Ad, Ad, Ad, Ad).
    OneMinusDstAlpha = 8,   //Blend factor is (1 - Ad, 1 - Ad, 1 - Ad, 1 - Ad).
    SrcAlphaSaturate = 9,   //Blend factor is (f, f, f, 1); where f = min(As, 1 - Ad).
    OneMinusSrcAlpha = 10   //Blend factor is (1 - As, 1 - As, 1 - As, 1 - As).
}

3.4 有限的自定义
在上面三个小小节中,我们了解了Unity自身提供的状态选项,而且每一个状态选项后都强制赋上了相应的数值,这是有原因的。因为我们写好的Shader不管怎样,都要首先经过Unity的编译等处理转换为目标平台的着色器语言。而Unity自己的Shader编译器,在没有源码的情况下我们是无法修改的,也就是说我们不能随意更改这些状态的数值。其实我们修改的这些状态值都是给Unity的Shader编译器看的,而编译器对状态的数值理解是固化好的。因此,虽然我们可以自定义这些状态选项,但编译器也不会任由我们随意定义,这就好比戴着镣铐跳舞,虽然有限制,但我们依然可以跳出优美的舞蹈。

自定义非常的简单,我们可以减少选项的数量,但是不能改变每一项的值,这就要求我们强行给每一个值赋上对应的值,依然还是用深度测试实验,如下所示:

这是之前的:

[Enum(UnityEngine.Rendering.CompareFunction)] _ZTest ("ZTest", Float) = 2

这是自定后的:

[Enum(Less,2,Greater,5)] _ZTest ("ZTest", Float) = 2

选项与数值全部使用逗号分隔,该示例中我只给出了两个选项,小于和大于,便于直观查看。

完整Shader如下:

Shader "ShaderCombine/03.ShaderCombineCustomState"
{
    Properties {
        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        [Enum(Less,2,Greater,5)] _ZTest ("ZTest", Float) = 2
    }
    SubShader {
        Tags { "RenderType"="Opaque"}
        LOD 200
        ZTest [_ZTest]

        CGPROGRAM
        #pragma surface surf Lambert

        sampler2D _MainTex;
        fixed4 _Color;

        struct Input {
            float2 uv_MainTex;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }

    Fallback "Legacy Shaders/VertexLit"
}

3.5 直观展示
在Unity中的样子是这样的:

请输入图片描述

细心的读者可能已经发现我们在第二章节中的完整示例中就有使用自定义,就是里面的那个写深度_ZWrite选项,因为没有在Unity里找到相应的枚举值,就直接使用了自定义,反正只要保证数值正确就可以任意发挥使用。更多的使用和应用场景就等你们去发现了,我这只是抛砖引玉。

PS:其实最初是想花一整章篇幅来讲解MaterialPropertyDrawer的各种使用,但其内部的扩展空间还比较广泛。写下来篇幅太长,而太长的篇幅,阅读起来也比较麻烦,所以还是拆成几章篇幅来慢慢絮叨吧,同时也遵从一次只讲一个问题。

以上便是MaterialPropertyDrawer的应用场景之一,对于MaterialPropertyDrawer的应用,在后续的篇章中也还会陆续出现。笔者计划把MaterialPropertyDrawer应用当成《合并Shader》系列中的一个分支,当然《合并Shader》系列不会仅且只有这一个分支的,O(∩_∩)O哈哈~,欢迎大家持续关注!


这是侑虎科技第179篇原创文章,感谢作者唐建伟(网名:七火,QQ:383684387)供稿。欢迎转发分享,未经作者授权请勿转载。作者博客: http://www.7fires.cn/。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

同时,作者也是U Sparkle活动参与者哦,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!