UI优化相关实现

初始情况

我们采取的是树形结构的UI,你可以理解成跟HTML类似的层级结构。做法也很传统,每一帧都从根节点出发遍历所有节点,对有渲染用途的节点会收集顶点数据并判断是否能合并批次。渲染也中规中矩,从后往前不开深度测试和深度写进行渲染

瓶颈一:大量无渲染功能的UI节点

UI树大部分情况下需要渲染的节点只占整棵树的一小部分,剩下的很大一部分要么隐藏了,要么只是起到布局作用,类似HTML中DIV的功能。当时就出现节点太多,遍历起来特别慢的情况。庆幸的是隐藏的节点可以大部分可以通过判断子树根节点是否显示来剔除整颗子树,所以只有布局功能没有渲染功能的节点就成了罪魁祸首,需要着手去优化

优化的方式是,给每个UI节点,新增以下几个字段用于加速遍历。思路是,假设当前节点是子树根节点,如果它的子树没发生任何变化,那么dirty为false,begin是子树第一个有渲染功能的节点(可能是子树根节点),end是子树最后一个有渲染功能的节点,next指向下一个有渲染功能的节点。那么只需要通过begin,一路next下去,直到end,就可以收集完整棵子树有渲染功能的节点。

struct
{
    bool dirty;
    UIElem* begin;
    UIElem* end;
    UIElem* next;
};

因为UI是变化的,随时有插入删除,所以我们只能在线Lazy处理,做法就是当有子节点状态发生改变或者插入删除时,会从该子节点的父节点出发一路dirty标记为true直到根节点。然后在遍历的时候对上面提到的字段进行动态维护。对于dirty为false,直接采用上面的做法。为true的话,因为我们是先序遍历节点,也就意味着如果当前子树根节点没有渲染作用不能作为begin的话,我们只需要遍历它的下一层节点,第一个有效的begin就可以作为子树根节点的begin,最后一个end可以作为子树根节点的end,只要一次遍历就可以将子树根节点的dirty标记为false。

这样优化之后性能消耗将至原来的1/4

瓶颈二:不同的图片和文字间交叉渲染

如果UI使用的不同的图片的话,它们是没法在一个批次里进行渲染的。文字也是一样,文字其实也是一张大贴图,不同字号的其实用的贴图也不一样。所以想优化这个问题的话,首先要做的,就是将用到的图片打到一张图集里,采样一张图里不同的部分即可,这样同一张图片的东西就能合到一起。

但其实这里还有一个文字,例如两个带文字的按钮,虽然按钮的背景是一样的原本可以合到一起,但由于遍历用的是先序遍历,所以只能是按这个顺序渲染:按钮背景、文字、按钮背景、文字。然后我们就直接优化掉这种情况,人为控制了所有文字都是最后画的,也就一般存在于树的根节点,并且它们不需要在文字间的遮挡关系,将除文字以外的东西都按原来的先后顺序进行渲染(这里依旧按先序遍历的方式,是因为图片间有遮挡关系),并收集文字,文字根据字号和批次最后进行渲染。所以问题就退化成了,除文字部分都在一张图集里就能1个批次解决问题。然后不同字号的文字也分别一个批次解决。

该方法不完美的地方在于,有时候一张图片放不下所有图片,依旧可能会出现交叉渲染

上述只是稍微解决了下图片和文字间的问题,但其实还是很难避免交叉渲染的问题,还有一个方法就是遍历整棵UI树的时候,收集所有有渲染功能UI,并根据渲染状态将相同渲染状态的加入到一个队列里,用类似Array>的数据类型来存储,最后只要遍历这个数据,就能将渲染状态相同的UI一起渲染了。因为我们是从后往前渲染,所以越底下的UI对应的UIBatchInfo(UI的渲染状态),就越会在最前面。这样可能会引发一个问题,如图情况1,A在最下面,B在A上面,C在B上面,理论上应该A、B、C按顺序画才是我们想要的效果。然而因为A和C都是黑色,如果合并了就会先画A和C,再画B,这样B因为后画就会盖在C上上,就跟我们想要的结果不一样了。而如果是情况2的话,因为A和C单独存在,合并反而是我们想要的结果。所以针对情况1这种情况,我们只需要提出层次的概念,并加入渲染状态的判断中,情况1中A在第一层,B在第二层,C在第三层,判断A和C不在同一层,渲染状态不一致无法合并,就能够按顺序单独画。而情况2,让A、B、C都在同一层让它们能合并。

UI优化相关实现_第1张图片

这里稍稍提一下实现这个做法遇到的一个坑,当时为了图方便针对不同的渲染状态创建了多个VertexBuffer,结果性能比较差,因为画不同渲染状态的UI时要重新给GPU设置VertexBuffer,这个设置带来了不少的开销。所以最终还是将所有顶点数据放在同一个VertexBuffer中性能比较好

瓶颈三:图片相互叠加带来的像素填充瓶颈

因为我们采用的是无深度测试从后往前画的做法,所以必然会带来这个后果。要优化的话,就需要给予每个UI控件深度信息,改成带深度写,并且深度比较函数是小于等于。

大致流程是遍历整颗UI树,对子节点先处理,子节点递归处理完再处理根节点就可以达到从前往后画的效果了,在遍历过程中先初始化初始深度是0,然后每遍历到一个UI控件就相比前一个控件加一点深度,目的是区分开哪个UI在前哪个UI在后。遍历的时候如果是不透明的可以直接画,对于透明的就得存起来,之后整棵树遍历完了,再从后往前画透明的UI。

对于不透明的UI的话,可以大大减少像素填充带来的消耗,可以用深度测试直接节省很多像素的填充。但如果充斥着大量的透明UI的话,这个过程会有些浪费,浪费就浪费在透明的UI得存起来,然后最后再遍历,相当于透明的UI遍历了两遍。所以需要权衡两者哪个带来的消耗更大。

题外话

再有一个是跟性能无关但跟UI相关的话题,那就是打图集的问题,打图集本身库和方法比较多,我也就随便推荐一个,可以用stb_rect_pack这个库。当然我不是来推销代码,我想说的是实际游戏中,可能会使用到一些比较廉价的UI做法,例如纯色块,只有横向或者纵向渐变的矩形,以及中间镂空的边框。偷懒的做法,往往是控件多大,图片就按一比一的比例塞到图集中。这其实对图集空间带来了很大的浪费,直接导致的后果就是图片位置不够用需要多一张图片,以至于会交叉渲染。所以要想办法提交图片利用率

纯色块:直接压成3x3的像素放在图集中,为什么不是1x1,是因为采样会有误差,以至于采样到周围的其他像素。虽然是3x3像素在图片中,但我们uv只指定1x1的部分

横向渐变的矩形:一般横向渐变,其实有效的也就1行像素,假设它的宽是N,那我们就将3xN放到图集中,原因和上面一样,所以分成3行,采样的时候只采样一行,由GPU自己去拉伸。

纵向渐变的矩形:道理同横向渐变,只需要图集保留3列

镂空图片:拆分成8张图片,8张图片可以根据情况看能不能用上面3种方式优化掉。由于是九宫格的做法,所以也就意味着UI控件需要支持九宫格

在制作图集工具时,别妄想根据图片自动选择优化方案了,还是老老实实人工选方案吧,因为美术给的图,大概率是会有误差的,纯色块并不一定每个像素都一样可能只是接近,横向渐变也并不是每一行都一样,可能会有误差。

你可能感兴趣的:(图形)