292 lines
8.5 KiB
Markdown
292 lines
8.5 KiB
Markdown
# 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 composite,`Scene` 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`
|
||
|
||
必要时预留:
|
||
|
||
3. `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 视图选中描边才会从“屏幕空间膨胀特效”升级为“真正可用的编辑器轮廓系统”。
|