multi_complie 和 shader_feature 编译指令往往用于正式游戏项目的优化
一、关键字与Shader变体
multi_complie 的用法:
#pragma multi_compile NAMEA, NAMEB, NAMEC, …,
参考代码:
Shader "Jaihk662/ShaderVariantTest"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
//枚举宏,注意在预编译指令中,宏要大写
[KeywordEnum(R,G,B)] _Color("Color", float) = 0
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 200
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _COLOR_R _COLOR_G _COLOR_B
#pragma multi_compile __ DB_ON
#include "UnityCG.cginc"
struct v2f
{
float4 vertex: SV_POSITION;
float2 uv: TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i): SV_Target
{
#if DB_ON
return fixed4(1, 1, 1, 1);
#elif _COLOR_R
return fixed4(1, 0, 0, 1);
#elif _COLOR_G
return fixed4(0, 1, 0, 1);
#elif _COLOR_B
return fixed4(0, 0, 1, 1);
#else
fixed4 color = tex2D(_MainTex, i.uv);
return color;
#endif
}
ENDCG
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestShader: MonoBehaviour
{
public Material mat;
void Update()
{
if (Input.GetKey(KeyCode.Q))
mat.EnableKeyword("DB_ON");
if (Input.GetKey(KeyCode.W))
mat.DisableKeyword("DB_ON");
}
}
其中 NAMEA 为枚举名,一个非空的枚举名为一个全局的关键字(Keyword)
关于关键字(Keyword):
- 指定的第一个 keyword 默认生效
- 对于一条预编译指令中的内容,当然同时有且只有一个 keyword 生效
- 在脚本中使用 material.EnableKeyword、Shader.EnableKeyword 或 CommandBuffer.EnableShaderKeyword 来指定激活哪个关键字,需要注意的是后两个方法是全局的,当然了 Keyword 不特殊声明也是全局的
- 同 ③ 可以使用 .DisableKeyword 方法取消激活,如果没有被激活的回到 ①
- 除此之外,也可以在 Properties 中定义对应的 Enum 或者 Toggle,并在材质面板中设定好 keyword,就如下图
这样做的目的是什么?对于 #pragma multi_compile _COLOR_R _COLOR_G _COLOR_B 指令,对应的 Shader 会被编译成三个变体(Variant):一是只包含 _COLOR_R 模块代码的变体 A;二是只包含 _COLOR_G 模块代码的变体 B,三是只包含 _COLOR_B 模块代码的变体 C,这就相当于是省掉了着色器中的 if 语句,直接将所有分支全部展出来
当然了,如果一个 shader 中有多个这样的预编译指令,那么生成的变体数量会是累乘的,就像上面的示例代码就会有 2 x 3 = 6 个变体
二、未定义宏(NoKeyword)与关键字限制
上面的代码中有下面这么一段,那么对于这第二行,__ 是什么东西呢?
#pragma multi_compile _COLOR_R _COLOR_G _COLOR_B
#pragma multi_compile __ DB_ON
#pragma shader_feature FANCY_STUFF
其实它代表着当前这一行预编译指令中,所有的关键字都不生效,可以理解为“空”,即未定义宏(NoKeyword),对于 #pragma multi_compile __ DB_ON,其 Shader 仍会编译成两个变体:一是不包含 DB_ON 模块代码的变体;二是包含 DB_ON 模块代码的变体,当然默认为__,即不包含 DB_ON 的变体生效
- 全局的 Keyword 只能有256个!因此可以尽量使用 __,这样可以省掉一个关键字
- 对于只有一个关键字的情况,如过是 #pragma shader_feature A 这种,它相当于是省略了前面的 __,是一种简便写法,但是 #pragma multi_compile A 并不是!它是一种错误的写法,相当于是 shader 此时只对应 A 这个变体,但无论如何,永远不建议这么写
局部的关键字:
同样能使用 #pragma multi_complie_local 的方法声明只在对应 shader 内部生效的关键字(keyword),对于局部的关键字(keyword):
- local keyword 仍有数量限制,每个 Shader 最多只能包含64个 local Keyword
- 只能使用局部方法 material.EnableKeyword 来指定激活
- 如果同时声明了全局和局部 keyword,局部的优先级高
三、multi_complie && shader_feature
multi_complie 和 shader_feature 作用完全一样,唯一的区别是:如果使用 shader_feature,build 项目时和 shader 在同一个 AB 包中没有任何 Material 用到的变体就不会打出来。这也意味着在非编辑器中运行代码 Material.EnableKeyword("B") 可能会不起作用,因为没有 Material 在使用变体 B,变体 B 没有被 build 出来,运行时也找不到变体 B,而在编辑器环境下,multi_complie 和 shader_feature 没有区别
shader_feature 的作用就是避免没有被使用的冗余变体(ShadeVariant)被生成
一般来讲,对于 Properties 中定义对的 Enum 或者 Toggle 关键字,使用 #shader_feature,而对于在脚本中设置的关键字,使用 #multi_complie,这个很好理解,又或者可以这么说:
- #multi_compile 往往同时适用于大部分 shader,与 shader 自身所带的属性无关
- 而 shader_feature 定义的宏多用于针对 shader 自身的属性
如果偏要使用 shader_feature,但又避免不了用到所有的变体,也可以通过把对应 Shader 加入到 “always included shaders” 中,这样它所有的变体都会被直接打包到游戏,又或者放在 Assets/Resources 文件夹,不走 AB 加载
当然这两种方法都不推荐,如果不想 Material 和 shader 在一个 AB 包里的话,又要生成足够的变体,还有一种靠谱的方法就是 SVC,基于篇幅这个就不介绍了
参考文档:
- Declaring and using shader keywords in HLSL - Unity 手册
- Stripping scriptable shader variants | Unity Blog
- 对Shader Variant的研究(概念介绍、生成方式、打包策略)_真像大白阿的博客-CSDN博客_shader variant
- https://docs.unity3d.com/cn/current/Manual/OptimizingShaderLoadTime.html