Unity shader中的multi_compile和shader_feature宏定义

关于multi_compile的使用知乎的这篇这篇讲的很好,本文转载结合一下。

一.什么是ShaderVariant(Shader变体)

在写shader时,往往会在shader中定义多个宏,并在shader代码中控制开启宏或关闭宏时物体的渲染过程。最终编译的时候也是根据这些不同的宏来编译生成多种组合形式的shader源码。其中每一种组合就是这个shader的一个变体(Variant)。

Material ShaderKeywords与ShaderVariant

Material所包含的Shader Keywords表示启用shader中对应的宏,Unity会调用当前宏组合所对应的变体来为Material进行渲染。

在Editor下,可以通过将material的inspector调成Debug模式来查看当前material定义的Keywords,也可在此模式下直接定义Keywords,用空格分隔Keyword。如图1

image

图1:设置keyword

在程序中,可用Material.EnableKeyword()、Material.DisableKeyword()、Shader.EnableKeyword()、Shader.DisableKeyword()来启用/禁用相应的宏。

Enable函数应与Disable函数相对应。若一个宏由Material.EnableKeyword()开启,则应由Material.DisableKeyword()关闭,Shader.DisableKeyword()无法关闭这个宏。Material中定义的Keywords由Material的函数进行设置。

二.multi_complie 还是 shader_feature

shader_feature 和 multi_complie 是两个很相似的预编译指令,在Editor模式下,他们是几乎没有区别的。

共同点是:

声明Keyword,用来产生Shader的变体(Variant)

#pragma multi_compile A B
//OR #pragma shader_feature A B

//-----------------------A模块----------------------
#if A
  return fixed4(1,1,1,1); 
#endif 
//---------------------------------------------------

//-----------------------B模块-----------------------
#if B
  return fixed4(0,0,0,1); 
#endif
//---------------------------------------------------
  • 这个Shader会被编译成两个变体:一是只包含A模块代码的变体A;二是只包含B模块代码的变体B;
    指定的第一个关键字是默认生效的,即默认使用变体A;
  • 在脚本里用Material.EnableKeyword或Shader.EnableKeyword来控制运行时具体使用变体A还是变体B;
  • 它们声明的Keyword是全局的,可以对全局的包含该Keyword的不同Shader起作用;
  • 全局最多只能声明256个这样的Keyword;
  • 请注意Keyword的数量和变体的数量之间的关系,并可能由此导致的性能开销,比如声明#pragma
    multi_compile A B和#pragma multi_compile D E 这样的两组Keyword会产生 2x2=4 个Shader变体,但若声明10组这样的keyword,则该Shader会产生1024个变体;
    区别在于:

如果使用shader_feature,build时没有用到的变体会被删除,不会打出来。也就是说,在build以后环境里,运行代码Material.EnableKeyword("B")可能不起作用,因为没有Material在使用变体B,所以变体B没有被build出来,运行时也找不到变体B。

如果想解决这个问题,可以采取以下办法中的其中一种:

使用multi_complie 代替 shader_feature,multi_complie 会把所有变体build出来;
把这个Shader加入“always included shaders”中 (Project Settings -> Graphic);
创造一个使用变体B的Material,强行说明变体B有用;

三.__

上文已经提到了,最多声明256个全局Keyword,因此我们要尽量节省Keyword的使用数量。其中一个技巧是使用 __(两条下划线),如:

#pragma multi_compile __ A
//OR #pragma shader_feature __ A

//-----------------------A模块----------------------
#if A
  return fixed4(1,1,1,1); 
#endif 
//---------------------------------------------------

return fixed4(0,0,0,1);
  • 此方式相比#pragma multi_compile A B 的方式,我们可以减少使用一个Keyword。
  • 此方式仍会编译成两个变体:一是不包含A模块代码的变体非A;二是包含A模块代码的变体A;
    默认为 __ ,即变体非A生效。

四.multi_complie_local

全局的Keyword只能有256个(而且Unity自身已经使用掉了一部分,所以实际可用的更少),这或许会最终对我们造成限制,而且大部分Keyword并不需要进行全局声明。

因此,我们可以使用multi_complie_local来声明局部的、只在该Shader内部起作用的Keyword,用法相似:

#pragma multi_compile_local __ A
//OR #pragma shader_feature_local __ A

//-----------------------A模块----------------------
#if A
  return fixed4(1,1,1,1); 
#endif 
//---------------------------------------------------

return fixed4(0,0,0,1);

但需要注意:

local Keyword仍有数量限制,每个Shader最多可以包含64个local Keyword
因为这种Keyword是局部的,Material.EnableKeyword仍是有效的,但对Shader.EnableKeyword或CommandBuffer.EnableShaderKeyword这种全局开关说拜拜
当你既声明了一个全局的Keyword A ,同时又声明了一个同名的、局部的Keyword A,那么优先认为Keyword A是局部的。

五.Shader编写规范

1.建议使用shader_feature时将定义语句写成完整模式,并且不要在一个语句中定义多个宏。

完整模式:#pragma shader_feature _ A,不建议写成#pragma shader_feature A。

不建议在一个语句中定义多个宏,如: #pragma shader_feature _ A B C,若一定要定义多个宏,请务必将其写成完整模式,不使用完整模式在切换shader时可能会与想要的效果不一致,具体原因尚未测得。

2.若在shader中使用shader_feature,请为这个shader指定一个CustomEditor

每个使用shader_feature来定义Keyword的shader都需要再末尾加个 CusomEditor “xxxx”,并在代码中实现类xxxx(需继承自UnityEditor.ShaderGUI),用来对Keywords定义进行设定。

这么做是因为Material中的部分Keyword是由shader中的属性(Properties)所控制的。比如shader中含有_NormalMap的属性并且定义了与_NormalMap相关的Keyword,这个Keyword需要在Material含有NormalMap时添加,不含NormalMap时移除。这个功能可由自定义的CustomEidtor实现。

具体如何写这个CustomEditor类可参考Unity builtin_shaders\Editor\StandardShaderGUI.cs。该文件可去Unity官网下载,下载时选择内置着色器即可。如图

image
3.其他减少变体数量的方案&注意事项

1.内存中ShaderLab的大小和变体成正比关系。从减少内存方面应该尽量减少变体数量,可以使用 #pragma skip_variants
2.在使用ShaderVariantCollection收集变体打包时,只对shader_feature定义的宏有意义,multi_compile的变体不用收集也会被全部打进包体
3.2018.2新功能OnProcessShader可以移除无用的shader变体。比#pragma skip_variants更合理
4.项目前期介入美术效果制作流程,规范shader宏定义使用,防止TA为了美术效果过度使用宏定义的情况,以过往项目经验来看,到后期进行此项工作导致的资源浪费非常之大
5.ShaderLab在相关shader加入内存时就已经产生,但如果没有被渲染的话不会触发CreateGPUProgram操作,如果提前在ShaderVariantCollection中收集了相关变体并执行了warmup的话,第一次渲染时就不会再CreateGPUProgram,对卡顿会有一定好处

六.Unity自身还内建的一些multi_compile,自建的multi_compile会产生较多的变体,实际开发中如果没有能力把控好变体建议慎用:

  • multi_compile_fwdbase:为基本前向渲染通道编译多个变体,不同的变体可能需要处理不同的光照贴图类型,或者一些使用了主平行光的阴影,而另一些禁用了。(参考Unity渲染管线)

  • multi_compile_fwdadd:为附加前向渲染通道编译多个变体,不同的变体可能需要处理不同类型的光源——平行光、聚光灯或者点光源,亦或者它们附带cookie纹理的版本。

  • multi_compile_fwdadd_fullshadows:同上,不过包含了可以让光源拥有实时阴影的功能。

  • multi_complie_fog:为处理不同的雾效类型(off/linear/exp/exp2)扩展了多个变体。

  • multi_compile_particles:主要包含了软阴影粒子的宏定义,可以在质量设置界面开关软阴影粒子。

<think>嗯,用户让我详细讲解Unity Shader中的宏的用法。首先,我需要确认自己对宏的理解是否正确。在Unity Shader中,宏通常指的是使用#define指令定义的预处理宏,或者Unity内置的一些平台相关的宏。用户可能想了解如何自定义宏,以及如何使用这些宏来优化Shader代码,或者处理不同平台之间的兼容性问题。 接下来,我得考虑用户的背景。用户可能有一定的Shader编写经验,但可能对宏的具体用法不太熟悉,或者想更系统地了解。他们可能在开发过程中遇到了需要条件编译的情况,或者希望提高Shader的可维护性。需要从基础开始,逐步深入,确保讲解清晰易懂。 首先,我应该解释宏的基本概念,比如#define的用法,以及条件编译宏如#if、#ifdef等。然后,举例说明如何用宏来简化代码,比如定义常量或者简写重复的代码块。接着,可能需要介绍Unity内置的宏,比如UNITY_UV_STARTS_AT_TOP,或者与平台相关的宏,帮助用户处理不同平台的差异。 另外,用户可能对跨平台编译感兴趣,所以需要讲解如何使用宏来区分不同的渲染管线,比如URP、HDRP内置管线。这部分可能需要提到UNITY_VERSION之类的宏,用于版本判断。 还要注意提到宏的作用域问题,比如在Shader中宏的作用域仅限于当前文件,或者在CGINCLUDE中定义的全局宏。可能用户在使用宏时遇到了作用域不明确的问题,需要特别说明。 然后,结合实例来说明宏的实际应用,比如根据不同的光照模式编译不同的代码,或者根据平台选择不同的纹理采样方式。这样用户能更好地理解如何将宏应用到实际项目中。 最后,总结宏的优势,比如提高代码复用性、简化维护、跨平台支持等,同时提醒注意事项,比如宏定义的顺序覆盖问题,避免潜在的错误。 需要检查是否有遗漏的部分,比如是否提到了#pragma multi_compileshader_feature的区别,或者是否应该建议使用属性开关配合宏。此外,确保所有示例代码符合Unity的语法规范,并且使用正确的中文术语,避免翻译不当造成的误解。</think>好的,我为您详细讲解Unity Shader中宏(Macro)的用法。这是Shader编程中非常重要的技巧,主要涉及以下核心内容: 一、基础宏定义 1. 使用#define定义宏 ```c #define PI 3.1415926 #define SAMPLE_COUNT 8 // 使用时 float angle = PI * 2; for(int i=0; i<SAMPLE_COUNT; i++){...} ``` 2. 带参数的宏 ```c #define CALC_SPECULAR(lightDir, normal) saturate(dot(normalize(lightDir), normal)) // 使用时 float spec = CALC_SPECULAR(_WorldSpaceLightPos0.xyz, worldNormal); ``` 二、条件编译宏 1. 平台判断 ```c #if defined(SHADER_API_GLES) // OpenGL ES // 移动端优化代码 #elif defined(SHADER_API_D3D11) // DirectX 11 // PC端高级效果 #endif ``` 2. 功能开关 ```c #pragma multi_compile __ USE_NORMAL_MAP #if defined(USE_NORMAL_MAP) sampler2D _NormalMap; #endif ``` 三、Unity内置常用宏 1. 跨平台宏 ```c UNITY_UV_STARTS_AT_TOP // 判断UV坐标系方向 UNITY_SINGLE_PASS_STEREO // VR单通道立体渲染判断 ``` 2. 渲染管线判断 ```c #if defined(UNITY_PIPELINE_URP) // Universal Render Pipeline专用代码 #elif defined(UNITY_PIPELINE_HDRP) // High Definition RP专用代码 #endif ``` 四、高级用法实例 1. 自动处理不同精度 ```c #if defined(SHADER_API_MOBILE) #define PRECISION half #else #define PRECISION float #endif PRECISION diffuse = saturate(dot(normal, lightDir)); ``` 2. 分支优化 ```c #define FAST_SQRT(x) (x * rsqrt(x)) #define APPROX_EXP(x) (1.0 + x + 0.5*x*x) ``` 五、使用技巧 1. 宏作用域管理 - 使用#undef重置宏定义 - 不同Pass之间的宏定义不会相互影响 2. 版本控制 ```c #if UNITY_VERSION >= 202120 // 针对2021.2+版本的特性 #endif ``` 六、注意事项 1. 宏展开顺序:最后定义的宏会覆盖之前的定义 2. 避免递归定义:如`#define A B` `#define B A` 3. 与MaterialProperty的区别:宏在编译期确定,属性可在运行时调整 实际应用案例:根据平台自动选择采样方式 ```c #if defined(SHADER_API_GLES) #define SAMPLE_TEX(tex, uv) tex2D(tex, uv) #else #define SAMPLE_TEX(tex, uv) tex2Dlod(tex, float4(uv,0,0)) #endif ``` 建议配合Shader变体收集器(Edit -> Graphics Tier Settings)查看宏的实际编译结果。掌握宏的使用可以显著提升Shader的跨平台兼容性代码复用性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值