分享一个通过改变提交渲染顺序来实现DrawCall减少的思路。

前言

昨天去某公司面试,被面试官抓住不放,问了我一系列关于DC的问题。可惜我这两年一直在做3d小游戏。对于2d方面的的DC的关注点几乎没有。只能说是回答的勉勉强强。所以今天开始狠狠的补习了一下cocos的源码,并且萌生了绕开节点树的渲染提交顺序,通过自己指定每个节点的提交顺序来最大化合并DC的效果。经过一下午的尝试,终于有所收获。

引擎版本

基于 3.3.1

代码质量

瞎写的。

DC合并的原理

  1. 我们先来观察一下,上边的这张图片。这是一个游戏中常用的列表的。
  • conten是列表的父亲节点
  • green0,green1,green2是三个列表的单元格。
  • 每个单元格由 green(绿色图片),yellow(黄色图片), label0(一个bmf文本构成)
  • 绿色图片和黄色图片已经被打入了同一个贴图集内。
  1. 那么根据cocos传统的节点数的深度访问遍历来提交渲染批次。那么提交渲染的顺序就会是
    Canvas->Camera->content->green0->yellow0->label0->green1->yellow1->label1->green2->yellow2->label2。
    再根据DC和批的原理。如果渲染提交的时候,多个连续提交的节点使用了相同的图集,那么这多个节点的DC会合并一下。所以如下图,会有6个DC。本来green0,yellow0可以合并,然后label0打断了这个合并,接着green1,yellow1可以合并,label1又打断了这个合并。也就是说每个单元格会占据2个DC。如果有 单元格有N个的话。那么这个列表就会有2N个DC。
    75D7955F-C4CE-4625-931F-8A59C24095EE

思路上的改进

  1. 此时,已经有聪明的小伙伴发现了。如果我提交渲染的顺序是
    green0->green1->green2->yellow0->yellow1->yellow2(到这个时候用了一次DC)
    ->label0->label1->label2(此时使用了2个DC)
    那岂不是美滋滋。升职加薪走上人生巅峰也不是梦想。
  2. 那我们需要一个功能,可以打破渲染提交的顺序。同时,如果我不用这个功能,就是按照传统的节点数去提交。最好的情况是,场景里的节点很多很杂。但是大部分都是按照原先的方式去提交批次。而我可以指定某一个节点,这个节点和它的子子节点按照我指定的顺序来渲染。

    如上图的一个节点树。其中ABCDEFG都是节点团。在渲染的时候,走的顺序还是 A-B->D->C->E->F->G。但是D,G这2个节点团渲染的时候,是走自己指定的顺序渲染。这样子,渲染逻辑上可D和G之间是相互独立的。不会发生错乱。但是G节点团的渲染永远是后与D节点团的。

透明度的叠加

  1. 经过评论区的一位同学的提醒,这里还需要考虑透明度的叠加问题。
  2. 还是以上图为例子。传统的节点树上
    D看起来的透明度 = A透明度 x B透明度 x D透明度
    但是在打乱提交顺序之后,透明度如果还按照上述来计算的话。代码消耗会增大很多,所以我修改成了
    任意子节点的透明度 = A节点透明度 x 任意子节点自身透明度

用法上的说明

  1. 我先选中content节点。
    4B182B24-9601-4710-AB86-FAFF30CD4C9F
    2.为了满足功能,我修改了UITransform.ts的源代码,为它添加了2个属性。
    F848CC7B-CFBD-4328-BAEF-BA421FEADF13
  • SpanEffected 被勾选上之后表示这个节点和它的所有子节点的已经成为了一个节点团。在这个节点团的内部,所有的渲染提交顺序由用户来自己指定了。
  • RenderIndex 则是渲染的优先级。在content这个节点团下,渲染优先级越小,则越先提交渲染。
  1. 接着我们将content节点团下的渲染提交优先级设置成如下图所示
    20A8B070-08CA-4767-B033-B2883864D095
    此时我们提交的渲染顺序变成了content->green0->green1->green2->yellow0->yellow1->yellow2->label0->label1->label2
    那么理论上就只有2个DC了。
    接着我们用spectorJs来验证一下。

    可以看到,确实如此,只有2个DC。到此,大功告成

代码修改

UITansform.ts文件修改如下

batcher-2d.ts代码修改行数在
610行-658行

在这里直接把修改后的源码打包了,开发者可以直接替换(源码里已经重新加入了计算透明度部分的代码)
修改后的源码.zip (14.7 KB)

后记

在这里笔者只是提供了一个改变渲染顺序的思路。代码写的并不规范。但是我认为这个思路是不错的。希望大家甚至引擎组都可以借鉴。直接把这个功能完美的集成到引擎中去。

测试

这里没有做大规模的测试。无法预估这个修改会不会引发未知的错误。如果要是用到实际中的项目,一定要多测试哦。项目崩了,老板跑路,工作丢了,笔者一概不负责。

13赞

想法很好···但是你这个没有考虑到透明度叠加的问题···

一直在想这个问题但是怕(lan)看源码,在不考虑遮挡的情况下应该是很方便了 :bowing_man:

思路很有启发,挺好

要是能完善完善就好了 :heart_eyes:

学习马克一下

是的。按照传统的节点树。比如说父节点,子节点,孙节点的结构。 那么
孙节点的透明度= 父节点透明度 x 子节点透明度 x 孙节点透明度
现在因为没有节点树的概念了。
这个透明度计算只能是 任意子节点或者孙节点透明度 = 最高父节点(即例子里的content的)透明度 x 自身透明度

大佬 请问下 “提交渲染” ,和 “顺序” 怎么立即呀? 能不能稍微讲讲这个知识点呀 :joy:

这个提交渲染的过程在 batcher-2d.ts的 walk函数里。建议在浏览器里调试项目的时候。在这个walk函数里打个断点。然后看调用堆栈。可以发现是从director.ts这个类里每帧都进行了渲染的排序。

cocos的drawcall节省更多的是从节点层级去动手,在放节点的时候就考虑到哪些节点可以进行合批。
比如你的个例子,需要做的是把按钮放在一个节点里,颜色放在一个节点里,文本放在一个节点里,
就这么说吧,如果有几百个精灵,你怎么去保证你调整过的渲染顺序的结果,是和按照层级关系的渲染顺序渲染的效果是一样的?

你说的这种方式我确实见过。但是有个弊端就是。比如说我点击一个按钮的时候,按钮有一个缩放的动作。在这种方式下,点击按钮,发现按钮上的字体文本没有跟着按钮一起缩放。当然也可以通过代码去控制一下这个缩放,但是有增加了代码的复杂度。
所以不论是我的方法,还是你说的方法,。都有利弊。只能从项目的实际需求出发。做出一个合理的选择。

就这么说吧,如果有几百个精灵,你怎么去保证你调整过的渲染顺序的结果,是和按照层级关系的渲染顺序渲染的效果是一样的?

这个问题是只有充分的测试才能保证。我没法保证。所以这个方法才是可以挑选一个局部的节点团来测试。保证你的修改是局限于一个可控制的节点团呀。

1赞

如何更換源碼呢?

就是你找到你下载的引擎目录呀。我mac的是 应用程序/CocosCreator/3.3.1/CocosCrreator.app。
然后右键“显示包内容”。
里边有源代码

其实就是深度搜索改为广度搜索,很适合优化大背包,其他应用场景就没这么简单粗暴了

2赞

这也许有用吧, 先点个赞。

这种做法只适合于定制化的场景,不可能做成通用的做法。
但如果是定制化的场景,去调整层级关系也可以实现。

所以总的来说,我不觉得你的这种做法是一个通用的优化方式。

当然,对你这种探索精神还是值得肯定的。

这种做法只适合于定制化的场景,不可能做成通用的做法。
但如果是定制化的场景,去调整层级关系也可以实现。

虽然实现方式不竟相同,但是在一次技术分享上,我依稀记得《梦求游戏》的《新斗罗大陆》里的背包为了降低 DC,就是用了这种修改提交渲染的思路。我真的很好奇,为什么你如此的自信,就断言

但如果是定制化的场景,去调整层级关系也可以实现。

:sweat_smile:

楼主这种方式对于 listview 优化 DC,相比改层级能简单不少

因为你的这种做法我想过也做过,绕了半天发现还不如就回过头来用节点顺序去处理。最主要的是,这渲染方式很绕,我个人评价那代码只能我自己一个人用。

掌教说的对