引擎开发随笔之OpenGL Shader的封装思考

354 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

在进行mini3d.js这个开源小项目的过程中,越来越体会到一个引擎的复杂性,从技术DEMO到可以完成实际工作的引擎,完全不是一个数量级的复杂度。因为工作量太大,且想做的事情太多,mini3d.js的开发笔记一直是很滞后的,当然主要原因还是开发mini3d.js对我来说已经是一个很大的工程了。为了防止时间长了忘了很多东西,以及记录一些暂时没法去做的事情,特别用随笔的形式简单记录一下,想到哪儿记到哪儿。

demo中工作的shader

写一个OpenGL/ES/WebGL的demo,里面包含一份写好的shader,无论这个shader有多牛多复杂,其实这个事情还是比较简单的。为什么?因为没有复用性的要求,没有效率的要求。要做是事情,无非是封装一下shader的compile, link, error check而已,提供一些接口可以传入unifrom,提供vbo和shader attribute的数据绑定接口即可。这也是mini3d.js一开始做的事情,很简单,简单即美好,一切这么简单多好。然而事实是残酷的,当我开始支持不同的材质,开始添加场景中的camera和灯光,以及开始考虑shader的复用时,复杂度不可避免的到来。

引擎中的shader需求

  • 提供公用uniform的填充 你都已经是一个引擎了,难道还让用户写材质shader的时候手动传入MVP矩阵?用户需要使用灯光位置方向颜色这些东西的时候,你让用户自己计算好了,通过uniform传进来吗?当然不行。不说MVP, object2world, world2object这些常用的矩阵了,就说camera, 灯光这些属于场景的内容,自然需要从引擎中获取。一般引擎提供自定义shader的功能,都得提供一些常用的公共uniform的自动填充。如果用户使用引擎内置的这些uniform的名字,则用户基本不用做额外的工作,就可以直接在shader中使用。
  • 前向渲染中多灯光处理和shader变体支持 shader中做灯光计算,需要知道光源的位置/方向,而且光源是有不同的类型的,他们的范围,方向计算方式,衰减计算方式都不同。而且你要考虑,是在一个pass里面同时计算多个灯光,还是每个pass计算一个灯光,然后把进行加法混合。例如在Unity内置的前向渲染管线中,标记为forwardBase的pass只计算一个最重要的平行光的逐像素光照,以及多个逐顶点光源的计算。而标记为forwardAdd的pass用来计算其他的逐像素光照光源的计算。这么做是为了考虑效率,如果引擎默认每个灯光都执行一次shader pass,那么如果这个shader里面都是复杂的逐像素计算,最后可能跑不动了。即便我们简化为每个灯光执行一次shader,还有一些事情要处理。首先,上面说了灯光有不同的类型,平行光点光源聚光灯,他们计算灯光方向的方式不同,计算衰减的方式不同,那么你需要在同一个shader里面去判断不同的光源类型,然后使用不同的计算方式。但是我们知道,shader里面不要用复杂的分支判断,特别是像素shader。那么为了只写一份shader,我们需要使用预处理宏。那么问题来了,包含预处理宏的shader,表面上是一个shader,但实际上编译之后是不同的shader。也就是说我们要提供不同的宏定义,然后编译shader,得到不同的变体。再根据灯光类型等等使用不同的变体。如果只有灯光类型这一个参数还好,如果shader中还有第二个,第三个参数,组合之后的shader数量就多了,而作为引擎,你就得管理好这些shader变体。当然你也可以将参数限制为一个,然后变化太多就直接提供全新的shader,但总之,你需要处理这个复杂度。
  • 提供#include功能 除了公共的uniform,有一些公共的函数,最好都能提供给用户写自定义shader时使用,并且引擎自己提供的默认shader也是很可能需要公用一些函数的,那么你就需要提供在shader里面#include其他shader代码的功能。貌似GLSL ES是不支持#include指令的,那么就需要在上一层自己实现。
  • shader复用 理论上,shader只是一个小程序,同一个shader program是可以复用来渲染不同的物体的,只要在渲染前设置不同的uniform就行。如果不复用,一组vs,fs创建一个program用来渲染一个物体,物体数量多了,显存中就有很多同样内容的shader program。这显然是浪费了显存,浪费了shader切换时间,并且浪费了编译shader的时间(针对只能在线编译的平台)。
  • 从shader到材质 shader只是渲染状态的一部分,另外还要考虑到深度测试,混合这些流水线状态,所以作为材质只管理shader还不行。当然只管理渲染状态也还不够。还得考虑多pass(不是上面针对光源的被动多pass),考虑fallback,考虑渲染队列渲染顺序,考虑到如何编辑材质。引擎是否厉害从材质编辑方式上就可以看出来一些。好的引擎还可以让你很容易的复用已有的材质(或其一部分),可以可视化的编辑shader效果等。