refactor: formalize scene viewport object-id picking

This commit is contained in:
2026-04-02 15:23:25 +08:00
parent 617b11f801
commit a5d2058818
3 changed files with 119 additions and 31 deletions

View File

@@ -0,0 +1,71 @@
# Renderer 阶段收口补充Object ID Picking 正式化
日期:`2026-04-02`
## 1. 这次补充收口解决什么
本次补充只收一件事:
- `SceneView` 选中主链路正式切到 `GPU object-id`
本次明确不做:
- render graph
- renderer 内更完整的多 pass 调度
- game/runtime 通用 picking 服务
原因很简单:这些属于下一阶段架构演进,不应该继续污染当前阶段的收口边界。
## 2. 本次收口后的正式行为
当前 `SceneView` 选中行为统一定义为:
1. 场景渲染时生成 `object-id` 纹理
2. 鼠标点击时读取对应像素
3. 颜色解码为实体 ID
4. `0` 视为“未选中任何对象”,但这仍然是一次成功的 GPU 采样
关键变化:
- editor 不再把 `CPU ray picking` 作为 `SceneView` 点击选中的静默回退主链路
- `CPU ray picking` 继续保留为独立几何工具能力,不再承担当前正式选中流程
- `object-id` 读取失败会被显式标记为 readback failure而不是与“没有有效帧”混在一起
## 3. 为什么这才算收口
之前的问题不是没有 `object-id pass`,而是“主路径”和“兜底路径”的语义不够硬:
- 成功采样
- 无有效 object-id 帧
- GPU 读回失败
这三种状态以前没有被清晰区分。
现在已经收紧为显式结果类型:
- `Unavailable`
- `Success`
- `ReadbackFailed`
这意味着:
- renderer/editor 的 `object-id` 交互已经形成可测试契约
- `0 id` 与“采样失败”不再混淆
- 后续若要继续升级成异步 readback、共享 picking 服务,也有稳定边界可接
## 4. 当前阶段完成后的边界
到这里,当前阶段可以正式视为完成:
- editor viewport 宿主链路已打通
- renderer 的 builtin post-process 已形成稳定接口
- `SceneView` 选中正式以 GPU object-id 为主链路
- 回归测试已覆盖 object-id 读回状态语义
下一阶段真正该做的是:
- renderer 内正式 render graph / pass graph
- 更完整的 renderer-owned picking 服务
- editor / runtime shared picking contract
而不是继续在这个阶段里反复修补 viewport host。

View File

@@ -4,7 +4,6 @@
#include "Core/ISceneManager.h" #include "Core/ISceneManager.h"
#include "Core/ISelectionManager.h" #include "Core/ISelectionManager.h"
#include "IViewportHostService.h" #include "IViewportHostService.h"
#include "SceneViewportPicker.h"
#include "SceneViewportCameraController.h" #include "SceneViewportCameraController.h"
#include "ViewportHostRenderFlowUtils.h" #include "ViewportHostRenderFlowUtils.h"
#include "ViewportHostRenderTargets.h" #include "ViewportHostRenderTargets.h"
@@ -159,27 +158,20 @@ public:
return 0; return 0;
} }
const Components::Scene* scene = context.GetSceneManager().GetScene(); if (context.GetSceneManager().GetScene() == nullptr) {
if (scene == nullptr) {
return 0; return 0;
} }
ViewportEntry& entry = GetEntry(EditorViewportKind::Scene); ViewportEntry& entry = GetEntry(EditorViewportKind::Scene);
uint64_t objectIdEntity = 0; const ViewportObjectIdPickResult objectIdPick =
if (TryPickSceneViewEntityWithObjectId( PickSceneViewEntityWithObjectId(
entry, entry,
viewportSize, viewportSize,
viewportMousePosition, viewportMousePosition);
objectIdEntity)) { if (objectIdPick.status == ViewportObjectIdPickStatus::ReadbackFailed) {
return objectIdEntity; SetViewportStatusIfEmpty(entry.statusText, "Scene object id readback failed");
} }
return objectIdPick.entityId;
SceneViewportPickRequest request = {};
request.scene = scene;
request.overlay = GetSceneViewOverlayData();
request.viewportSize = Math::Vector2(viewportSize.x, viewportSize.y);
request.viewportPosition = Math::Vector2(viewportMousePosition.x, viewportMousePosition.y);
return PickSceneViewportEntity(request).entityId;
} }
void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis axis) override { void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis axis) override {
@@ -576,14 +568,12 @@ private:
targets.hasValidObjectIdFrame = false; targets.hasValidObjectIdFrame = false;
} }
bool TryPickSceneViewEntityWithObjectId( ViewportObjectIdPickResult PickSceneViewEntityWithObjectId(
ViewportEntry& entry, ViewportEntry& entry,
const ImVec2& viewportSize, const ImVec2& viewportSize,
const ImVec2& viewportMousePosition, const ImVec2& viewportMousePosition) {
uint64_t& outEntityId) {
if (m_device == nullptr) { if (m_device == nullptr) {
outEntityId = 0; return {};
return false;
} }
ViewportObjectIdPickContext pickContext = {}; ViewportObjectIdPickContext pickContext = {};
@@ -596,7 +586,7 @@ private:
pickContext.viewportSize = viewportSize; pickContext.viewportSize = viewportSize;
pickContext.viewportMousePosition = viewportMousePosition; pickContext.viewportMousePosition = viewportMousePosition;
return TryPickViewportObjectIdEntity( return PickViewportObjectIdEntity(
pickContext, pickContext,
[this](const ViewportObjectIdReadbackRequest& request, std::array<uint8_t, 4>& outRgba) { [this](const ViewportObjectIdReadbackRequest& request, std::array<uint8_t, 4>& outRgba) {
return m_device != nullptr && return m_device != nullptr &&
@@ -607,8 +597,7 @@ private:
request.pixelX, request.pixelX,
request.pixelY, request.pixelY,
outRgba); outRgba);
}, });
outEntityId);
} }
UI::ImGuiBackendBridge* m_backend = nullptr; UI::ImGuiBackendBridge* m_backend = nullptr;

View File

@@ -9,6 +9,7 @@
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <utility>
namespace XCEngine { namespace XCEngine {
namespace Editor { namespace Editor {
@@ -32,6 +33,21 @@ struct ViewportObjectIdReadbackRequest {
uint32_t pixelY = 0; uint32_t pixelY = 0;
}; };
enum class ViewportObjectIdPickStatus : uint8_t {
Unavailable = 0,
Success,
ReadbackFailed
};
struct ViewportObjectIdPickResult {
ViewportObjectIdPickStatus status = ViewportObjectIdPickStatus::Unavailable;
uint64_t entityId = 0;
bool HasResolvedSample() const {
return status == ViewportObjectIdPickStatus::Success;
}
};
inline bool CanPickViewportObjectId(const ViewportObjectIdPickContext& context) { inline bool CanPickViewportObjectId(const ViewportObjectIdPickContext& context) {
return context.commandQueue != nullptr && return context.commandQueue != nullptr &&
context.texture != nullptr && context.texture != nullptr &&
@@ -67,28 +83,40 @@ inline bool BuildViewportObjectIdReadbackRequest(
} }
template <typename ReadPixelFn> template <typename ReadPixelFn>
bool TryPickViewportObjectIdEntity( ViewportObjectIdPickResult PickViewportObjectIdEntity(
const ViewportObjectIdPickContext& context, const ViewportObjectIdPickContext& context,
ReadPixelFn&& readPixel, ReadPixelFn&& readPixel) {
uint64_t& outEntityId) { ViewportObjectIdPickResult result = {};
outEntityId = 0;
ViewportObjectIdReadbackRequest request = {}; ViewportObjectIdReadbackRequest request = {};
if (!BuildViewportObjectIdReadbackRequest(context, request)) { if (!BuildViewportObjectIdReadbackRequest(context, request)) {
return false; return result;
} }
std::array<uint8_t, 4> rgba = {}; std::array<uint8_t, 4> rgba = {};
if (!readPixel(request, rgba)) { if (!readPixel(request, rgba)) {
return false; result.status = ViewportObjectIdPickStatus::ReadbackFailed;
return result;
} }
outEntityId = static_cast<uint64_t>(Rendering::DecodeObjectIdFromColor( result.status = ViewportObjectIdPickStatus::Success;
result.entityId = static_cast<uint64_t>(Rendering::DecodeObjectIdFromColor(
rgba[0], rgba[0],
rgba[1], rgba[1],
rgba[2], rgba[2],
rgba[3])); rgba[3]));
return true; return result;
}
template <typename ReadPixelFn>
bool TryPickViewportObjectIdEntity(
const ViewportObjectIdPickContext& context,
ReadPixelFn&& readPixel,
uint64_t& outEntityId) {
const ViewportObjectIdPickResult result =
PickViewportObjectIdEntity(context, std::forward<ReadPixelFn>(readPixel));
outEntityId = result.entityId;
return result.HasResolvedSample();
} }
} // namespace Editor } // namespace Editor