在 Unity UGUI 性能优化之旅中,我们已经学习了基础的资源管理和 Canvas 与 UI 元素的管理。现在,我们将把目光转向更深层次的 渲染层面,特别是如何优化 像素填充率(Pixel Fill Rate)。在这个环节中,Overdraw(过度绘制) 是一个我们必须理解和解决的关键问题,因为它直接关系到 GPU 的工作效率。
想象一下你在纸上画画。如果你先画了一个绿色的圆,然后又用一个蓝色的方块完全覆盖了它,最后再用一个黄色的三角形覆盖了方块的一部分。那么在最终的作品上,你只看到了黄色的三角形、蓝色的方块和绿色圆形的未被覆盖部分。但是,在绘制过程中,你实际上在某些区域画了不止一次。
在 GPU 渲染中,Overdraw(过度绘制) 就是指在一个像素点上,GPU 进行了多次绘制操作。当多个 UI 元素重叠时,处于上层的 UI 元素会遮挡下层的 UI 元素,但 GPU 仍然可能需要对被遮挡的像素进行绘制。
危害:
Overdraw 在 UI 渲染中尤为常见,因为 UI 元素往往是层叠放置的,特别是背景图、面板、文本、图标等常常会发生重叠。
Unity 提供了强大的工具来帮助我们检测和可视化 Overdraw,从而找出问题所在。
这是最直观的检测 Overdraw 的方法。
Draw Mode
下拉菜单(通常默认为 Shaded
)。Draw Mode
下拉菜单,选择 Overdraw
。通过这个模式,你可以非常直观地看到哪些 UI 区域存在严重的 Overdraw 问题,并且可以旋转摄像机,检查 3D 场景中的 Overdraw 情况(尽管我们这里主要关注 UGUI)。
Profiler 是更深入分析性能问题的工具,可以帮助我们量化 Overdraw 的影响。
Window > Analysis > Profiler
。Active Profiler
设置为 CPU Usage
,同时确保 Deep Profile
(如果需要,但会增加开销)和 Record Editor
是勾选的。Add Profiler
按钮,添加 GPU Usage
模块。GPU Usage
模块中,你可以看到 UI.Render
相关的耗时。更重要的是,你可以看到 Draw Calls
的数量。虽然 Draw Calls
增加不一定直接意味着 Overdraw,但过多的 Draw Calls 往往会伴随着 Overdraw。GPU Usage
的详细视图中,你可以看到 Overdraw
的具体百分比,以及像素填充率的图表。虽然没有像 Scene 视图那样直观的颜色显示,但这里提供了量化的数据,可以帮助你评估优化效果。Frame Debugger 是一个强大的工具,可以逐帧地查看渲染过程中的每一个 Draw Call。虽然它不能直接显示 Overdraw 颜色图,但你可以通过它观察 Draw Call 的顺序和内容,从而推断出哪些 Draw Calls 导致了 Overdraw。
Window > Analysis > Frame Debugger
。Enable
按钮。State Change
信息。如果看到频繁的 Blend State
变化(例如,从不透明渲染切换到半透明渲染),这也可能是 Overdraw 的信号。结合使用: 推荐先用 Scene View 的 Overdraw 模式 快速定位问题区域,然后用 Profiler 量化性能影响,最后用 Frame Debugger 深入分析 Draw Call 顺序和状态变化,找出具体的优化点。
减少 Overdraw 的核心思想是:让 GPU 绘制每个像素的次数尽可能少。 这可以通过合理组织 UI 元素、使用裁剪和剔除、优化透明度处理等多种方式来实现。
UI 元素的渲染顺序直接影响 Overdraw。Unity 的 UGUI 渲染顺序遵循以下规则(从后往前绘制):
Render Mode
:
World Space
Canvas 先于 Screen Space
Canvas 渲染。Screen Space
模式下,Screen Space - Camera
Canvas 的渲染顺序由其所引用的摄像机的 Depth
属性决定。Screen Space - Overlay
Canvas 总是最后渲染,位于所有 3D 物体之上。Canvas
组件的 Sorting Layer
和 Order in Layer
: 用于控制不同 Canvas 之间的渲染顺序。GameObject
在 Hierarchy 窗口中的顺序决定了它们的渲染顺序。下层的 GameObject
会覆盖上层的 GameObject
。优化建议:
- UI Root
- Background_Opaque_Canvas (最底层,所有不透明背景)
- MainUI_Opaque_Canvas (中间层,所有不透明按钮、图标、文本)
- Overlay_Transparent_Canvas (最高层,所有半透明特效、提示、弹窗背景)
裁剪(Clipping)和剔除(Culling)是减少不必要绘制的关键手段,尤其对于大型可滚动列表。
Rect Mask 2D
或 Mask
组件Mask
组件通过在渲染管线中修改模板缓冲区(Stencil Buffer)来实现裁剪。只有模板缓冲区中对应像素的值符合条件,像素才会被绘制。Mask
组件会修改渲染状态,导致其内部和外部的 UI 元素无法合批。即使是 Rect Mask 2D
也会引入额外的 Draw Call。Mask
组件。Mask
的作用范围,不要让一个 Mask
影响过多的 UI 元素。Image
组件的 Type
设置为 Filled
或 Sliced
。
Image.Type = Filled
:可以将图片按比例填充(圆形、水平、垂直等),实现裁剪效果。Image.Type = Sliced
:用于九宫格切图,可以拉伸中间部分而保持四角不变,常用于按钮背景、面板。这两种类型本身不会引入 Mask
的额外 Draw Call。对于大型滚动列表,除了 Rect Mask 2D
提供的裁剪,我们还可以通过代码来 手动剔除(Culling) 那些完全不在屏幕或视口范围内的 UI 列表项。
Rect Mask 2D
裁剪了,它们的 GameObject
和组件仍然是激活的,仍然可能产生布局计算和 Mesh 生成的开销。手动剔除是指将那些完全移出可见区域的列表项 GameObject.SetActive(false)
,并将其放回对象池。EnhancedScroller
, Loop ScrollRect
)都实现了这种虚拟化列表(Virtual Scroll List)的功能。它们只创建和维护可见区域内的少量列表项,并复用这些项来显示数据。ScrollRect
的 onValueChanged
事件。Rect Transform
是否与 ScrollRect
的 viewport
矩形区域有交集。viewport
范围,就将其 SetActive(false)
并放回对象池。SetActive(true)
。GameObject
不会参与布局计算和渲染。正如前面提到的,透明和不透明对象的渲染方式是不同的。GPU 通常会先渲染所有不透明对象,然后渲染所有半透明对象。
优化建议:
Blend Off
和 AlphaTest Greater 0.5
可以实现。不过 UGUI 默认 Shader 并不直接支持 Alpha Test,可能需要自定义 Shader。Canvas Group
的 alpha
属性通常不会触发 Canvas 重建。然而,它仍然会改变渲染状态,导致所有子 UI 元素都以半透明模式渲染,从而可能增加 Overdraw。因此,只有在需要淡入淡出效果时才使用它。Pixel Perfect
是 UGUI 中一个经常被忽视的属性,但它对 UI 渲染质量和性能都有影响。
什么是 Pixel Perfect?
当一个 UI 元素的原始像素与屏幕上的渲染像素精确对齐时,就是 Pixel Perfect。如果不对齐,Unity 可能会进行抗锯齿处理(Anti-aliasing)或模糊处理,以使图像看起来更平滑。
为什么重要?
如何实现 Pixel Perfect?
UI Scale Mode
: 选择 Scale With Screen Size
,并设置一个 Reference Resolution
(参考分辨率)。Screen Match Mode
: 选择 Expand
或 Shrink
,或者 Match Width Or Height
,根据你的适配策略来选择。Reference Pixels Per Unit
: 默认是 100。在导入 Sprite 时,也需要确保其 Pixels Per Unit
与这个值匹配,这样 Image
组件的尺寸单位才能与原始像素对应。Canvas
组件的 Pixel Perfect
属性:
Canvas
组件的 Inspector 窗口中,勾选 Pixel Perfect
复选框。Pixel Perfect
时,Unity 会尝试调整所有子 UI 元素的 Rect Transform
坐标和尺寸,使它们的像素与屏幕像素对齐。这可以消除由于非整数坐标导致的模糊和潜在的 Overdraw。Pixel Perfect
可能会导致 UI 元素的位置和大小与设计图有微小偏差,在某些情况下可能会导致 UI 抖动或闪烁(通常是由于频繁的四舍五入操作)。因此,需要根据实际情况权衡。对于静态、不动的 UI 元素,这是一个很好的优化。对于频繁移动的 UI 元素,可能需要测试其副作用。Image
或 Text
组件进行过度的缩放。如果需要不同大小的同一张图片,考虑提供不同分辨率的图片或使用九宫格切图。Graphic Raycaster
是 UGUI 事件系统用来检测 UI 交互的关键组件。它的性能开销也需要被关注。
Graphic Raycaster
的原理与性能开销Graphic Raycaster
挂载在 Canvas 上,负责将输入事件(如鼠标点击、触摸)转换为射线检测,并判断射线是否击中了 Canvas 上的可交互 UI 元素(如 Button
, Toggle
, ScrollRect
等)。
Graphic Raycaster
会从事件点发出一条射线。然后,它会遍历 Canvas 上所有设置为 Raycast Target
的 UI 元素(通常是 Image
和 Text
组件),判断射线是否与这些元素的 Rect Transform
相交,如果相交,还会进一步判断是否与 UI 元素的实际像素(如果有透明像素)相交。Raycast Target
: Graphic Raycaster
需要遍历所有 Raycast Target
的 UI 元素,这在 UI 元素数量庞大时会带来可观的 CPU 开销。Image
或 Text
的 Raycast Target
被勾选,并且它们是透明图片,Graphic Raycaster
还会进行像素级的 Alpha 测试,以判断射线是否击中了非透明像素。这会增加额外的计算量。Ignore Reversed Graphics
和 Blocking Objects
等属性进行优化Graphic Raycaster
提供了一些属性来帮助我们优化性能。
Ignore Reversed Graphics
Graphic Raycaster
将忽略那些被反向绘制的 UI 元素。当 UI 元素被旋转 180 度或被负缩放时,它们的“正面”可能会朝向相反方向,此时它们仍然会拦截射线。勾选此项可以避免这种情况,减少不必要的射线检测。Blocking Objects
Graphic Raycaster
是否会与 3D 对象或 2D Collider
发生阻塞。
None
:不与任何 3D/2D 对象阻塞。2D Physics
:射线会与 2D Collider
发生阻塞。3D Physics
:射线会与 3D Collider
发生阻塞。All
:同时与 2D 和 3D Collider
阻塞。Screen Space - Overlay
模式,并且不与游戏世界中的 3D/2D 对象交互,那么将 Blocking Objects
设置为 None
。这可以避免不必要的物理射线检测,减少开销。All
: 如果 UI 需要与 3D/2D 对象交互,例如游戏世界中的 UI 按钮,那么才需要设置 Blocking Objects
为 All
或相应类型。Graphic Raycaster
Graphic Raycaster
应该只挂载在需要接收输入的 Canvas 上。 如果一个 Canvas 只是用于显示静态信息或背景,它就不需要 Graphic Raycaster
。Raycast Target
属性:
Image
和 Text
组件都有一个 Raycast Target
复选框。默认情况下是勾选的。Image
和 Text
组件,务必取消勾选 Raycast Target
。
Raycast Target
的 UI 元素,Graphic Raycaster
在每次输入事件发生时就需要多遍历一个元素,进行射线检测。取消勾选可以显著减少遍历开销。GameObject
的 Image
或 Text
是 Raycast Target
,而其子级是可交互组件(如 Button
),那么子级依然可以接收事件。但如果父级不需要拦截射线,就取消其 Raycast Target
。示例:
一个复杂的聊天界面,包含了背景图、很多聊天气泡(每个气泡有背景图和文本),以及输入框和发送按钮。
Image
的 Raycast Target
取消勾选。Image
的 Raycast Target
取消勾选。Text
的 Raycast Target
取消勾选。InputField
和发送按钮的 Button
:它们会默认勾选 Raycast Target
,这是正确的。通过这种方式,可以大大减少 Graphic Raycaster
的遍历范围,从而优化 UI 交互的性能。
尽管 UGUI 自动生成 Mesh,但我们可以通过一些方式来控制 Mesh 的复杂度,从而降低 GPU 的渲染负担。
Image
组件的 Type
选择Image
组件的 Type
属性会影响其生成的 Mesh 顶点数量。
Simple
:
Simple
类型。Sliced
(九宫格):
Simple
类型。Tiled
(平铺):
Rect Transform
区域。Filled
(填充):
核心思想: 选择最简单的 Type
类型,以减少不必要的顶点生成。
Mask
组件再次强调 Mask
组件(包括 Rect Mask 2D
):
Mask
会导致其内部的 UI 元素 Mesh 生成后,再通过模板测试进行裁剪。这意味着即使被裁剪掉的像素,其 Mesh 仍然被生成和上传。Mask
组件。Mask
。UI Builder
等工具优化 UI 布局和 MeshUnity 提供了 UI Builder
工具来创建 UXML 和 USS 布局,类似于 Web 开发中的 HTML/CSS。
优势:
UI Builder
生成的 UI 可能在底层优化方面做得更好,例如自动减少冗余的 GameObject
和 Rect Transform
嵌套。如何帮助优化 Mesh:
Rect Transform
变化。UI Builder
本身并不直接优化 UGUI 的 Mesh 生成,但它提供了一种更结构化的方式来构建 UI,这有助于开发者避免手动构建 UI 时可能犯的导致 Mesh 复杂化的错误。注意: UI Builder
主要用于 UI Toolkit
系统,与传统的 UGUI (Canvas, Image, Text) 并不是完全相同的技术栈。虽然你可以用 UI Toolkit
来实现游戏 UI,但目前 UGUI 仍然是 Unity 游戏 UI 的主流选择。这里提到 UI Builder
更多是作为未来发展方向的参考,或者针对一些特定工具界面的开发。对于纯 UGUI 项目,重点还是在于前面提到的针对 Canvas 和组件的优化。
虽然 UGUI 默认不直接支持 GPU Instancing
,但对于某些重复性高、渲染状态相同的 UI 元素(例如大量相同的血条、地图上的标记),如果能够将它们转化为 World Space
UI,并使用自定义 Shader 支持 GPU Instancing
,就可以大幅减少 Draw Calls。
GPU Instancing
允许 GPU 一次性渲染多个相同的 Mesh,但每个 Mesh 可以有不同的位置、旋转、缩放或颜色等属性。这大大减少了 CPU 到 GPU 的 Draw Call 数量。Screen Space - Overlay
和 Screen Space - Camera
模式下的 UGUI 默认不支持 GPU Instancing
。World Space
的 UI。例如,在 3D 世界中,所有怪物头顶的血条,或者地图上大量的固定标记点。如果这些 UI 元素的 Mesh 结构相同,并且可以通过 Material Property Block
来传递位置、颜色等少量差异数据,就可以考虑 GPU Instancing
。World Space
Canvas。Render Queue
是正确的(例如 Transparent
)。GPU Instancing
(#pragma instancing_options
for
_Graphics
)。Graphics.DrawMeshInstanced
或 Graphics.DrawMeshInstancedIndirect
来批量渲染这些 UI 元素的 Mesh,而不是通过 Image
组件。这需要更底层的图形编程知识。GPU Instancing
。但如果你遇到了海量同类 UI 元素(数百上千个)的 Draw Call 瓶颈,这会是一个非常强大的解决方案。本篇文章深入探讨了 UGUI 渲染层面的优化,特别是如何对抗 Overdraw(过度绘制) 这一 GPU 性能杀手:
Rect Mask 2D
的优缺点以及如何通过手动剔除屏幕外 UI 元素来优化滚动列表。Graphic Raycaster
,包括合理设置 Ignore Reversed Graphics
和 Blocking Objects
,以及最重要的是 取消不需要交互的 UI 元素的 Raycast Target
属性。Image
组件的 Type
选择 和 避免不必要的 Mask
组件 来减少 Mesh 的顶点和三角形数量。通过本篇的学习,我们现在应该能够更精细地控制 UGUI 的渲染过程,减少不必要的 GPU 负载,从而大幅提升游戏的帧率和流畅度。
在最终章,我们将涵盖一些更高级的优化技巧,如动画、Shader、内存优化,以及性能分析工具的深度使用和编码实践的最佳实践。