0


Unity 性能优化的手段(对象池、静/动态批处理、GPU实例化、垃圾回收、LOD、LightMap)【更新中】

对象池

使用对象池:频繁地创建和销毁对象会导致性能下降和内存碎片化。对象池可以预先创建一些对象,然后在需要时从池中取出,不再使用时再放回池中。

对象池具体怎么实现可以见我另外一篇文章:

在unity中使用状态机编写敌人的AI(纯代码方式)_unity 敌人ai-CSDN博客

扩容策略

对象池的扩容策略需要在性能和资源利用率之间进行权衡。以下是一些常见的扩容策略:

  1. 倍增扩容(Exponential Growth):- 当对象池耗尽时,将容量增加到当前的两倍或其他固定倍数。- 这种策略可以减少频繁扩容的次数,但可能会导致一定程度的内存浪费。
  2. 线性扩容(Linear Growth):- 每次扩容时,增加固定数量的对象。- 相比倍增扩容,这种策略会更加节省内存,但如果对象请求量很大,可能会导致更频繁的扩容操作。
  3. 按需扩容(On-demand Growth):- 根据当前的需求和预期的未来需求进行扩容,可能是一个固定的增量,也可能是动态计算出的数量。- 这种策略更加灵活,但需要更精确的预测模型。
  4. 预分配策略(Pre-allocation):- 对象池在初始化时就预分配足够多的对象,以满足预期的最大需求。- 这种方法可以避免运行时扩容,但如果估算不准确,可能会造成资源浪费。
  5. 阈值扩容(Threshold-based Growth):- 当可用对象数量低于某个阈值时进行扩容。- 这要求有一个监控机制来跟踪对象池的使用情况,并在必要时触发扩容。
  6. 自适应扩容(Adaptive Growth):- 根据历史使用数据,自适应地调整扩容的大小和时机。- 这需要复杂的算法来分析使用模式,但可以提供最优的性能和资源利用率。

收缩策略

对象池的扩容和收缩是为了平衡性能和内存使用。在确定销毁不常用对象的策略时,通常考虑以下因素:

  1. 最后使用时间(Least Recently Used, LRU): 可以为池中的每个对象维护一个“最后使用时间”戳。在进行收缩时,优先销毁那些最长时间未被使用的对象。
  2. 空闲时间阈值: 设置一个时间阈值,如果一个对象在这段时间内没有被使用,则将其销毁。
  3. 内存使用情况: 监控程序的总内存使用量。当内存使用达到某个阈值时,可以触发回收不常用对象的逻辑。
  4. 对象创建成本: 如果某些对象的创建成本很高(如初始化时间长,需要进行复杂的资源加载等),可能会选择保留这些对象,而不是销毁。
  5. 对象池大小限制: 为对象池设置最大和最小边界。当对象池的大小超过最大值时,可以根据一定的策略销毁多余的对象;当对象数量低于最小值时,不进行销毁操作,以保证池的基本性能。

DrawCall

DrawCall的过程

DrawCall,顾名思义就是绘制调用。在图形渲染中,Draw Call是指从CPU向GPU发送的指令,用于渲染一组几何体(通常是一组三角形)。这个过程是3D图形渲染中的核心环节,涉及多个详细的步骤和操作。

准备数据

  • 顶点数据:Draw Call开始之前,需要准备好顶点数据。这包括顶点的位置、法线、颜色、纹理坐标等信息。
  • 材质和着色器:还需要指定用于渲染的材质和着色器。材质定义了物体的外观,如颜色和纹理,而着色器则是控制如何渲染这些材质的程序。
  • 状态设置:在发送Draw Call之前,通常还需要设置渲染状态,如混合模式、深度测试、剔除模式等。

CPU到GPU的指令:一旦准备好所有必要的数据,CPU就会向GPU发送一个Draw Call,指令GPU渲染指定的几何体。

然后GPU就会进行处理:

  • 顶点着色器阶段:GPU首先使用顶点着色器处理每个顶点的数据。这可能包括变换顶点的位置、计算光照等。
  • 图元装配:处理过的顶点被组装成图元,通常是三角形。
  • 光栅化:图元被光栅化为像素。这个过程决定了哪些像素将被涂色。
  • 像素着色器阶段:每个像素通过像素着色器进行处理,计算最终颜色和其他像素属性。
  • 深度和模板测试:进行深度测试和模板测试,以确定哪些像素应该被写入帧缓冲。

为什么减少DrawCall可以实现性能优化?

过多DrawCall带来的影响

CPU和GPU的通信:每个Draw Call都涉及CPU向GPU发送渲染命令的过程。这种通信本身就有开销,因为它需要时间和资源来协调两者之间的数据传输。如果每个渲染对象都需要单独的Draw Call,那么渲染许多小对象时CPU到GPU的通信开销会变得非常大,导致性能下降。

无法及时处理其他任务:当CPU忙于发送渲染命令时,可能无法及时处理其他任务,如游戏逻辑、物理计算等。

状态变更:每次发生Draw Call时,GPU可能需要改变它的状态(如切换材质、着色器、纹理等),这会清除或更新相关的缓存,并加载新的渲染状态。还涉及加载新的资源(如纹理或着色器数据)到GPU内存中,或从内存中卸载不再需要的资源。这些都会增加性能开销。

减少DrawCall带来的好处

减少CPU和GPU间的负担:通过减少Draw Calls,减少了CPU和GPU之间的通信频率,使得GPU能更高效地处理每次渲染,同时CPU也可以分配更多资源处理游戏逻辑和其他计算。

提高渲染效率:较少的Draw Calls意味着GPU可以在更短的时间内完成渲染任务,提高了帧率和整体渲染性能。

减少Draw Call的方法

减少Draw Call可以通过批处理,合并网格,使用贴图集等方法实现

批处理:批处理的思想是将多个渲染操作组合成一个较大的批次(Batch),以减少Draw Calls的总数。这通常涉及将使用相同材质多个对象渲染为一个大的绘制操作。

静态批处理(Static Batching)

原理:静态批处理会在游戏构建时,将场景中标记为“静态”的多个使用相同材质和着色器的游戏对象合并成一个较大的网格。这意味着它们可以在一个Draw Call中被一起渲染。

时机:这种合并在游戏编译构建时(或在场景加载前)完成。Unity会自动检测标记为静态的游戏对象,并尝试将它们合并到一起。

使用场景:适用于不会在运行时移动、旋转或缩放的游戏对象,如建筑物、地面等。

限制:只适用于静态对象,不适用于运动或经常变化的对象。

优点:能够显著减少Draw Calls,提高静态场景的渲染效率。

使用方法

在Unity中开启静态批处理相对简单:

1. 启用项目设置中的静态批处理

  1. 打开Unity编辑器。
  2. 选择“Edit”(编辑)菜单中的“Project Settings”(项目设置)。
  3. 在“Player”设置中,找到“Other Settings”(其他设置)部分。
  4. 在“Other Settings”里,确保勾选了“Static Batching”(静态批处理)选项。这样做会让Unity在构建游戏时自动对静态游戏对象进行批处理优化。

2. 将游戏对象标记为静态

在Unity编辑器中,选中一个游戏对象并勾选Inspector窗口中的“Static”选项来标记它为静态。这表明该对象在游戏运行时不会改变位置、旋转或缩放。

动态批处理(Dynamic Batching)

适合的对象:

  • 小型动态对象:动态批处理适用于渲染一组小型、动态的游戏对象,例如移动的2D元素、小型3D模型、粒子系统等。
  • 顶点数较少的物体:它适用于顶点数量较少的对象,因为这样的对象更容易合并在一起,且不会超过动态批处理的顶点限制。
  • 相同材质和着色器的对象:当多个对象使用相同的材质和着色器时,它们更容易被动态批处理合并。

时机

动态批处理发生在运行时,每一帧都在进行:

  • 在游戏运行时,Unity引擎会实时检测是否有符合条件的游戏对象可以进行动态批处理。
  • 它在渲染每一帧之前,自动尝试合并可以批处理的对象。

工作原理

动态批处理的工作原理包括以下几个关键步骤:

  1. 顶点数据合并:首先在在CPU上将所有顶点转换到世界空间,然后Unity会在CPU上动态地将符合条件的多个游戏对象的顶点数据(如顶点位置、颜色、纹理坐标等)合并成一个更大的顶点缓冲区。
  2. 单个Draw Call渲染:合并后的顶点数据可以通过单个Draw Call发送到GPU进行渲染,这减少了渲染这些对象所需的Draw Calls数量。
  3. 动态性:由于对象可能在每一帧都发生变化(移动、旋转或缩放等),所以这种数据合并是在每一帧中实时进行的。
  4. 顶点限制:动态批处理有顶点数量的限制(在Unity中通常是300个顶点),因此只有顶点数较少的对象才适合用这种方式处理。

在一些现代高性能的硬件上,GPU能够高效地处理大量的Draw Calls,而在这些平台上,减少Draw Calls所带来的CPU预处理开销可能不会带来显著的性能提升。

因此,在进行渲染优化时,重要的是找到一个平衡点,既要减少Draw Calls以减轻GPU的负担,同时也要注意不要过度增加CPU的工作量。

在Unity的Player Settings中,开启了“Dynamic Batching”选项即可开启动态批处理。

GPU Instancing(GPU实例化)

原理:GPU Instancing允许在单个Draw Call中渲染多个对象的实例,这些对象共享相同的网格和材质,但可以有不同的变换(位置、旋转、缩放)和材料属性。

使用场景大量相似对象:如游戏中的植被、建筑物群等,这些对象虽然相似,但可能需要不同的位置或轻微的外观变化。

限制:需要支持Instancing的材质和着色器。不同实例间的差异仅限于材质属性和变换。

内存优化:由于所有实例共享相同的网格和材质,这减少了内存占用和资源加载的需求。

具体过程:

  1. 数据准备在运行时的渲染阶段,在渲染每一帧之前,CPU会根据当前的游戏状态和逻辑来为每个实例准备一个包含特定数据的数组,如每个实例的变换矩阵(位置、旋转、缩放)和可能的其他属性(例如颜色变化)。这个数组作为整体传递给GPU。
  2. 使用支持Instancing的着色器,这种着色器能够处理前面传入的实例数据数组。
  3. 在GPU Instancing中,GPU利用其并行处理能力来同时渲染多个对象实例。即使这些实例有不同的属性(如变换矩阵或颜色),GPU也能高效地同时处理它们。

适用性:静态批处理适用于静态场景元素,动态批处理适用于较小的动态对象,而GPU Instancing适用于大量相似的对象。

实现批处理的技巧和最佳实践

  1. 共享材质:确保尽可能多的对象使用相同的材质。这是批处理能否成功的关键因素。
  2. 使用图集:将多个小纹理打包到一个大的纹理图集中,这样不同的对象即使使用不同的纹理,也仍然可以合批。
  3. 减少材质属性的变化:例如,避免频繁更改材质的颜色或其他属性。
  4. 优化网格:对于动态批处理,保持网格简单(低顶点数)是重要的。
  5. 标记静态对象:在Unity编辑器中,确保场景中不会移动的对象被标记为“静态”。
  6. 合理使用LOD和遮挡剔除:这些技术可以减少渲染的对象数量,间接减少Draw Calls。
  7. 性能监控:使用Unity的Profiler工具监控Draw Calls和其他性能指标,以评估批处理的效果。

合批的缺点

  • 内存使用:合批会增加内存使用,因为合并后的网格需要更多的内存来存储。
  • 灵活性降低:合批后,单独操作原始对象变得更困难。

贴图集

在3D图形和游戏开发中,“使用贴图集(Texture Atlas)”是一种常用的优化技术。贴图集是将多个不同的纹理图像合并到一个单一的、更大的纹理图中的做法。以下是关于贴图集的详细解释:

贴图集的基本概念

  • 贴图集(Texture Atlas):一个大的纹理图(通常是矩形),包含了多个小的纹理。这些小纹理可能是不同的游戏元素的纹理,如角色的服装、游戏场景中的物体等。
  • 单一纹理调用:使用贴图集意味着多个对象可以共享同一个大纹理。在渲染时,这允许GPU通过单一的纹理调用来访问多个纹理,从而减少Draw Calls。

如何减少Draw Calls

  • 材质共享:由于多个对象可以共享同一个贴图集,这意味着它们也可以共享相同的材质。在图形渲染中,使用相同材质的多个对象可以被更容易地组合到一个批处理中。
  • 减少纹理切换:在渲染过程中,切换纹理是一个代价高昂的操作。使用贴图集可以减少这种切换,因为更多的纹理细节都包含在同一张大纹理图中。

垃圾回收的优化

垃圾回收是自动管理内存的过程,用于回收不再使用的内存空间。然而,垃圾回收过程本身可能引起性能问题,尤其是在需要高帧率和平滑运行的游戏中。因此,延迟垃圾回收和避免不必要的GC成为了性能优化的关键策略。

垃圾回收的性能影响

  1. 中断和延迟:垃圾回收过程可能导致应用程序的执行暂时中断,这在游戏中可能表现为短暂的卡顿或延迟,影响玩家体验。
  2. CPU资源消耗:GC需要CPU资源来检查内存中的对象,并确定哪些内存可以被回收。频繁的GC会消耗可观的CPU资源。

延迟垃圾回收

  1. 控制GC时机:在一些编程环境中,开发者可以控制GC的执行时机,以避免在关键时刻(如游戏的高强度战斗场景)执行GC。
  2. 手动触发GC:在某些情况下,开发者可能会选择在游戏的自然暂停点(如场景切换)手动触发GC,从而减少关键游戏时刻的性能影响。

避免GC

  1. 对象池(Object Pooling):使用对象池来重用对象,而不是频繁创建和销毁,这样可以减少GC的触发。
  2. 减少临时对象分配:避免在频繁调用的方法中创建大量的临时对象,特别是在循环或更新方法中。
  3. 优化数据结构:使用高效的数据结构,减少不必要的内存分配。例如,使用StringBuilder而不是string直接相加,避免频繁的字符串拼接。

UI预加载

为什么要预加载?

当UI实例化时,需要将Prefeb实例化到场景中,期间会有网格的合并、组件初始化、渲染初始化、图片加载、界面逻辑初始化等,会消耗大量的CPU,可能导致我们在打开某个界面时频繁出现卡顿现象。UI预加载的方法,在游戏开始前/进入某个场景之前预先加载部分UI,使得实例化和初始化平摊到等待的时间线上。

如何进行UI预加载?

最简单直接的方法就是在游戏开始前加载UI资源但不实例化,这样做只是提前把资源加载到内存中,在显示UI时CPU只需要实例化和初始化。

LOD

根据物体与摄像机的距离,动态调整物体的细节级别,从而减少渲染负担。

LOD(Level of Detail)技术是一种在3D图形渲染中常用的优化手段,旨在提高渲染效率,同时尽量保持视觉质量。LOD的基本原理是根据对象与观察点的距离,动态地调整对象的复杂度。这里是LOD技术的一些关键点:

基本原理

  • 多版本模型:对于一个3D对象,创建多个不同复杂度的版本。这些版本从高到低详细度排序,例如:高、中、低多边形模型。
  • 视距感知:根据对象与相机(观察点)的距离,实时选择合适的模型版本进行渲染。

应用

  • 近处使用高详细度模型:当对象靠近相机时,使用高多边形、高分辨率纹理的模型,以提供更精细的视觉效果。
  • 远处使用低详细度模型:当对象远离相机时,切换到低多边形、低分辨率纹理的模型。由于远距离的视觉效果不那么明显,这样做可以大幅减少渲染负担,同时对视觉效果的影响最小。

优点

  • 提高渲染效率:通过减少远处对象的多边形数量,降低了渲染过程的计算负担。
  • 节省内存和带宽:使用较低分辨率的纹理和模型可以减少内存的使用和数据传输量。

挑战

  • 无缝过渡:在不同LOD级别之间切换时,需要小心处理,以避免突兀的视觉跳变。
  • 平衡选择:合理选择何时切换LOD级别,以及每个级别的详细程度,是LOD技术的关键。

这是

Unity

官方文档的一个案例,可以看出,随着渲染距离的增加,渲染精度逐渐下降,直到最终被剔除,这样做的优势是保证游戏画面表现的同时,可以最大程度降低渲染压力。

LightMap

Lightmap(光照贴图)是一种在3D图形和游戏开发中常用的技术,用于提高场景的光照效果的同时优化性能。在这种技术中,光照信息被预先计算并存储在一张或多张纹理中,这些纹理随后被应用到场景中的对象上。以下是关于Lightmap的更详细的解释:

基本概念

  • 预计算的静态光照:Lightmap包含了场景中静态物体表面的光照信息,这些信息通常在游戏或应用的开发阶段预先计算。
  • 纹理:光照信息被存储在一种特殊的纹理中,这种纹理被映射到3D对象上,以模拟复杂的光照效果,如软阴影、反射和间接光照。

如何工作

  • 光照烘焙:在开发过程中,使用特殊的工具(如Unity的光照烘焙功能)计算场景的光照,并将结果“烘焙”到Lightmap中。
  • 映射到几何体:每个对象的表面细节(如几何形状和材质)与Lightmap中的相应区域相结合,从而在渲染时显示预计算的光照效果。

优点

  • 性能优化:由于光照信息是预先计算的,运行时不需要进行复杂的光照计算,这可以显著提高性能。
  • 高质量的光照效果:可以实现高质量的光照效果,包括软阴影、光线传播和光线反射。

缺点

  • 仅限于静态场景:Lightmap通常用于静态物体,因为它们是预先计算的。对于动态物体或变化的光源,需要其他光照技术。
  • 内存使用:高质量的Lightmap可能占用大量的纹理内存。

资源异步加载

优化脚本

避免在Update函数中进行大量的计算或者频繁的内存分配,尽量减少使用Find系列函数,避免频繁的GC。

在Unity中,Find系列函数(如FindObjectOfType,Find,FindChild等)是非常消耗性能的操作,因为它们需要遍历整个场景或者对象的所有子对象。如果在Update或者频繁调用的函数中使用Find系列函数,会大大降低游戏的性能。

更好的做法是在Start或Awake函数中使用Find系列函数,将找到的对象保存在一个变量中,然后在需要的地方直接使用这个变量。这样就只需要在游戏开始时执行一次Find操作,而不是每帧都执行。

另外,如果可能的话,尽量使用public变量或者单例模式来引用需要的对象,这样可以完全避免使用Find系列函数。

使用Profiler工具:Profiler可以帮助你找到性能瓶颈,从而进行针对性的优化。

优化物理:减少物理模拟的复杂度,比如使用简化的碰撞体,减少不必要的物理计算

遮挡剔除

Occlusion Culling:隐藏摄像机看不见的物体,减少渲染负担。

  1. 使用Shader优化:使用更简单的Shader,或者针对特定平台优化Shader。
  2. 优化UI:避免频繁更新UI,尽量使用Canvas Group和Layout Group。

在3D图形和游戏开发中,材质(Material)和纹理(Texture)是两个基本且关键的概念,它们在创建视觉效果时扮演着不同的角色。理解它们之间的区别对于正确地使用它们来创建丰富、逼真的3D场景是非常重要的。

材质(Material)

  1. 定义:材质是一个用来定义对象表面外观的属性集合。它不仅包括颜色和纹理,还包括光照如何与对象表面交互的信息,例如光泽度、透明度、反射性等。
  2. 作用:材质决定了物体看起来是金属的、木制的、塑料的还是布料的等等。它是物体表面视觉特征的综合表现。
  3. 属性:材质包含多种属性,如漫反射颜色、镜面高光、法线贴图、反射率、透明度等。这些属性可以通过调整材质中的参数来改变。
  4. 渲染管线:在渲染管线中,材质决定了对象如何响应光照和环境,影响着对象的最终视觉效果。
  5. 使用方式:在游戏引擎如Unity中,材质通常通过材质编辑器创建和修改,可以应用到一个或多个3D模型上。

纹理(Texture)

  1. 定义:纹理是一种图像,用于给3D模型的表面添加细节。它是一个2D图像文件,可以通过UV映射将其贴在3D模型的表面。
  2. 作用:纹理直接决定了物体表面的具体外观,如颜色、图案等。纹理可以非常详细地描绘表面特征,比如砖墙的纹理、木纹或皮肤纹理等。
  3. 类型:纹理有多种类型,包括漫反射贴图(决定物体颜色)、法线贴图(模拟表面凹凸)、镜面高光贴图(定义高光区域)等。
  4. UV映射:为了将2D纹理应用到3D模型上,需要进行UV映射,这是一个将3D表面坐标转换为2D纹理坐标的过程。
  5. 使用方式:纹理被创建为图像文件,然后在材质中被引用。通过将纹理应用到材质上,可以赋予材质具体的视觉外观。

纹理(Texture)通常被视为材质(Material)的一部分,在3D图形和游戏开发中,它们共同工作以定义对象的表面外观。

合并网格:将多个网格合并成一个网格,可以减少 Draw Call。可以使用 Unity 中的 Mesh.CombineMeshes 方法来实现网格的合并。
合并材质:将多个使用相同材质的物体合并成一个物体,可以减少 Draw Call。可以使用 Unity 中的 MaterialPropertyBlock 来实现材质的共享。
使用静态批处理:将多个静态物体合并为一个批次进行渲染,可以减少 Draw Call。可以在 Unity 中开启静态批处理来实现。
使用动态批处理:将多个动态物体合并为一个批次进行渲染,可以减少 Draw Call。可以在 Unity 中开启动态批处理来实现。
使用 GPU Instancing:使用 GPU 实例化技术可以将多个相同的物体实例化,减少 Draw Call。可以通过创建 MaterialPropertyBlock 对象并调用 MaterialPropertyBlock.SetVectorArray 方法来实现 GPU Instancing。
使用 Atlas 贴图:将多个小贴图合并成一个大贴图,可以减少 Draw Call。可以使用 Unity 中的 SpritePacker 工具来实现贴图的合并。
减少动态物体的数量:动态物体需要每帧重新绘制,因此数量过多会导致 Draw Call 增加。可以通过使用静态物体、使用 LOD 等方式来减少动态物体的数量。
减少透明物体的数量:透明物体需要额外的渲染步骤,因此数量过多会导致 Draw Call 增加。可以通过使用不透明物体、使用 Alpha Test 等方式来减少透明物体的数量。
使用 Occlusion Culling:根据摄像机视锥体内的可见 UI 元素,减少需要渲染的 UI 元素数量,从而提高渲染性能。


本文转载自: https://blog.csdn.net/weixin_43757333/article/details/134767376
版权归原作者 晴夏。 所有, 如有侵权,请联系我们删除。

“Unity 性能优化的手段(对象池、静/动态批处理、GPU实例化、垃圾回收、LOD、LightMap)【更新中】”的评论:

还没有评论