首页
/ Bevy UI 裁剪迁移指南:CalculatedClip 如何存储经过变换的裁剪矩形

Bevy UI 裁剪迁移指南:CalculatedClip 如何存储经过变换的裁剪矩形

2026-09-05 15:48:40作者:申梦珏Efrain

在 Bevy 的 UI 渲染管线中,CalculatedClip 组件负责记录每个 UI 节点从祖先节点继承下来的所有裁剪区域。近期一次 API 变更(对应 PR 24148)将 CalculatedClip 从简单的矩形列表重构为带 Rects / FullyClipped 两个变体的枚举:每个裁剪区域不仅记录一个 Rect,还附带一个 Affine2 世界空间到裁剪节点局部空间的变换矩阵,从而正确支持祖先节点被缩放、旋转后的裁剪行为;同时新增了 FullyClipped 状态,表示节点及其后代被完全裁剪、不参与渲染与拾取。读完本文,你将掌握新 CalculatedClip 的类型结构、各查询/修改方法的语义(尤其是 contains_point 的坐标系约定)、组件在 UI 更新系统中如何逐层传播,以及渲染与拾取两条消费链路如何使用它,最后附一份旧代码的迁移适配清单。

变更总览:从“矩形列表”到“带变换的裁剪区域 + 完全裁剪状态”

迁移说明的核心内容可以概括为三点:

  1. CalculatedClip 现在是枚举,包含 RectsFullyClipped 两个变体:
    • Rects:一个 UI 实体从其祖先继承的裁剪区域列表,每个区域由一个 Rect(裁剪矩形)和一个 Affine2(世界空间到该裁剪节点局部空间的变换)共同定义;
    • FullyClipped:表示该 UI 实体被完全裁剪,既不会被渲染,也不参与拾取(picking)。
  2. 坐标系语义明确化:裁剪矩形存储在其所属裁剪节点的局部空间中,Affine2 变换用于把世界坐标(物理像素)的点转换回局部空间再做包含判断。这一点在旋转/缩放的 UI 场景下尤为关键。
  3. 新增点包含查询CalculatedClip::contains_point 可用于判断一个物理像素坐标系下的点是否被裁剪掉。

类型定义:CalculatedClip 与 CalculatedClipRect

CalculatedClip 定义在 bevy_ui 的 ui_node.rs,与其配套的单个裁剪区域结构 CalculatedClipRect 就在同一文件上方:

/// A single local-space clipping rect.
pub struct CalculatedClipRect {
    /// The clip rect in the clipping node's local space.
    pub rect: Rect,
    /// Transform from world space into the clipping node's local space.
    pub world_to_clip_local: Affine2,
}

/// The calculated clipping inherited by the node.
pub enum CalculatedClip {
    /// Clip rects inherited from ancestors.
    Rects(SmallVec<[CalculatedClipRect; 2]>),
    /// The node and descendants are fully clipped.
    FullyClipped,
}

几个值得注意的实现细节:

  • Rects 内部使用 SmallVec<[CalculatedClipRect; 2]>。典型 UI 树中嵌套裁剪层的数量很少(0~2 层),SmallVec 让常见情况避免堆分配;嵌套超过两层时再转入堆内存。
  • CalculatedClip 派生了 ComponentCloneDebugPartialEqReflect,是标准 ECS 组件,可通过查询获取,也支持 ron 等序列化生态。
  • Default 实现为 Rects(空 SmallVec),即“没有任何继承裁剪”的初始状态(见 ui_node.rs)。
  • CalculatedClip 配套的还有 OverrideClip 标记组件:带有该组件的节点会忽略任何继承的裁剪矩形,无论其祖先如何设置 Overflow 都不会被裁剪。

常用方法:判断、查询与修改

CalculatedClip 提供了四个核心方法(见 ui_node.rs),迁移后应统一通过它们访问状态,而不是直接拆枚举字段:

方法 签名要点 语义
is_fully_clipped const fn -> bool 节点及其后代是否被完全裁剪(matches!(self, Self::FullyClipped)
rects -> Option<&[CalculatedClipRect]> 未完全裁剪时返回继承的裁剪矩形切片;FullyClipped 返回 None
contains_point (point: Vec2) -> bool 判断物理像素坐标下的点是否同时位于所有继承裁剪矩形之内;FullyClipped 恒返回 false
push_rect (rect: Rect, world_to_clip_local: Affine2) -> mut 在未完全裁剪时向列表追加一个裁剪区域(已完全裁剪则无效)

其中 contains_point 的实现清晰展示了“Rect + Affine2”这一组合的用法——先把世界坐标点变换到裁剪节点局部空间,再做矩形包含测试:

pub fn contains_point(&self, point: Vec2) -> bool {
    match self {
        Self::Rects(rects) => rects.iter().all(|clip_rect| {
            clip_rect
                .rect
                .contains(clip_rect.world_to_clip_local.transform_point2(point))
        }),
        Self::FullyClipped => false,
    }
}

with_rect(rect, transform) 则是“继承 + 追加”的组合糖:若当前状态未完全裁剪且给定变换可逆(transform.try_inverse()),就克隆自身并把新的世界空间矩形变换成裁剪局部空间后追加;否则直接返回 CalculatedClip::FullyClipped

组件如何被计算:update_clipping_system 的逐层传播

CalculatedClipbevy_ui 的 update_clipping_system 在每帧 UI 更新阶段沿 UI 树自顶向下计算。理解这段逻辑,才能正确理解两个变体在什么条件下出现:

  1. 从根节点开始递归:系统遍历所有 UI 根节点,对每个节点先处理“继承来的裁剪状态”,再决定要传递给子节点的状态。
  2. 新裁剪上下文的开启:若节点带有 OverrideClipFixedNode 组件,继承的裁剪状态被直接丢弃(maybe_inherited_clip = None),相当于“打开新的裁剪上下文”,祖先的裁剪不再影响该子树(见 update.rs)。
  3. Display::None 触发 FullyClippedDisplay::None 的节点及其整棵后代子树都会被标记为 FullyClipped(见 update.rs),这正是迁移说明中“既不会被渲染也不参与拾取”的来源。
  4. 组件的增删维护:如果本帧没有继承裁剪,系统会移除该节点上的 CalculatedClip 组件(而非保留空枚举),下游渲染因此以 Option<&CalculatedClip> = None 表示“无裁剪”;有继承裁剪时才插入/更新组件(见 update.rs)。
  5. 子节点裁剪区域的推导:当前节点若不裁剪(overflow.is_visible())或已完全裁剪,原样下传;否则取自身全局变换的逆矩阵(transform.try_inverse()),把 resolve_clip_rect(overflow, overflow_clip_margin) 解析出的裁剪矩形连同“世界→局部”变换一起 push_rect 到继承列表后传给子节点;若变换不可逆,则子树整体变为 FullyClipped(见 update.rs)。这里正是每个裁剪区域携带 Affine2 变换的代码落点:局部矩形 + 逆世界变换,二者缺一不可。

相关行为有专门的单元测试覆盖:fixed_node_opens_new_clipping_context 验证了 FixedNode 使后代 CalculatedClip 组件被移除(裁剪上下文重置),override_clip_opens_new_clipping_context 验证了 OverrideClip 的同样效果(测试位于 update.rs 附近的 tests 模块)。

下游消费:渲染裁剪与拾取判定

渲染:clip_polygon 的 Sutherland–Hodgman 裁剪

bevy_ui_render 侧的各管线(UI 材质管线、文本、box shadow、渐变、纹理切片等)在提取渲染实体时都会携带 clip: Option<CalculatedClip>,并以 Changed<CalculatedClip> / RemovedComponents<CalculatedClip> 作为重提取触发条件(如 ui_material_pipeline.rstext.rs)。真正消费该组件的是 clip_polygon:它对输入多边形按“每条裁剪边依次裁剪”的 Sutherland–Hodgman 流程处理,FullyClipped 直接返回空顶点列表(clip.rects()None 的分支),否则对每个裁剪区域的四条边逐一执行 edge_clip——边裁剪时先用 region.world_to_clip_local.transform_point2(...) 把顶点换算到裁剪局部空间再求有向距离,从而保证旋转/缩放祖先下的裁剪边界依然精确。该模块内建了一组单元测试,覆盖无裁剪、完全裁剪、轴对齐裁剪、嵌套旋转裁剪区域组合(nested_clip_rects_compose)以及完全越界等场景(见 clipping.rstests 模块)。

拾取:contains_point 直接排除被裁剪节点

bevy_ui 的 picking 后端在遍历 UI 节点做指针命中测试时,除了节点自身的 contains_point 判断,还会用继承裁剪做第二道过滤(见 picking_backend.rs):

if node.node.contains_point(*node.transform, *cursor_position)
    && node
        .calculated_clip
        .is_none_or(|clip| clip.contains_point(*cursor_position))

注意这里的语义闭环:组件不存在(None)表示没有任何裁剪;Rects 时逐区域判断指针位置;FullyClippedcontains_point 恒为 false,节点自然被跳过——与迁移说明“FullyClipped 的节点不可拾取”的表述一致。

迁移适配清单

如果你之前访问过 CalculatedClip 的旧字段,以下是对应的新写法:

  1. 读取裁剪矩形列表:旧结构体字段访问改为 clip.rects(),注意其返回 Option<&[CalculatedClipRect]>FullyClipped 时为 None,需要先匹配或 is_some_and 处理。
  2. 判断是否完全裁剪:使用 clip.is_fully_clipped() 代替对旧字段为空/特殊的约定判断。
  3. 点包含测试:使用 clip.contains_point(point)point 必须是物理像素坐标下的点;实现内部会完成到局部空间的坐标变换,调用方无需自行处理 Affine2
  4. 追加裁剪区域:使用 clip.push_rect(rect, world_to_clip_local) 或基于已有状态调用 with_rect(rect, transform)(后者以世界空间 Rect + 节点全局变换为入参,自动求逆并短路处理不可逆变换与 FullyClipped)。
  5. 枚举匹配:需要区分两种状态时按 match clip { CalculatedClip::Rects(rects) => ..., CalculatedClip::FullyClipped => ... } 处理;FullyClipped 分支应等价于“不可见、不可拾取”。
  6. 新增标记组件语义:如果你的 UI 需要豁免裁剪(例如工具栏悬浮层),优先复用 OverrideClipFixedNode,它们在 update_clipping_system 中统一重置继承裁剪,避免手写裁剪逻辑与引擎传播流程打架。

小结

这次变更让 Bevy UI 的裁剪模型在两个方向上更精确:裁剪区域携带“世界→裁剪局部空间”的 Affine2 变换,使旋转、缩放后的祖先裁剪边界在渲染(clip_polygon)与查询(contains_point)两条路径上保持一致;FullyClipped 变体把“不可见 + 不可拾取”的语义显式化,并由 Display::None 等场景统一生成。配合 update.rs 中的传播逻辑clip_polygon 裁剪实现 以及 picking 后端 的测试用例,可以为自定义 UI 管线或裁剪相关工具的开发提供完整的参照。

登录后查看全文
热门项目推荐
相关项目推荐