PDF文件下载地址:(https://download.csdn.net/download/JianZuoGuang/21943001)

目录

第一章 渲染原理和流程

1.1. 概述

1.2. 应用阶段

1.2.1. 概述

1.2.2. 参与硬件

1.2.3. 阶段任务

1.2.4. 数据准备

1.2.5. 粗粒度剔除

1.2.6. 设置渲染状态

1.2.7. 调用DrawCall

1.3. 几何阶段

1.3.1. 概述

1.3.2. 参与硬件

1.3.3. 阶段任务

1.4. 光栅化阶段

1.4.1. 概述

1.4.2. 参与硬件

1.4.3. 阶段任务

1.5. UGUI渲染过程分析

第二章 影响性能的因素

1.1. 硬件因素

1.1.1. CPU

1.1.2. GPU

1.1.3. 内存

1.2. 软件\平台\系统因素

1.2.1. Mono虚拟机

1.2.2. IL2Cpp

1.2.3.
图形API(DirectX\OpenGL\Vulkan\Metal)

第三章 资源标准以及优化方案

1.1. 美术资源制作标准

1.1.1. PC平台制作标准

1.1.2. 移动平台制作标准

1.2. 美术资源优化方案

1.3. U3D优化方案

1.3.1. CPU优化

1.3.2. GPU优化

1.3.3. 内存优化

1.3.4. UGUI优化

第四章 Todo(附录)

1.1. 渲染方案(URP)

1.1.1. 正向渲染(Forward Rendering)

1.1.2. 延迟渲染(Deferred Rendering
当前版本不支持)

1.1.3. 总结

1.2. 光照方案(URP)

1.2.1. 混合光照+反射探针+光照探针

1.2.2. 低端硬件设备光照方案

1.2.3. 中、高端硬件设备光照方案

1.3.
MeshBake资源(网格\材质\贴图)烘焙方案

1.4. UnIty3D资源制作标准参考

渲染原理和流程

概述

渲染到设备屏幕的每一帧画面,其过程都是将场景中的三维模型、纹理渲染到二维的屏幕上,以像素的形式展现。看似一个简单的过程,分别需要CPU\GPU\内存等硬件资源的参与和配合;CPU\内存\GPU相互调用的过程,简称为渲染流水线,图形的渲染流水线主要分为以下三个阶段:应用阶段、几何阶段、光栅化阶段。

(图:图形渲染流水线)

应用阶段

概述

获取需要渲染的网格、材质、贴图等信息,并针对不同的绘制需求,设置相应的渲染状态。将收集的数据加载到显存中,并通过DrawCall调用底层DirectX/OpenGL/Vulkan
API 执行图元绘制。

参与硬件

由CPU端主导,涉及硬件包括RAM、显存。

阶段任务

  1. 收集需要参与绘制的数据,讲数据加载到显存中。

  2. 设置渲染状态。

  3. 调用DrawCall,提交GPU执行渲染。

数据准备

  1. 将需要渲染的数据从硬盘中加载到系统内存中,纹理、网格、材质等数据需要被加载到显存中。

  2. 从内存RAM中移除渲染数据(顶点坐标、法线方向、顶点颜色、纹理坐标)等,此部分数据已经存储在显存中了。RAM中只保留一些CPU仍需要访问的数据。

粗粒度剔除

  1. 剔除一些不在摄像机视野范围内的网格数据。

  2. 视椎体剔除(Frustum Culling):剔除不在视椎体范围内的三角形网格。

  3. 遮挡剔除(Occlusion Culling):剔除被遮挡的三角形网格。

设置渲染状态

  1. 指定着色器、材质、光照。

  2. 配置材质参数(渲染模式、渲染队列)

  3. 输出下一渲染阶段所需的几何信息-渲染图元(点、线、三角形等)

调用DrawCall

输出渲染图元,CPU发起绘制命令,向GPU指定一个图元列表;通知GPU执行渲染。

几何阶段

概述

几何阶段(GPU):获取应用阶段提交的数据,进行顶点变换计算;将模型顶点从模型空间中变换到屏幕空间中。几何阶段在整个渲染流水线中的主要任务就是:变换三维顶点坐标和执行逐顶点的光照计算。

(图:几何阶段渲染流程)

参与硬件

GPU/显存

阶段任务

顶点着色器(Vertex Shader)

  • 顶点着色器通常用于实现坐标变换、顶点空间变换,逐顶点光照计算,为片元着色器阶段提供数据来源。计算顶点中包含的法线、纹理坐标、色彩、光照等信息。

  • 顶点着色器的目标:将顶点坐标从模型空间变换到齐次裁剪空间。

  • GPU针对每个顶点执行变换操作(在此阶段可编程实现一些特殊的顶点动画),且为了提供顶点着色器的运行效率,顶点之间无法获取顶点与顶点之间的关系。

  • 基于GPU的并行特性,可执行大量的顶点运算。

(图:顶点坐标变换过程)

曲面细分着色器(Tessellation Stage)

曲面细分着色器是一个可选着色器。通过曲面细分着色器可以实现对三角面进行细分,增加网格的三角面数,提升网格的表现细节。

  • 可对三角网格/三角面进行细分。

  • 基于高度图/法线图进行细分,增加细节(置换贴图)

几何着色器(Geometry Shader)

  • 几何着色器是可选着色器,用于执行逐图元着色操作;或生成更多图元。开发者可以控制GPU对顶点进行增删改操作。

  • 要求硬件支持Shader Model 3.0以上

投影(Projection)

  • GPU将顶点从摄像机观察空间转换到裁剪空间

  • 投影方式:正交投影和透视投影


(图左:透视投影/图右:正交投影)

裁剪(Projection)

裁剪由硬件通过固定算法来实现,无法通过编程来控制裁剪的过程;但可以通过自定义裁剪操作来配置裁剪。

  • 对摄像机视野范围外的三角形顶点,根据摄像机的视角范围生成新的顶点。

(图:图元裁剪过程)

屏幕映射

屏幕映射阶段接收的输入坐标仍然是三维坐标系下的坐标。屏幕映射的目的是将每个图元的x和y坐标转换到屏幕坐标系下,屏幕坐标系是一个二维坐标系,和屏幕分辨率有很大的关系。

(图:屏幕映射过程)

光栅化阶段

概述

光栅化阶段(GPU):获取经过几何阶段变换后的三角形片元,并对每个三角形片元进行设置、遍历、逐片元操作。其中片元着色器用于实现逐片元操作,可编程实现;而逐片元操作阶段则执行一些如:颜色修改、深度测试、alpha测试等操作,最后提交渲染结果到帧缓冲中。

(图:光栅化阶段)

参与硬件

GPU/显存

阶段任务

三角形设置(Triangle Setup)

三角形设置的目标是计算光栅化一个三角网格所需要的必要信息。由于几何阶段输出的对象为三角网格的顶点。一个三角形由基础的点、线、面组成,我们需要获取三角网格对像素的覆盖情况,我们必须计算每条边上的像素坐标。

三角形遍历(Triangle Traversal)

三角形遍历的目标是检查每个像素是否被一个三角网格所覆盖,如果像素被三角网格覆盖的话,就会生成一个片元。GPU查找像素被哪些三角网格覆盖的过程就是三角形遍历的过程。

片元着色器(Fragment Shader)

片元着色器是一个非常重要的可编程着色阶段,片元着色器也被称之为像素着色器。三角形设置和三角形遍历并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来描述一个三角形网格是怎样覆盖每个像素的。

  • 片元着色器接收上一个阶段对顶点信息插值得到的结果。

  • 对纹理进行纹理采样,根据顶点着色阶段输出的每个顶点对应的纹理坐标(UV),以光栅化的方式对三角网格的3个顶点对应的纹理坐标进行插值运算,获取当前片元对应的纹理坐标(UV)。

  • 执行纹理采样、UV插值、光照计算等图形计算操作。

(图:顶点/片元着色器处理流程)

逐片元操作(Per-Fragment Operation)

逐片元操作作为渲染流水线的最后阶段,也称之为输出合并阶段,针对片元着色器输出的每一个片元进行一些相关的操作。同时将片元颜色与颜色缓冲区中的颜色进行混合。

这个阶段以片元作为数据基础,决定片元需要被渲染还是丢弃。即逐片元操作阶段是决定片元可见性问题的一个重要阶段。

  • 检测片元的可见性:检测过程包括对片元执行(Alpha测试\剪切测试(Scissor
    Test)\模板测试(Stencil Test)\深度测试(Depth
    Test)\混合测试(Blending))等。

  • 将通过测试检验的片元与颜色缓冲区中的颜色按照指定的方式进行混合,将没有通过检测的片元丢弃(Discard)。

    (图:逐片元操作流程)

模板测试(Stencil Test)

模板测试:在模板测试中主要参与对象包括某个片元与模板缓冲区,片元为片元着色器阶段的输出的对象;模板缓冲是一个特定的内存块,与屏幕缓冲的大小一致。每个片元在执行模板测试时,首先取得当前片元所在的位置,并将当前位置与模板缓冲区中的位置进行对比。如果当前片元满足模板测试指定的测试条件,即模板测试通过,模板测试通过之后,当前片元的会被写入到模板缓冲中,在整个渲染帧结束前,模板缓冲区不会被重置。

(图:模板测试流程图)

在模板测试中,由开发者指定一个引用参考值,这个参考值为当前物体的片元提供了标准参考。然后这个参考值会与模板缓冲(Stencil
Buffer)区中当前片元位置的模板值进行比较。模板缓冲区的值,是由之前通过其它测试的片元写入的值,比较当前片元的位置与缓冲区中的值,根据比较的结果决定片元是否需要丢弃(Discard),判断语句如下:

模板缓冲条件 说明
Grater 当前片元值>模板缓冲中的值,通过
GEqual 当前片元值≥模板缓冲中的值,通过
Less 当前片元值<模板缓冲中的值,通过
LEqual 当前片元值≤模板缓冲中的值,通过
Equal 当前片元值=模板缓冲中的值,通过
NotEqual 当前片元值≠模板缓冲中的值,通过
Alaways 总是通过
Nerver 总是不通过
深度测试(Depth Test)

深度测试:深度测试主要目的是将当前片元的深度值与已经存在深度缓冲区中的深度值进行比较。根据比较的结果判断当前片元是否需要被丢弃。通常情况下,我们仅想渲染距离摄像机进且在摄像机可是范围内的物体,对于那些被遮挡的物体,我们不希望它被渲染到屏幕上。

深度测试的工作分为两块:其中一块是将片元与缓冲区中的值进行比较,即ZTest;另外一块是:将片元深度值写入到缓冲区中,即ZWrite.

深度值:在几何阶段,片元顶点坐标由局部坐标空间变换到了屏幕坐标空间;由一个三维坐标系变换到二维坐标系。在变换过程中,顶点的Z坐标在变换过程中被转化为深度值。深度值的记录主要用于记录片元距离摄像机的远近程度。

(图:深度测试过程描述)

UGUI渲染过程分析

Unity3D中UI渲染的本质也是也是基于网格(Mesh)和材质(Material),所以针对UGUI的渲染过程,其实也和其它三角图元的渲染流程一致。UGUI中的渲染主要分为三个部分:

  • CanvasUpdateRegistry:负责通知需要渲染的UI组件。通常UI组件需要自己记录当前是否需要被重新渲染,并把渲染的事件注册给Registry,当UI本身的状态发生改变,由Registry去出发重新渲染事件,当前UI提交需要重新渲染的数据,进行UI重新绘制。由于UI的各种数据可能会在一帧内发生多次改变,如果每次UI发生改变都去通知重新渲染会极大的影响渲染效率。通过一帧处理一次绘制调用会极大的提高UI渲染效率。

  • 每个UI组件均继承自Graphic组件,Graphic的核心功能是组织Mesh和Materail传递给底层API进行绘制。即CanvasRenderer类,CanvasRenderer连接画布(Canvas)和其它UI渲染组件,CanvasRenderer把网格绘制到Canvas上,Canvas对当前接收到的网格进行合批处理,Canvas合批之后,再通过DrawCall调用底层API进行渲染。CanvasRenderer中有两个方法,分别是:SetMesh和SetMaterial;如果网格或者材质没有发生变化,UGUI通过调用底层缓存进行绘制,不需要每一帧都去调用SetMesh或者SetMaterial。

  • 每个组件都继承自Graphic,每个Graphic都保存了当前UI组件的Mesh和Material;但是并不会每个Graphic都会调用一个DrawCall进行绘制调用;而是在底层将这个组件传递给Canvas,由Canvas对当前节点下的Graphic进行批处理合并,再由Canvas去进行绘制调用。

  • 如果一个UI组件如果对Dirty进行了设置,整个Canvas都需要重新计算合批,会导致较大的性能消耗。

(图:UGUI渲染过程)

UI重绘触发条件(Rebatch和Rebuild):

  • 组件Enable\Disable\Validate都会触发UI组件重新绘制

  • SetVerticesDirty操作:MeshEffect改变\Shadows属性改变\Transform大小改变\Image类型、层级、填充方式改变\Rawimage:Texture、UVRect、动画效果\Text内容改变,开关Rich\Text内容的变化是最影响dirty的。

  • SetMaterialDirty操作:Material替换、Image触发动画、Transform层级变化、重新计算Mask\Rawimage替换Texture\Layout布局变化,包括Horizontal
    Layout、Vertical Layout和Content Size Fill数据变化。

影响性能的因素

硬件因素

(图:移动平台硬件架构)

(图:PC平台硬件架构)

(图:移动端3D应用画面渲染过程)

CPU

CPU在游戏运行过程中,主要承担游戏中的逻辑运算、资源调用、网络操作、物理检测、GC垃圾回收、绘制调用(DrawCall)等一些计算操作。CPU性能通常受需要渲染的批次数限制,需要渲染的批次数越多,需要消耗的CPU计算量越大,CPU的占用率越高。针对CPU端的性能优化可以从这些方面入手。

  • 生成绘制命令(收集需要渲染的信息)

  • 业务逻辑/物理检测/

  • 网络通信

  • I/O操作

  • GC垃圾回收

  • 绘制调用

GPU

GPU在游戏中主要承担图形着色的功能,包括顶点坐标变换、顶点着色、片元着色、光照计算等操作。GPU性能通常受填充率或内存带宽限制,如果降低游戏分辨率后,游戏帧率有明显的提升,表明GPU填充率是影响游戏运行的主要因素。

  • 顶点坐标变换、法线、切线

  • 顶点着色/纹理采样

  • 片元着色/光照计算/像素绘制

    GPU在图形渲染流水线中主要承担两大流水线操作:几何着色阶段和光栅化阶段。

内存

内存主要用来存储游戏运行过程中的各种数据资源,包括:资源内存占用、引擎模块自身内存占用、程序托管堆内存占用。

  • Mono托管内存:栈和托管堆

  • 资源存储内存:网格、纹理、材质、Shader、UI、音频、视频、图片、文字、Assetbudle

软件\平台\系统因素

Mono虚拟机

Mono虚拟机的组成组件:C#编译器、CLI虚拟机以及核心程序集。

Mono虚拟机的三种转译方式:

  • 即时编译(Just In Time,JIT):程序运行过程中,将CIL的byte Code
    转译为目标平台的原生码。(IOS端不支持JIT编译)

  • 提前编译(Ahead of time,AOT):程序运行之前,将.exe或.dll文件中的CIL的byte
    code部分完全转译为目标平台的原生码并存储,程序运行中任有部分CIL的byte
    code需要JIT编译。

  • 完全静态编译(Full Ahead Of
    Time,Full-AOT):程序运行前,将所有源码编译成目标平台的原生码。

(图:MonoVM代码执行过程)

特点

  • 构建、打包应用非常快

  • 由于Mono的JIT机制,支持更多托管类库

  • 支持运行时代码执行

  • 必须代码发布成托管程序集(.dll文件,由mono或者.net生成)

  • 各平台分别需要对应多个虚拟机(WebGL和UWP仅支持IL2Cpp)

  • IOS不支持32位Mono

IL2Cpp

  • 基于AOT编译(静态编译),把IL中间语言转换成CPP文件

  • 运行时库(libil2cpp)

其中AOT将IL转换为C++代码,再提交给各平台的C++编译器进行编译,达到平台兼容的目的;运行时库则会提供诸如垃圾回收、线程、文件获取、内部调用直接修改托管数据结构的原生代码与抽象。

(图:IL2Cpp执行过程)

特点:

  • 可调式生成的C++代码。

  • 可以启用引擎代码剥离(Engine Code Stripping)来减少代码大小。

  • 程序运行效率相比Mono更高,执行速率更快。

  • 多平台移植较为方便

  • 相比Mono构建、打包速度较慢。

  • 只支持AOT(Ahead Of Time)编译,不支持JIT编译。

  • 代码执行效率是基于Mono虚拟机的2倍。

图形API(DirectX\OpenGL\Vulkan\Metal)

资源标准以及优化方案

美术资源制作标准

PC平台制作标准

移动平台制作标准

美术资源优化方案

U3D优化方案

CPU优化

DrawCall优化

DrawCall是CPU去调用底层图形API告诉GPU进行图形渲染的一个过程中产生的一个绘制命令。DrawCall的数量决定了CPU调用底层API的次数,DrawCall的数量与CPU的性能成负相关,即DrawCall的数量越多,占用的CPU计算性能资源越多,造成游戏的性能下降。

DrawCall的优化目的主要是降低DrawCall的调用次数,DrawCall的主要目的在于将网格、材质、纹理等信息传递到显存中。有几个网格就需要传递,就有几个DrawCall需要调用,因此我们可以通过合并网格的数量,来减少DrawCall的调用次数。

静态批处理(Staic Batch)

静态批处理是Unity3D默认提供的网格合并功能,只需要在Inspector面板中,将不会移动、缩放、旋转的对象且需要批处理的对象标记为静态的(勾选Static)。且保证对象之间相互使用相同的材质。其原理就是:把物体的网格进行合并,变成一个静态的更大的网格体,再使用同一的材质进行渲染。

优缺点(使用限制和说明)
  • 使用静态批处理需要额外的内存开销来保存合并之后的网格数据,这会增加较多的内存占用。

  • 降低CPU计算资源,减少DrawCall调用次数。

静态批处理的时间点
  1. 打包时,在Player Setting中勾选static
    batching,在导出包时会进行批处理,导出来的包包体就会大。

    1. 在游戏场景中勾选场景物体的static选项,在加载场景的时候,会进行一次静态批处理的合并,这样导出来的包体不大,但是当运行时,运行时内存占用会增大。
静态批处理的基本原理

场景中有四个物体ABCD,如果在Inspector面板中勾选静态选项,在进行静态批处理时,引擎会判断这四个物体使用公用同一材质球,如果共用材质球,则引擎会将四个独享视为可以进行静态批处理的对象,引擎会基于单个渲染对象的大小拷贝出3个,总共变成4个Mesh,此时这四个Mesh会存储在一个Index
Buffer中,此时内存的占用会增大4倍;渲染时会将批处理之后的大网格传递给GPU进行渲染。此处DrawCall由4变成1.

为什么需要使用静态批处理

游戏运行过程中,如果性能瓶颈在CPU端,如果CPU端的运行速度较慢,则会出现GPU等待CPU的状态。CPU在游戏运行过程中主要工作为:设置渲染状态和调用DrawCall。设置渲染状态主要包括:游戏资源的加载(贴图、网格、材质、shader、灯光)等。如果每个物体的材质和贴图都不一样,则CPU会花费较多的时间来设置渲染状态(SetPass),同时也会产生更多的DrawCall.因此,在游戏中,对于大量不需要改变位置的物体,均可采用静态批处理的方式来解决CPU端的性能瓶颈。

由于静态批处理会增加内存的占用,在Unity中针对静态批处理的对象,需要声明一个较大的内存缓冲来存储对象,当把网格传递给GPU进行渲染时,并不会对视锥体范围内的网格进行裁剪,导致渲染压力上升。因此如果游戏中大量的静态物体均使用静态批处理方式,会占用较大的内存buffer.因此,可以通过对需要静态批处理的对象进行一个分块的处理,将场景中的对象分成多个块,每个小块更加实际项目来做设定。这种方式可以减少批处理的内存占用,同时也利于进行视锥体裁剪。

动态批处理(Dynamic Batch)

动态批处理是专门为优化场景中共享同一材质的动态Gameobject的渲染而设计的,为了合并渲染批次,动态批处理每一帧都会消耗一些CPU性能;当开启动态批处理时,Unity会自动的将所有符合条件的共享同一材质的动态Gameobject进行批处理合并,通过一个DrawCall进行绘制。其原理即:将场景中所有共享同一材质的模型的顶点信息变换到世界空间中,然后通过调用一次DrawCall来绘制这个大网格,达到合批的目的。

优缺点(使用限制和说明)
  • 弥补了静态批处理无法处理相同材质的动态对象的不足。

  • 增加了CPU计算消耗,需要通过CPU来将顶点坐标变换到世界空间坐标系中。

  • 降低DrawCall调用次数。

  • 动态批处理仅支持网格顶点小于900的网格体。

  • 缩放比例不同,Unity将无法对物体进行批处理,比如:缩放为(1,1,1)和(1,2,2)的对象就不会进行动态批处理,但是(1,1,1)和(2,2,2)会进行动态批处理。

使用MeshBake工具进行网格/材质/贴图合并

MeshBake烘焙工具主要用于减少CPU端的计算压力,提供以下方式来降低CPU的DrawCall调用次数。使用MeshBake优化的结果是:降低Batches和SetPass。

  • 网格合并(多个网格合并为一个网格)

  • 材质合并(多个材质合并为一个材质)

  • 贴图合并(多张贴图合并为一个到一张图集中)

SetPass Call 设置渲染状态

SetPassCall
代表渲染状态的切换,主要出现在材质不一致的时候,会进行渲染状态的切换。一个批处理的过程包括:提供顶点缓冲、索引缓冲,提交shader,设置硬件渲染状态,设置光源属性等。如果一个batch和另外一个batch使用的是不同的材质或者是同一个材质不同的pass,那么就需要触发一次SetPassCall来重新设定渲染状态。

通常SetPassCall的数量和DrawCall的数量成正相关,只需要降低其中一个的调用次数,那么另外一个的调用次数也会随之下降。

业务逻辑(核心逻辑/UI交互逻辑)

业务逻辑的优化主要集中在代码上的优化,即C#代码的优化;包括托管堆栈的优化、变量申请、逻辑运算优化。

  • 不使用的对象尽量不使用Destroy方法进行销毁,而是SetActive(fasle)

  • 使用For循环代替Foreach,每次Foreach会产生一个Enumator,迭代器会额外分配内存。

  • 控制StartCorountin()的次数,开启一个一个协程,至少分配37B的内存。

  • 使用StringBuilder来代替String做字符串拼接:StringBuilder.Append方法在拼接字符串时,变换总是发生在同一个内存块中。而String+String这种字符串拼接方式会频繁申请内存释放,导致GC频繁调用。

  • 组件缓存:每次GetComponent均会分配一定的GCAlloc;每次获取对象名称Object.name会分配39B的堆内存。

  • 避免使用Gameobject.Find\GameObject.FindWithTag\Object.FindObjectOfType等查询方法。

  • 使用对象池ObjectPool来管理对象;避免频繁的Institiate和Destroy,避免频繁的IO操作。

  • 尽量不要在Update()函数中做复杂的计算;可使用间隔帧计算。

  • 避免大量的装箱\拆箱操作,比如:int类型转string类型,int.Parse32。

网络通信、I/O操作

GC垃圾回收

GC本质上是用来回收内存的,但是每一次进行GC操作,均需要消耗CPU的计算性能,因此避免频繁的GC调用,是优化CPU性能的重要一步。

C#值类型和引用类型区别

C#中值类型变量的声明和引用类型的声明时在内存中有以下区别:

  1. 创建引用类型时,CLR运行时为为其分配两个空间,一块空间分配在内存堆上,存储引用类型本身的数据;另一块空间分配在栈上,存储对堆上数据的引用(即堆上存储对象的地址,即指针)。

  2. 创建值类型时,CLR运行时会为其向内存申请一个地址,这块内存在变量申请的地方,如:

    1. 如果值类型是在方法内部创建的,则跟随方法入栈,分配到栈上存储。

    2. 如果值类型是引用类型的成员变量,则跟随引用类型,存储在内存堆上。

GC产生的原因
  1. 频繁的变量声明和内存操作。

  2. 装箱、拆箱操作(值类型和引用类型互相转化)。

(图:GC产生的原因)
GC的工作流程

GC的工作流程主要分为以下几个步骤:

标记(Mark)->计划(Plan)->清理(Sweep)->引用更新(Relocate)->压缩(Compact)

(图:GC的工作流程)

Unity内存管理机制

Unity中主要采用自动内存管理的机制,开发时在代码中不需要详细地告诉Unity如何进行内存管理,Unity内部自身会进行内存管理。

关于Unity的内存管理可以理解为以下几部分:

  1. Unity中的两大内存类型;栈类型和堆内存;栈内存主要用来存储存储较小和短暂的数据,堆内存主要用来存储较大和存储时间较长的数据。

  2. Unity中的内存分配主要在堆栈和托管堆中进行,要么分配在堆栈中,要么分配在托管堆中。

  3. 只要变量处于激活状态,则其占用的内存会被标记为使用状态,则该部分的内存处于被分配的状态。

  4. 一旦变量不再激活,则该变量所占用的内存不在需要,该部分内存可以回收到内存池中被再次使用,即内存回收。处于堆栈上的内存回收速度较快,而处内托管堆中内存并不是及时回收的,此时其对应的内存依然会被标记为使用状态。

  5. 垃圾回收主要是指托管堆内存的分配和回收,Unity会根据当前的内存使用状况,定时对堆内存进行GC操作。

  6. Unity会定时进行垃圾回收操作,回收没有有效引用的对象的内存。

堆栈内存分配和回收机制

堆栈上的内存分配极其简单和快捷,因为堆栈上只会存储短暂或者较小的变量。内存分配和回收都会以一种顺序和大小可控制的形式进行。

堆栈的运行方式和Stack类似:其本质只是一个数据的集合,数据的进出都以一种固定的方式运行;栈以先进后出的方式进行数据管理。

托管堆内存分配和回收机制

堆内存的分配和回收相对复杂,主要是堆内存上可以存储短期较小的数据,也可以存储各种类型和大小的数据。其上的内存分配和回收顺序并不可控;当变量分配在内存堆上时,主要分为以下几步:

  1. 首先,unity检测是否有足够的闲置内存单元用来存储数据,如果有,则分配对应大小的内存单元。

  2. 如果没有足够的内存单元,Unity会触发垃圾回收GC机制来释放不再被使用的堆内存;如果垃圾回收GC之后有足够的内存单元,则进行内存分配。

  3. 如果进行垃圾回收之后仍没有足够的内存空间,则Unity会扩展内存堆的大小,重新向操作系统申请更大的内存空间,这是一个非常耗时的过程。

进行垃圾回收(GC Collect)时的操作

当堆上一个变量不再处于激活状态的时候,其所占用的内存并不会被立刻回收,不再使用的内存只会再GC调用的时候才会被回收。

GC运行时,会进行如下操作:

  • GC检查堆内存上存储的每个存储变量

  • 对每个变量检测其引用是否处于激活状态

  • 如果变量的引用不处于激活状态,则会被标记为可回收

  • 被标记的变量会被移除,其所占用的内存会被回收到堆内存上。

垃圾回收(GC Collect)的触发时间

主要由以下三个操作时会触发垃圾回收:

  1. 在堆内存上进行内存分配操作而当前内次不够时会触发垃圾回收来利用闲置的内存。

  2. GC的自动触发机制触发,不同平台触发机制不一样。

  3. 显式调用System.GC.Collect()方法。

  4. 当在托管堆内存上频繁进行内存分配且剩余内存单元不足时,GC会被频繁触发,这也就意味着频繁的堆内存分配和回收会触发GC频繁操作。

垂直同步(VSync)

垂直同步(VSync,Vertical
Synchronization)是一种显示设置,可以用来限制游戏的帧率来匹配显示器的刷新率,以防止图像撕裂。

简单来说,垂直同步的作用是用来防止画面撕裂的,因为画面的渲染并不是整个画面一起渲染的,是逐行\逐列进行渲染的,如果关闭垂直同步,而电脑的硬件性能跟不上的话,在画面\摄像机高速移动过程中会出现当前画面还没有渲染完毕就开始渲染下一个画面的情况,会导致画面撕裂。

垂直同步的开启和关闭主要区别在于游戏画面\摄像机是否处于高速运动状态。如果开启垂直同步,可以防止画面\摄像机在高速运动过程中画面被撕裂。

Unity3D中的帧率渲染机制

如果设置了VSync
Count的属性,将会忽略TargetFrameRate设置,游戏将使用VSyncCount的设置和平台默认的渲染率来确定目标帧率。例如,如果平台的默认渲染速率为60帧且VSyncCount的设置为2,则游戏将以每秒30帧作为渲染目标。

VSync Count(每帧之间的垂直同步数)
  1. Don’t’t Sync :不开启垂直同步

  2. Every V Black: VSync
    将游戏帧率同步到显示器的刷新速率,传统显示器刷新率为60HZ;即游戏帧率设置为60FPS/S;在一些120HZ和144HZ的显示器上,当设置VSync
    Count为Every V Blank时,目标帧率被限制在120FPS/S和144FPS/S.

  3. Every Second V Black:当把VSync Count设置为Every Scecond V
    Blank时,即目标帧率为显示器刷新率的一半;即(60Hz刷新率的显示器其目标帧率为30FPS/S).

TargetFrameRate(指定目标渲染帧率)

Application.targetFrameRate指示游戏以指定的帧率渲染。TargetFrameRate的默认值为-1,表示游戏以平台的默认帧率渲染。

  1. PC平台,默认帧率为平台硬件可实现的最大帧率。

  2. 移动平台,由于需要节省电池电量,默认帧率小于可实现的最大帧率。移动平台的默认帧率通常为30FPS/S.

  3. 所有移动平台可实现的最大帧率都有固定的上限,和移动端设备的屏幕刷新率有关(60Hz=60fps,40Hz=40FPS).

  4. IOS会忽略Quality.Setting.VSyncCount设置。

  5. 基于VR的平台,Unity将使用SDK指定的目标帧率并忽略游戏指定的帧率值。

  6. 设置了targetFrameRate的值不保证会实现帧率,其取决于平台的硬件规格。如果设置了QualitySettings.vSyncCount的属性,将会忽略/targetFrameRate/,而游戏将使用VsyncCount和平台的默认刷新率来确定
    目标帧率。例如:如果平台的默认渲染速率为每秒60帧且VsyncCount设置为2,则游戏将以每秒30帧作为渲染目标。

物理模拟(PhysX)

Unity内置的物理模拟引擎为Navida
PhysX物理引擎,来模拟物理世界中的一些效果,比如:重力、阻力、弹性、碰撞等,其中使用一些内置的组件来实现这些模拟,如:刚体、碰撞器、恒力、物理材质、铰链关节、弹簧关节等。

Unity3D提供了一个专门针对物理计算的刷新方法:FixedUpdate().FixedUpdate()和Update()的区别在于,两个函数处于不同的帧循环中,FixedUpdate()处于Physical循环中,而Update()的更新频率受场景对象的渲染影响,和游戏帧率有关,Update()中帧与帧之间的间隔时间是不相等且不固定的。而FixedUpdate()在每个帧渲染之间的时间是固定的。所以一些设计物理计算的过程,都需要在FixedUpdate()中进行。

  1. 将物理模拟的时间步长间隔设置合适的的大小。一般大于16ms,小于30ms.

  2. 谨慎使用网格碰撞器(MeshCollider),使用更加简单的BoxCollider\SphereCollider替代。

  3. 自己使用数学算法模拟替代真实物理模拟。

GPU优化

GPU性能影响因素

CPU通过底层图形API向GPU发送渲染指令,再通过硬件驱动程序发送给GPU设备,CPU产生的指令会放在命令缓冲区的队列中,这些指令由GPU接收逐一处理,直到命令缓冲区为空;只要GPU能再下一帧开始之前能够跟上指令的速度和复杂度,游戏就能够保证以稳定的帧率运行。如果GPU跟不上或者CPU的负载过高,会导致绘制的延迟,造成帧率的下降。

造成GPU渲染压力,往往有几个重要的性能指标:填充率、像素和几何复杂度
。如果能够找到一种方法来将更多的渲染器剔除,均可降低填充率、像素和几何复杂度的计算压力。

性能瓶颈判断

在移动平台,GPU性能本质上受填充率的限制(填充率=屏幕像素*着色器复杂度*过度绘制);

  • 过于复杂的着色器会是导致性能产生瓶颈的原因之一,尽可能简化着色器的复杂度可提高性能。

  • 如果降低纹理质量(Quality->Texture
    Quality)能提高游戏运行速度,表明游戏可能受到内存带宽的限制。可对纹理进行压缩、使用Mipmap、减小纹理大小等方式。

填充率

填充率:GPU每秒可以渲染到屏幕的像素数量(填充率=总像素数*Shader复杂度*OverDraw)如果游戏受到填充率的限制,那么说明我们的游戏每一帧输出到屏幕的像素数量超过了GPU承载限度。如果一个片元未通过任意测试(Alpha测试、模板测试、深度测试、混合测试),就被丢弃,可跳过性能消耗昂贵的绘制步骤,直接处理下一个片元。

过度绘制(OverDraw)

由于渲染对象的顺序问题,我们总是会重复的绘制一些相同的像素,将这以绘制过程称之为过度绘制,过度绘制越多,覆盖片元数据浪费的填充率也就越多。可通过Unity的Scene窗口下的OverDraw来观察填充率。

Unity中有两种类型的队列用于渲染对象:不透明对象和透明对象;不透明队列中渲染的队列可以通过Z-Test剔除片元。但是在透明队列中,仅通过Z-Test并不能解决这个问题,不管有多少对象挡在前面,都不能假设它们不需要被绘制,这就必然会导致大量的过度绘制(OverDraw)。Unity
UI对象通常在透明队列中进行渲染,这也是过度绘制产生的主要来源。

内存带宽

GPU VRAM
的某个部位将纹理拉入更低级别(更快)的内存中,就会消耗内存带宽。这个过程通常发生在纹理采样过程,片元着色器尝试选择匹配的纹理像素,以便在给定的位置绘制给定的片元。由于GPU多核心并行运算的特性,每个核心都可以访问显存相同的区域。如果需要被采样的纹理已经被存储在内核的本地纹理缓存中,会加快采样的速率,否则将需要从VRAM中提取纹理信息。这个操作会消耗一定数量可用的内存带宽。

如果内存带宽方面遇到瓶颈,GPU将继续获取必要的纹理文件,但整个过程受到限制,因为纹理缓存将等待数据获取后,才会处理给定的片元。GPU无法及时将数据推送到帧缓冲区中并渲染到屏幕上,整个过程都会被阻塞,帧速率也会被降低。

提升GPU渲染性能的方案

  1. 保证材质的数量尽可能的少,这样Unity可以更容易进行批处理。

  2. 使用纹理图集(包含子图像集合的大图像)代替多个单独的纹理;使用纹理图集可以保证纹理图集的加载速度更快、状态切换更少,且支持批处理。

  3. 如果使用纹理图集和共享材质,使用Renderer.sharedMaterial替换Renderer.material.

  4. 前向渲染的像素光源(Pixel
    Light)的成本很高,尽可能使用光照贴图替换实时光照。

  5. 调整Quality设置中的像素光源数量:本质上只需要主光源(Diretional
    Light)采用像素光(Per Pixel),其它Additional Light采用逐顶点(Per
    Vertex)光照模式。

  6. 避免使用镂空着色器(Alpha测试),保持透明(Alpha混合)屏幕覆盖率最小化。

  7. 尽量避免多个光源为单个网格体提供光照,减少着色器通道(阴影、像素光源、反射)的总数。

  8. 保证正确的渲染顺序。一般正常渲染顺序为:完全不透明对象渲染->Alpha测试对象->天空盒->Alpha混合对象。

  9. 屏幕后处理在移动端的使用成本非常高。

  10. 检查游戏是否受到GPU填充率的影响很简单,通过降低游戏运行的分辨率,观察游戏运行速度是否会加快。如果是:表明当前游戏的性能受限于填充率的影响。

  11. 降低着色器的复杂性:

    1. 避免使用Alpha Test 测试着色器,采用Alpha Blend混合着色器

    2. 使用简单、优化的着色器代码,如(URP中的Simple Lit)

    3. 避免在着色器代码中使用成本过高的数学函数(pow\exp\log\cos\sin\tan)

    4. 采用精度较低的数字精度(float\half\fixed)以获得最佳性能。

视锥体剔除(View Frustum Culling)

视锥体剔除是在Unity3D摄像机视野范围内针对场景对象进行剔除的一种方式,其基本思想是:判断对象是否在相机的视锥体内(包含相交),不在则将其剔除,在CPU提交DrawCull绘制时,不会将此对象传递到GPU进行渲染。其主要的判断原理是:根据对象的BoundBox(边界包围盒)与摄像机视锥体的六个裁剪平面的相交关系来判断对象是否在视锥体范围内。

由于视锥体剔除的对象针对的是对象,即场景中的网格体;根据网格体的AABB包围盒检测视锥体与网格碰撞体的相交情况来进行检测。所以,在做合并大网格进行DrawCall优化时需要尤其注意;如果对象的网格体过大,其相应的BoundingBox也会相应变大,做视锥体剔除时,视锥体的每个面都会和BoundingBox相交,导致视视锥体无法剔除对象,加大GPU的渲染压力。

(图:视锥体剔除)

遮挡剔除(Occlusion Culling)

遮挡剔除,顾名思义就是当对象被其它对象阻挡而不能被摄像机看到时,使用遮挡剔除功能会禁用对象的渲染。Unity中对象的绘制机制为,距离摄像机最远的对象最先绘制,而距离摄像机较近的对象则在先前对象的基础上进行绘制,这种绘制方式称之为过度绘制(OverDraw)。遮挡剔除与视锥体剔除不同。视锥体仅禁用摄像机视野之外的渲染器,而不会禁用由过度绘制而被隐藏的任何对象的渲染器。遮挡剔除和视锥体剔除可以同时工作。总之:视锥体剔除可以用于解决部分过度绘制(OverDarw)的问题。



(图左:未执行遮挡剔除;图右:执行遮挡剔除)


(图:未执行遮挡剔除,过度绘制(OverDraw)严重)

未执行遮挡剔除,主要性能指标如下:

指标名称 指标性能
Batches 296
Triangles 428.9k
Verts 616.3k
SetPassCalls 247
ShadowCasters 2706

(图:执行遮挡剔除,过度绘制(OverDraw)大幅降低)

执行遮挡剔除,主要性能指标如下:

指标名称 指标性能
Batches 62
Triangles 150.2k
Verts 210.0k
SetPassCalls 53
ShadowCasters 1820

明显看出使用遮挡剔除技术之后:各项主要的性能指标都有明显的下降。

遮挡剔除使用参考手册:https://docs.unity3d.com/cn/2018.4/Manual/OcclusionCulling.html

多层次细节LOD(Level Of Detail)

LOD也称之为多层次细节,根据物体在游戏画面中所占视图的百分比来调用不同复杂度的模型,简单来说,就是当一个物体距离摄像机较远时,使用低模进行渲染,当摄像机距离摄像机比较近时使用高模进行渲染。这是优化游戏运行效率的常用手法,缺点就是内存占用较大。一般用来解决游戏运行时的流畅度问题,采用的是空间换时间的方式。

实现方式
  1. 准备三组不同精度的模型:高精度模型、中精度模型、低精度模型。

  2. 根据显示效果,调整LODGroup的显示距离。

优缺点
  1. 使用LOD,由于需要三套不同精度的模型,会增加内存负载。

  2. 由于可根据与摄像机的距离实时切换不同精度的模型,当摄像机切换到大视角或者摄像机在场景移动过程中,CPU向GPU传递不同精度的三维模型,可极大的降低GPU端的渲染压力。

LOD解决方案
  1. 美术制作三套不同精度的三维模型(高模\中模\低模),手动制作LOD.

  2. 使用Unity3D的解决方案

    1. Unity HLOD System.

    2. AutoLOD\Mantis LOD Editor\Mesh Baker LOD.

    3. Mesh Combine Studio2\Amplify Impostors.

烘焙光照贴图(Mixed Lighting)

Unity3D的光照计算处于渲染流水线中的片元着色阶段。计算机图形学中的光照技术是为了模拟真实世界的光照计算,通过各种方法计算光源发射的光线和物体之间交互作用后到达视线的能量强弱。

Unity3D光照模型及解决方案

Unity3D使用的是GI全局光照模型:全局光照=直接光照+间接光照。

简介光照不仅会计算光源和物体,还会计算光的能量在物体之间的间接传递,得到更加真实的渲染效果。一般来说,全局光照技术是为了更好的计算间接光照的算法,所以我们也将Indirect
ighting称之为GI.

(图:U3D光照模型解决方案)

  1. 光照和阴影的渲染过程

对象的渲染很少能够在一个步骤中完成,主要原因是光照和阴影。这些任务通常在片元着色器的多个(Pass)中进行处理,对于对各光源中每一个都处理一次,最后将渲染结果进行合并,以及使用更多的灯光效果。

阴影信息的收集需要执行多个复杂的计算过程,首先需要未场景设置阴影投射器和阴影接收器,分别用来创建和接收阴影;然后,每次渲染阴影的接收器时,GPU都会从光源的角度将阴影投射器对象渲染成纹理(深度纹理),目标是手机每个片元的距离信息。对阴影接收器进行同样的动作,除了阴影投射器和光源重叠的片元之外,GPU可将片元的颜色渲染得更暗,因为这类片元位于阴影投射器产生得阴影下。

简而言之,主要分为两个过程:

  • 一个是灯光空间,用相机渲染一张深度图,把深度值写入其中。

  • 而是渲染过程中:把接收阴影得物体从模型空间变换到灯光空间中,得到深度值,将得到得深度值拿去和深度缓冲中得值进行比较,如果当前Z值大于胜读纹理中保存得值,就说明这个片元在灯光空间中被遮挡了。

光照和阴影往往会消耗大量得计算资源,我们需要为每个顶点提供法矢方向,来确定光线如何从表面反射出去,同时需要附加顶点颜色属性,来应用一些额外得着色,这要求CPU和前端提供更多传递得信息。由于片元着色器需要多次传递信息来完成最终得渲染,因此GPU光栅化阶段在填充率(绘制、重绘、合并像素)和内存带宽将处于高负荷状态。

  1. Unity3D光源类型以及光源数量支持

Unity3D URP渲染管线中主要包含以下几种光源类型:Directional Light/Point
Light/SpotLight/Area Light;

以下表格对比除Dirrectional主方向光之外的其它附加光源,AdditionalLight

平台 光源类型 支持光源数量(场景中) 支持数量(摄像机裁剪后)
Direct3D 11/Switch Point Light 无限制 256 个
Spot Light 无限制 256 个
Area Light 无限制 256 个
OpenGL3.0/Metal/Vulkan Point Light 无限制 32 个
Spot Light 无限制 32 个
Area Light 无限制 32 个
每个渲染对象支持的光源数量上限 D3D 8个
OpenGL ES 2.0 4个
OpenGL ES 3.0 8个

在UnityEngine.Rendering命名空间下有一个PerObjectData类,它负责持有每帧进行视锥体裁剪之后视野内的逐对象渲染参数,在渲染对象时将这些对象传递给GPU,其中就包括当前的光源数据。相机对场景对象进行视锥体裁剪之后,即渲染管线中的应用阶段;相机主要对光源做如下两件事;

  1. 从所有的方向光中挑一个最亮的光作为主光源,即Directional Light.

  2. 将剩下的其它类型(Additional
    Light)的光源按照光照强度进行排序,放进一个数组里;准备好光源数据之后,将光源分配给其需要渲染的对象。

如果摄像机视锥体范围内的光源数量超过当前平台所能支持的最大光源数量,则根据排序规则,多余的光源会被忽略。

(图:视锥体裁剪之后场景光源数据)

Baked Global ILLumination (烘焙全局光照)
  1. Baked Indirect (烘焙间接光照)

Baked Indirect
:烘焙间接光照,顾名思义将间接光照烘焙到光照贴图中,使用MixedLighting会为游戏对象投射实时阴影,阴影投射距离根据渲染管线Shadow
Distance的设定进行。将场景中的LightingMode设置为Baked
Indirect时,混合光源的行为如下:

  1. 为游戏对象提供直接光照。

  2. 烘焙间接光照(使用光照探针)

  3. 在Shadow Distance范围内,为动态游戏对象提供实时阴影。

  4. 在Shadow Distance范围内,为静态游戏对象提供实时阴影。

    1. 优缺点(Bake Indirect)
  5. 所有对象的阴影都是实时的,可能会影响游戏性能。可通过设置Shadows
    Distance属性来降低影响。

  6. 可在运行时修改光源属性,由于只是烘焙间接光照,直接修改MixedLight的属性会影响场景中的实时光照,但不会影响已经烘焙好的间接光照。

    1. Subtractive (减性烘焙)

      Subtractive:减式光照模式,基于这种光照模式,场景中所有的混合光源(Mixed
      Lighting)都提供直接光照和间接光照。Unity将静态对象的投射的阴影烘焙到光照贴图中。除了烘焙的光照阴影,Mixed
      Lighting还会为动态的游戏对象提供实时阴影。

      由于静态对象的阴影被烘焙到了光照贴图中,所以Unity在运行时缺少将烘焙阴影和实时阴影准确的结合在一起所需的信息。但是,通过Unity提供的Realtime
      Shadow
      Coor属性可以减少来自光照贴图的影响,从而在烘焙阴影和实时阴影之间创建正确的混合视觉效果。

      Subtractive特别适用于低端硬件设备,因为低端硬件设备更加注重运行性能,并且只需要一个实时阴影投射光源,适合卡通风格。基于Subtractive模式进行光照烘焙,静态对象不接受高光。

      将场景光照模式设置为Subtractive时,其光照行为如下:

动态游戏对象将接收

  1. 实时直接光照。

  2. 烘焙间接光照(使用光照探针)。

  3. 在Shadow Distance 距离内,为动态游戏对象提供实时阴影。

  4. 静态对象实时阴影(光照探针)

静态游戏对象将接收

  1. 烘焙直接光照(光照贴图)

  2. 烘焙直接光照(光照贴图)

  3. 静态游戏对象的阴影烘焙(光照贴图)

  4. 在Shadow Distance 距离内,为动态游戏对象提供实时阴影。

    1. ShadowMask (阴影遮罩)

URP 10 以下版本不支持ShadowMask光照模式。

类似于Baked
Indirect光照模式,Shadowmask光照模式将实时直接光照与烘焙间接光照结合在一起。但是,ShadowsMask光照模式与Maked
Indirect光照模式的不同之处在于渲染阴影的方式不同。Shadowmask光照模式允许Unity在运行时结合烘焙阴影和实时阴影,并允许渲染远处的阴影,实现这一点的方法是使用”阴影遮罩”的附加光照贴图纹理并将附加信息存储在光照探针中。

Shadowmask质量设置

  1. Distance Shadowmask:以更高的性能成本提供更高保真度的阴影。

  2. Shadowmask:以更低的性能成本提供更高保真度的阴影。

GPUInstance

使用GPUInstance可以使用少量的绘制调用(DrawCall)一次性渲染同一网格的多个副本,它对于绘制诸如建筑物、树木和草地之类的在场景中重复出现的对象非常有用。GPU实例化在每次绘制调用时仅渲染相同的网格,但每个实例可以具有不同的参数(基于CBuffer
常量缓冲),可以增加变化并减少外观上的重复。基于GPUInstance实例化技术可以降低每个场景使用的绘制调用数量,可以显著提高项目的渲染性能。

GPUInstance的使用限制
  1. Unity自动选取需要实例化的网格渲染器(MeshRender)和Graphics.DrawMesh调用,注意:不支持SkinnedMeshRenderer(网格蒙皮)。

  2. Unity
    仅在单个GPU实例化绘制调用中批量处理那些共享网格和相同材质的游戏对象;使用少量网格和材质可以提高实例化的效率,如需要创建变体,需要修改着色器脚本为每个实例添加数据(CBuffer
    常量缓冲)。

GPUInstance支持平台
支持平台 支持API
Windows DirectX11和DirectX12
Windows/Macos/Linux/IOS/Android OpenGL Core 4.1+/OpenGL ES 3.0+
macOS和IOS Metal
Windows/Linux/Android Vulkan
PlayStation和Xbox one
WebGL WebGL 2.0 及以上API
URP针对网格材质的GPUInstance
  1. 相同材质(Same Material)和相同属性(Same Property)值的网格体
SRP Batcher On Material GPU Instance OFF SRP Batched
SRP Batcher On Material GPU Instance On SRP Batched (如果开启SRP Batched,GPUInstancing究竟被忽略)
SRP Batcher Off Material GUP Instance On GPUInstance
  1. 不同材质(Different Material)和不同属性(Different Property)值的网格体
SRP Batcher On Material GPU Instance OFF SRP Batched
SRP Batcher On Material GPU Instance On SRP Batched (如果开启SRP Batched,GPU Instancing究竟被忽略)
SRP Batcher Off Material GUP Instance On 无法启用SRP Batcher 和GPU Instance (因为材质不同)
  1. 相同材质(Same Material)和通过Material PropertyBlock设置不同属性(Same
    Property)值
SRP Batcher On Material GPU Instance OFF 无法启用SRP Batched和GPU Instance (因为MaterialPropertyBlock值不一样)
SRP Batcher On Material GPU Instance On 无法启用SRP Batched和GPU Instance (因为MaterialPropertyBlock值不一样)
SRP Batcher Off Material GUP Instance On 无法启用SRP Batcher 和GPU Instance (因为属性值未被实例化)

Unity传统的基于MaterialPropertyBlock的GPU Instance在URP不再适用。

URP 不兼容传统管线GPU Instance的原因:

  1. URP 中Shader 中的Properties 需要使用基于SRP Batcher
    定义的宏”CBUFFER_START(UnityPerMaterial)

  2. GPU Instance 需要使用“UNITY_INSTANCEING_BUFFER_START(MyProps)定义

内存优化

Mono内存优化(栈\托管堆)

  1. 尽量避免重复的Instantiate和Destroy
    Object。对应需要频繁创建的对象;应使用ObjectPool对其进行缓存。频繁的实例化对象,会频繁触发GC调用。

  2. 使用StringBuilder进行字符串操作,避免直接使用string进行字符串拼接。

  3. 避免再Update函数中进行组件\标签\对象的查找,如:GameObject.FindWithTag()\GetComponent();可以再Start()函数中将需要查找的组件预先缓存起来。

  4. 数组的声明尽量使用Gameobject[]\Transform[],避免使用C#
    自带的ArrayList或Array类,子类数组再进行内存分配的时候会进行大量的拆箱、装箱操作,频繁进行托管堆栈的内存操作。

  5. 单场景使用单例模式,声明的资源实例,如:VideoClip\Texture\GameObject\Sprite时,当卸载场景时需要手动释放场景资源;

void OnDestroy(){

videoClip=null;

sprite=null;

texture=null;

instance=null;

}

Unity3D资源内存

AssetBundle

AssetBundle是一种用来动态加载资源的资源加载方式,AssetBundle一般从网络地址下载或者从本地磁盘中加载。

Assetbundle系统提供了一种压缩文件格式,可以把一个到多个文件进行索引和序列化。Assetbundle和传统的压缩包类似,由两个部分组成:包头和数据段。包头包含Assetbundle相关的信息,比如:标识符、压缩类型和内容清单。数据段包含通过序列化Assetbudle中的Assets而生成的原始数据,如果指定LZMA为压缩方案,则对所有序列化Asset后的完整字节数组进行压缩,如果指定LZ4压缩,则单独压缩单独的Assets的字节,如果不压缩数据段将保持原始字节流。

(图:Assetbundle加载卸载过程)

AssetBundle加载机制

Assetbundle可以通过不同的API进行加载,针对Assetbundle行为的不同,其API调用的形式也不相同。

  1. Assetbundle的压缩方式:LZMA\LZ4\未压缩

  2. Assetbundle的加载平台。(UnityWebRequest\WWW\Memory\LocalFile)

    Assetbundle加载API:

  3. Assetbundle.LoadFromMemory(Async optional)

  4. AssetBundle.LoadFromFile(Async oprional)

  5. UnityWebRequest’s DownloadHandlerAssetBundle

  6. [WWW.LoadFromCacheOrDownLoad(Unity](http://WWW.LoadFromCacheOrDownLoad(Unity)
    5.6 or older)

    1. AssetBundle.LoadFromMemory(Async)

LoadFromMemory(Async)是从托管代码的字节数组里加载Assetbundle.也就是说需要提前用其它方式将资源的二进制数组加入到内存中,然后该接口会将数据源从托管代码字节数组中复制到新分配的、连续的本机内存块中。不建议使用此API进行Assetbundle加载,由于此API消耗的最大内存是AssetBundle本身的两倍:本机内存中的一个副本和LoadFromMemory(Async)从托管字节数组中复制的一个副本。

通过此API进行Assetbundle加载的资产将在内存中冗余三次:第一次在托管代码的字节数组中,第二次在Assetbundle的栈内存副本中,第三次在GPU或系统内存中,用于Asset本身。

  1. AssetBundle.LoadFromFile(Async)

LoadFromFile是一种高效的API,用于从本地存储加载未压缩或者LZ4压缩格式的Assetbundle。

在桌面、移动平台上,API将只加载Assetbundle的头部数据,并将剩余的

数据段资源留在磁盘上。

Assetbundle的Objects会按需加载,比如:调用Assetbundle.Load加载方法或者InstanceID被间接引用时,不会消耗过多的内存。

但在Editor环境下,API还是会把整个AssetBundle加载到内存中。Editor环境中加载过程和Assetbundle.LoadFromMemoryAsync一样。

需要注意的是:这个API只针对未进行压缩或者LZ4压缩格式的Assetbundle。因为基于LZMA压缩,是对整个生成后的数据包进行压缩的,所有在未解压前是无法拿到AssetBundle的头信息的。

  1. AssetbundleDownloadHandler

DownloadHandlerAssetBundle的操作是通过UnityWebRequest的API来完成的。基于UnityWebRequest
API
能精确的指定Unity应如何处理下载的数据,并允许开发人员消除不必要的内存使用。使用UnityWebRequest下载Assetbundle的最简单方法是调用UnityWebRequest.GetAssetBundle.其下载过程如下:将下载的数据流存储到一个固定大小的缓冲区,然后根据下载处理程序的配置方式将缓冲数据放到临时存储或者Assetbundle缓存中。所有的这些操作都发生在非托管代码中,消除了增加堆内存的风险,此外,该下载处理程序并不会保留所有下载字节的栈内存副本,从而减少了下载Assetbundle的内存开销。LZMA压缩的Assetbundle在下载和缓存是更改为LZ4压缩。如果将缓存信息提供给UnityWebRequest对象,一旦有请求的Assetbundle以及存在于Unity的缓存中,即Assetbundle可以被立即实例化,并且此API的行为将会与Assetbundle.LoadFromFile相同。

AssetBundle卸载
  1. Assetbundle.Unload(false)是释放Assetbundle文件的内存镜像,不包含Load创建的Asset内存对象。

  2. Assetbundle.Unload(true)是释放Assetbundle文件内存镜像并销毁所有用Load创建的Asset内存对象。


(图:Assetbundle加载卸载过程)

Texture/Material

针对不同的平台采用不同的纹理压缩方案,使用压缩纹理可以节省大量内存和加快资源读取速度。。

平台 压缩类型
Android OpenGL 2.0 ETC(default)
Android OpenGL 3.0 ETC2.0
IOS PVRTC
Windows DXT

2D 纹理如果没有必要,请关闭mimap选项,关闭read/write选项

Mesh
  1. 减少网格顶点数量,降低模型精度,进行模型资源压缩和优化。

  2. 合并网格顶点(MeshBake网格、材质、贴图合并)

UGUI优化

Sprite打包(Texture Packer和Unity Sprite Packer)

Rebuild和Rebatch调用优化

UGUI将UI的渲染分为两部分,对Mesh的操作称之为Rebatch,对Material和Layout的操作称之为Rebuild;UGUI的性能消耗主要集中在这两部分。影响UI运行性能的原因。

  • GPU片段着色器使用率过高(即过度使用填充率)。

  • CPU花费过多时间在Canvas的批处理合并上。

  • Canvas批处理次数重建次数过多。

  • CPU花费过多时间来生成网格顶点。

Rebatch(重合批)
Rebatch触发原因

Rebatch发生在C++层面,由Canvas对当前Canvas下的UI节点进行分析,并形成一个最优合并批次的过程,节点的数量过多会导致合批的耗时较长。对应调用SetVerticesDirty,当一个Canvas中包含的Mesh发生改变时就会触发,例如:SetActive\Transform改变\颜色改变\文本内容改变等,性能消耗点主要在于对Mesh按照深度和重叠情况进行排序、共享材质检测等。

Batch以Canvas为单位,同一个Canvas下的UI元素最终都会被Batch到一个Mesh中,合批前,UGUI根据UI材质以及UI的渲染顺序进行重排,在不改变渲染结果的前提下,尽可能将相同材质的UI元素合并在同一个SubMesh中,以减少DrawCall调用。合批只在U元素发生变化时进行,合成的Mesh越大,耗时越长。Rebuild对Canvas下的所有UI元素都生效,不论是UI否有变动。

针对Rebatch的优化方法
  • Canvas动静分离,将经常需要更新的UI和不动的UI分离到不同的Canvas下。

  • 减少节点的层次和数量,提升合批速度和效率。

  • 使用相同材质的UI时尽量保持深度相同,对合批算法友好。

  • 修改Image的Color属性,其本质时修改顶点的颜色,会引起网格Rebatch,同时触发Canvas.SendWillRenderCanvas.

Rebuild(重绘)
Rebuild触发原因

Rebuild发生在C#层,是指UGUI库中的Layout组件调整RectTransform尺寸、Graphic组件更新Materail以及Mask执行Cull的过程,耗时和发生节点数量基本成正相关。

  • 更改LayoutGroup(Horizontal/Vertical/Grid)的直接子节点,并且子节点的基类型为Graphic时,会触发SetLayoutDirty。

  • 改变Graphic的大小、旋转以及文字的变化、图片更换等修改会触发SetMaterialDirty。

针对Rebuild的优化方法
  • 减少Layout的频繁使用,在编辑器中布好局之后,删除相关布局组件。

  • Canvas动静分离,按类型划分不同的Canvas.

UGUI通用优化方法

  • 不要使用空Image,只接收事件不显示的对象,继承自Graphic

  • 不显示的对象,不使用SetActive(false),而是设置CanvasGroup的Alpha为0,Scale为0,这样VBO顶点缓冲对象不会被清楚,或者设置CanvasRenderer.Cull为True.

  • 分离事件相机和UI渲染相机,设置Canvas的渲染模式为:World Space或者Screen
    Space Camera.

  • 减少Mask组件的使用,采用RectMask2D代替

  • 替换原生文字组件(TextMeshPro)

  • 压缩图片大小,降低内存占用

  • UI打包图集\动态图集

  • 简化UI结构,减少空节点数量\层级

Todo(附录)

Unity渲染顺序从近到远,根据渲染对象的排序,会为每一个渲染对象的每一个材质,生成一个渲染批次的batch,在不考虑动态批处理和静态批处理的情况下,总的batch量就是每个渲染对象所包含材质的总和。

渲染方案(URP)

根据物理学的定理:人眼能够看到物体的颜色,有黄色、红色、白色、紫色,波长从红光到紫光,白光是由多种颜色的光复合而成,如我们所见的太阳光,由赤、橙、黄、绿、青、蓝、紫等构成;人的眼睛之所以能够看到世界中各式各样的颜色,并不是由于物体本身会发光,而是由于物体反射了对应颜色的光。同样。在计算机图形学中,对物体光照的计算是非常重要的,通过为每个顶点计算一次光照颜色,然后再通过顶点所在多边形覆盖的区域对像素颜色进行插值,其光照值取决于光线角度,表面法线和观察位置。逐像素光照计算,即在片元着色器中进行光照计算,对每一个像素执行光照计算,无论是逐顶底光照还是逐像素光照,都造成GPU的运算负载。计算机图形学中,执行光照渲染的方案主要分为正向渲染和延迟渲染两大方案。

正向渲染(Forward Rendering)

URP中,URP实现了一个渲染循环告诉Unity如何渲染一帧得数据。

(图:Unity URP RenderLoop)

特性

  • 先执行光照着色计算,再执行alpha测试、模板测试、混合测试。Unity中,可对场景中的光源类型进行光照类型配置(逐顶点、逐像素执行光照着色计算)

  • 正向渲染中:所有的光照在单个Pass中进行;GI+自发光+雾

  • 进行光照计算的过程:根据光照的强度以及重要程度进行排序,对每一个光源都单独执行一次光照着色计算。

  • 使用正向渲染的物体,其光照计算复杂度与光源数量成正比,渲染m个物体在n个光源下的着色,其复杂度O(m*n)

  • 正向渲染适用于实时光源数量较少的场景。

渲染流程

(图:正向渲染流程)

图形渲染管线中,深度测试是用来消除场景中不可见的面,比如图元的遮挡,由于三维场景的Z轴层次关系,一个物体可能覆盖到另外一个物体上,那么对被覆盖住的物体执行计算是没有任何必要的,因为它根本不会在屏幕上显示出来。由于正向渲染的特性,先执行着色计算,再进行深度测试,会导致一些不需要再显示的片元也参与了着色计算,导致渲染的性能浪费。

延迟渲染(Deferred Rendering 当前版本不支持)

特性

  • 先执行深度测试,再执行着色计算。

  • 相比正向渲染过程,将光照着色计算过程延后。先对片元执行深度测试,将被片元遮挡的其它片元从渲染管线中剔除和丢弃,最后再对有效的片元执行光照着色计算。

  • 对片元执行几何变换、深度测试之后,将指定的片元以及相关信息存储到G-Buffer中,根据现有光源数量以及重要程度,分别对G-Buffer中的片元进行光照计算。

渲染流程

(图:延迟渲染流程)

延迟渲染的光照着色过程和正向渲染的光照着色过程不同,延迟渲染首先将简单的几何信息(顶点坐标、法向量、纹理坐标)等存储到中间缓冲区中;即G-Buffer(几何缓冲中),着色的过程只需要渲染出一个屏幕大小的矩形,使用G-Buffer中的几何数据与光照模型进行光照计算。

总结

正向渲染与延迟渲染各自有各自的优势;两者之间最大的区别在于允许参与光照计算的光源数量的支持程度;同样,有光源意味着,光源照射物体会产生阴影,阴影的计算是很消耗图形设备的性能的,不仅会造成CPU的计算负载,同时也会加大GPU的渲染压力。

光照方案(URP)

混合光照+反射探针+光照探针

全局光照=直接光照+间接光照+自发光

  1. 主光源采用Mixed方式提供:直接光照+场景对象实时阴影(不论对象动态还是静态)

  2. 间接光照采用Bake
    Indirect光照烘焙方案:将光照信息存储到光照贴图和光照探针中。(间接光照无法提供实时影音)

  3. 环境光照采用:Gradient
    模式(最终环境光照信息也会被烘焙到光照贴图和反射探针中存储起来,为场景对象提供环境光照)0.15

  4. 反射探针:根据场景内容和需求,可选择烘焙模式和手动刷新模式(为场景对象提供周围环境的光照信息)

光照方案 方式 说明
主光源(Directional Light) Mixed 提供直接光照+实时阴影+高光反射
烘焙方案(Bake) Baked Indirect 主光源提供直接光照,间接光照烘焙到光照贴图和光照探针中
光照探针(Light Probe) Bake 记录间接光照的光照信息,为动态加入的对象提供间接光照
反射探针(Reflection) Bake或Awake或脚本烘焙 存储场场景中周围物体的相互反射信息,为对象提供环境反射信息
环境光(Enviorment) Gradient 为场景提供间接光照(被烘焙到光照贴图+光照探针中)

低端硬件设备光照方案

  1. 无需为静态游戏对象渲染阴影和提高光照。

  2. 为动态游戏提高直接直接光照和实时阴影。

  3. 无法针对静态对象表现高光效果。

光照方案 方式 说明
主光源(Directional Light) Mixed 为动态游戏对象提供实时直接光照和实时阴影渲染。
烘焙方案(Bake) Subtractive 为静态对象提供直接光照+间接光照烘焙,动态对象接收实时直接光照并将为动态对象得阴影渲染在静态对象上。
光照探针(Light Probe) Bake 记录间接光照的光照信息,为动态加入的对象提供间接光照
反射探针(Reflection) Bake或Awake或脚本烘焙 存储场场景中周围物体的相互反射信息,为对象提供环境反射信息
环境光(Enviorment) Gradient(Bake) 为场景提供间接光照(被烘焙到光照贴图+光照探针中)

中、高端硬件设备光照方案

光照方案 方式 说明
主光源(Directional Light) Mixed 提供直接光照+实时阴影+高光反射
烘焙方案(Bake) Baked Indirect 主光源提供直接光照,间接光照烘焙到光照贴图和光照探针中
光照探针(Light Probe) Bake 记录间接光照的光照信息,为动态加入的对象提供间接光照
反射探针(Reflection) Bake或Awake或脚本烘焙 存储场场景中周围物体的相互反射信息,为对象提供环境反射信息
环境光(Enviorment) Gradient/Skybox 为场景提供间接光照(被烘焙到光照贴图+光照探针中)

MeshBake资源(网格\材质\贴图)烘焙方案

UnIty3D资源制作标准参考

资源类型 说明建议
场景 同屏三角面数控制在10万面以内(Triangle)
Additional光源数量控制在50盏以内(点光、聚光等)
粒子系统数量控制在50以内
尽量使用效率较高得Shader(如Simple Lit)
尽可能采用共用材质(shareMaterial)、动画
尽量采用压缩纹理(Android ETC2.0/IOS )
纹理压缩 平台 颜色模型 Normal Hight Low
Windows RGB RGB24 位 RGB_C_DXT1 RGB(A)_C_BC7 RGB_C_DXT1
RGBA RGB32 位 RGBA_C_DXT5 RGB(A)_C_BC7 RGBA_C_DXT5
Android RGB RGB24 位 RGB_C_ETC RGB_C_ETC RGB_C_ETC
RGBA RGBA32位 RGBA_C_ETC2 RGBA_C_ETC2 RGBA_C_ETC2
纹理选项 尽量对纹理采取压缩方案
保证纹理尺寸满足长宽位2得n次幂,即256*256\1024*1024\2048*2048
针对目标终端内存情况决定是否启用mipmap(会提升渲染效率,但会增加内存消耗);如果没有必要,请关闭,贴图得Read/Write选项
音频 时间长的音频采用.ogg或者.mp3压缩格式
时间短的音频采用.wav或.aif未压缩格式
灯光类型 控制光源数量(Additional Light在摄像机裁剪之后最多渲染50盏)
限定Per Pixel 光照类型,Additional Light尽量采用Per Vertex光照
                                     | RGBA     | RGBA32位 | RGBA_C_ETC2 | RGBA_C_ETC2  | RGBA_C_ETC2 |

| 纹理选项 | 尽量对纹理采取压缩方案 | | | | | |
| | 保证纹理尺寸满足长宽位2得n次幂,即256*256\1024*1024\2048*2048 | | | | | |
| | 针对目标终端内存情况决定是否启用mipmap(会提升渲染效率,但会增加内存消耗);如果没有必要,请关闭,贴图得Read/Write选项 | | | | | |
| 音频 | 时间长的音频采用.ogg或者.mp3压缩格式 | | | | | |
| | 时间短的音频采用.wav或.aif未压缩格式 | | | | | |
| 灯光类型 | 控制光源数量(Additional Light在摄像机裁剪之后最多渲染50盏) | | | | | |
| | 限定Per Pixel 光照类型,Additional Light尽量采用Per Vertex光照 | | | | | |

Unity3D最全性能优化参考手册(渲染、代码、UI)相关推荐

  1. 【Android 性能优化】布局渲染优化 ( CPU 渲染优化 | 减少布局的嵌套 | 测量布局绘制时间 | OnFrameMetricsAvailableListener | 布局渲染优化总结 )

    文章目录 一. 减少布局嵌套 二. 布局渲染时间测量 1. FrameMetrics 使用流程 2. FrameMetrics 参数解析 3. FrameMetrics 代码示例 三. 布局渲染优化总 ...

  2. 【Android 性能优化】布局渲染优化 ( GPU 过度绘制优化总结 | CPU 渲染过程 | Layout Inspector 工具 | View Tree 分析 | 布局组件层级分析 )

    文章目录 一. GPU 过度绘制优化总结 二. CPU 渲染过程 三. CPU 渲染性能调试工具 Layout Inspector 四. Layout Inspector 组件树 DecorView ...

  3. 【Android 性能优化】布局渲染优化 ( 过渡绘制 | 背景设置产生的过度绘制 | Android 系统的渲染优化 | 自定义布局渲染优化 )

    文章目录 一. 背景设置产生的过度绘制 二. Android 系统的渲染优化 1. 透明组件数据传递 2. GPU 存储机制 3. Android 7.0 之后的优化机制 三. 自定义布局渲染优化 一 ...

  4. 【前端性能优化】浏览器渲染原理与性能优化

    目录 1. 浏览器渲染基本步骤 2. 构建DOM树.CSSOM树 3. 构建渲染树 4. 计算渲染树的布局 5. 将布局渲染到屏幕上 6. 渲染优化 1. 浏览器渲染基本步骤 浏览器主要有以下步骤: ...

  5. MySQL史上最全性能优化方式

    MySQL有哪些性能优化方式?这个问题可以涉及到 MySQL 的很多核心知识,就像要考你计算机网络的知识时,问你"输入URL回车之后,究竟发生了什么"一样,看看你能说出多少了. 所 ...

  6. C#实用杂记-EF全性能优化技巧

    原文链接:http://www.makmong.com/947.html#comment-31 EntityFramework 优化建议 2016年1月15日 下午4:54 LEILINKANG En ...

  7. Next.js性能优化之ISR渲染入门和原理探索

    前言 术语说明: SSR -- 服务端渲染 SSG -- 静态生成 ISR -- 增量静态化 Date Fetch 函数 -- 本文特指服务端数据获取的几种函数 getStaticProps . ge ...

  8. 全网最全性能优化总结!!(冰河吐血整理,建议收藏)

    大家好,我是冰河~~ 随着互联网的高速发展,互联网行业已经从IT时代慢慢步入到DT时代.对于Java程序员的要求越来越高,只是单纯的掌握CRUD以不足以胜任互联网公司的相关职位,大量招聘岗位显示:如果 ...

  9. iOS最全性能优化(下)

    续 性能优化(中) 22. 加速启动时间 快速打开app是很重要的,特别是用户第一次打开它时,对app来讲,第一印象太太太重要了. 你能做的就是使它尽可能做更多的异步任务,比如加载远端或者数据库数据, ...

  10. iOS最全性能优化(中)

    续 性能优化(上) 9. 重用和延迟加载(lazy load) Views 更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的ap ...

最新文章

  1. 序列化/反序列化,我忍你很久了,淦!
  2. 【约束布局】ConstraintLayout 13 种相对定位属性组合 ( 属性组合 | 用法说明 )
  3. 美团O2O排序解决方案——线下篇
  4. 有米android sdk,有米积分墙Android SDK开发者常见问题
  5. 三菱fx5u编程手册_实用分享 | 三菱FX 5U特点是什么?
  6. jQuery Object 和 HTML Element间的转换
  7. sqlite工具类 java_Java之泛型、集合工具类
  8. java web 开发之写在前面(0)
  9. 10-10-030-简介-Kafka之数据存储
  10. -分类数组-创建//修改(添加/改变原有/合并/删除)分类数组(categorical)
  11. ubuntu下有没有类似于imagewatch的软件_大家有没有什么好的app推荐下,学习的类似timing小众点的?...
  12. mysql 07001_MySQL迁移文件的小问题
  13. StanfordDB class自学笔记 (6) 关系代数
  14. 4万字的“整洁三部曲”干货,全浓缩在这一篇里了
  15. Idea通过svn更新项目失败报 Node remains in conflict
  16. 泽风大过:改过自新;坎为水:坦然面对
  17. 搭建Kubernetes多节点集群
  18. 小说作者推荐:焦糖冬瓜合集
  19. 国风雅韵之琴瑟(页面文章不知道放哪,于是放CSDN当跳转链接系列QWQ Sorry辣~)
  20. PHP开发之-微信网页授权获取用户基本信息

热门文章

  1. usb转232串口线驱动android,usb-rs232线驱动下载、Z-tek usb转串口驱动 usb转串口驱动...
  2. 计算机控制面板没,没有nvidia控制面板,手把手教你电脑没有nvidia控制面板
  3. 计算机控制技术课程设计温度控制系统,计算机控制技术课程设计PWM温度自动控制系统的设计...
  4. Unity开发——CPU优化篇
  5. Glide在github上的jar包下载方法
  6. 全球DEM下载 90米、30米、12.5米等各种精度DEM数据
  7. 斐讯k1潘多拉专版固件_斐讯路由器K2刷机-斐讯k1-k2华硕及潘多拉固件下载__飞翔下载...
  8. 向android模拟器中复制文件报out of memory错误解决
  9. 用计算机算cos1,cos计算器(数学三角函数计算器)
  10. DB2数据库添加 更改字段