Files
XCEngine/docs/used/Scene选中描边彻底修复计划_2026-04-08.md

8.5 KiB
Raw Blame History

Scene 选中描边彻底修复计划

日期2026-04-08

1. 文档定位

本文档只解决一个问题:

  • Scene 面板里,选中对象的描边效果需要达到稳定、可扩展、接近商业编辑器的质量。

本文档不讨论:

  • 层级面板右键菜单
  • 灯光 gizmo 图标
  • 运行时高亮
  • 游戏内选中反馈

2. 当前问题结论

当前实现不是“可见外轮廓提取”,而是“基于 object-id 的屏幕空间膨胀”。

现状链路如下:

  1. 主场景正常渲染。
  2. BuiltinObjectIdPass 把屏幕上最前面的物体 id 写入 objectIdTexture
  3. BuiltinObjectIdOutlinePass 在全屏 shader 中检查当前像素周围是否存在“选中对象的 object-id”。
  4. 只要邻域中存在选中 object-id当前像素就会被涂成 outline。

这个算法的根本缺陷是:

  • 它只知道“邻域里有没有选中对象”。
  • 它不知道“当前像素前面是不是别的物体挡住了选中对象”。
  • 它也不知道“这条边是可见外轮廓,还是选中大平面与前景遮挡物之间的内部接触边界”。

所以当选中一个大平面时,平面上方的 cube、sphere、capsule 这些前景物体边缘也会被错误描边。

这不是数据污染,也不是拾取错误,而是算法层面的必然结果。

3. 根因

根因可以归纳为两条:

3.1 描边输入选错了

当前描边 pass 直接消费 objectIdTexture

objectIdTexture 的职责本质上是:

  • 告诉编辑器“当前屏幕像素最前面的对象是谁”,用于 picking。

它不适合作为“选中描边”的唯一依据,因为它不包含:

  • 选中对象的可见 mask
  • 选中对象自己的深度
  • 当前场景深度与选中对象深度之间的遮挡关系

3.2 描边算法缺少深度裁决

当前 outline shader 的判断规则是:

  • 当前像素不是选中对象
  • 但周围像素里存在选中对象

满足这两个条件就画 outline。

这套规则没有使用场景深度,也没有使用选中对象的可见性信息,因此无法区分:

  • 真正应该画的外轮廓
  • 不应该画的前景遮挡边界

4. 修复目标

本次彻底修复后的目标是:

  1. 选中大平面时,前景物体边缘不再被错误染成 outline。
  2. 选中普通 mesh 时outline 仍然能稳定出现在可见外轮廓处。
  3. 被遮挡区域不画“伪外轮廓”。
  4. picking 与 selection outline 两套能力彻底解耦。
  5. 后续如果要做 Unity 风格的 x-ray 选中、被遮挡高亮、hover outline可以在当前结构上继续扩展而不是推倒重来。

5. 非目标

这次修复不做以下内容:

  1. 不做运行时游戏内高亮系统。
  2. 不做完整的“透视遮挡时仍显示淡色轮廓”的 x-ray 选中风格。
  3. 不借这次机会重写整个 editor render pipeline。
  4. 不继续在现有 object-id-outline.shader 上做只针对单个 case 的临时补丁。

6. 总体方案

正确方案是把“拾取”和“描边”拆开,改成:

  • object id for picking
  • visible selection mask for outline
  • depth-aware outline composite

6.1 picking 继续走 object-id

现有 objectIdTexture 继续只用于:

  • 场景点击选中
  • object-id readback

它不再直接决定 outline 的视觉结果。

6.2 新增 selection mask pass

新增一张仅服务于 outline 的 selectionMaskTexture

它的规则是:

  1. 只渲染当前选中的对象。
  2. 渲染时绑定主场景已有的深度缓冲。
  3. 依赖深度测试,只把“真正可见的 selected fragment”写进 selectionMaskTexture

这样得到的不是 picking id 图,而是“选中对象可见区域 mask”。

6.3 scene viewport 暴露深度 SRV

为了做真正的 depth-aware compositeScene viewport 需要同时持有:

  1. depthView
  2. depthShaderView

也就是说Scene viewport 的深度纹理既要能作为 DSV 使用,也要能在全屏 composite shader 中作为 SRV 读取。

底层 RHI 已经具备这条能力,阴影缓存已有相同用法,因此这不是架构冒险,而是 viewport 资源层尚未接线。

6.4 重写 outline composite pass

新的 outline shader 不再直接从 objectIdTexture 做邻域膨胀,而是读取:

  1. selectionMaskTexture
  2. sceneDepthTexture

必要时预留:

  1. selectionDepthTexture

新的裁决规则应为:

  1. 中心像素若属于 selected mask本像素不直接着色。
  2. 若邻域存在 selected mask则该像素可能位于 selected 的边界附近。
  3. 只有在深度关系证明“当前像素不是更近的前景遮挡物”时,才允许输出 outline。

这一步的本质是:

  • 只画可见 selected 外轮廓。
  • 不把前景遮挡物边界误判为 selected outline。

7. 具体实施步骤

Phase 1补齐 viewport 资源

目标:

  • Scene viewport 持有可采样深度。
  • Scene viewport 持有单独的 selection mask render target。

需要修改:

  1. editor/src/Viewport/ViewportHostRenderTargets.h
  2. 相关 viewport resource reuse / destroy / create 流程

新增资源建议:

  1. depthShaderView
  2. selectionMaskTexture
  3. selectionMaskView
  4. selectionMaskShaderView
  5. selectionMaskState

验收标准:

  • Scene viewport 创建/销毁/复用逻辑能完整覆盖新资源。
  • 编译通过。
  • 不影响现有 Scene 视图展示。

Phase 2新增 selection mask pass

目标:

  • 只把当前选中对象的可见区域写入 mask。

实现建议:

  1. 新建 editor 专用 SelectionMaskPass
  2. 输入为选中对象 id 列表。
  3. 输出到 selectionMaskTexture
  4. 渲染时绑定主场景同一套深度。

实现关键点:

  1. 这个 pass 的语义不是 picking。
  2. 它只关心“选中对象哪些像素当前可见”。
  3. 它不需要写 object id只需要写统一 mask 值或选中组 id。

验收标准:

  • 选中对象时可以输出稳定的可见 mask。
  • 未选中时 pass 可被跳过。

Phase 3重写 outline composite

目标:

  • outline 只出现在选中对象的可见外轮廓。

实现建议:

  1. 保留全屏 pass 形式。
  2. 输入从 objectIdTexture 改为 selectionMaskTexture + depthShaderView
  3. 邻域检查继续保留,但结果必须经过深度裁决。

最低正确规则:

  1. 中心像素若是别的前景几何,且深度显著近于 selected 边界,不画 outline。
  2. 中心像素若是背景或更远表面,可以画 outline。

验收标准:

  1. 选中大平面时,前景 cube/sphere/capsule 边缘不再被错误描边。
  2. 选中单个 mesh 时,外轮廓仍连续稳定。

Phase 4移除旧耦合

目标:

  • 彻底消除 “outline 直接依赖 object-id picking 纹理” 这一旧逻辑。

需要完成:

  1. object-id-outline.shader 逻辑下线或重命名。
  2. SceneViewportSelectionOutlinePass 的输入契约改为新资源。
  3. 保证 objectIdTexture 仅服务于 picking。

验收标准:

  • 代码层不再存在“outline 直接读 object id 邻域并膨胀”的核心路径。

8. 测试计划

至少补以下回归验证:

8.1 功能场景

  1. 选中大平面,前面摆放 cube / sphere / capsule
    • 前景物体边缘不能被错误描边。
  2. 选中单个 cube背景是地面
    • outline 能稳定显示在 cube 可见外轮廓上。
  3. 选中被部分遮挡的物体:
    • 只允许可见区域出现 outline。
  4. 多选相邻物体:
    • outline 不串边,不污染第三方前景物体。

8.2 回归类型

  1. shader 资源加载测试
  2. render plan 接线测试
  3. pass 构建与资源存在性测试
  4. 视口级人工验证截图

9. 风险与边界

9.1 风险

  1. 仅使用 sceneDepth + selectionMask 可能仍有少数边界 case 需要 selectionDepth 才能更稳。
  2. Scene viewport 新增 render target 后,资源管理和状态切换复杂度会上升。
  3. 若后续要兼容 Vulkan / OpenGL需要同步确认深度 SRV 路径与资源状态语义。

9.2 边界

本计划的第一目标不是“做最炫的 outline”而是

  • 先把语义做对
  • 再把视觉做稳

只要还在复用 picking 的 objectIdTexture 直接做 outline问题就不可能彻底解决。

10. 最终结论

这次 bug 不能靠继续修补当前 object-id-outline.shader 解决。

彻底方案必须满足三点:

  1. picking 与 outline 解耦
  2. 选中对象生成独立可见 mask
  3. outline composite 引入深度裁决

只有这样Scene 视图选中描边才会从“屏幕空间膨胀特效”升级为“真正可用的编辑器轮廓系统”。