type
status
date
slug
summary
tags
category
icon
password
DrawCall 和 Batch 的区别
DrawCall : CPU发送渲染命令给GPU
如:glDrawElements(OpenGL) / glDrawArrays(OpenGL) / DrawIndexedPrimitive(DX)。
Batch : CPU发送渲染的数据给GPU(CPU Write)
如 设置顶点数据 glBufferData(OpenGL) / glBufferSubData(OpenGL) 等。
Batching , SRP Batcher 和 SRP Instancing
Static Batching
- 开关设置:PlayerSettings ⇒ Other Settings ⇒ Static Batching
- 条件:相同的材质球 , 可以不同的Mesh
- 优点
- 节省顶点信息的绑定
- 节省几何信息的传递
- 相邻材质相同时, 节省材质的传递
- 缺点
- 离线合并 , 离线包体变大(重复Mesh)
- 运行时合并 , CombineMesh会造成CPU短时间峰值
- 内存变大(重复Mesh)
SRP Batcher
- 条件:相同的Shader(变体一样),可不同的Mesh。
- 优点:节省Uniform Buffer的写入操作
- 按Shader 分 Batch , 预先生成Uniform Buffer
- 相同shader , uniform 变量相同
- PerDraw 合成一个大的Buffer
- PerMaterial 格子合成一个小的Buffer
- 每个Batch开始时,通过map(memcpy)的方式 一次性传入Uniform Buffer
- Batch 内部无 CPU Write
- 缺点
- Constant Buffer(CBuffer)的显存固定开销
- 1024个PerDraw 384KB
- 1个PerMaterial 4KB
- 不支持MaterialPropertyBlock
GPU Instancing
- 条件:相同的Mesh相同的材质球
- 缺点
- 可能存在负优化 (Instancing 有时候会让DrawCall变高 , 一些机器DrawCall敏感 , 所以开Instancing 需要大幅度降低DrawCall 才适用)。
- Instancing有时候被打乱 , 导致不完美Instancing (可以自己分组用API渲染,保证不被打乱)。
其他
- SRP Batcher 和 Static Baching 可以兼容同时开启
- 优先级建议 SRP Batcher / Static Batching > GPU Instancing > Dynamic Batching
- 适用情况
- Static + SRP Batcher : 主城,副本建筑
- SRP Batcher Only: 种类繁多的植被
- GPU Instancing: 种类单一的植被
- Dynamic Batching: UI , 粒子 , Sprite 等
DrawMeshInstance VS DrawMeshInstanceIndirect
- 单次上限
- DrawMeshInstance : 128 / 256 / 512 (不同平台)
- DrawMeshInstanceIndirect : 无
- API兼容版本
- DrawMeshInstance : GLES 3.0
- DrawMeshInstanceIndirect : GLES 3.1(Shader Model 4.5)
- Culling
- DrawMeshInstance : C# Math Culling 和 Unity CPU CullingGroup
- C# MathCulling
- Unity CPU CullingGroup
- 当数量过多的时候 , CullingGroup 性能比 MathCulling 要好很多
- DrawMeshInstanceIndirect : GPU Culling
- 基于ComputeBuffer/StructuredBuffer , 利用Compute Shader去执行GPU Culling
- 直接将计算结果交给DrawMeshInstancedIndirect
可以提前通过 九宫格/距离 剔除一些不可见的 , 达到更好的性能
在Camera Culling阶段之后获取结果
子线程并行执行
- 耗时
DrawMeshInstance+CullingGroup 相对于 DrawMeshInstanceIndirect+GPU Culling , DrawCall 变多(数量限制) , CPU耗时高 , GPU耗时低 , 总耗时在不同设备不一样
UIRebuild
Graphics.Rebuild(顶点修改)
- 性能瓶颈环境
- 血条
- 飘字
- 技能遮罩
- 解决办法:降低刷新的频率
LayoutRebuilder.Rebuild(Layout组件)
- 性能瓶颈环境
- 子物体修改时 , 会标记父节点的Layout通知修改
- Layout修改时 , 会标记Layout的父节点的Layout通知修改 (递归向上)
- 触发的情况
- 影响顶点位置的
- Text:Set_Text
- Image:Set_Image
- 事件回调
- OnEnable/OnDisable
- OnRectTrasnformDimensionsChange
- OnTrasnformParentChanged
- OnDidApplyAnimationProperties
- 解决办法
- 避免LayoutGroup的嵌套
- 如果只是为了第一次的自动排序的,可以在显示之后禁用相关的组件
CullRegistry.Cull(RectMask2D裁剪)
- 触发的情况
- 每帧遍历底下的元素进行裁剪
- 有新的元素被加进来
- 解决办法
- 性能消耗不大 , 可以不用专门关注
- 减少底下节点的数量(几十个内没有大问题)
- 保持节点没有改变减
其他
UIRebatch(Canvas为单位)
触发情况
Canvas.BuildBatch
解决办法
- Canvs动静分离
- Shader动画(代价很小)
- 用RawImage能拿到正确UV , Sprite不一定可以拿到正确的UV
- uv-0.5 = 离中心的方向
- 制作平移和缩放等的效果
Overdraw
Overdraw 指同一像素被重复渲染的次数 , 当被重复渲染的像素过多的时 或 重复的次数过多时候 , 则造成性能浪费.
一个像素被重复n次 光栅化 , fragment , 输出合并 . 这些都会占用 GPU像素填充率 , ALU运算 , 读取贴图的带宽.
全屏界面与场景的叠加
关闭场景相机的渲染
半屏界面与背景的叠加
- 降低场景的分辨率(需要把场景绘制到RT上)
- 降低场景相机的更新频率(需要把场景绘制到RT上)
- 改成Blur静态背景(需要把场景绘制到RT上)
全透明点击区域
- empty4raycast(不建议 , 参考钱康来的文章 , 老版本下用且有Overdraw)
- CullTransparentMesh(新版本 , Alpha 为0 的时候则无Overdraw)
大面积Mask组件
- Mask有两层Overdraw
- 子节点的数量始终时,改为RectMask2D, 切裁剪区域外无Overdraw
- 不过还是要观察一下CullingRegister.Cull(Canvas.SendWillRenderCanvas中) , 这两个组件的性能比较
大面积透明元素
- Sprite多边形模式取出顶点,再塞入UI使用(不建议 , 参考钱康来文章 , 老版本使用)
- Sprite:Tight模式 , Quad Mesh 变成了多边形Mesh , 但是此时只是用于Sprite2D 并不是UGUI
- Image: Use Sprite Mesh , 配合上面的Sprite Tight 来完成UGUI Overdraw减少
边框元素的FillCenter
九宫格的时候(Image Type 为 Sliced/Tiled) 去掉 FillCenter
Shader
常见问题
- 卡顿
- ShaderLab占用内存过大
原因
- Shader.Parse-->Shader加载和解析
- 变体数控制-->Shader被编译成多份,占用内存变大
- 缓存控制-->避免Shader多次加载卸载,比如Shader在场景包中,切换场景被卸载加载
- 冗余控制-->同一Shader冗余在不同Bundle中被多次加载
- Shader.CreateGPUProgram-->GPU对要渲染Shader的目标平台编译
- 冗余控制
- Shader过多-->过多的时候,会导致编译过久、卡顿
解决
- 编译过久=⇒ 预先Warmup Shader.WarmupAllShaders()
- 变体控制
- 可以用shader_feature 代替 multi_compile
- 收集需要的Variants
- Graphics Settings --> Shader Stripping
- Shader Variants Collection(SVC)
- 剔除不需要的Variants =⇒ OnProcessShader(2018之后)
- 冗余控制
- 打包策略
- UWA检测
- 缓存控制 =⇒ Asset+脚本引用+DontDestory
场景加载
Async Upload Pipeline(AUP)
渲染线程异步上传资源
- 条件
- 只对纹理和Mesh生效
- 要关闭Read/Write
- 策略
- 加载场景时
- QualitySettings ⇒ Async Upload Time Slice = 4ms或者8ms
- QualitySettings ⇒ Async Upload Buffer Size = 16 or 32
Texture Stream
- Settings->Quality里面开启Texture Streaming
- 贴图设置mipmap
- Memory Budget ⇒ 给纹理的内存预算越少,纹理加载的mip level基本就越低
- Max Reduction ⇒ 分层mipmap最大限制, 减少level越大, 图片越小
Mesh.Bake PhsX CollisionData
- 常见性能问题
- Mesh过大/复杂 , 每次进场景都需要运行时计算Collision数据 , 让加载过久
- 需要开启Read/Write , 这样就不能用AUP了
- 解决
- Prebake Collision Meshes(Settings -> Player) 注意:开启后包体大小上升,内存占用上升
- 使用简化Mesh或者LOD作为碰撞体
C#
实例化/激活
- 比如动画组件的激活/禁用
Incremental GC
- 2019可以在Player->Use incremental GC 开关增量GC
- GarbageCollector.GCMode == GarbageCollector.Mode.Disabled/Enabled
- 可以开关GC , 比如在战斗时候关掉GC回收 , 但是要注意泄漏 ( WebGL 和 Editor 不生效)
- VSync/Application.targetFrameRate 限制帧数
- 很多GC的产生都是在Update里面 , 帧数下去了 , 则GC就产生就减少了
- 因为帧数限制 增量GC 会把GC处理分配到要等待的时间里面
- GarbageCollector.incrementalTimeSliceNanosecords(逐帧花费多少时间在GC上面)
堆内存累积分配
- Boxing--dnspy看编译
- 拆/装箱
- 新建数组
- 新建对象
- ZString
- 堆内存分配明显变低
- CPU耗时变高
内存驻留(泄漏)
- Lua索引引用导致的Mono泄漏 -- Unity释放,Lua抓着,导致中间层的Mono内存驻留了
- C#引用 -- 如在Update里不规范使用C#代码 , 导致堆内存越来越多
- Texture.GetPixels/WWW.bytes Bug -- 一次分配较大的数据可能会概率导致泄漏 , 用GetPixels32代替 , 可以用C++代码下载或别的API下载
Lua
Lua到C#的调用次数
- 过多的Lua->C#->Lua穿梭会导致耗时 , 可以C#写一个大方法 , 让Lua一次性去调用 , 然后一次性返回值
Lua函数耗时
- Lua的代码性能 , 不如C# ,所以一些性能瓶颈方法可以写到C#里面
LuaGC(Incremental)
- Lua_GCSETPAUSE
- Lua_GCSETPSTEPMUL
临时内存
- Vector3 , Color 等
ILRuntime
- 解译语言,速度还是差20~100倍,可将耗时的方法放入原生语言
- 动态更新DLL打Relase版本 + DISABLE_ILRUNTIME_DEBUG 否则性能会下降
- Android可平台特殊处理
- 通过Assembly.Load动态加载DLL
- 通过GetType获取类型
- 通过AddComponent挂载组件执行逻辑
- 堆内存
- 会有缓存让内存增加
- 会缓存中间变量,临时变量,执行过的方法等
- 但是缓存有上限.
- 比如配置表用ILRuntime的内存比预想的内存多
- PDB占用
- 移除PDB文件的加载
- 控制热更新的DLL代码
物理
- Auto Simulate 自动物理模拟
- 在Settings->Auto Simulation开关
- 默认开启,如果关闭则Rigibody.OnTrigger等物理方法就无法使用了
- 如果碰撞很少,则可以关闭,自己可以通过距离模拟进行检测
- 但是可以手动调用 Physics.Simulate 即模拟一帧触发物理
- Auto Sync Transforms 自动更新物理世界坐标
- 在Settings->Auto Sync Transforms开关
- 不勾选(默认),物体Trasnform发生改变,在下一次物理Update()前,碰撞体属性不会改变.
- 勾选,物体Trasnform发生改变,碰撞体属性立即更新.同一帧数内频繁更新Trasnform则会有性能消耗.
- 如果想在同一帧数改变位置且进行物理检测, 则可以用Physics.SyncTransform() 立即更新物理信息
动画
- Animator.Initialize
- 在每次Enable的时候会调用一次 , 有性能消耗
- 可以只禁用Render , 不禁用Animator
- 新版本Animator.keepAnimatorControllerStateOnDisable
- AnyState
- AnyState连线过多的时候,导致检测过多,耗时过久
内存
- GfxDriver
- 粒子系统Mesh过大,造成内存占用过多,最终VBO = Mesh内存 * Mesh个数
- SRP Batcher , 1024个材质球约384KB , 1个材质球实例化约4KB
- Audio
- AudioClipLoadType.DecompressOnload
- 从磁盘读取就全部解压到内存中
- 多AudioSource使用同一份音频时,只有一份音频内存
- CPU开销比较低 , 因为没有动态解压缩
- 适合短音频 , 多AudioSource , 小文件(大文件勿开 , 因为音频压缩比为10倍 , ADPCM 编码约为 3.5 倍)
- AudioClipLoadType.Streaming
- 从磁盘读取时音频内存小 , 边播放 边从磁盘中逐渐读取加载到内存中.
- 如果多AudioSource播放 , 因为播放的区间不同 , 则要加载多份音频区间到内存中
- 即使没有加载任何音频数据,也有大约 200KB 的内存占用
- 适合长音频 , 单AudioSource , 大文件
- 作者:zuig
- 链接:https://blog.zuig.net//article/81d50f45-2c5d-4040-bd8f-28c6c6ec81e2
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。