ShaderLab流程

ResourceResourcehttp://www.geekfaner.com/unity/blog8_Resource.html【Unity笔记】ShaderLab与其底层原理浅谈 - 知乎https://zhuanlan.zhihu.com/p/400470713?utm_source=qq&utm_medium=social&utm_oi=961194728759406592

ShaderLab结构

ShaderLab是Unity构建的一种方便开发者做跨平台Shading开发的语言体系,它主要包括如下四种:

ShaderLab Text

  • ShaderLab Text也就是ShaderLab的文本,指的其实就是我们按照一定语法规则在 .shader 文件中写的那些代码。

ShaderLab Compiler

  • 类似于一种后台提供的服务,用来帮助我们把写的ShaderLab Text翻译成最终目标机器上能够认可和执行的语言。
  • 对于多核的CPU有多个Compiler,并行Shader的编译工作。在任务管理器中,我们就可以看到它的身影
  • 编译完成后会变成汇编语言

ShaderLab Asset

  • 当我们的ShaderLab Text通过Compiler翻译过后,得到的东西就叫做ShaderLab Asset。
  • Shader打成的AssetBundle和打出来的包里面的level或sharedassets包可以见到ShaderLab Asset

ShaderLab Runtime

在unity的Profiler里可以看见

 ShaderLab工作流

在我们创建、修改或者导入Shader进Unity系统的时候,Unity的Shader并不是一次性的编译到一个目标平台上的。Unity会把原始的ShaderLab Text发给ShaderLab Compiler的预处理器(Preprocessor)做一次预处理(Preprocess)。

       Prepocess(预处理):先检查写的shader有没有问题,有问题就会报错,shader报错就是这一阶段产生的。解析完后会把每一种不同的语言Shader Program从Shader Text里切割出来,切割出来后再用对应语言(例如HLSL)的Preprocess Compiler做一遍对应于这个语言的解析检查。通过这几次的检查之后,最终会得到一个完整的Shader Compilation Info,然后写到ShaderCache里。

 如果我们有很多的Shader或者经常修改Shader,那么就会导致ShaderCache文件夹特别的大,有时我们可以将ShaderCache删掉,让Unity进行重新编译一下。此外有时可能出现写完一个Shader之后得到的效果不太对或者是有点问题,比如感觉没有进行重新编译,那么最简单的一个方法也是把ShaderCache给删掉,强制重新编译一次,有的时候就会解决这个问题。

在unity2020版本Unity引入了一个新的Preprocessor:Caching Preprocessor.它可以使Shader的编译更加快速,可以在project Setting或者选中某个Shader进行单独设置勾选这个选项 

Binary compile 

前面提到的Preprocess是运行在编辑器下,Unity Editor拿到了Shader Compilation Info,但是它并不能用于渲染,也不能打到最终的包体里,它只是Unity所使用的一种中间状态。那么如何把它最终编译成可运行的版本呢?

Shader Compiler里面包含了很多的服务,除了Preprocess外,还有一个叫作Binary compile也就是将我们Shader Program里面的代码输出到对应平台上去。Preprocess后,Unity会把Shader Compilation Info(可以从ShaderCache里取,如果里面没有,就走一遍Preprocess,重新产生Shader Compilation Info)再送到Shader Compiler里,执行Binary compile。

那么什么时候会触发这个过程呢?

  1. 在点Play按钮启动Unity的时候,这个时候Unity会做一件事情叫Unity Editor Shader All Warmup(在第一次导入资源的时候Unity也会做)。这就是为什么2020之前的版本大家点Play的时候感觉卡半天,实际上中间有个过程是把你内存里面或者说你资源里面的所有的shader的变体全都Warmup一次。但是在真机上不会这么干,Unity实际上是两个版本,运行时和编辑期是两套完全不同的东西,两者策略是会有些差异的,我们再做一些性能分析,分析内存、CPU、GPU,不要在跑在编辑器里看。编辑期的目的是为了帮助大家以最流畅的速度去编辑,它不去考虑运行时的资源环境占用,例如CPU占用、内存占用等,它会认为你的电脑都足够的好,内存不会爆,CPU不会卡,可以挥霍这些资源,尽量保证编辑体验是好的。但是在运行时,Unity会去考虑实际的运行环境(手机或者PC)。
  2. 打包的时候,比如要打一个Android平台的AssetBundle,或者说Build一个Android的APK,这个时候也会触发。

综上所述,当我们知道中间的东西最终需要翻译成什么,输出到什么设备上去,当一个平台确定了后这个过程才会发生 

运行

       编译完了之后就来到了运行时,这个时候要真正的在真机上把Shader给跑起来了(这里说的不考虑编辑器运行的情况)。真机发起的入口一般是用户的代码,这个用户代码可能是各位写的Warmup,也可能是通过某些引用调用UnityAPI,API再去调用底层。我们把Unity的上层系统简单的抽象成User Code,当User Code说 I need a shader,这个时候去加载一个Shader,怎么加载呢? 

有了Instance后,最后把它叫醒即可,也就是Awake操作,实际上名字叫Awake from main thread,从主线程唤醒,这个里面会做一个类似于我们的C# Awake或者是Start里面做的工作。比如说我们的数据填充进来要做一些处理,有些工作是不能在构造函数里做的,必须在数据进来之后才能做。比如说要确定使用哪个SubShader,如果SubShader的数据都没进来呢,那就没法先确定。再比如使用SRP,我要构建SRP Constant Buffer的结构,如果Shader数据没进来,同样无法构建。所以数据进来之后我们还有个处理操作,就是Awake from main thread。 

除了Shader Class Instance外,Unity内所有的类在构造的时候基本都是这三步(当然了每个类的行为不太一样):

  1. Produce一个空类
  2. Transfer数据
  3. Awake

Warmup Variant

Shader加载进来之后呢,我们经常面临的另外一个问题就是Warmup,此时Unity到底干啥了,为什么有的时候感觉Warmup这么卡?

举个例子, 假如我们的Shader Program如下:

HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile TEST1 TEST2
#pragma multi_compile TESTA TESTB

......

float4 frag(v2f i) : SV_Target
{
#if TEST1 & TESTA
    return float4(1, 0, 0, 1);
#elif TEST1 & TESTB
    return float4(0, 1, 0, 1);
#elif TEST2 & TESTA
    return float4(0, 0, 1, 1);
#elif TEST2 & TESTB
    return float4(1, 1, 1, 1);
#endif
}
ENDHLSL

那么运行时的产生的Shader Class Instance里面一共有四种变体组合,TEST1 TESTA、TEST1 TESTB、TEST2 TESTA、TEST2 TESTB,如下图(省略了TEST):

变体实际上是Unity带给大家的一个语法糖。大家都知道所有的糖都是很好吃,但是很有害的,语法糖也一样。大部分语法糖最终都会导致你的代码体积膨胀,比如说我们写c++、c#用到的模板泛型,最终都会导致你发胖。Shader Variant也是一样,当我们用大量的变体排列组合的时候啊,实际上它会把每一种排列组合单独的变成一段代码(点击Compile and show code即可看到)。它们在内存中都是单独的一套完整代码,每个变体都是一个独立的个体,它们彼此之间没有任何联系,通过大家给出的keyworld的排列组合进行索引的。

如果此时我们用Shader.EnableKeyword的API去Warmup 1A,那么使用这个Shader的物体就会变为红色,说明Variant 1A Warmup成功了。但是如果我要去Warmup 2C,由于没有TESTC这个keyword,也就没有Variant 2C,那么又会怎么样?首先不会发生fallback,因为fallback的前提是这个Shader Class Instance没了。其实通过代码测试一下,会发现物体变为了蓝色,也就是Variant 2A被Warmup了。

这是因为Uinty会有一套奇特的打分机制,它会根据你给出的keyworld和现在所有的keyworld进行一个打分。比如说我Enable了 TEST2 和 TESTC,先会拿TEST2到里头去找,看它在不在我有的排列组合里,如果这个排列组合里有TEST2,那么它会得到一个比较高的分。然后再去找TESTC,里面如果没有TESTC再去减分。通过这样的机制分别对拥有的变体进行打分,最后打出来分最高的那个变体,就是Unity要给你的。但是至于是不是你想要的,Unity就不管了,因此上面的例子我们会得到Variant 2A。所以有的时候会出现,有些Shader效果你看起来差不多,但是不太对,可能就是你的变体没有打进去,但是Unity为了保证不崩溃,选了一个打分最高的变体还给你。

当我们去Warmup 一个变体的时候,实际上我们在内存的统计上是会看到一点点变化的,是什么意思呢?我们知道一个变体底下会带有一段代码,一段Binary Code,这段Code在内存里是要占大小的。那么这段Code会一直在内存里面吗?不会,当我们成功的Warmup了某个变体之后,该变体的Code在CPU里的内存就消失了,因为它已经到GPU那块了,到显存里去了,所以Unity会很聪明的帮你把它删掉。示意图如下:

所以当大家观察到我们的ShaderLab非常非常大的时候,那么有一种可能是你打包了非常多的变体进去,但是很多其实你都没有用,都留在了CPU这一端。因此对于不是C#控制的keyworld我们应该使用shader_feature而非multi_compile,这也是一个优化上的小技巧。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值