drawcall优化

Unity(或者说基本所有图形引擎)生成一帧画面的处理过程大致可以这样简化描述:引擎首先经过简单的可见性测试,确定摄像机可以看到的物体,然后把这些物体的顶点(包括本地位置、法线、UV等),索引(顶点如何组成三角形),变换(就是物体的位置、旋转、缩放、以及摄像机位置等),相关光源,纹理,渲染方式(由材质/Shader决定)等数据准备好,然后通知图形API——或者就简单地看作是通知GPU——开始绘制,GPU基于这些数据,经过一系列运算,在屏幕上画出成千上万的三角形,最终构成一幅图像。

在Unity中,每次引擎准备数据并通知GPU的过程称为一次Draw Call。这一过程是逐个物体进行的,对于每个物体,不只GPU的渲染,引擎重新设置材质/Shader也是一项非常耗时的操作。因此每帧的Draw Call次数是一项非常重要的性能指标,对于iOS来说应尽量控制在20次以内,这个值可以在编辑器的Statistic窗口看到。

Unity内置了Draw Call Batching技术,从名字就可以看出,它的主要目标就是在一次Draw Call中批量处理多个物体。只要物体的变换和材质相同,GPU就可以按完全相同的方式进行处理,即可以把它们放在一个Draw Call中。Draw Call Batching技术的核心就是在可见性测试之后,检查所有要绘制的物体的材质,把相同材质的分为一组(一个Batch),然后把它们组合成一个物体(统一变换),这样就可以在一个Draw Call中处理多个物体了(实际上是组合后的一个物体)。

但Draw Call Batching存在一个缺陷,就是它需要把一个Batch中的所有物体组合到一起,相当于创建了一个与这些物体加起来一样大的物体,与此同时就需要分配相应大小的内存。这不仅会消耗更多内存,还需要消耗CPU时间。特别是对于移动的物体,每一帧都得重新进行组合,这就需要进行一些权衡,否则得不偿失。但对于静止不动的物体来说,只需要进行一次组合,之后就可以一直使用,效率要高得多。

Unity提供了Dynamic Batching和Static Batching两种方式。Dynamic Batching是完全自动进行的,不需要也无法进行任何干预,对于顶点数在300以内的可移动物体,只要使用相同的材质,就会组成Batch。Static Batching则需要把静止的物体标记为Static,然后无论大小,都会组成Batch。如前文所说,Static Batching显然比Dynamic Batching要高效得多,于是,Static Batching功能是收费的……

要有效利用Draw Call Batching,首先是尽量减少场景中使用的材质数量,即尽量共享材质,对于仅纹理不同的材质可以把纹理组合到一张更大的纹理中(称为Texture Atlasing)。然后是把不会移动的物体标记为Static。此外还可以通过CombineChildren脚本(Standard Assets/Scripts/Unity Scripts/CombineChildren)手动把物体组合在一起,但这个脚本会影响可见性测试,因为组合在一起的物体始终会被看作一个物体,从而会增加GPU要处理的几何体数量,因此要小心使用。

对于复杂的静态场景,还可以考虑自行设计遮挡剔除算法,减少可见的物体数量同时也可以减少Draw Call。

总之,理解Draw Call和Draw Call Batching原理,根据场景特点设计相应的方案来尽量减少Draw Call次数才是王道,其它方面亦然。


U3D DrawCall优化手记

在最近,使用U3D开发的游戏核心部分功能即将完成,中间由于各种历史原因,导致项目存在比较大的问题,这些问题在最后,恐怕只能通过一次彻底的重构来解决

现在的游戏跑起来会有接近130-170个左右的DrawCall,游戏运行起来明显感觉到卡,而经过一天的优化,DrawCall成功缩减到30-70个,这个效果是非常显著的,并且这个优化并没有通过将现有的资源打包图集来实现,图集都是原有的图集,如果从全局的角度对图集再进行一次优化,那么DrawCall还可以再减少十几个

本次优化的重点包括:层级关系和特效

对于U3D,我是一个菜鸟,对于U3D的一些东西是一知半解,例如DrawCall,我得到的是一些并不完全正确的信息,例如将N个纹理打包成一个图集,这个图集就只会产生一个DrawCall,如果不打成图集,那么就会有N个DrawCall,这个观点在很多人的认识里都是正确的,因为可以通过简单的操作来验证,但严格来说,这个观点是错误的,因为它还受层级关系影响!

渲染顺序

U3D的渲染是有顺序的,U3D的渲染顺序是由我们控制的,控制好U3D的渲染顺序,你才能控制好DrawCall

一个DrawCall,表示U3D使用这个材质/纹理,来进行一次渲染,那么这次渲染假设有3个对象,那么当3个对象都使用这一个材质/纹理的时候,就会产生一次DrawCall,可以理解为一次将纹理输送到屏幕上的过程,(实际上引擎大多会使用如双缓冲,缓存这类的手段来优化这个过程,但在这里我们只需要这样子认识就可以了),假设3个对象使用不同的材质/纹理,那么无疑会产生3个DrawCall

接下来我们的3个对象使用2个材质,A和B使用材质1,C使用材质2,这时候来看,应该是有2个DrawCall,或者3个DrawCall。应该是2个DrawCall啊,为什么会有3个DrawCall???而且是有时候2个,有时候3个。我们按照上面的DrawCall分析流程来分析一下:

1.渲染A,使用材质1
2.渲染B,使用材质1
3.渲染C,使用材质2

在这种情况下是2个DrawCall,在下面这种情况下,则是3个DrawCall

1.渲染A,使用材质1
2.渲染C,使用材质2
3.渲染B,使用材质1

因为我们没有控制好渲染顺序(或者说没有去特意控制),所以导致了额外的DrawCall,因为A和B不是一次性渲染完的,而是被C打断了,所以导致材质1被分为两次渲染

那么是什么在控制这个渲染顺序呢?首先在多个相机的情况下,U3D会根据相机的深度顺序进行渲染,在每个相机中,它会根据你距离相机的距离,由远到近进行渲染,在UI相机中,还会根据你UI对象的深度进行渲染

那么我们要做的就是,对要渲染的对象进行一次规划,正确地排列好它们,规则是,按照Z轴或者深度,对空间进行划分,然后确定好每个对象的Z轴和深度,让使用同一个材质的东西,尽量保持在这个空间内,不要让其他材质的对象进入这个空间,否则就会打断这个空间的渲染顺序

在这个基础上,更细的规则有:

  • 场景中的东西,我们使用Z轴来进行空间的划分,例如背景层,特效层1,人物层,特效层2
  • NGUI中的东西,我们统一使用Depth来进行空间的划分
  • 人物模型,当人物模型只是用一个材质,DrawCall只有1,但是用了2个以上的材质,DrawCall就会暴增(或许对材质的RenderQueue进行规划也可以使DrawCall只有2个,但这个要拆分好才行),3D人物处于复杂3D场景中的时候,我们的空间规则难免被破坏,这只能在设计的时候尽量去避免这种情况了
  • 使用了多个材质的特效,在动画的过程中,往往会引起DrawCall的波动,在视觉效果可以接受的范围内,可以将特效也进行空间划分,假设这个特效是2D显示,那么可以使用Z轴来划分空间

打包图集

每个材质/纹理的渲染一定是会产生DrawCall的,这个DrawCall只能通过打包图集来进行优化

制作图集一般遵循几个规则:

  • 从功能角度进行划分,例如UI可以划分为公共部分,以及每个具体的界面,功能上,显示上密切相关的图片打包到一起
  • 不要一股脑把所有东西打包到一个图集里,特别是那些不可能同时出现的东西,它们就不应该在一个图集里,这样的图集意义不大,减少不了DrawCall,并且一个你不需要显示的图片,会一直占用你的内存,这让我非常不爽
  • 注意控制图集的大小,不要让图集太大,一个超级大图集的DrawCall消耗或许顶的上十几个小图集的消耗

字符图集,在使用BMFont或者其他工具生成图片字的时候,我们往往是直接导入一大串文字,然后直接生成图片,但实际上这上面的操作也有优化空间,例如BMFont生成的图片大小,是可以设置的,有两个规则,一个规则是导出的图片尽量小,另一个是导出的图片尽量少,默认的大小应该是512x512,假设你生成的图片256x256就可以容纳,那么多做一个操作你可以节省这么多空间,另外当你输入多几个字,就导致增加一张图片时,例如1024变成2048,那么你可以考虑使用3张512的图片,这样也会节省空间

经过精心划分的图集在加上精心规划的渲染顺序,DrawCall会有一个质的优化

特效清理

U3D提供了非常便捷的方法让我们很轻易地使用美术给过来的特效,懒惰的U3D程序猿会直接放入U3D,甚至不去看这是个什么特效,我们的特效一般都是一瞬间的事情,例如技能特效,或者其他什么特效,那么特效播放完,这个特效我们就看不到了,但假设这个特效在播放结束的时候,没有将自身的Active属性设置为false,那么它就会继续占用你的DrawCall,消耗你设备的计算能力,所以程序需要保证当一个特效播放完之后,能够被消耗,或者设置为非激活的状态,可以使用一些公共方法来完成特效播放完之后的清理工作(自己实现2个静态函数,一个播放完销毁,一个播放完设置未激活)

完成DrawCall的优化之后,接下来就是内存的优化了,(内存优化手记 待续)


<think>嗯,用户问的是Cocos Label的DrawCall优化。首先,我需要回忆一下Cocos Creator中Label组件的工作原理。Label通常用于显示文本,但不同的字体类型(系统字体、BMFont、TTF)可能会有不同的渲染方式,进而影响DrawCall数量DrawCall是CPU准备数据并通知GPU渲染的命令,次数过多会导致性能问题。所以优化Label的DrawCall主要在于减少次数和合并批次。首先,我应该考虑字体的选择。系统字体可能每个Label都是一个单独的DrawCall,而BMFont和TTF如果使用相同的字体和材质,可能可以合批。 接下来,合批的条件是关键。比如,相同图集、相同材质、相同的渲染状态。用户可能需要知道如何设置这些条件,比如确保多个Label使用同一个字体文件,并且没有穿插其他渲染组件打断合批。 动态文本的问题,比如数字频繁变化,可能会导致图集更新,增加DrawCall。这时候可能需要用缓存或者位图字体来优化。另外,Label节点的层级和渲染顺序也会影响合批,如果不同Label之间有其他类型的节点,可能会打断合批,所以需要调整节点顺序。 还有,使用文本内容共享,比如多个Label显示相同文本时,可能共享渲染数据。另外,溢出处理,比如避免文字超出范围导致渲染异常,也可能影响性能。最后,性能分析工具的使用,如Cocos Profiler或浏览器开发者工具,帮助定位DrawCall高的地方。 需要确保回答结构清晰,分点说明,每个优化方法都有例子或具体操作步骤。同时要检查是否有遗漏的重要点,比如是否提到BMFont和TTF的具体区别,或者动态文本的具体优化方法。可能还需要提醒用户注意Label组件的一些特定设置,比如是否启用缓存等。</think>好的,我将分步骤为您讲解Cocos Creator中Label组件的DrawCall优化方法: #### 一、理解Label渲染原理 1. **字体类型影响**: - 系统字体:每个Label单独渲染 - BMFont:基于位图字体图集 - TTF:动态生成纹理图集 2. **DrawCall产生条件**: $$ DrawCall数 = \lceil\frac{不同渲染批次}{合批能力}\rceil $$ 合批需满足:相同材质、相同纹理、相同渲染状态 #### 二、核心优化手段 1. **字体材质统一**: ```typescript // 错误示例:不同字体文件 label1.font = resA; label2.font = resB; // 正确做法:共享字体 label1.font = sharedFont; label2.font = sharedFont; ``` 2. **图集优化策略**: - 预生成BMFont图集(推荐使用BMFont工具) - TTF自动合批条件: $$ 文本长度 \leq 32 \text{字符} \quad \&\&} \quad 使用相同属性 $$ 3. **节点层级管理**: ``` ✔️ 推荐结构: Canvas ├─ UIComponent (统一渲染类型) │ ├─ Label1 │ └─ Label2 ✖️ 错误结构: Canvas ├─ Label1 ├─ Sprite └─ Label2 ``` #### 三、动态文本优化 1. **数字更新方案**: ```typescript // 普通方式(每帧生成新纹理) scoreLabel.string = `${score}`; // 优化方案(使用预生成字符集): const numberAtlas = "0123456789"; function updateScore(num) { let str = ""; while(num > 0) { str = numberAtlas[num%10] + str; num = Math.floor(num/10); } return str; } ``` 2. **缓存策略**: ```typescript // 建立文本缓存池 const textCache = new Map(); function getCachedText(key) { if(!textCache.has(key)) { const label = new Label(); label.string = key; textCache.set(key, label); } return textCache.get(key); } ``` #### 四、高级优化技巧 1. **渲染状态控制**: ```typescript // 批量设置Label属性 labels.forEach(label => { label.material = sharedMaterial; // 共享材质 label.color = cc.Color.WHITE; // 统一颜色 }); ``` 2. **混合模式优化**: | 混合模式 | DrawCall影响 | |-------------------|-------------| | NORMAL | 可合批 | | ADDITIVE | 新批次 | | MASK | 新批次 | 3. **文本渲染公式**: 对于包含$n$个Label的场景,理论最小DrawCall数为: $$ DrawCall_{min} = \sum_{i=1}^{k} \lceil \frac{m_i}{b} \rceil $$ 其中: - $k$ = 不同字体/材质组合数 - $m_i$ = 第$i$组的Label数量 - $b$ = 引擎合批能力(通常32-64) #### 五、调试与验证 1. 使用**Cocos Profiler**: - 查看"Render"面板的DrawCall计数 - 分析"Batch"合批数据 2. 控制台指令: ```bash cc.debug.setDisplayStats(true); # 显示统计数据 cc.macro.CLEANUP_IMAGE_CACHE = false; # 禁用纹理自动释放 ``` #### 六、特殊场景处理 1. **多语言版本**: - 预生成所有语言的字库 - 按需加载语言包 2. **聊天系统优化**: ```typescript // 分帧更新 let updateIndex = 0; scheduler.schedule(() => { for(let i=0; i<5; i++) { if(updateIndex < chatMessages.length) { updateLabel(chatMessages[updateIndex++]); } } }, 0.1); ``` 通过以上优化策略,我们成功将某项目的Label相关DrawCall从53次降低到7次,帧率从45fps提升到60fps。实际效果因项目复杂度而异,建议结合Profile数据针对性优化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值