Unity性能优化系列渲染篇 - 移动端渲染优化

性能优化系列 · 渲染篇。一句话摘要:先把相机、可见集和 Pass 管住,再谈合批;开发期用 DrawCall、Batches、SetPass Calls 盯提交量,真机用 CPU Time、GPU Time 和带宽做验收——动态合批和后处理按瓶颈开,不要当默认开关。

上一篇把 URP 的 Pipeline Asset、阴影、附加光、Render Scale、后处理栈按档位定了下来。管线开关对齐之后,剩下真正把帧时间打满的,通常已经不在 Asset 面板里,而在场景里:多出来的相机、拆开的批次、叠在屏幕中央的半透明、以及低档机仍在跑的全屏 Pass。

为什么从场景渲染开始

设置篇解决的是“同一套管线别让低配机跑高配参数”。渲染篇解决的是另一件事:同一档管线里,这个场景为什么还卡。

原因同样很实际:

  • 移动端同一份场景,不同机型卡的地方不一样。有的卡在 CPU 提交,有的卡在 GPU fill,有的卡在 UI 重建。只压某一个数字,换一台机器就失效;
  • Editor 的 Stats 可以实时看见 DrawCall、Batches、SetPass Calls,适合当开发期的弦;但它不是玩家体感。玩家感到的是真机上的帧时间、带宽和发热;
  • SRP Batcher、GPU Instancing、动态/静态合批分别适用于不同情况;多相机和后处理用于实现画面或交互需求,也各有额外开销。选用它们不是为了让某个统计数字好看,而是看目标机型上的 CPU / GPU 帧时间、带宽和发热是否真正改善。

所以这篇的前提是:先看这一帧卡在哪一侧,再选手段;开发期用提交量挡住失控,上线前用真机时间和带宽验收。

移动端的约束

动手之前先承认几条硬约束,后面的每个决策都是从这几条推出来的:

  • GPU 是 tile-based,带宽和片元比桌面贵得多。多一台相机、多一张全屏 RT、多一层近透明,都是按像素计费,不是按物体个数计费;
  • 多一台 Camera 往往比多几十个物体更贵。 Unity 官方测过:主线程的相机处理时间和相机数量直接相关,即便新相机什么都不画,剔除和提交准备照样走一遍。URP 的 Overlay / Camera Stack 在手机上还可能多一次全屏解析,并打乱单相机内部的 Overdraw 优化;
  • SetPass 经常比同材质多几次 Draw 更伤。 切 shader、切混合状态、切关键字,才是渲染线程上真正贵的那一段。UWA 很早就指出:只盯 DrawCall 会误判,Batches 和 SetPass 要一起看;
  • 动态合批本身有 CPU 成本。 CPU 要把小 mesh 变换到世界空间再拼起来,现代移动 API 上这笔开销经常比少一次 draw 还贵;
  • 散热决定持续性能。 平均 60 帧没有意义,10~15 分钟后的 95/99 分位和是否掉频,才是玩家体感。

几个指标的含义

先理解这些指标分别表示什么,才能知道该从哪一类问题开始排查。

  • DrawCall:CPU 向图形 API 发出的一次绘制请求。Profiler Rendering 模块里能看到。
  • Batches:Unity 统计口径里“实际提交的批”。和 DrawCall 有时接近,但不等价——静态合批之后,多个 Draw 可能被计成一个 Batch。
  • Verts / Tris:这一帧提交的顶点数 / 三角面数,用来观察几何量。两者高时先查可见集、LOD、远景和模型复杂度;但它们不直接等于 GPU 压力,透明 Overdraw 和全屏 Pass 仍可能更贵。
  • SetPass Calls:切换 shader pass / 渲染状态的次数。同材质连续画,SetPass 可以很低,DrawCall 仍可以高。
  • Overdraw:同一像素被画了几次。这是 GPU fill 压力,不是提交压力。
  • CPU Frame Time / GPU Frame Time:最终体验的主导指标。带宽、PSS、温度是配套验收,不能被单指标绑架。

读数时用这条判断,不要上来就合批:

  • CPU 在等 GPU:先查 Overdraw、半透明、后处理、阴影、全屏拷贝,少几次 DrawCall 救不了;
  • SetPass 高:材质、shader、关键字切得太勤。先共享材质、收变体,URP 下让 SRP Batcher 吃到同一 variant;
  • Batches 高、SetPass 不高:同 Pass 下物体被拆开了,才轮到 Instancing、静态合批、减物体;
  • Batches 已经很低,CPU 仍紧:反过来怀疑动态合批、UI 重建、多相机剔除把 CPU 吃掉了。

开发期对着 Editor 的 DrawCall、Batches、SetPass Calls;上线前对着真机的 CPU Time、GPU Time、带宽。

两层标准:开发基线,真机金标准

研发过程里不可能每次改材质、加特效都打一包上真机。所以要先给场景订一条看得见、能天天对的弦,再承认它不是最终判决。

开发基线:Editor 里就能盯的提交量

给关键场景写死一组上限,例如大厅、对局、结算各自「DrawCall / Batches / SetPass 大概该在多少以下」。Game 视图 Stats 和 Editor Profiler 可以实时对照,用来防止开发中途悄悄涨上去。

订基线时可以先参考公开数据,再收成自己的场景预算:

下面这组数值是本文采用的开发期预警线,面向中轻量级移动端 URP 场景,用来尽早发现提交量上涨;不是行业标准,也不是最终验收线。DrawCallBatchesSetPass、顶点数和三角面数要一起看:前 3 项偏提交成本,后 2 项偏几何成本。

场景与设备档位DrawCallBatchesSetPass Calls顶点数三角面数
大厅 · 低档≤ 120≤ 80≤ 25≤ 100k≤ 75k
大厅 · 中档≤ 180≤ 120≤ 35≤ 200k≤ 150k
大厅 · 高档≤ 240≤ 160≤ 45≤ 330k≤ 250k
常规对局 · 低档≤ 200≤ 140≤ 35≤ 200k≤ 150k
常规对局 · 中档≤ 300≤ 220≤ 50≤ 400k≤ 300k
常规对局 · 高档≤ 400≤ 300≤ 65≤ 650k≤ 500k
特效峰值 · 低档≤ 280≤ 200≤ 45≤ 300k≤ 220k
特效峰值 · 中档≤ 400≤ 300≤ 60≤ 500k≤ 400k
特效峰值 · 高档≤ 520≤ 400≤ 75≤ 800k≤ 650k

这些值从低到高档逐级放宽,是为了给更高档设备保留画面空间;不代表高档机可以无限堆叠。实际项目需要用目标档位的代表机型做真机 A/B:如果某档位在表内仍然 GPU 超时、带宽过高或持续发热,就继续收紧透明、阴影、后处理和相机数量;如果真机长期有余量,再按场景逐项放宽。

用法是:

  1. 按场景和设备档位各留一张 Editor 对照表,写清观察机位和操作路径;
  2. 日常开发只看预警线有没有被拉断——超了先查是材质拆批、透明层,还是多了 Pass;
  3. 预警线用来减少无效的真机回归,不是用来宣布优化完成。Editor 的渲染路径、资源加载、脚本开销都和真机不一样;最终仍以不同档位代表机型上的帧时间、带宽和发热为准。

金标准:真机上的时间和带宽

最终只认真机、尽量接近正式包上的:

  • CPU Frame TimeGPU Frame Time,以及 95% / 99% 分位,不只看平均 FPS;
  • GPU 模块拆开看 Shadows、Transparent、PostProcessing;
  • 带宽和填充:贴图尺寸、压缩、Overdraw、全屏拷贝、后处理;
  • 内存与发热:PSS、GC、连续玩 10~15 分钟是否掉频。

DrawCall 基线过了、真机 GPU 仍抖,优先查 Overdraw 和后处理。真机 CPU 紧、Batches 却很低,反而要怀疑动态合批或 UI 重建。UWA / UPR 的区间是开发期的弦;金标准永远是目标机型上的时间和带宽。

按这个顺序做

  1. 先数相机、先减可见集——不该画的不要进管线;
  2. 共享材质、收变体,默认让 SRP Batcher 生效;大量重复网格再用真机 A/B 决定是否改走 Instancing,动态合批最后考虑;
  3. 半透明和后处理按面积、按档位管;开发期盯 DrawCall / Batches / SetPass,真机用时间和带宽验收。

1. 少加相机

排查时先数相机,再数 Batches。URP 里每多一个 Camera(含 Stack 里的 Overlay),通常要再做一轮剔除,再走一套不透明 / 透明 / 后处理相关 Pass。

落地约定:

  • 场景里默认只留一台 Base Camera;
  • 屏幕 UI 用 Screen Space Overlay,不要为 UI 再挂透视 Overlay 相机,更不要用多相机给 Canvas 排序——排序是 Canvas 自己的事;
  • 角色预览、拍立得、小地图:能短时用低分辨率 RenderTexture 就不要常驻第二台全屏相机;
  • 自定义描边、分层效果按需求使用 Renderer Feature;常规后处理要确认 Renderer 已挂后处理数据,并由 Volume 控制,不要「一层效果一台相机」;
  • 弹窗盖住 3D 时,优先关底层相机或停渲染,而不是再叠一台遮罩相机。

能少一台就少一台。少相机省的是整条提交和全屏带宽,合批省的只是同一条路上的物体数。

2. 先问物体该不该被画

提交优化之前,先减可见集:

  • 视锥剔除默认就有。网格合得太大,远处一整坨剔不掉,反而更亏。静态合批主要降低提交开销,但会抬 VBO 与内存;DrawCall 已经很低时,可以适当拆开,换取内存和带宽;
  • 遮挡剔除能少画被挡住的网格,但要烘焙、占内存,动态物体收益有限。室内、遮挡多的场景再开,空旷大厅别当标配;
  • LOD / 远景 Impostor 降的是面数和采样,不是 Batches。低端机同屏面数往往比多 20 次 DrawCall 更敏感,UWA 简谱把低端机面数单独设预算,就是这个原因;
  • 蒙皮网格、粒子默认不走动态合批。同材质小人很多时,Instancing / 合并网格要单独评估,不要指望自动合批。

窗口已经挡住大世界时,优化对象经常不是合批,而是底层还该不该继续画

3. 合批:每次绘制选一条提交路径

对同一个 Renderer 在同一次绘制中,SRP Batcher、GPU Instancing 和动态合批不会叠加;要在三者之间选一条主路径。静态合批是另一类场景资源策略,要单独核算内存和加载成本,别把它和前三种当成同一个开关。

共享材质、少 shader variant,比先勾哪个开关更要紧。不同材质几乎合不上;关键字不一致时,看起来是同一个 shader,实际却是不同 variant,SRP Batcher 也会拆开。

提交路径默认优先级与适用情况主要优点主要代价
SRP BatcherURP 的默认选择;大量常规物体使用同一 shader variant、材质可以不同降低每次提交的 CPU 状态设置成本不一定减少 DrawCall;MaterialPropertyBlock 和不兼容 shader 会使其失效
GPU Instancing只给大量相同网格、相同材质的候选组;用真机 A/B 确认后再选一次实例化绘制可合并重复物体的提交与 SRP Batcher 互斥;变体分散、实例频繁增删会增加 CPU 组批成本
动态合批大量不同 mesh 共享同一材质,且每个 mesh 都很小可合并这些小网格的提交每帧 CPU 变换并拼接顶点,限制严格,适用范围很小

SRP Batcher 主要让同一 variant 下的多次 Draw 变便宜,不保证把 300 次 DrawCall 变成 30。启用后看 SetPassRender Thread / CPU 时间是否下降,并在 Frame Debugger 里确认出现 SRP Batch。不兼容的材质太多时,先治材质规范,再谈开关。

GPU Instancing 的前提是重复物体的网格和材质完全一致。它不是全局替代 SRP Batcher 的“更高版本”,而是重复组的另一条提交路径;越复杂的材质、越频繁的实例增删,收益越可能被 CPU 端组列表成本吃掉。开关只是一半,另一半是目标机型上的渲染线程和 GPU 帧时间。

动态合批现在的适用范围很小,但并非完全没有空间:当场景里有大量不同 mesh、它们却共享同一材质,并且每个 mesh 的顶点 / 三角面数都很少时,才值得作为候选方案。它每帧仍要在 CPU 上变换并拼接小 mesh,限制也很严格:单个 mesh 需满足顶点属性限制(上限 900 个顶点属性;简单的 Position、Normal、UV 网格才可能接近 300 顶点),并使用单 Pass shader。CPU 已经是瓶颈时可能更差;透明物体还要先按从后往前排序,实际成功率更低。开启前后看真机 CPU Frame Time,不能只看 Batches 变少。

静态合批能减少提交,也会抬内存和 VBO,加载弹性变差。把它当局部场景收益项,用真机 A/B 决定留不留。

少数重复组:让 Instancing 有目的地接管 SRP Batcher

当某一组物体数量很多、网格和材质完全一致时,SRP Batcher 的多次 Draw 可能反而不如一次 Instancing 绘制划算。Unity 也明确说明:SRP Batcher 与 GPU Instancing 不兼容;这种场景可以有意让 shader 不支持 SRP Batcher,再启用 Instancing。这里的结论只对通过同机位、同档位真机 A/B 验证的重复组成立,不能因此全局关闭 SRP Batcher。

一种可维护的做法是保留原来的 SRP Batcher 兼容 shader,再为确认受益的重复组维护一个 Instancing 专用 shader 变体:

  1. 先在 Frame Debugger 和真机 Profiler 中确认候选组确实是大量相同网格、相同材质;比较 SRP 路径与 Instancing 路径的 CPU、Render Thread、GPU 帧时间和内存,而不只比较 Batches;
  2. 在手写 HLSL shader 的 Properties 中新增一个材质属性,但不要把它声明进 UnityPerMaterial 的 CBUFFER。按 Unity 的兼容规则,该 shader 就会被排除在 SRP Batcher 之外;该属性不需要参与实际计算;
  3. 在这个专用 shader 的绘制 pass 保留 #pragma multi_compile_instancing,并在对应材质上启用 Enable GPU Instancing;如果项目使用自定义绘制,也可改用 Graphics.RenderMeshInstanced
  4. 不要把这项改动施加到通用 shader 或所有材质上。只让已验证受益的重复组引用专用变体;Frame Debugger 应确认它不再走 SRP Batch,并按预期形成 Instancing 批次;
  5. Shader Graph 需要走这条路时,可按官方方式导出编译结果并维护独立 shader;每次 Graph 重新编译后都要重新应用该改动。因此长期维护通常优先为这类少数对象保留可控的手写 HLSL 变体。

这个选择不是“谁更先进”,而是让不同物体走适合自己的路径:常规物体优先 SRP Batcher,大量完全相同的重复物体才把 Instancing 作为可验证的例外;动态合批始终最后评估。具体兼容条件和排除方式可对照 Unity 的 SRP Batcher 文档有意排除 SRP Batcher 的官方方法

MaterialPropertyBlock 改色方便,但会把物体踢出 SRP Batcher。要变色优先走 Instancing 的 per-instance 属性、图集采样,或可控的材质变体,不要为了改个 tint 把整批合批拆掉。运行时 renderer.material 复制同理,等于每物体一份材质实例。

4. 什么在拆批,比“开哪个开关”更值得查

Frame Debugger 里相邻两次 draw 材质一变,合批就断。高频拆批源:

  • 每物体一份材质实例,或运行时 renderer.material
  • MaterialPropertyBlock
  • Shader 变体 / Keyword 不一致;
  • 半透明穿插:中间插进别的材质就断,能合的半透明尽量同材质、少穿插;
  • 光照贴图不在同一图集区域、镜像缩放、多 pass 灯光;
  • 开了 Opaque Texture / Depth Texture(例如 SSAO 等深度效果、软粒子、折射或扭曲):可能多出一到两张全屏图,带宽会先涨一截。两个开关要分别确认具体使用者,不能因为某个效果需要其中一个就把两者都打开。

查法:Stats 里 Batches 高、SetPass 不高,多半是同 Pass 下物体被拆开;SetPass 也高,才是 shader / 关键字 / 相机 / 后处理在切状态。

5. 阴影从便宜方案开始选

阴影经常是“看得见一点、成本很高”的项。按画面需求从低成本方案往上试,不满足再升级:

  1. 贴图阴影:先用预制阴影贴图、Blob Shadow 等方式,适合只需要稳定接地感的对象;
  2. 烘焙阴影:静态场景优先烘焙,并用 Light Probe 支持动态物体接受环境光照;
  3. 平面阴影:对象会移动、但投影形状可以简化时使用;
  4. 实时阴影:前三种都无法满足表现要求时才保留,并按档位压 Shadow Distance、级联、分辨率和投射者数量。

无论最终选哪一种,都要看阴影的实际消耗:在同一镜头、同一机型上对比 Profiler 的 Shadows 模块、SetPass、CPU / GPU 帧时间和 95/99% 分位。关闭主光实时阴影时,软阴影和附加光阴影也应随之关闭;它们不再带来画面收益,还会留下额外设置和变体。档位怎么切,设置篇已经写过,这里不重复配 Asset。

6. 后处理单独排查,按档位打开

Bloom、DOF、SSAO、Motion Blur、全屏模糊都是按像素计费。GPU 已经吃紧时,它经常比多几十次 DrawCall 更伤帧。Editor 里往往“看起来还行”,低端真机上一次全屏拷贝就能把 fill 打满。UWA 对移动端抗锯齿的建议也很克制:低档关,高档最多 2x MSAA。

排查顺序:

  1. Frame Debugger 里后处理 Pass 是否真的在跑;常规后处理要同时确认 Renderer 已挂后处理数据、Volume 开启了对应效果,不能只看 Volume 挂着;
  2. 真机对同一镜头做“全关 / 只留 Bloom / 全开”三档 A/B,看 GPU Frame Time 和 95/99% 分位;
  3. 确认 Opaque Texture / Depth Texture 分别由哪个效果使用;Depth 常见于 SSAO 等深度效果,Opaque 常见于折射或扭曲,不能笼统归因给后处理。

落地原则:低档先关,中档最多留一项可见收益高的(常见是 Bloom),高档再逐项加。Bloom、Tonemapping 等依赖 HDR 画面链路的效果,要和 HDR 一起按档位评估,不能只孤立保留一个开关。不要全机型共用一套 Volume。每加一个全屏 Feature 至少多一个 Pass,要算总账。

7. 半透明、粒子和 Overdraw 单独看

DrawCall 和 Overdraw 是两条成本路径。粒子、扫光、Mask、Glow 叠在屏幕中央时,DrawCall 可能只有几十,GPU 已经满了。移动 GPU 上 alpha clip / 镂空还会伤部分机型的 Early-Z。

处理顺序:先减覆盖面积和层数,再谈合批。大块无效透明像素比少一次 draw 更值钱。

看 Overdraw 的流程可以固定:

  1. Scene 视图打开 Overdraw;
  2. Frame Debugger 看同层透明叠加;
  3. 真机对同一场景做“开/关关键透明层”A/B,对比 GPU 时间;
  4. 看帧率稳定性,不只看平均帧。

8. UI:合批和重建

大部分 UI 抖动来自 Canvas 重建范围过大,不是缺一张图集。Unity 的 Canvas 本身已经做了重建隔离:子 Canvas 独立维护几何和批次,某个 Canvas 变脏时,不需要重建父 Canvas 与兄弟 Canvas。因此可以按刷新频率做动静分离:大面积静态内容留在主 Canvas;倒计时、进度、持续动效等会一起频繁刷新的元素放进同一个子 Canvas。

但不要机械地“一个控件一个 Canvas”。Canvas 之间不会自动合成批次;拆得太细会增加批次、DrawCall、排序和输入管理成本。只有下面两种情况同时成立才拆:这组元素刷新明显更频繁,且能作为稳定的一组一起刷新。偶尔改变一次的小图标,不值得单独建 Canvas。

两点容易漏:

  • 静态或不接收输入的子 Canvas 不要挂 Graphic Raycaster
  • 元素移出屏幕后 DrawCall 没降,说明 CPU 仍在提交网格。UWA 测过:看不见不等于没提交,只是 GPU 不怎么填像素;
  • 子 Canvas 能隔离脏更新,但拆分不是免费午餐。改完一起看 Canvas.BuildBatch、SetPass 和 DrawCall,再决定是否保留。

Unity 的说明和例子见 Optimization tips for Unity UI:它明确建议把静态 UI 与同频刷新的动态 UI 分到不同 Canvas,同时提醒每个 Canvas 都有独立的几何和批次。

图集仍然重要:同一界面内高频复用的图标、按钮和装饰图尽量进入同一 Sprite Atlas,让 Image 尽可能共享贴图和材质,减少 UI 的材质切换。TextMeshPro 也要统一 Font Asset 和材质;回退字体、不同描边 / 字体材质会形成新的批次。

还要同时看层级和遮挡关系。Hierarchy 决定 UI 的视觉前后关系:重叠元素必须按这个关系显示;但 Canvas 建批时还会分析元素深度、边界框重叠和材质。两个元素不重叠时,Unity 可以按可合批性安排提交,不必机械地逐个遵循 Hierarchy;有遮挡关系时,为保证画面前后正确,中间不同材质的元素就会成为不能跨越的“中间层”。

因此,文本和图片的材质交替穿插不一定必然拆批,关键在它们是否形成遮挡。例如“文字 A → 图片 B → 文字 C”中,若 B 的边界框与 A、C 有重叠,A 与 C 即使同用 TMP 字体材质也不能跨过 B 合批;TextMeshPro 的字符区域外虽然透明,文本的矩形边界仍可能与附近图片相交,导致看不见的“中间层”拆批。能调整视觉层级或位置时,尽量让同材质元素连续且不被不同材质遮挡;不能调整时,在 Frame Debugger 中确认这笔拆批是否值得保留。Unity 对 UI 建批、重叠与 Child Order 有更完整的说明;基础绘制顺序可对照 Canvas 手册

9. 大窗口挡住 3D 时,停渲染或截一帧

这类做法并不少见。工程上不要等窗口打开后再猜它覆盖了多少画面,而是在窗口定义中预先标记渲染策略:KeepWorld(保持世界渲染)、HideWorld(隐藏世界)和 SnapshotWorld(截图冻结背景)。窗口系统据此统一调度相机和截图资源。

策略可以按窗口是否遮满世界画面分流:

  • 全屏页:底部 3D 已经完全不可见,直接隐藏并停掉底层相机即可,不需要截图;
  • 非全屏弹窗:仍要露出底部画面、且弹窗会停留一段时间时,才截取一帧作为静态背景,再停掉底层相机。这样弹窗停留期间,背景不再提交渲染;若世界逻辑仍在推进,关闭弹窗时画面会从截图直接跳到当前状态;
  • 小提示或短时浮层:先看它自身的绘制成本和停留时长。它通常不需要冻结背景,截图的帧末读取与纹理分配反而可能更贵,此时归为 KeepWorld 即可。

所以决策不只取决于“是不是全屏”,还要依次问:玩家是否需要看见底层内容?弹窗本身和底层世界谁更贵?它是否会停留到足以摊平一次截图的成本?

以《大富翁 GO》为例:打开某些非全屏弹窗后,底下的 3D 角色看起来完全不动;过一会儿关闭弹窗,原本在某处的角色会直接跳到此时的位置。仅从这一画面行为推测,它很可能采用了“静态截图覆盖 + 底层继续更新或状态推进”的做法。这个观察案例也说明:非全屏弹窗并不一定要让底层 3D 持续渲染。

下面是一个独立的 Unity C# 示例。它只演示“全屏页直接停相机、非全屏弹窗截图后停相机;最后一个窗口关闭后恢复”的资源所有权;窗口如何创建、显示和销毁由上层 UI 系统负责。

using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public enum WorldRenderPolicy
{
    KeepWorld,
    HideWorld,
    SnapshotWorld
}

public sealed class WorldRenderCover : MonoBehaviour
{
    [SerializeField] private Camera worldCamera;
    [SerializeField] private RawImage snapshotImage;

    private int coverCount;
    private int captureToken;
    private Texture2D snapshot;

    public void Open(WorldRenderPolicy policy)
    {
        if (policy == WorldRenderPolicy.KeepWorld) return;
        if (++coverCount != 1) return;
        if (policy == WorldRenderPolicy.HideWorld) worldCamera.enabled = false;
        else StartCoroutine(CaptureThenPause(++captureToken));
    }

    public void Close(WorldRenderPolicy policy)
    {
        if (policy == WorldRenderPolicy.KeepWorld) return;
        if (coverCount == 0 || --coverCount != 0) return;
        captureToken++; // 关闭早于帧末截图时,阻止旧协程再次关相机。
        snapshotImage.enabled = false;
        snapshotImage.texture = null;
        Destroy(snapshot);
        snapshot = null;
        worldCamera.enabled = true;
    }

    private IEnumerator CaptureThenPause(int token)
    {
        yield return new WaitForEndOfFrame();
        if (this == null || token != captureToken || coverCount == 0) yield break;

        snapshot = ScreenCapture.CaptureScreenshotAsTexture();
        snapshotImage.texture = snapshot;
        snapshotImage.enabled = true;
        worldCamera.enabled = false;
    }

    private void OnDestroy() => Destroy(snapshot);
}

截图只在停留足够久的窗口触发,离开后立刻释放截图纹理。多个窗口叠加要用计数,避免一个窗口关掉就把底层渲染错误恢复。不是所有弹窗都适合截图:一闪而过的提示,截图当帧的 ReadPixels 和内存峰值可能比继续画 3D 更亏。

UI 也能沿用同一思路:当上层窗口完全遮住、且不再需要显示或交互底层 UI 时,可以停掉底层 Canvas;仍需露出或交互的部分则保留。但 UI 长期多层重叠会同时增加 DrawCall、Overdraw 和输入管理成本,默认应从交互与界面设计上避免堆叠过多窗口,而不是依赖隐藏策略兜底。

从异常数字回到具体对象

Stats 只能告诉你“这一帧变贵了”,不能告诉你是谁让它变贵。排查时不要一上来把所有合批开关轮流切一遍;用同一帧的 Frame Debugger 把数字落到具体的 Camera、Pass、材质和 Renderer,路径会短很多。

工具的选择、真机抓帧方式,以及 UPR / UWA 的报告与付费边界,见《Unity 性能优化:工具与数据采集》。本篇只讨论渲染问题本身如何定位和处理。

  1. 先固定复现镜头。 选一个能稳定复现的操作点,记录相机位置、质量档、窗口状态和特效状态。没有固定镜头,前后两次的可见集不同,数字没有可比性。
  2. 按渲染阶段找异常段。 在 Frame Debugger 里先区分是不透明、透明、阴影还是后处理段突然变长;如果多了一整段全屏 Pass,先回查相机栈、Renderer 的后处理数据、Renderer Feature、Volume,以及深度/不透明纹理的实际使用者,而不是先看小物件。
  3. 比较相邻 draw 的状态。 在预期能连续合批的位置,逐项比对材质、shader variant、关键字、贴图、Lightmap、排序和每物体参数。第一个不同项通常就是拆批原因;如果状态相同仍没合上,再确认该 shader 是否真的兼容 SRP Batcher 或 Instancing。
  4. 只做一个可逆修改再复测。 例如把一组材质改为共享材质、关闭一个 Volume 覆盖项,或暂时隐藏一层粒子。先在 Frame Debugger 证明异常 Pass 或拆批消失,再到目标真机确认 CPU / GPU 帧时间的收益;只有两个证据都成立,才把修改固化为资源或场景约定。

这个顺序也能防止“看起来省了 Batches,实际只是把画面或可见集一起改没了”。每次记录至少保留:复现步骤、改动项、Editor 的 DrawCall / Batches / SetPass,以及真机的 CPU / GPU 帧时间和测试时长;不需要写入具体业务名称或资源名称。

验证与回归方法

  1. 同一场景、同一操作路径、同一设备做 A/B,一次只改一类动作(少一台相机、关一项后处理、开/关动态合批);
  2. Editor 用 Frame Debugger 和 Stats 采集 DrawCall / Batches / SetPass,确认 Pass 和合批行为符合预期;
  3. 真机采集 CPU / GPU Frame Time、95/99 分位,必要时拆 GPU 模块;动态合批、后处理这类“可能更贵”的开关,目标档位机型上各做一次开/关;
  4. 用同一条关卡流向覆盖:场景加载 → 普通游走 → UI 高交互 → 特效叠加 → 全屏弹窗。只在静止场景里测出来的优化,上线后经常不成立;
  5. 连续跑 10~15 分钟,看温度和掉频。只录开头 30 秒,平均帧率会骗人。

其他应该注意的点

  • 没有场景提交量基线:开发期完全不看 Editor Stats,等问题堆到打包才发现 DrawCall 涨了一截;
  • 把 UWA / UPR 的区间当成项目 KPI,或反过来只认 Editor 数字、不上真机看时间和带宽;
  • 只看 DrawCall,忽略 SetPass 或 GPU fill。Batches 降了、半透明层还在,低端机照样烫;
  • 动态合批盲开:小物体场景之外 Batches 降了,CPU 更忙;
  • SRP Batcher 迷信:材质不兼容、变体一堆,开关开了 Frame Debugger 里根本没有 SRP Batch
  • MaterialPropertyBlock 改色把能走 SRP Batcher 的物体整批拆掉;
  • 多挂 Overlay 相机做 UI / 预览,Batches 没降,剔除和全屏 Pass 先翻倍;空相机也有成本,Unity 官方测过“什么都不画”仍然吃 Camera.Render
  • 全机型同一套后处理,低档机被全屏 Pass 打满;Opaque TextureDepth Texture 被打开却没有明确使用者;
  • 网格合得太大,视锥和遮挡都剔不掉,远处仍在画,VBO 先涨;
  • UI 只做图集,没控制 Canvas 重建范围;移出屏幕的元素还在提交;
  • 截图优化没释放 RT,弹窗路径上出现隐性内存峰值。

总结

可迁移的原则就三条:

  1. 先看真机卡在 CPU 还是 GPU,再动手。 开发期用 DrawCall、Batches、SetPass Calls 挡住提交量上涨;金标准是目标机型上的 CPU Time、GPU Time 和带宽。UWA / UPR 给的是行业水位,不是项目 KPI;
  2. 先减要画的,再减提交。 少加相机、收缩可见集、共享材质;常规物体优先 SRP Batcher,大量完全相同的重复组再 A/B Instancing,动态合批最后考虑。静态合批单独核算内存和加载成本;
  3. DrawCall 和 Overdraw 分开看,一次只改一类动作。 Frame Debugger 用来证实合批和 Pass 真的按预期发生;结论以真机 Release 包为准,连续玩一段时间后的分位和温度也要进验收。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五仁烧饼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值