【读书笔记】万字浅析游戏场景中常见的渲染性能优化手段

一、前言

看标题大伙想必应该知道了这是个读书笔记,所以大佬看个乐就完了,主要还是新人用来做记录的,主要参考的是霜狼大佬的书《游戏场景开发与设计》,当然了,笔记肯定不是单纯照着抄一遍,那样没有任何意义,会加入我自己的理解和其它文章的所见所闻,这部分不敢保证全都准确,所以想获得无信息传播损失的一手资料的建议还是去看原书籍和参考部分原文。同时如果有错误敬请指出,另外性能优化是一个很高深的问题,牵扯到很多硬件架构上面的东西,且没有任何一个方法是万金油的,屡试不爽的,任何技术手段都有适用性,何况游戏项目千百万,找到合适的那一个才是正道,连物理定律都有适用条件,优化手段亦是如此,总之,实际项目中,抱着不断测试和实验的态度总不会错,没什么是绝对的和想当然的,所以这篇文章在我经验一点点积累后有可能也会继续更新。

另外,如果真的要抱着学习的心态来看这篇文章,你需要掌握一点点计算机组成原理相关的知识,可以移步到我的计组专栏。计算机组成原理笔记__Yhisken的博客-CSDN博客


二、硬件相关消耗

所有的性能优化手段目的最终都是减少以下的消耗,所以无论是项目中特有的优化手段,还是新研究出来的通用优化手段和新的优化架构,掌握了目的,就都可以看得懂了。

1.CPU方面

CPU消耗主要来源于两个方面:常规逻辑计算消耗渲染逻辑计算消耗。

而常规通常包括:游戏逻辑,动画物理,加载卸载,UI等模块。

大多时候占大头的通常为:渲染模块,UI模块,加载卸载模块。

2.GPU方面

GPU消耗主要源于**:Shader的计算量** 和Overdraw两个方面。

Shader计算量过大:分辨率过高,Mesh顶点过多,渲染算法太复杂。一般来说Fragment Shader的计算量往往大于Vertex Shader,所以优化算法主要也在Fragment Shader。

Overdraw:Overdraw主要集中在植被(如AlphaTest的草,叶子),UI和特效(如叠多层Blend)

3.内存

内存想必大家都知道,就是存数据的地方, 在游戏运行时,和其它计算机软件运行时一样,内存会把需要的数据从硬盘读入内存,那么游戏读的自然就是游戏资源:Texture,Mesh,AnimationClip,AudioClip,Material,Shader等等等等。其中Texture,Mesh,Anim和Audio通常占大头。所以尽可能控制他们的大小。

4.带宽

带宽对移动端比较重要,这主要关乎到手机的发热耗电问题。

我们主要针对的是CPU和GPU的内存传输带宽,简言之,降低CPU,GPU与内存数据的交互,因为这条通道本就"很挤"。


三、性能优化的相关指标和方法

1.DrawCall优化

(1)DrawCall相关概念

DrawCall 相关的另一个概念是Set Pass Call ,同时还有Batch

DrawCall只是一个命令,是CPU发给GPU的绘制命令,这个命令本身并不会有多少消耗,主要的消耗来源是发送DrawCall前的一系列准备把数据加载到显存,设置渲染状态及数据等:如写OpenGL时的,准备数据,绑定VAO VBO EBO,绑定Shader等等。

因此,Unity做了概念优化,在Unity的分析器中,并没有直接的DrawCall数量显示,而是显示为Batch批次。由前面所讲,一个Batch至少包含一个Drawcall,而开启合批处理后可能包含多个。在不做任何优化的情况下,一般渲染一个物体就会产生一个Batch

Set Pass Call的解释比较混乱,在有些文章中说"有多少个Pass就有多少个Set Pass Call",但是经本人实测下来其实并非这样。Set Pass Call顾名思义其实就是指渲染状态切换,如果一个batch和另一个batch使用的不是同种材质或者同一个材质的不同pass,那么就要触发一次set pass call来重新设定渲染状态。

具体体现就是,你现在有两个材质球,虽然他们来自同一个Shader,且这个Shader有2个Pass,场景里有2批物体(Mesh可以不同),一批装材质球A,另一批装材质球B,那么此时SetPassCall的数量是2。简单的说,SetPassCall的数量取决于两个Batch之间,到底有没有渲染状态的切换,而非Pass的数量。
3个SetPassCall是因为管线的CoreBlit

上图的Unlit Shader无论是写2个Pass还是1个Pass,SetPassCall的值都显示为3。因为在正方体渲染时,Batch之间的渲染状态并没有被切换,直到渲染Plane时,由于切换了材质球,导致增加了1个SetPassCall。

(2)DrawCall的优化方法


--- 动态合批(Dynamic Batching

Unity开启动态合批

动态合批是Unity特有 的优化方案,在使用相同材质球 的情况下,Unity会在运行时对于正在视野中 的符合条件的动态对象的顶点数据合并到一个临时缓冲区中,并在一个Draw call 内绘制,所以会降低DrawCall的数量。

但实际在一些手机硬件上,合并顶点数据这个操作可能会增加额外的计算量,特别是对于一些针对绘制命令进行性能优化的图形API。所以实际优化效果需要在目标平台进行测试后才能确定。此外,如果动态合批的物体在不断进行位置旋转,则每一帧都在动态合批,性能消耗将大幅增加,这种更适用于GPU Instancing。

动态合批的限制

  • 无法应用于包含超过 900 个顶点属性和超过 225 个顶点的网格(例如,如果着色器使用顶点位置、顶点法线和单个 UV,则 Unity 最多可以批处理 225 个顶点。但是,如果着色器使用顶点位置、顶点法线、UV0、UV1 和顶点切线,则 Unity 只能批处理 180 个顶点。)
  • 材质球,纹理必须相同, 且代码不能动态修改材质变量的值。
  • 如果使用lightmap,则合批的物体的lightmap UV,Scale,Offset等额外参数要相同。
  • 对象的缩放旋转平移变换不能影响顶点位置,缩放不能为负。
  • 如果合批对象的Shader有多个Pass,那么动态合批只会合批第一个Pass。主要被打断的原因还是切换渲染状态。
  • 对于不同材质的Shadow Caster Pass,虽然材质不同,但只要使用参数相同,也可以进行动态合批。
  • 位置不相邻且中间夹杂着不同材质的其他物体,不会进行同批处理(涉及批处理顺序)
  • 动态批处理的优先级最低,在SRP Batcher,Static Batching,GPU Instancing都不生效时,动态批处理才会启用
  • 由于动态合批是在进行场景绘制之前将所有的共享同一材质的模型的顶点信息变换到世界空间中,然后通过一次Draw call绘制多个模型,达到合批的目的。模型顶点变换的操作是由CPU完成的,所以这会带来一些CPU的性能消耗。并且计算的模型顶点数量不宜太多,否则CPU串行计算耗费的时间太长会造成场景渲染卡顿,所以Dynamic batching只能处理一些小模型。内存占用和包体大小优于静态合批,但是会增加CPU的消耗。在实际项目中,应该考虑CPU合批的消耗是否小于drawcall的消耗来判断是否使用动态批处理。

综上所述,要满足动态批处理的条件十分苛刻,且并不一定会100%的增加性能,实际项目中应该具体情况具体分析。


--- 静态合批(Static Batching)

Unity开启静态合批

静态合批会将所有标记为Static的物体合并。Unity会把顶点变换至世界空间,放到合并后网格的Vertex buffer和Index buffer,并记录每个子模型的Index Buffer数据的起始位置和结束位置,运行时提交整个大模型的Buffer,对于可见的子网格,只设置一次渲染状态,然后分多次drawcall分别渲染每个子模型 。**也就是说,Static Batching并不减少drawcall。**但由于我们前面说过了,Unity把drawcall的概念优化成了Batch,所以在Unity的显示是可以看到Batch数量减少了的。

构建时
运行时

Static Batching的优化在于运行时cpu不需要再次执行顶点变换操作,节约了少量的计算资源,并且因为这些子模型共享材质,所以在多次Draw call调用之间并没有渲染状态的切换,渲染API(Command Buffer)会缓存绘制命令,起到了渲染优化的目的 。

同时,静态合批也存在一定限制和缺点:

  • 首先,由于物体的顶点数据已经转换到了世界空间并且存储于场景中的离线数据中,因此基于模型空间的顶点坐标计算可能会发生一些错误。
  • 且在Unity执行Build的时候,场景中所有引用相同模型的GameObject都必须将模型顶点信息复制,并经过计算变化到最终在世界空间中,存储在最终生成的buffer中。这就导致了打包的体积及运行时内存的占用增大。因此对于植被这种频繁复制的对象通常使用GPU Instancing。
  • 静态批处理可以包含的顶点数同样存在限制。每个静态批处理最多可以包含 64000 个顶点(OpenGL ES为48000,Metal为32000)。如果有更多,Unity 会创建另一个批次。
  • 要一起批处理的网格使用相同的顶点属性。(例如,Unity 可以对使用顶点位置、顶点法线和一个 UV 的网格进行批处理,但不能对使用顶点位置、顶点法线、UV0、UV1 和顶点切线的网格进行批处理。)
  • 如果开启了网格优化,则会在构建生效顶点的缓冲区中删除所有未被变体加载的顶点数据
  • 如果使用lightmap,则合批的物体的lightmap UV,Scale,Offset等额外参数要相同。
  • 位置不相邻且中间夹杂着不同材质的其他物体,不会进行同批处理(涉及批处理顺序)

--- GPU Instancing

GPU Instancing是指,CPU指提交一个网格和材质,但是提交多个实例的差异化信息,对同一个网格在GPU中进行多次变换绘制。
满足条件,勾选GPU Instancing

Unity会在运行时对于正在视野中 的符合要求的所有对象使用Constant Buffer 将其位置、缩放、uv偏移、lightmapindex 等相关信息保存在显存中的Constant Buffer 中,然后从中抽取一个对象作为实例送入渲染流程,当在执行DrawCall操作后,从显存中取出实例的部分共享信息与从GPU常量缓冲器中取出对应对象的相关实例信息一并传递到下一渲染阶段,与此同时,不同的着色器阶段可以从缓存区中直接获取到需要的常量,不用设置两次常量。

由此可知,GPU Instancing可以有效避免静态合批产生的内存增加问题。并且由于将所有实例数据一次性全部提交给了GPU,因此可以减少drawcall,大幅降低了CPU和GPU的通信消耗。

在UE中,GPU Instancing为静态,需要一开始就确定好实例具体的数量和位置,渲染时无法对其中的个体进行剔除,移动端上无法设置逐实例的材质参数。

Unity为动态GPU Instancing,可以耗费剔除和组装缓冲区的性能去换取更少的实例渲染,在实际项目中需要权衡剔除所带来的消耗和冗余实例渲染的开销大小进行选择。

GPU Instancing的相关限制:

  • 必须相同材质相同网格。且shader编写要满足一定格式条件
  • Static Batching和SRP Batcher都会打断GPU Instancing,因为它们有更高的优先级。如果项目中开启了SRP Batcher同时又想要使用GPU Instancing,则可以使用Graphics.RenderMeshInstanced。此 API 绕过了 GameObjects 的使用,使用指定的参数直接在屏幕上绘制网格。(总的来说,静态合批和SRP Batcher > GPU Instancing > 动态合批)
  • 受限于Constant Buffer在不同设备上的大小的上限,移动端支持的个数可能较低。
  • 缩放为负值的情况下,会不参与加速。
  • 顶点数量较少的网格无法使用 GPU 实例化有效处理,因为 GPU 无法以充分利用 GPU 资源的方式分配工作。这种处理效率低下可能会对性能产生不利影响。效率低下开始的阈值取决于 GPU,但通常来说,尽量不要对顶点数少于 256 的网格使用 GPU 实例化。
  • 网格必须来自MeshRenderer组件(SkinnedMeshRenderer不可以)或Graphics.RenderMesh,Graphics.RenderMeshInstanced,Graphics.RenderMeshIndirect调用,并会根据行为分组,每个调用行为都会发送单独的drawcall,彼此之间不会合并。
  • GPU Instancing的物体如果像正确采样lightmap需要在脚本中手动将lightmap uv 的scale, offset传递给Shader。
  • 代码动态改变材质变量后不算同一个材质会打断GPU Instancing,但可以通过使用材质属性块为每个实例单独分配变量如颜色,粗糙度等,来实现一定程度的变化产生不同外观。

--- SRP Batcher

SRP Batcher的原理和GPU Instancing类似,但不同之处在于,SRP Batcher可以是不同网格,并且只要Shader变体相同(Keyword组合相同),Shader编写符合要求,就可以启用SRP Batcher进行合批。

SRP Batcher不减少DrawCall,只减少Set Pass Call,也就是降低GPU切换设置渲染状态的消耗。

上图中显示了SRP Batch的原理,简单来说,就是把数据一次性提交到GPU,并且数据在GPU驻留的时间更长,以此避免两个DrawCall之间过多的设置消耗。

上图中右上角Per Object large buffer部分,里面8个小方块就代表着8个小的buffer(PerDraw),意味着一次的batch里面有8次的draw call。每个draw call都需要一个小的buffer,会把我们引擎内部一些内置的数据(比如unity_ObjectToWorld)填充进去。然后我们为每个Object准备这些小buffer,然后组成一个大Buffer,最后把这个大Buffer一次性的传到GPU上。

并且由于所有的材质在GPU中都有持久的CBUFFER(PerMaterial)以供随时使用,可以避免数据重新创建,即拿即用。渲染时,直接从显存抽取相关数据,发送DrawCall即可。

SRP Batcher相关限制:

  • 不支持粒子,不直接支持蒙皮网格,需要相同Shader,相同Keywords组合。
  • 一个批次渲染内,如果中途有不同Shader/Keyword组合物体插入,则会分为两个批次渲染。这部分可以去参考【Unity】SRP底层渲染流程及原理 - 知乎
  • 多Pass Shader的不同Pass的CBUFFER要相同。
  • 材质不能在运行中被代码修改MaterialPropertyBlock。

关于第2点,我这里简单做个演示。

上图所示的Plane,Cube,都是使用了同一个Shader的不同材质球,在Frame Debugger里可以看到,它们在一个SRP Batch内渲染。这是没问题的。

但如果我这中间加入一个不同Shader的物体,此时这个场景变成了3个SRP Batch渲染,第一个SRP Batch只渲染了离我们最近的绿色Cube,第二个SRP Batch只渲染了灰色的Cube,第三个SRP Batch把剩下的Cube包括Plane在内全部渲染了出来。一切的根源就是这个插入到中间的不同Shader的Cube导致的。


2.带宽的优化

(1)纹理优化

常见的概念这里不解释,参考[计算机图形学]重心坐标&应用纹理(前瞻预习/复习回顾)_重心坐标插值-CSDN博客https://blog.csdn.net/ZDEWBYE/article/details/130001955https://blog.csdn.net/ZDEWBYE/article/details/130001955https://blog.csdn.net/ZDEWBYE/article/details/130001955https://blog.csdn.net/ZDEWBYE/article/details/130001955

--- 纹理压缩:

当把纹理拖入Unity引擎时,Unity会自动压缩纹理为压缩格式,也可以手动选择压缩格式和非压缩格式。压缩格式的纹理占用内存和带宽压力都会减小,并且纹理压缩格式基于块压缩,能够更快读取像素所属字节块进行解压缩以支持随机访问。

在霜狼佬的书中,推荐统一使用ASTC格式,优点在于高质量压缩(可在较低的比特率下保持较好的图像质量),可以自定义压缩块大小,支持多种纹理类型,兼容性好和节省资源等方面。缺点是非无损压缩,压缩时间长等。

其它压缩格式详情可参考:[自学记录02|百人计划]纹理压缩_支持随机读取的纹理格式-CSDN博客https://blog.csdn.net/ZDEWBYE/article/details/130852888?sharetype=blogdetail&sharerId=130852888&sharerefer=PC&sharesource=ZDEWBYE&spm=1011.2480.3001.8118

--- 纹理分辨率和纹理合并

纹理分辨率自然不用多说,过大分辨率的纹理会同时造成内存和带宽的压力增加。并且当有许多小尺寸纹理时,建议把它们合并成一张大纹理,这点尤其在UI上常用,使用一张图集代替原本多个小的贴图。这样可以降低读取多个贴图的带宽压力和计算压力。同时如果有多张单通道贴图,也可以合并成一张多通道贴图,也可以减轻带宽压力。

--- Mipmap

Mipmap总的来说就是用较少的多出的空间(4/3)去换取更好的性能和渲染质量。一般3d角色和场景都会开启Mipmap,但UI不需要,因为UI始终显示在最上层,开启反而会徒增内存占用。

--- 纹理过滤方式

纹理过滤方式一般情况下都会选择双线性过滤,选择三线性或各向异性虽然能带来更好的质量,但是会增加采样点,以此可能会导致Cache Miss的概率变大,导致GPU去从内存读取,增加带宽压力。所以一般建议采用双线性过滤。

(2)后处理

后处理包括MSAA,Bloom,DOF等,需要对FrameBuffer进行读写。一方面是因为它要在一帧内触发多次FrameBuffer的切换,另一方面是它要频繁读写FrameBuffer。

其次如果申请并采样RT,那么采样次数不宜过多,申请的RT分辨率也不宜过大。这点在我的另一篇关于模糊的优化中有体现:详情参考[自学记录10*]探究降采样顺序对模糊性能优化的影响_模糊采样-CSDN博客https://blog.csdn.net/ZDEWBYE/article/details/141032268?spm=1001.2014.3001.5502https://blog.csdn.net/ZDEWBYE/article/details/141032268?spm=1001.2014.3001.5502https://blog.csdn.net/ZDEWBYE/article/details/141032268?spm=1001.2014.3001.5502https://blog.csdn.net/ZDEWBYE/article/details/141032268?spm=1001.2014.3001.5502

另外就是要减少主存和Tile Memory的数据传输,像切换Render Target这种操作,要非常非常谨慎的使用。 每次切换Render Target都需要等待前面的指令全部执行完毕,把数据写入主存。切换到新的Render Target后,还需要把数据从主存Load到Tile Memory中。频繁的与主存交互不仅很慢,而且消耗大量带宽。所以类似后处理这样必须使用Render Target的,应当把多个Pass尽可能合并成一个Pass。

3.OverDraw的优化

(1)OverDraw的产生

--- Late Z

Late Z自然是相对于Early Z说的,但其实也就是传统的深度测试,我们知道深度测试的目的是确定渲染不会出现视觉上的错误,保证遮挡关系的正确性,但它并不会因为把被遮挡的地方不进行渲染就提升了性能,因为传统的Z Test是在Fragment Shader后,此时,已经完成了片元着色的计算,而此时如果该片元因为遮挡被剔除,那就会出现Fragment Shader算了半天白算了的情况,白白浪费计算性能,因此也就产生了Overdraw。

因此,现在各个GPU架构上都带有自己的Early Depth Test技术,但该技术也不是时刻生效。

--- Alpha Test和Alpha Blend

Alpha Test和Alpha Blend一般以植被,特效,UI为主,Alpha Test和Alpha Blend会导致前面所说的Early DT失效,硬件自带的天然优化手段也就没有了,因此也就产生了Late Z的Overdraw。虽然我们可以开启Pre-Z,去强行开启Early DT,但是实际上这可能会导致Overdraw翻倍,有时可能是负优化,具体情况还是要具体分析。

并且由于开启Alpha Blend通常为半透明物体, 为了确保透明物体绘制正确,一个比较常用的方法是透明物体(根据到相机的距离)从后往前提交,并且应该关闭深度测试,这存在非常严重的Overdraw,因为要从后往前混合多层。(大面积的少量重叠比小面积的大量重叠消耗更大)

所以在制作裁剪模型时尽量在建模软件里裁剪好,而不是使用alpha test。针对半透应该减少模型在屏幕中重叠的概率。制作植被时制作多层级的LOD,并且将多层模型合并可以有效缓解性能消耗。

--- Quad Overdraw

在光栅化流程中,GPU会计算某个像素在哪个三角面范围,从而把几何王个体的图元从三角面转换到一片像素范围中,GPU通常将三角面分解为2x2的像素进行处理(当然也有更大的比如8x8的,但是最小处理单位是2x2),而这一的一个2x2的像素块我们称之为一个Quad,而当一个Quad被多个三角形覆盖,GPU需要为每个三角形重新计算一次Quad,得到像素着色结果,这会导致不必要的计算开销和损失。
图来源:知乎Coresi7

这里借用一下知乎一位大佬的图,如上图所示,最左下角的Quad被四个三角形占据,那么对于每个三角形和Quad的每个像素,总共要计算4x4=16个像素。 蓝色计算4个丢掉4个,绿色计算4个丢掉2个,橙色计算4个丢掉3个,灰色计算4个丢掉1个,产生4次Quad Overdraw。

在制作模型时应该提前判断,如一座山在屏幕上仅占100x100个像素,那就没必要包含200W个三角面。以面产生过多Quad Overdraw。

(2)优化手段

--- Early Z

在解释为什么传统的Z Test会产生Overdraw时已经说过了,其实就是把Z Test提前到片元着色器之前,以此来提前剔除不需要进行着色的片元,当然它会被Alpha Test和Alpha Blend打断。虽然可以通过开启Pre-Z强行开启Early-Z,但前面说了这有可能是负优化,具体在于比较多出的PreZ pass的消耗和Overdraw消耗。具体参考

[自学记录05|百人计划]Early-Z和Z-Prepass_early-z 是在哪个极端进行的-CSDN博客https://blog.csdn.net/ZDEWBYE/article/details/132057179https://blog.csdn.net/ZDEWBYE/article/details/132057179https://blog.csdn.net/ZDEWBYE/article/details/132057179https://blog.csdn.net/ZDEWBYE/article/details/132057179


--- HSR(IOS平台)

HSR,全称Hidden Surface Removal,也就是隐藏面消除。是PowerVR TBDR架构下的一种优化Overdraw的解决方案。(因为对于复杂场景来说,不透明物体不可能严格的由近及远渲染,所以EarlyZ不会完全解决Overdraw,常见的例子就是一个大地面和远处的房子,很难说谁远谁近。tips:我知道有些人可能会在这有点小误区,你要知道ZTest是逐像素,但是渲染是逐物体啊喂)。

HSR在硬件层面解决了Overdraw问题,并且可以做到像素级别的剔除。

当一个片元通过了Early-Z准备执行Fragment Shader进行绘制前,先不计算,只记录标记这个fragment归哪个三角形来画。并将这个信息存在On Chip Tile Buffer中。

之后用存储的三角形数据计算出它在这个tile上每个像素的深度值,也放在On Chip Tile Buffer中。

等到这个Tile上所有的三角形都处理完了,对于每个片元,将后续的三角形深度值与前面的三角形深度值进行比较,最后再真正的开始对每个三角形中被标记上能着色的片元进行着色计算。这样每个片元上实际只执行了最后通过Early-Z的那个Fragment Shader,而且由于TBR的机制,Tile块中所有图元的相关信息都在片上,可以极小代价去获得。
HSR in PowerVR Serires6 Core Pixel Processing Pipeline
TBDR

关于TBDR请移步[自学记录03|百人计划]移动端GPU的TB(D)R架构基础_tbr gpu-CSDN博客https://blog.csdn.net/ZDEWBYE/article/details/131021759https://blog.csdn.net/ZDEWBYE/article/details/131021759https://blog.csdn.net/ZDEWBYE/article/details/131021759https://blog.csdn.net/ZDEWBYE/article/details/131021759

同样的,开启Alpha Test和Alpha Blend会终端HSR,所以应尽量避免。

在HSR处理不透明物体的过程中突然来了一个AlphaTest的图元,那么为了保证渲染结果正确,HSR就必须要终止当前的Defer,先把已标记好的像素都绘制出来,再进行后面的绘制。 AlphaBlend虽然也要中断HSR的Defer,强制开始绘制,但是比AlphaTest好那么一点点的是它不影响后续图元并行地继续开始进行HSR处理。
Image Signal Processor

至于哪个fragment属于哪个三角形,主要依赖于PowerVR的一个硬件ISP,全称Image Signal Processor,如上图所示。一共两个模块:HSR和TagBuffer。

HSR包含了Raster和深度测试等模块,该模块会从Parameter Buffer中读取顶点数据和图元列表,针对每个可见图元来计算各个图元在每个像素的深度并进行深度测试,判断是否需要剔除等等。说白了就是用来做测试的。

Tag Buffer用于跟踪和记录每个像素和对应的图元的映射关系,这是PowerVR的TBDR能够做到绘制顺序无关并且减少OverDraw的关键 。

需要注意的是Parameter Buffer和Tag Buffer都是有容量上限的,当场景复杂度过高时,Parameter Buffer会塞满,一个Tile内图元过多也会导致Tag Buffer塞满,导致硬件Flush,两次HSR结果分开则导致其OverDraw增加(上一次HSR记录好的遮挡关系,因为分成两次导致上次HSR遮挡关系丢失了)。


--- LRZ(Android平台)

LRZ,全称Low Resolution Z,顾名思义就是低分辨率的深度图。是基于Adreno A5X及以上的GPU下的TBR架构中优化Overdraw的解决方案。

Adreno GPU架构下比渲染流程中比传统架构多了一个Binning Pass模块。简单来说,就是在正常的渲染流程之前,Binning Pass会运行一个简化版的Vertex Shader,只进行顶点位置的计算,生成一个低分辨率的Z-Buffer,用于标记哪些三角形需要跳过后续正常渲染流程的计算。Binning Pass下的顶点数通常不是性能消耗的主要问题,主要问题产生在:如果Vertex Shader中存在与位置相关的计算信息,并且有较为复杂的依赖关系(如采样贴图和对顶点做位移)时,会对性能产生较大的消耗,因为相当于在两个阶段都进行了计算。

需要注意的是,LRZ并不是一个逐像素剔除的过程,而是根据LRZ分辨率和屏幕对应几个像素的精度来进行剔除的。和Early-Z不同,LRZ和物体渲染顺序无关,且速度高于Early-Z,目前Early-Z和LRZ可能存在一定冲突不能共存,可能会导致一些额外bug。并且使用Alpha Test不会更新LRZ的缓存。

Binning Pass支持两种模式,Direct Mode和Binning Mode,负责Binning Pass的关闭和开启。

在Fragment Shader中执行写入深度信息,Vulkan中使用辅助命令缓冲区时,需要使用IMR的任何条件下,使用模板测试等情况下,LRZ会被禁用。


--- FPK(Android平台)

FPK,全称Forward Pixel Kill,是Arm Mali GPU架构中一种消除Overdraw的方法。

正常情况下一个片元通过 Early-Z 之后会立马生成一个Fragment Shader线程去执行,并且这个过程是不可逆的。但是开启了FPK的GPU,像素着色的线程即便启动也不会不可逆转地完成。 如果渲染管线发现后面的线程将把不透明的数据写入相同的像素位置,那么正在进行的计算可以在任何时候终止。

FPK是在Early-Z之后在Fragment Shader之前的一个步骤,在完成Early-Z后,不会直接将一个Quad(ARM将一个2X2的像素区域称为一个Quad)去生成像素着色器的线程去计算,而是进入一个FIFO队列中。

FIFO是一个先进先出的队列,新进入的深度相同的Quad会被和已在FIFO中的Quad进行深度对比,深度更远的会被踢出队列 。当然,如果FIFO满了的话,会和HSR的Parameter Buffer一样产生Flush,需要让最前面的Quad出去被渲染才能够处理新的DrawCall,所以重叠的三角形不能过多。

FPK某种程度上是给Early-Z打了一个补丁(因为不依赖顺序),而且依赖Early-Z的功能,如果Early-Z关闭了的话,FPK也会关闭。并且Mail官方不建议依赖FPK,一是Early-Z总是更节能、结果上更一致,二是FPK是从Mali-T62X和T678以后的芯片才内置的功能。官方还是会建议对于物体进行排序来充分的利用Early-Z的效果,并且FPK对于透明物体也无法生效。


--- 关于Alpha Test和Alpha Blend

关于Alpha Test和Alpha Blend单独的性能谁更好的问题有很多长篇大论,但是个人还是认为具体情况要具体分析,有些情况利好Alpha Test那么就用Alpha Test,有些情况利好Alpha Blend就用Alpha Blend。

Alpha Test:只有当自己执行完像素着色器,才知道自己会不会被丢弃,会不会写入深度,之后,相同位置的后续片元才能继续执行,否则就必须阻塞等待其返回结果,这会阻塞管线。

Alpha Blend:多层半透明叠加时,由于不写深度,完全无法做剔除,会导致overdraw很高,在移动平台上很容易出现性能问题。

4.内存优化

内存优化主要集中在游戏资产的大小控制和压缩方面。

(1)纹理

纹理的内存优化其实在带宽部分已经说的差不多了,一个是纹理压缩,还有就是控制纹理的分辨率大小,以及多通道合并。这里不再赘述。

(2)动作

一般来说引擎自带动作压缩算法,虽然这些算法在一些情况下会产生抖动,但可以根据实际情况调整。一般来说大于200KB且时长较长的动画资源我们认为是占用内存较大的动画资源,这些资源需要我们进行优化。

(3)网格

网格的顶点属性应避免多余冗余,如果网格的顶点属性只用到了3个,如位置,法线,uv,那么就没必要带有顶点色之类的。

同时,应该控制网格的顶点数和面数,过多的顶点和三角面会造成过高的内存占用,同时也不利于裁剪,容易增加渲染面数。

(4)音频

音频压缩技术取决于对音质,文件大小,CPU占用和兼容性的需求。有损压缩格式(MP3,OGG Vorbis和AAC等)适用于需要高压缩比和较小文件大小的场景,而无损压缩格式(如FLAG)适用于对音质要求较高的场景。我们需要根据实际需求和性能预算权衡各种压缩技术的优缺点,选择合适的音频压缩格式。

(5)Shader

Shader对内存的占用主要体现在变体数量上,变体过多会造成Shader占用内存过大,包体过大。在开发过程中,应该避免出现不必要的变体。(这也是为什么实际项目中不允许使用连连看的原因)

5.CPU/GPU计算量的优化

(1)CPU计算量

  • 谨慎使用后处理,如果Graphics.Blit消耗很大,可能需要减少后处理或降低分辨率。
  • 大量的实例化很容易产生CPU压力,需要善用对象池减轻CPU压力。
  • Get/Find等调用不要在Update里面运行(事实上一切的"一锤子买卖",如初始化等都尽量不要在逐帧更新的函数里运行)。
  • String相关处理会在内存堆栈上分配很多String对象,可以在处理数据时直接使用二进制格式,反序列化需要处理的对象,减少文本转换造成的消耗。
  • C#编码习惯,注意内存的分配释放,及时检查,避免GC过高。

(2)GPU计算量

--- 光照烘焙

光照烘焙自然不用多说,就是将静态的光照预计算好,存储到一张lightmap上,运行时不需要计算,直接采样lightmap上即可。也可以预计算阴影存储到shadowmask上,运行时同样是直接采样。本质上还是空间换时间。烘焙常常指的是烘焙漫反射,因为高光与视线方向有关,而视线是动态的无法直接烘焙。

Unity的Lightmap烘焙有3种模式,分别是Subtractive,Baked Indirect,(Disitance) Shadowmask,这里有一位知乎大佬解释的已经非常明白了,我就不献丑了。Unity烘焙方案说明 - 知乎https://zhuanlan.zhihu.com/p/655224461https://zhuanlan.zhihu.com/p/655224461https://zhuanlan.zhihu.com/p/655224461https://zhuanlan.zhihu.com/p/655224461

· Light Probe

由于一个动态物体进入烘焙光照区域是不会接收到光照的,因为动态物体不会去采样LightMap,因此出现了LightProbe。Light Probe可以组成Light Probe Group用来预计算并存储场景中穿过空白区域的光线信息。在运行时,场景中移动的物体进入区域后所被打到的光线,通过离物体最近的几个Light Probe的数据来做插值得到该物体在该区域受到的间接光照。

由于是离线计算,所以Light Probe一定程度上也可以节省计算量。但由于布置需要耗费一定时间,当场景过于复杂时,布置巨量的Light Probe往往是很耗费时间的工作。同时,获取的光照精细度一定程度上取决于Light Probe的布置精细度,布置太少,光照质量会低,布置太多会导致内存占用提高。

· Reflection Probe

Reflectiob Probe同样规定了一个区域范围,只不过相比于Light Probe,Reflection Probe相当于只记录镜面反射信息,并将它存储在一张Cubemap中,跟天空盒类似。当物体进入区域内,则会采样Reflection Probe生成的CubeMap作为间接镜面反射的贴图。

弊端是虽然有实时的Reflection Probe,但是开销很大,但如果使用静态的烘焙Reflection Probe又只支持静态的反射。并且CubeMap的存储和采样会带来很大的内存和带宽占用。

--- 阴影

阴影这块,Unity默认的做法是CSM,当然,如果追求质量和效率的情况下也可以不使用或者在此基础上加以改造。如果追求质量,想要软阴影,PCF,PCSS,VSSM,SDF Shadow都可以选择使用。理论部分可以参考我的GAMES202专栏阴影部分。【GAMES202】Real-Time Shadows1---实时阴影1_游戏中的阴影是计时计算的吗-CSDN博客https://blog.csdn.net/ZDEWBYE/article/details/131754920https://blog.csdn.net/ZDEWBYE/article/details/131754920https://blog.csdn.net/ZDEWBYE/article/details/131754920https://blog.csdn.net/ZDEWBYE/article/details/131754920【GAMES202】Real-Time Shadows2---实时阴影2_vssm-CSDN博客https://blog.csdn.net/ZDEWBYE/article/details/132404360https://blog.csdn.net/ZDEWBYE/article/details/132404360https://blog.csdn.net/ZDEWBYE/article/details/132404360https://blog.csdn.net/ZDEWBYE/article/details/132404360

如果对阴影的质量要求没那么高也可以使用一张渐变半透贴图和一个小的Plane放在角色脚下,或者使用把角色模型沿光方向挤扁投影在地上的做法,这都是移动端在不追求高质量阴影的情况下常用的节省性能开销的做法。

--- 贴图通道合并

多个贴图通道合并不仅可以降低带宽压力,也可以降低GPU计算量,sample采样函数也是一种计算量消耗较大的函数。比如在PBR的金属工作流中,就可以将金属度,粗糙度,AO合并到一张RGB贴图中。

--- LOD技术

LOD,全称Level of Detail,核心思想就是距离摄像机越远处的物体,占画面比例少,细节不明显,因此可以简化这些物体的计算和表示。比如降低远处物体的网格面数,降低纹理分辨率,降低Shader计算复杂度,可以减轻GPU计算负担,并且由于近处物体使用较高层次的细节,不会产生影响。LOD也可以用于双端高低平台,同一平台高低画质,同一平台不同性能的设备等。Unity中使用LOD Group进行设置。

网格简化方面:不同LOD等级的模型面数不同,可以人工减面确保质量,也可以自动减面提高效率,具体则需要去权衡。

材质和Shader方面:

同Shader不同材质情况下,一般来说只依靠Mipmap自动切换贴图层级采样不需要进行额外操作。特殊情况下,网格简化到极致会导致UV结构变化,这时候需要重新绘制贴图,对应LOD使用对应贴图材质,但会导致多个材质的绘制调用,因此可以材质合并,如UE的HLOD和代理几何体。又由于贴图不同,这些方案都需要对贴图合并。

不同Shader情况下,则逐级递减Shader计算复杂度就可以了,如LOD0,1完整PBR,LOD2去除法线,LOD3去除高光。

· Billboard

Billboard和Impostor都是基于图像的LOD优化技术,是将远离观察者的物体用预渲染的图像面片代替的方法。常应用于植被远景的LOD优化方案,也可以用于火焰,烟雾,云等效果。
Billboard

Billboard就是将事先做好的图片放在一个面片上,然后让其始终朝向摄像机,达到一个以假乱真的效果。这样做可以有效提升渲染性能,节省内存和制作成本,而且由于常用在远景,不那么容易露馅,多数情况下可以达到一个比较好的渲染效果。

当然Billboard的缺点也很明显,首先画面质量 肯定无法和三维网格相比,其次严重依赖视角 ,如果不锁定y轴,向下看时面片容易平铺在地上,如果锁定y轴,向下看又会露馅。并且作为LOD远景切换,容易产生视觉不连续 问题。其次,多个Billboard位置接近且相互遮挡时,它们之间的深度关系精确度因无法做到十分精准而产生跳变问题。

并且Billboard无法接收精确投影。常规阴影渲染方案下,远处多为烘焙阴影或无阴影,当烘焙阴影时如果不额外采用一张全局阴影图用于判断公告板整体面片是否处于阴影中,则无法正确判断其位置,也无法正确处理阴影相关问题。

· Impostor

Impostor是Billboard的升级版,就是将原本只拍一个角度的照片换成拍多个角度的照片,并且根据视角不同,在面片上进行切换不同视角的照片的方法.

拍摄时根据续期,创建4x4,8x8或16x16个角度的相机矩阵,拍摄目标模型,生成一组各个角度带有透明通道的图片,然后在Shader中反推序列图片排列公式,进行采样贴图达到切换目的。

值得一提的是,Impostor只是解决了Billboard的视角问题,其它Billboard存在的问题,Impostor也同样存在,但不可否认的是,基于图像面片的方法确实较大程度上提高了渲染的性能。

· LOD的过渡方案

LOD避免不了的一个问题就是不同层级切换产生的跳变问题,然而渐进式LOD又会产生很大的性能消耗,尤其体现在移动端,所以常用的LOD过渡方案大致有两种。

  • Alpha Test

此类方案通常让模型使用均匀镂空的方式来实现模型逐渐消失和出现(比如使用抖动渐变类的算法 如Dither Fade),以此解决跳变问题。这类思路也可以用在贴图之间的混合算法,很多时候也可以实优化性能的目的,对色彩渐变出现梯度断层变化的问题来说同样有很好的效果。

关于Dither Fade,个人认为这篇Dither Fade效果学习 · undefix写的很好,可以参考。

  • Alpha Blend

Alpha Blend的目的和Alpha Test镂空的目的一样,都是起到渐变的目的,使用Alpha Blend更直接。

--- 减少复杂运算

少用三角函数,反三角函数,log等复杂函数。对于复杂函数如Color Grading可以使用一张LUT代替。

---剔除
· 距离剔除

距离剔除顾名思义就是按照物体距离摄像机的距离进行剔除,物体若距离摄像机太远,那么占屏幕的比例非常少,因此没必要进行渲染,剔除以节省性能开销。

在Unity中,可以使用Camera.layerCullDistances属性用来设置摄像机基于层的剔除距离,当然距离设置要小于摄像机远平面距离,不然没有意义。

· 视锥剔除

视锥剔除顾名思义就是把视锥外的东西剔除,以节省不必要的性能开销。视锥剔除通常发生在渲染管线CPU阶段的粗粒度剔除,也就是物体级别的剔除,这部分不需要我们自己写,因为引擎自带。

· 遮挡剔除

遮挡剔除顾名思义就是把在视野内被其它物体遮挡住的物体剔除掉,以节省不必要的开销。

Unity会在Editor下烘焙相关场景数据,将设置为Static Occluder和Static Occludee的场景物体(物体可以既是Static Occluder又是Static Occludee)划分为多个单元并生成用于描述单元内集合体及相邻单元之间的可见性数据,然后会尽可能合并这些数据单元以降低生成数据大小,在运行时,Unity会将这些数据加载进内存,对开启剔除的相机进行查询数据,渲染剔除后的内容。

Static Occluder(遮挡物)要满足:有挂载Terrain/Mesh Renderer组件,不透明,静态。

Static Occludee(被遮挡物)要满足:有任何类型的Renderer组件,静态。

需要注意的是,当场景的复杂度较低,遮挡关系没那么复杂的时候,遮挡剔除并没有多大的作用,并且还可能会是负优化。

· GPU-Driven Culling

前面我们所说的所有剔除操作都是在CPU上进行的,在传统的CPU驱动流程中,GPU只负责执行DrawCall,其他的所有工作都有CPU完成。一旦场景物件过多,CPU就要花很多时间来准备DrawCall,而此时GPU在白白等着,因此出现了GPU驱动的剔除,代替执行原本在CPU上的剔除和DrawCall操作
CPU Culling
GPU Culling

这样一来,CPU就可以处理更多其他事情。另外,由于GPU高并发的特性,在GPU上执行的剔除效率远高于CPU,能够显著提升性能。

GPU-Driven Culling也包括GPU的视锥剔除,以及GPU的遮挡剔除算法如Hiz(Hierarchical-Z map based occlusion culling)。这部分内容过于庞大了,所以就不展开说了,知乎上有很多大佬已经有很全面的讲解。


6.资产的制作上的优化

(1)一级资源优化处理

一级资源通常为重要的地标性建筑,除了合理控制模型面数和做好LOD,还需要考虑的就是庞大建筑的拆分问题,尤其是类似大型宫殿,长城墙,必须考虑拆分问题。模型的渲染加载是对整个模型进行加载和计算,哪怕这个模型只在视野内出现了一小部分,也会把整个模型加载到内存中进行渲染的相关计算。虽然在移动端有LRZ和HSR这样的提出策略,但是实际项目开发中并不能完全依赖它们,一是有些情况会不生效且存在各种硬件差异问题,而是减少同时进入管线资源可以有效降低性能上的压力。

--- 空间拆分

假如上图中是某大型地标建筑,可见,每个区块相对独立,则可以拆开。

这样一来,某些视角下,遮挡剔除生效就可以剔除一部分信息,避免将整个建筑物进行加载。

--- 组件拆分

组件拆分,如上图的墙壁,如果一面很长二方连续的墙,没有必要整块制作,而是做成可复用的一块,然后拼合而成一面很长的墙,一来如果视角离得很近,那么屏幕内只会显示1~2块墙壁,其余的可以被视锥剔除,二来,相同的Mesh和材质可以使用GPU Instancing加速。

--- 通用物件拆分

一级资源中可能存在一些特殊的通用物件,比如门窗和装饰物,这些物件主要用于丰富细节,如何处理好这些物件的拆分是很困难的事情,拆分过多会导致drawcall过多,拆分少又会导致大量多余网格进入渲染管线。我们需要利用好视锥剔除,遮挡剔除,GPU Instancing和LOD技术。

首先考虑近距离下,近距离下LOD级别较高,所有物体基本处于全质量显示。那么窗户,门等复用资源可单独拆出来,使用GPU Instancing减少消耗,并借助视锥剔除和遮挡剔除多余的物件。

另外如果一些通用物件较多,且物件贴图分辨率较小时,需要将这些物件贴图合并,如多张256合成一张1024。这样一来可以降低读写的带宽压力,二来更利于制作上的管理。

中距离下,屏幕中的内容量会大大增加,进而导致画面中的网格和drawcall增加。此时GPU Instancing仍然可以达到一个比较稳定的状态,并且可以LOD来使用更低一级的网格优化来平衡效率。

远距离下屏幕画面内物体进一步增加,区域拆分已经没什么作用,通用物件拆分也收效甚微,如果要以使用之前的方案达到较优秀的性能为目标,需要对LOD进行较大程度修改,大体思路分为整合模型和剔除模型。

剔除方面,LOD最后一级可以设为剔除,可以减少网格和drawcall数量。主体建筑也可以使用低模将LOD模型完整代替。

整合方面如UE中可以使用HLOD方案生成合并的低精度模型。

(2)二级资源优化处理

二级资源优化方面有两个方向,分别是组件式资源优化常规二级资源优化

组件式优化 在各个3A大作中经常用到,整体思路和一级资源优化处理逻辑相似,但难点在于规范,标准,组件规划上。因为这样一套组件有着极高的通用性,需要满足一类关卡所有二级资源的制作需求,所以规范要求十分苛刻。受限于大多数团队的能力,如果疏忽将会产生不可控的局面。所以并不常用。因此,技术方案和制作方案的选型,并非越先进越好,适合的才是最好的

常规二级资源制作流程是先制作出相对完整的建筑或物体,然后通过改变比例,方向,组合等方式将其组合成更大的建筑或物体来丰富画面内容。虽然这种方式的优化难度相对更大,但对场景制作人员的要求不会太苛刻。因此除了常规的LOD减面和更换Shader,材质的处理方案外,制作上的细节也要更加严谨,否则容易出现很多性能上的问题。

一般建议二级资源中一个建筑只使用一个材质球,但如果建筑类型多了,以然无法有效控制绘制命令数量,带宽压力同样也会增加,此时为了更进一步优化可以把贴图合并为一张图集使用,进一步的也可以使用动态合批和SRP Batcher。需要注意的是,如果二级资源占有量比较少的情况下,合并图集可能并不会有什么优势,也就没必要如此操作了。

而如果一个建筑中有几个材质球,想要达到削减材质数的目的,需要手动对模型UV重新拆解,排布及贴图合并,以达到较为理想的效果。如果使用工具暴力进行合并,贴图UV利用率会大幅降低,贴图精度会下降。当然如果经过充分测试后性能允许,确实可以在LOD0使用原贴图,其它LOD使用合并后的贴图。但对写实项目老师,合并后贴图细节流失严重,因此如果有其它方案,仍然不建议如此操作。

其次,资源制作方面尽量做到一步一检,包括比例规范,性能相关:如隐藏面检查,光华组检查,法线检查,废点检查,模型面数检查,贴图尺寸检查,材质数量检查等。可以通过工具一键检查并处理。

另外当资源是一些大规模植被,如树木,此时使用GPU Instancing容易产生模型过于相似的问题,那么在制作模型时就要考虑在不同角度的剪影尽可能制作出区别,这样在大规模布置时显得没那么重复。

(3)三级资源优化处理

三级资源在画面中占比较少,主要用于装饰和丰富场景细节,如垃圾袋,小木箱,花盆等小物件。这些物件虽然体积较小 ,但是数量和种类特别多,因此容易产生drawcall与贴图采样数量过多的问题,所以对于三级资源来说,处理简单的面数问题的重点在于合批和贴图合并上。

以垃圾袋为例,假如场景中存在5种类型的垃圾袋,有的单个存在,有的成组堆放,分别存在于场景的不同位置。最理想的状态当然是使用GPU Instancing,但当垃圾袋堆积成山的时候,即使使用GPU Instancing,消耗也是远大于把这堆垃圾袋合并成单个模型渲染的消耗的。所以对于大量堆积的情况,制作一个完整的新模型远比合批的方式有效的多。

由于存在5种垃圾袋,使用GPU Instancing最理想的情况也是只能优化到5个批次,使用SRP Batcher虽然可以合并为1个批次,但性能上未必有GPU Instancing高。并且GPU Instancing虽然解决了drawcall问题,但是对GPU的性能消耗仍然是存在的,GPU的占用也可能是性能瓶颈存在的一种因素。

同时,也要进行贴图合并处理和同贴图同材质球处理,这样静态合批时也将减少大量问题,虽然可能增加内存占用,但会减少带宽占用。同时LOD可将最后一级设为Cull。

如果特殊情况从技术层面无法合批,那么建议在制作时采用打组的形式,假设限制为5类组合,并且在制作时就要注意贴图合并,并使用相同材质球,不允许出现多个材质ID。这样一定程度上也可以减少drawcall。

四、结尾

总的来说,个人认为性能优化是一种通用的思想,它规定了大家都要在这个计算机硬件限定的范围内戴着镣铐跳舞,要么障眼法,要么从逻辑顺序上解决。文章提到的也只是冰山一角,关于某一特定渲染效果方案的单独算法优化这里没有提。其次,缓存未命中(Cache Miss)也是一个话题,但考虑到哥们当初体系结构学的不怎么样就先不献丑了,因为有些资料对我来说确实有点难读了,这点之后有时间再补(也许吧)。

最后个模块我并没有都介绍的很细致,还是希望这篇文章当一个简易的目录,供日后的自己查询。

五、参考

《游戏造梦师:游戏场景开发与设计》正式上线_哔哩哔哩_bilibili

关于静态批处理/动态批处理/GPU Instancing /SRP Batcher的详细剖析 - 知乎

【Unity】SRP底层渲染流程及原理 - 知乎

PowerVR USC Series 6 Core Overview

性能优化之降低OverDraw - JeasonBoy - 博客园

那些关于OverDraw的前世今生(2) - 知乎

移动设备GPU架构知识汇总 - 知乎

剖析虚幻渲染体系(12)- 移动端专题Part 2(GPU架构和机制) - 0向往0 - 博客园

浅谈移动端GPU架构 - 个人文章 - SegmentFault 思否

Low-resolution-Z on Adreno GPUs - Danylo's blog

移动端GPU 带宽功耗优化 - 知乎

「万字长文」GPU渲染管线和硬件架构_处理_性能_PreZ

浅谈移动端GPU架构 - 知乎

移动端高性能图形开发 - 移动端GPU架构探究 - 知乎

Unity性能优化与分析--GPU_unity gpu优化方案-CSDN博客

优化图形性能 - Unity 手册

Minimizing Cache Misses.

Overdraw概念、指标和分析工具 - 知乎

Billboards in Unity (and how to make your own) - Game Dev Beginner

Octahedral Impostors

Dither Fade效果学习 · undefined

openGL环境中, 应该避免过于频繁的申请和删除各种缓冲区对象 - 惊雷阁 - 博客园

[引擎开发] 深入GPU和渲染优化(基础篇)_quad overdraw-CSDN博客

移动平台Unity3D 应用性能优化 - 腾讯WeTest - 博客园

从零开始的 GPU Driven 管线------GPU Culling篇 - 知乎

相关推荐
liwulin05069 小时前
【Unity】cinemachine核心知识
数码相机·unity·游戏引擎
Dr.勿忘11 小时前
C#常考随笔1:const和readonly有什么区别?
开发语言·前端·unity·面试·c#·游戏引擎
Forest 森林11 小时前
【性能调优】高性能实践
性能优化
John_ToDebug12 小时前
chrome源码剖析—进程通信
chrome·程序人生·性能优化
河西石头12 小时前
双向链表在系统调度、游戏、文本编辑及组态方面的应用
游戏·链表·文本编辑器·双向链表·资源调度·组态软件·系统调度
阿芯爱编程17 小时前
vue3底层原理和性能优化
前端·vue.js·性能优化
舞者H17 小时前
一文了解性能优化的方法
性能优化
yangshuo128117 小时前
解决老游戏不兼容新系统win10win11问题
数据库·游戏
scott.cgi1 天前
「Unity3D」在Unity中使用C#控制显示Android的状态栏
unity·unity3d·android状态栏·c#控制android状态栏·unity显示状态栏
是店小二呀1 天前
【Linux】 冯诺依曼体系与计算机系统架构全解
linux·unity·系统架构