原文:轩辕传奇场景优化笔记
推荐作者的知乎专栏:MACK的游戏开发笔记
最近在做一些优化相关的内容,每个版本的性能review。顺便翻了翻以前的优化笔记,找到了一篇写的还算正式的笔记。13年的工作生涯大半时间都在做渲染和优化相关的内容,优化这东西真的是扁鹊三兄弟的故事,只有痛了才知道它的重要性,而当它不是问题的时候却很少有人会注意到它。
目的和需求:
- 客户端安装包过大,玩家下载不方便,希望能减少安装包容量。
- 由于客户端Camera拉平,希望能显示更远更大的模型,同时希望模型能平滑出现,提升客户端表现。
- 由于Camera拉平导致客户端效率有一定程度的下降,希望能提高效率,提高帧数。
- LightMap,阴影等存在模糊配置不合理的问题,希望显示效果能更好。
- 客户端有一些奇怪的花屏渲染错误,随机出现,猜测是显存碎片较多。
目标:
- 减少10%客户端容量;
- 在保证效率的前提下提升客户端表现,看的更远;
- 在显示范围更大,显示效果更好,Camera拉平的前提下,提高渲染效率,维持甚至超过之前版本的帧数;
- 减少内存显存碎片,优化Shader,消除渲染错误,加入限帧等,避免一些花屏死机等渲染错误;
容量方面优化:
针对客户端较大的问题,我们首先分析了轩辕的资源文件大小。其中比重最大的是场景数据,未压缩前有2G,压缩后有425M,共85张地图并且策划还会添加新的地图。
场景数据是由地图编辑器导出,主要包括地形的模型数据,水的索引类型数据高度图,草的模型数据,LightMap数据等。通过分析发现地形数据,水的高度图,草的数据,LighMap比较大,占绝大部分。
地形数据
我们的地形数据由Chunk组成,每个Chunk有16x16个网格,每格代表2米,每个顶点有顶点索引(4个byte),顶点坐标(12个Byte),法线(12个Byte),顶点色(16个byte),2套UV坐标(BaseMap和LightMap16个Byte),具体结构如下。
- 因为每个Chunk的大小规格都是一样的,所以每个Chunk的顶点索引都是固定的,这样我们可以在客户端计算。客户端有全局唯一一份地形顶点索引,这样编辑器导出数据每个顶点可以节省4个byte。
- 首先顶点的x,y坐标可以通过Chunk在地形中的偏移和当前地形网格在当前Chunk所有网格中的索引计算出来,这样每个顶点可以节省12个byte,为了减轻CPU的负担我们在GPU中计算,计算方式如下:
WorldPos.xy= g_fOffset.xy * CHUNK_SIZE + _f3Position.xy * GRID_SIZE;
WorldPos.z= _f3Position.z;
(_f3Position.xy是当前地形网格在当前Chunk所有网格中的索引,g_fOffset是当前Chunk在整个场景中的索引,CHUNK_SIZE是每个chunk的大小,GRID_SIZE是每个网格的大小。)
- 然后Chunk的UV也是固定的。我们的Chunk有两套UV,第一套UV用于采样5张基本纹理,1张高光纹理;第二套纹理用于采样1张混合纹理,1张阴影AO纹理,1张点光纹理。Chunk的2套UV中第二套UV固定是0-1,第一套UV存在一个循环次数系数(循环几次由美术在编辑器指定)也可以计算的出,这样每个顶点可以节省16个byte,计算方式如下:
_f3OutTex2.xy= _f3Position.xy / 16.0f;
_f3OutTex1.xy= _f3OutTex2.xy * g_fOffset.z;
(其中g_fOffset.z是循环系数。)
- 其次法线和顶点色我们可以通过各使用一个DWORD来压缩。顶点色可以将颜色的RGBA各一个float压缩成0-255的一个byte然后拼成一个DWORD以D3DCOLOR的格式直接传给显卡(D3D会自动转换成float),这样每个顶点可以节省8+12共20个byte。法线也同样可以压缩成一个DWORD,不过因为法线会有负数需要转换一下。为了减轻CPU负担,避免客户端加载时遍历所有顶点我们也在GPU中计算,计算方式如下:
编辑器CPU压缩:
bytebX = (kNormal.x - (-1.0f)) / 2.0f *255 ;
bytebY = (kNormal.y - (-1.0f)) / 2.0f *255 ;
bytebZ = (kNormal.z - (-1.0f)) / 2.0f *255 ;
客户端GPU解压:
Normal= Normal / 255 * 2.0f - 1.0f;
- 最后因为GameBryo的常规格式不能指定DWORD作为顶点色(必须是float,GB最终会遍历每个顶点转换成DWORD),所以使用了多流的方式,将顶点坐标放在一个流,顶点色和法线放在另一个流中传入GPU。
草
草的做法是每个地形的网格上对应两个交叉的四边形面片,使用多流,GPU计算顶点,减小容量;
水
水的相关数据比较大的部分是水的高度图,在客户端用于计算水花的高度。轩辕的水是对应每个地表格子,但是因为水主要为了减小数据大小。不导出水的高度图,只导出每片水的高度;
LightMap缩小
最终我们采用了以下几种优化方案:
- 地形方面,主要是减小地形数据大小。不导出地形顶点索引,x,y坐标,UV坐标,通过Shader计算;使用 dword导出法线和颜色,使用多流的方式渲染地形;
- 草,主要为了减小数据大小。编辑器不导出模型数据,客户端计算顶点,顶点索引,UV等;
- 水,不导出高度图,减小容量;
- 减小地形和特殊地表的LightMap,ShadowMap的大小;
测试结果:
因为在我们优化后的地图又新增增了25张,取新老都有的85张地图作比较:

优化后减少了130M,约30.6%,如果考虑新增的25张地图预计实际减少170M。再次优化后比第一次优化又缩小了10%,估计压缩后会再节省20M左右(因为全部烘一遍时间太长陈悦使用全黑贴图,压缩后的数据属于估计。),总计减少150M,约35.3%。
渲染方面
首先我们为了使游戏视角拉平的情况下表现较好的效果,做出了如下调整:
- 扩大地形的显示范围从5x5个Chunk改为最高配情况下地形地表11x11,物件7x7;
- 同时加入了物件的半透明渐变出现和消失;
- 地形地表和模型分开加载,优先保证地形地表加载,同时优先保证视野范围内的Chunk优先加载;
- 为了Shader扩展调试方便,我们使用ubershader自动生成shader,去掉冗余运算。
通过使用GPA,Pix等分析软件再次分析Camera拉平显示范围扩大后的场景,最后发现:
- 我们的Shader还是比较复杂,特别是高配置Shader;
- 游戏Shader还有优化余地,例如雾效,例如物件,虽然有的物件不受点光影响但是还是使用带点光贴图的Shader等;
- 从前面LightMap合并前的PIX分析结果可以看出,物件虽然已经分类渲染(例如地形一批,场景物件一批,人物一批等),但是由于不断增加的需求,同一类物件渲染的Shader也有很多变化,例如同一批渲染的场景物件,也区分是否有Glow是否实时点光等等,这样就导致会反复设置渲染状态。;
- 渲染状态切换次数太多,通过GPA和PIX分析,发现目前客户端渲染状态切换次数太多。平均渲染一个物件可能要设置7.6次渲染状态,其中大部分地形和物件有一张保存点光和点光阴影的贴图,一张保存阴影(R通道)和AO(G通道)的贴图,渲染时要设置两次,并且每张贴图都十分小,大部分只有16x16个像素。如下图:
- 文字和UI的效率太低,目前每帧创建NiScreenElement每帧需要多次Lock;
- 存在一些冗余的渲染;
- 阴影比较耗时;
最终我们采用了以下几种优化方案:
- 雾效shader近一步优化,,优化计算深度shader,水的shader,同时利用切换shader的方式避免一个shader计算量太大(例如有半透明和没有半透使用不同shader,如果没有半透明时不需要计算);
- 针对不同的图形渲染级别准备不同的渲染Shader,特别针对超低配置的显卡实现了简单Shader,去掉了实时光照、实时阴影、AO、高光、雾等,简化了地表纹理混合;
- 材质Lod,主要是提升渲染效率。对较远范围的模型,地形和水等使用低级别的材质;
- 修改了一些反复切换shaderbug,加入了shader排序;
- 修改文字的创建,不在每帧创建NiScreenElement,利用缓冲避免频繁lock,渲染时合并到一个批次渲染,消除Lock VB IB的等待;
- UI的使用画布,只在脏了后更新;
- 去掉GPA分析出的一些冗余渲染,例如弑神,马等UI即使不可见也创建RT并渲染等;
- 阴影优化,减小阴影图大小,修改阴影差值方式,去掉了阴影第四级别这个基本看不出效果又比较费的功能;
- 将LightMap和ShadowMap合并,主要为了提高效率,减少设置贴图切换次数,减少显存碎片,数据大小会增大1%左右;
- 关闭地形实时阴影,主要为了提高效率,开启特殊地表实时阴影,为了美术效果。
- LightMap和ShadowMap分辨率调整,主要为了提升显示效果同时缩小客户端容量;
- 主次物件区分,主要是提升客户端表现和提高客户端效率。对物件划分主次,主要物件和特殊地表地形显示加载范围一样,次要物件显示较小范围并有半透明渐变;
- 我们加入了Shader排序功能。通过GPA分析发现加入Shader排序可以减少大概200次渲染状态切换
测试结果:
性能方面,在高端电脑(CPU:i72600;显卡:5800;内存8G),中端电脑(主要测试组测试,在各种中端电脑上测试,反馈是优化前优化后帧数内存基本没变化),低端电脑(CPU:P42.0;显卡:5200;内存1G)帧数都没有明显变化,其中高端和低端的一些数据如下,红色表示更糟糕,有些变化是因为地图本身美术修改过了:
高端电脑(帧数分渲染帧和逻辑帧,括号内是逻辑帧!):

低端电脑,因为是超低配置所以LightMap合并,多流,材质LOD都不需要,主要物件范围和一般物件也是一样的,仅次要物件显示范围变小了,优化主要表现在地形数据的减少上:

需要说明的是轩辕城优化前后地图本身变化太大,所得数据不能代表实际结果。低配和高配测试在不同电脑上,测试位置略有不同。
使用GPA分析殇州城:
- 优化前,796个DP,5976次状态切换,12次VBLock,平局每个dp切换7.5次渲染状态。

- 优化后,772个DP,5883次状态切换,12次VBLock,平局每个dp切换7.6次渲染状态。

可见优化后渲染的物件少了,主要是主次物件的原因,但是状态切换并没有减少多少。通过Pix分析,合并贴图后有大量32x32的小贴图再加上渲染顺序的原因减少的设置贴图状态次数减少不多,Shadere中对Lightmap等贴图设置MipFilter为Line但实际上没有GB会自动设置成None,因为使用多流会多设置一次流数据,增大了状态切换次数。同时因为加入材质LOD,近景半透明渐变等,使Shader种类多了但是由没有排序,也增大了状态切@换次数。
再次优化后(合并Ligmap和ShadowMap到一起,修改Shader贴图采样方式,加入Shader排序,重新划分主次物件,限制最小LightMao为32x32最大为256x256地图减小了近10M),822个DP(DP数增多是因为策划新加入一些表现ID物件,同时加入了特殊地表的实时阴影,并且此屏没有近景物件),5631次状态切换,12次VBLock,平局每个dp切换6.8次渲染状态减小了约9%,总计节省状态切换约558次。

使用Pix分析第二次优化版本:

通过顶点格式,可以看出优化后地形使用了多流,近景地形的渲染状态切换较多;草,玩家的渲染状态相对较少;场景物件在合并LightMap和ShadowMap后减少了切换次数。下图是比较。
优化前场景物件:

优化后场景物件:

可见优化后最好情况下物件渲染少了两次设置texture的渲染状态,但是因为部分物件没有点光贴图,并且合并贴图的顺序不一定完全符合渲染顺序,并没有减少模型数*2那么多次,一屏三百多个物件总计节省了418次左右渲染状态切换,地形多流增加几十次状态切换,shader排序减少200多次状态排序,所以最终减少558次状态切换。
逻辑方面:
通过使用内部的profile分析工具和Intel的ParallelAmplifier,分析,发现在多人情况下玩家的更新渲染占很大比重,其中动作更新占很大比重。
- 更新频率优化,降低更新频率,限制帧数为40帧;
- 对于玩家的更新加入范围判断,对较远的(超出屏幕范围)的玩家不进行更新和显示;
- 在大量玩家的情况下加入多线程骨骼更新;
- 拾取优化,加入包围盒剔除,并对Camera碰撞的拾取做了优化;
- 排序优化,去掉半透明排序节约时间,手工调整渲染顺序利用,利用反画家算法进行earlyz的优化;
结论:
- 此次客户端优化大幅减少了编辑器导出数据大小(大约30%);
- 客户端可视效果比之前更好可以看的更远。
- 在更好效果的基础上,帧数和内存都与优化前基本保持。
- 大幅减少了状态切换次数和显存碎片,消除了一些渲染错误的问题
- 优化资源应该尽可能的考虑周全,并做大量测试减少反复性,特别在游戏后期,每一次涉及地图格式的改动都会引起较大的美术工作量和测试工作量
此次客户端优化大幅减少了编辑器导出数据大小(大约30%),减少了渲染状态切换次数(9%),并且可视效果比之前更好可以看见更远的较大的模型,在此基础上客户端帧数和内存都基本保持,高配下帧数还略有提高内存略有减小。总体来说达到了预先期望的在不影响性能的前提下减小安装包容量的需求。