Unity性能优化系列渲染篇(下)- 阴影、后处理与 UI 渲染优化

发布时间:2026/9/5 4:32:45
Unity性能优化系列渲染篇(下)- 阴影、后处理与 UI 渲染优化 性能优化系列 · 渲染篇下。一句话摘要阴影、后处理、半透明与 UI 往往不是提交次数的问题而是 GPU 填充、带宽和 Canvas 重建的问题按面积、按档位、按真机时间逐项处理。相机、可见集、合批和拆批的处理顺序见渲染篇上。本篇继续处理画面成本和界面成本先从便宜的阴影方案开始再控制全屏 Pass、透明覆盖、UI 重建与大窗口叠加。阴影从便宜方案开始选阴影经常是“看得见一点、成本很高”的项。按画面需求从低成本方案往上试不满足再升级贴图阴影先用预制阴影贴图、Blob Shadow贴在角色脚底的软边椭圆阴影等方式适合只需要稳定接地感的对象烘焙阴影静态场景优先烘焙并用 Light Probe 支持动态物体接受环境光照平面阴影对象会移动、但投影形状可以简化时使用实时阴影前三种都无法满足表现要求时才保留并按档位压Shadow Distance、级联、分辨率和投射者数量。无论最终选哪一种都要看阴影的实际消耗在同一镜头、同一机型上对比Profiler的 Shadows 模块、SetPass、CPU / GPU 帧时间和 95/99% 分位。关闭主光实时阴影时软阴影和附加光阴影也应随之关闭它们不再带来画面收益还会留下额外设置和变体。档位怎么切设置篇已经写过这里不重复配 Asset。后处理单独排查按档位打开Bloom、景深DOF、SSAO、Motion Blur、全屏模糊都是按像素计费。GPU 已经吃紧时它经常比多几十次 DrawCall 更伤帧。Editor 里往往“看起来还行”低端真机上一次全屏拷贝就能把像素填充打满。UWA 对移动端抗锯齿的建议也很克制低档关高档最多 2x MSAA。排查顺序Frame Debugger 里后处理 Pass 是否真的在跑常规后处理要同时确认 Renderer 已挂后处理数据、Volume 开启了对应效果不能只看 Volume 挂着真机对同一镜头做“全关 / 只留 Bloom / 全开”三档 A/B看GPU Frame Time和 95/99% 分位确认Opaque Texture/Depth Texture分别由哪个效果使用Depth 常见于 SSAO 等深度效果Opaque 常见于折射或扭曲不能笼统归因给后处理。落地原则低档先关中档最多留一项可见收益高的常见是 Bloom高档再逐项加。Bloom、Tonemapping 等依赖 HDR 画面链路的效果要和 HDR 一起按档位评估不能只孤立保留一个开关。不要全机型共用一套 Volume。每加一个全屏 Feature 至少多一个 Pass要算总账。半透明、粒子和 Overdraw单独看像素填充DrawCall 和 Overdraw 是两条成本路径。粒子、扫光、Mask、Glow 叠在屏幕中央时DrawCall 可能只有几十GPU 已经满了。移动 GPU 上 alpha clip按阈值剔除透明像素的镂空还会影响部分机型提前跳过被遮挡像素的能力Early-Z。处理顺序先减覆盖面积和层数再谈合批。大块无效透明像素比少一次 draw 更值钱。看 Overdraw 的流程可以固定Scene 视图打开 OverdrawFrame Debugger 看同层透明叠加真机对同一场景做“开/关关键透明层”A/B对比 GPU 时间看帧率稳定性不只看平均帧。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 手册。大窗口挡住 3D 时停渲染或截一帧这类做法并不少见。工程上不要等窗口打开后再猜它覆盖了多少画面而是在窗口定义中预先标记渲染策略KeepWorld保持世界渲染、HideWorld隐藏世界和SnapshotWorld截图冻结背景。窗口系统据此统一调度相机和截图资源。策略可以按窗口是否遮满世界画面分流全屏页底部 3D 已经完全不可见直接隐藏并停掉底层相机即可不需要截图非全屏弹窗仍要露出底部画面、且弹窗会停留一段时间时才截取一帧作为静态背景再停掉底层相机。这样弹窗停留期间背景不再提交渲染若世界逻辑仍在推进关闭弹窗时画面会从截图直接跳到当前状态小提示或短时浮层先看它自身的绘制成本和停留时长。它通常不需要冻结背景截图的帧末读取与纹理分配反而可能更贵此时归为KeepWorld即可。所以决策不只取决于“是不是全屏”还要依次问玩家是否需要看见底层内容弹窗本身和底层世界谁更贵它是否会停留到足以摊平一次截图的成本以《大富翁 GO》为例打开某些非全屏弹窗后底下的 3D 角色看起来完全不动过一会儿关闭弹窗原本在某处的角色会直接跳到此时的位置。仅从这一画面行为推测它很可能采用了“静态截图覆盖 底层继续更新或状态推进”的做法。这个观察案例也说明非全屏弹窗并不一定要让底层 3D 持续渲染。下面是一个独立的 Unity C# 示例。它只演示“全屏页直接停相机、非全屏弹窗截图后停相机最后一个窗口关闭后恢复”的资源所有权窗口如何创建、显示和销毁由上层 UI 系统负责。usingSystem.Collections;usingUnityEngine;usingUnityEngine.UI;publicenumWorldRenderPolicy{KeepWorld,HideWorld,SnapshotWorld}publicsealedclassWorldRenderCover:MonoBehaviour{[SerializeField]privateCameraworldCamera;[SerializeField]privateRawImagesnapshotImage;privateintcoverCount;privateintcaptureToken;privateTexture2Dsnapshot;publicvoidOpen(WorldRenderPolicypolicy){if(policyWorldRenderPolicy.KeepWorld)return;if(coverCount!1)return;if(policyWorldRenderPolicy.HideWorld)worldCamera.enabledfalse;elseStartCoroutine(CaptureThenPause(captureToken));}publicvoidClose(WorldRenderPolicypolicy){if(policyWorldRenderPolicy.KeepWorld)return;if(coverCount0||--coverCount!0)return;captureToken;// 关闭早于帧末截图时阻止旧协程再次关相机。snapshotImage.enabledfalse;snapshotImage.texturenull;Destroy(snapshot);snapshotnull;worldCamera.enabledtrue;}privateIEnumeratorCaptureThenPause(inttoken){yieldreturnnewWaitForEndOfFrame();if(thisnull||token!captureToken||coverCount0)yieldbreak;snapshotScreenCapture.CaptureScreenshotAsTexture();snapshotImage.texturesnapshot;snapshotImage.enabledtrue;worldCamera.enabledfalse;}privatevoidOnDestroy()Destroy(snapshot);}截图只在停留足够久的窗口触发离开后释放截图纹理。多个窗口叠加要用计数避免一个窗口关掉就把底层渲染错误恢复。不是所有弹窗都适合截图一闪而过的提示截图当帧的读回与纹理分配可能比继续画 3D 更亏。UI 也能沿用同一思路当上层窗口完全遮住、且不再需要显示或交互底层 UI 时可以停掉底层 Canvas仍需露出或交互的部分则保留。但 UI 长期多层重叠会同时增加 DrawCall、Overdraw 和输入管理成本默认应从交互与界面设计上避免堆叠过多窗口而不是依赖隐藏策略兜底。从异常数字回到具体对象Stats只能告诉你“这一帧变贵了”不能告诉你是谁让它变贵。排查时不要一上来把所有合批开关轮流切一遍用同一帧的 Frame Debugger 把数字落到具体的 Camera、Pass、材质和 Renderer路径会短很多。工具的选择、真机抓帧方式以及 UPR / UWA 的报告与付费边界见《Unity 性能优化工具与数据采集》。本篇只讨论渲染问题本身如何定位和处理。先固定复现镜头。选一个能稳定复现的操作点记录相机位置、质量档、窗口状态和特效状态。没有固定镜头前后两次的可见集不同数字没有可比性。按渲染阶段找异常段。在 Frame Debugger 里先区分是不透明、透明、阴影还是后处理段突然变长如果多了一整段全屏 Pass先回查相机栈、Renderer 的后处理数据、Renderer Feature、Volume以及深度/不透明纹理的实际使用者而不是先看小物件。比较相邻 draw 的状态。在预期能连续合批的位置逐项比对材质、shader variant、关键字、贴图、Lightmap、排序和每物体参数。第一个不同项通常就是拆批原因如果状态相同仍没合上再确认该 shader 是否真的兼容 SRP Batcher 或 Instancing。只做一个可逆修改再复测。例如把一组材质改为共享材质、关闭一个 Volume 覆盖项或暂时隐藏一层粒子。先在 Frame Debugger 证明异常 Pass 或拆批消失再到目标真机确认 CPU / GPU 帧时间的收益只有两个证据都成立才把修改固化为资源或场景约定。这个顺序也能防止“看起来省了 Batches实际只是把画面或可见集一起改没了”。每次记录至少保留复现步骤、改动项、Editor 的 DrawCall / Batches / SetPass以及真机的 CPU / GPU 帧时间和测试时长不需要写入具体业务名称或资源名称。验证与回归方法同一场景、同一操作路径、同一设备做 A/B一次只改一类动作少一台相机、关一项后处理、开/关动态合批Editor 用 Frame Debugger 和 Stats 采集 DrawCall / Batches / SetPass确认 Pass 和合批行为符合预期真机采集 CPU / GPU Frame Time、95/99 分位必要时拆 GPU 模块动态合批、后处理这类“可能更贵”的开关目标档位机型上各做一次开/关用同一条关卡流向覆盖场景加载 → 普通游走 → UI 高交互 → 特效叠加 → 全屏弹窗。只在静止场景里测出来的优化上线后经常不成立连续跑 1015 分钟看温度和掉频。只录开头 30 秒平均帧率会骗人。其他应该注意的点没有场景提交量基线开发期完全不看 EditorStats等问题堆到打包才发现 DrawCall 涨了一截把 UWA / UPR 的区间当成项目 KPI或反过来只认 Editor 数字、不上真机看时间和带宽只看 DrawCall忽略 SetPass 或 GPU 像素填充。Batches 降了、半透明层还在低端机照样烫动态合批盲开小物体场景之外 Batches 降了CPU 更忙SRP Batcher 迷信材质不兼容、变体一堆开关开了 Frame Debugger 里根本没有SRP BatchMaterialPropertyBlock改色把能走 SRP Batcher 的物体整批拆掉多挂 Overlay 相机做 UI / 预览Batches 没降剔除和全屏 Pass 先翻倍空相机也有成本Unity 官方测过“什么都不画”仍然吃Camera.Render全机型同一套后处理低档机被全屏 Pass 打满Opaque Texture或Depth Texture被打开却没有明确使用者网格合得太大视锥和遮挡都剔不掉远处仍在画顶点缓冲先涨UI 只做图集没控制 Canvas 重建范围移出屏幕的元素还在提交截图优化没释放 RT弹窗路径上出现隐性内存峰值。总结可迁移的原则就三条先看真机卡在 CPU 还是 GPU再动手。开发期用 DrawCall、Batches、SetPass Calls 挡住提交量上涨金标准是目标机型上的 CPU Time、GPU Time 和带宽。UWA / UPR 给的是行业水位不是项目 KPI先减要画的再减提交。少加相机、收缩可见集、共享材质常规物体优先 SRP Batcher大量完全相同的重复组再 A/B Instancing动态合批最后考虑。静态合批单独核算内存和加载成本DrawCall 和 Overdraw 分开看一次只改一类动作。Frame Debugger 用来证实合批和 Pass 真的按预期发生结论以真机 Release 包为准连续玩一段时间后的分位和温度也要进验收。下一篇可以把切图、图集和纹理带宽单独展开渲染侧把提交和填充管住之后超大图、错误压缩和混乱图集仍会把 GPU 带宽打满那是资源链该收的账。创作不易。如果这篇文章对你有所帮助欢迎关注、点赞、收藏我会持续输出游戏开发相关内容关注不迷路。ღ( ´ᴗ )比心。