From dec171cd1ee4fe05e38458ed26ea7ed28a98b8f8 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Mon, 5 Oct 2026 00:39:48 +0800 Subject: [PATCH] Fix selection outline never showing for the first entity entt entity 0's integral bits are 0, colliding with the "no selection" sentinel: u32 selected == 0 meant "clear selection", so the scene's first-created entity could never be outlined. Replaced with std::optional + FrameSceneData::selectedEntityValid. Also restore per-batch instance contiguity broken by the batchMats flattening (e038cb3): entities sharing a mesh scatter across the flat instance array in collection order. ViewScratch now tracks per-batch singly-linked lists and appendView aggregates each batch into a contiguous range, preserving the flat-array performance intent. Hierarchy click selection now routes through SelectEntity so outline stays in sync when picking from the panel. --- App/editor/Editor.cpp | 11 ++- App/editor/Editor.h | 11 +-- Engine/include/Engine/Renderer/Renderer.h | 7 +- .../include/Engine/Renderer/SceneRenderer.h | 25 +++++-- Engine/include/Engine/Renderer/SceneTypes.h | 5 +- Engine/src/Renderer/Passes/OutlinePass.cpp | 5 +- Engine/src/Renderer/Renderer.cpp | 2 +- Engine/src/Renderer/SceneRenderer.cpp | 71 +++++++++++++------ 8 files changed, 97 insertions(+), 40 deletions(-) diff --git a/App/editor/Editor.cpp b/App/editor/Editor.cpp index 6be2b17..946b2cd 100644 --- a/App/editor/Editor.cpp +++ b/App/editor/Editor.cpp @@ -131,7 +131,16 @@ void Editor::Draw() ImGui::End(); // 常规面板窗口(窗口名 = docking 身份) - if (m_ShowHierarchy) m_Hierarchy->Draw(m_Ctx, m_Selected); + if (m_ShowHierarchy) + { + const entt::entity prev = m_Selected; + m_Hierarchy->Draw(m_Ctx, m_Selected); + // 树点选同步描边(面板直写 m_Selected,不走 Viewport 的 + // SelectEntity 返回值通道;对比前后变化触发) + if (m_Selected != prev) { + SelectEntity(m_Selected); + } + } if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected); if (m_ShowViewport) { diff --git a/App/editor/Editor.h b/App/editor/Editor.h index 0353e2f..dd7ae57 100644 --- a/App/editor/Editor.h +++ b/App/editor/Editor.h @@ -21,6 +21,7 @@ #include #include +#include class HierarchyPanel; class InspectorPanel; @@ -67,16 +68,18 @@ private: void DoStop(); // TogglePlay 的 Stop 半程(恢复 + 回编辑器 + 冻结) // ---- 选中状态 ---- - // 同步描边:Renderer 落帧后 SelectionMask/Outline 只画选中实体 - //(bits = 实体位模式,0 = 清选中) + // 同步描边:Renderer 落帧后 SelectionMask/Outline 只画选中实体。 + // nullopt = 清选中——实体 0 号(场景首个创建)位模式 = 0, + // 不能拿位值兼作哨兵("第一个模型无描边"的根因) void SelectEntity(entt::entity e) { m_Selected = e; if (m_Ctx.renderer) { m_Ctx.renderer->SetSelectedEntity( e == entt::null - ? 0u - : static_cast(entt::to_integral(e))); + ? std::optional{} + : std::optional( + static_cast(entt::to_integral(e)))); } } entt::entity GetSelected() const { return m_Selected; } diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index bcccfcd..956e8c3 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -164,9 +164,10 @@ namespace Engine { std::optional TakePickResult(); // ---- Editor 选中描边(UE 编辑器选中轮廓)---- - // UI 段选中变化时调用(bits = 实体位模式,0 = 清选中); - // 下一帧 SelectionMask 只画该实体,Outline 在其轮廓叠加描边色 - void SetSelectedEntity(u32 entityBits); + // UI 段选中变化时调用;下一帧 SelectionMask 只画该实体,Outline + // 在其轮廓叠加描边色。nullopt = 清选中(实体 0 号位模式 = 0, + // 不能拿位值兼作哨兵) + void SetSelectedEntity(std::optional entityBits); // 垂直同步开关(present mode 切换会重建 swapchain), // 只应在帧 UI 构建段调用(Render 之前) diff --git a/Engine/include/Engine/Renderer/SceneRenderer.h b/Engine/include/Engine/Renderer/SceneRenderer.h index e1c23fc..841b414 100644 --- a/Engine/include/Engine/Renderer/SceneRenderer.h +++ b/Engine/include/Engine/Renderer/SceneRenderer.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -73,8 +74,12 @@ namespace Engine { { m_PendingPick = PickRequest{true, ndc}; } - // Editor 选中实体(描边):UI 段写入,BuildFrame 落帧并筛出绘制 - void SetSelectedEntity(u32 entityBits) { m_PendingSelected = entityBits; } + // Editor 选中实体(描边):UI 段写入,BuildFrame 落帧并筛出绘制。 + // nullopt = 清选中(实体 0 号位模式 = 0,不能拿位值兼作哨兵) + void SetSelectedEntity(std::optional entityBits) + { + m_PendingSelected = entityBits; + } private: // CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影 @@ -91,7 +96,7 @@ namespace Engine { FrameSceneData m_Frame; EditorCamera m_EditorCamera; // 编辑器相机(视口 UI 状态,见 GetEditorCamera) PickRequest m_PendingPick{}; // 拾取请求缓冲(BuildFrame 落帧后清除) - u32 m_PendingSelected = 0; // 选中实体缓冲(描边) + std::optional m_PendingSelected; // 选中实体缓冲(nullopt = 无选中) // ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)---- struct ViewScratch { @@ -102,12 +107,18 @@ namespace Engine { u32 skinBase = 0; // palette 区段基址(0 = identity 槽) u32 entityBits = 0; // 实体位模式(HitProxy id pass 消费) }; + // 链表尾哨兵:kMaxInstances 远小于 u32 最大值,安全 + static constexpr u32 kNoInstance = 0xFFFFFFFFu; CullResult result; // 扁平实例数组(所有 batch 的实例共用一份连续内存)。 - // InstanceBatch::first/count 在本数组里切片——大场景下避免 - // vector 的小块分配(每个 batch 自己一个 vector,push_back - // 触发各自重分配,cache miss 严重)。每帧 clear() 复用容量 + // 实体收集序下同 batch 实例会散布(多实体共享 mesh 交错插入), + // 组内连接关系由 instanceNext 链表维护——appendView 时按 batch + // 聚合拷成连续区间(firstInstance 连续语义的前提)。大场景下 + // 避免 vector 的小块分配,每帧 clear() 复用容量 std::vector instances; + std::vector instanceNext; // 与 instances 平行:同 batch 下一实例 + // 每 batch 实例链头(与 result.batches 平行;kNoInstance = 空) + std::vector batchHead; // 每 batch 实例距离累加(透明排序用:平均距离降序 = 远→近) std::vector batchDistSum; // batch 查找:数组寻址(下标 = Mesh::GetSlot(),值 = 每 LOD 的 @@ -119,6 +130,8 @@ namespace Engine { result.batches.clear(); result.instanceCount = 0; instances.clear(); // 容量保留,复用 + instanceNext.clear(); + batchHead.clear(); batchDistSum.clear(); for (auto& e : lookup) e.fill(0); // 容量保留,置零复用 } diff --git a/Engine/include/Engine/Renderer/SceneTypes.h b/Engine/include/Engine/Renderer/SceneTypes.h index 838231a..295289d 100644 --- a/Engine/include/Engine/Renderer/SceneTypes.h +++ b/Engine/include/Engine/Renderer/SceneTypes.h @@ -167,9 +167,12 @@ namespace Engine { // HitProxy/HitProxyCopy 两 pass 据此决定是否绘制/拷贝) PickRequest pickRequest{}; - // 选中描边(UE 编辑器语义):选中实体的位模式(0 = 无选中)。 + // 选中描边(UE 编辑器语义):选中实体 + 位模式。位模式不能兼作 + // "无选中"哨兵——entt 首个实体 0 号的 to_integral == 0("点击 + // 场景第一个创建的实体没有描边"的根因),有效性与位值解耦。 // BuildFrame 据此从实例表筛出 selectionDraws,SelectionMask pass // 只画选中实体 → Outline pass 在 mask 边缘叠加描边色 + bool selectedEntityValid = false; u32 selectedEntityBits = 0; std::vector selectionDraws; diff --git a/Engine/src/Renderer/Passes/OutlinePass.cpp b/Engine/src/Renderer/Passes/OutlinePass.cpp index a7ab4a8..8398d75 100644 --- a/Engine/src/Renderer/Passes/OutlinePass.cpp +++ b/Engine/src/Renderer/Passes/OutlinePass.cpp @@ -38,8 +38,9 @@ namespace Engine::Passes void OutlinePass::Register(RG::RenderGraph& rg) { rg.AddPass("Outline", [this](RG::RenderGraphContext& ctx) { - // 无选中零 GPU(mask 已由 SelectionMask pass 清 0,无描边) - if (m_Frame.selectedEntityBits == 0) return; + // 无选中零 GPU(mask 已由 SelectionMask pass 清 0,无描边)。 + // 有效位判断:实体 0 号位模式 = 0,不能拿位值当哨兵 + if (!m_Frame.selectedEntityValid) return; const vk::CommandBuffer cmd = ctx.cmd; diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 6e57d8d..a2630ae 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -251,7 +251,7 @@ namespace Engine m_SceneRenderer->SetPickRequest(ndc); } - void Renderer::SetSelectedEntity(u32 entityBits) + void Renderer::SetSelectedEntity(std::optional entityBits) { m_SceneRenderer->SetSelectedEntity(entityBits); } diff --git a/Engine/src/Renderer/SceneRenderer.cpp b/Engine/src/Renderer/SceneRenderer.cpp index 36c496d..8cc5ade 100644 --- a/Engine/src/Renderer/SceneRenderer.cpp +++ b/Engine/src/Renderer/SceneRenderer.cpp @@ -289,8 +289,10 @@ namespace Engine // 放在相机校验之后:跳帧时请求保留,不丢点击 m_Frame.pickRequest = m_PendingPick; m_PendingPick.active = false; - // 选中实体落帧(持续状态,不做一次性清除) - m_Frame.selectedEntityBits = m_PendingSelected; + // 选中实体落帧(持续状态,不做一次性清除)。 + // 有效性与位值分离:0 号实体(场景首个创建)位模式 = 0 + m_Frame.selectedEntityValid = m_PendingSelected.has_value(); + m_Frame.selectedEntityBits = m_PendingSelected.value_or(0); const f32 aspect = static_cast(targetExtent.width) / static_cast(targetExtent.height); @@ -613,18 +615,23 @@ namespace Engine if (batchIdx == 0) { batchIdx = static_cast(s.result.batches.size()) + 1; lodSlots[lod] = batchIdx; - // first = 当前 instances 数组大小(batch 还没实例) - // push_back 实例后区间自然连续 - s.result.batches.push_back( - {mesh.get(), static_cast(s.instances.size()), 0, lod}); + // first 是占位:真实基址在 appendView 聚合时 + // 回填(收集序下 batch 实例散布,见链表注释) + s.result.batches.push_back({mesh.get(), 0, 0, lod}); s.batchDistSum.push_back(0.0f); + s.batchHead.push_back(ViewScratch::kNoInstance); } - // 实例归入自己的 batch(mesh 分组),保证同 batch - // 实例在扁平数组里连续——firstInstance 语义的前提 + // 实例归入自己的 batch(mesh 分组):收集序散布, + // 组内连接用链表维护(O(1) push,无 per-batch 分配) const u32 bi = batchIdx - 1; s.result.batches[bi].count++; + const u32 ii = + static_cast(s.instances.size()); s.instances.push_back( - {t.World(), prevWorld, skinBase, entKey}); // 单 vector push + {t.World(), prevWorld, skinBase, entKey}); + s.instanceNext.push_back(s.batchHead[bi]); + s.batchHead[bi] = ii; // 头插:链序 = push 逆序(无妨, + // 实例对齐性由 (矩阵,id) 成对保证) s.batchDistSum[bi] += dist; // 透明排序用 s.result.instanceCount++; ++totalInstances; @@ -657,15 +664,16 @@ namespace Engine dst.batches.reserve(s.result.batches.size()); for (size_t i = 0; i < s.result.batches.size(); ++i) { InstanceBatch b = s.result.batches[i]; - // ✅ 保存原 first(s.instances 数组里的下标),b.first 改成 - // m_Frame 数组里的基址 - const u32 srcFirst = b.first; b.first = base; // 扁平化后的连续区间基址 base += b.count; dst.batches.push_back(b); - // 按 batch 的 (srcFirst, count) 切片扁平数组 - for (u32 j = 0; j < b.count; ++j) { - const auto& m = s.instances[srcFirst + j]; + // 链表遍历:收集序下同 batch 实例散布在扁平数组各处 + //(多实体共享 mesh 交错插入),按组聚合拷成连续区间 + //——firstInstance 连续语义的前提 + for (u32 idx = s.batchHead[i]; + idx != ViewScratch::kNoInstance; + idx = s.instanceNext[idx]) { + const auto& m = s.instances[idx]; m_Frame.instanceModels.push_back(m.cur); m_Frame.prevInstanceModels.push_back(m.prev); // 与实例序平行对齐(shader 按 gl_InstanceIndex 取) @@ -688,10 +696,9 @@ namespace Engine // PackIndirect(m_Frame.shadowCascades[c], false); // } - // ---- 5. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排 // ---- 5. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排。 - // 扁平 instances 数组按 order 顺序构建新数组(每个 batch 的 - // (first,count) 切片搬过去,新 first = 当前已搬数量)---- + // 实例按链表逐 batch 搬运(收集序散布,不能按 (first,count) 切片); + // 搬完的新数组天然按 batch 连续 → 链表确定性重建(顺序遍历) { auto& tb = m_TranslucentScratch.result.batches; if (tb.size() > 1) { @@ -704,11 +711,15 @@ namespace Engine // 临时构建(容量保留由 swap 进 m_TranslucentScratch 复用) std::vector sortedBatches; std::vector sortedInstances; + std::vector sortedNext; + std::vector sortedHead; std::vector sortedDist; sortedBatches.reserve(tb.size()); + sortedHead.reserve(tb.size()); sortedDist.reserve(tb.size()); // 同场景实例总数不变,capacity 可直接复用(reserve 不浪费) sortedInstances.reserve(m_TranslucentScratch.instances.size()); + sortedNext.reserve(m_TranslucentScratch.instances.size()); for (const u32 i : order) { // 新 batch first = 已搬过去的新 instances 数量 @@ -716,13 +727,29 @@ namespace Engine b.first = static_cast(sortedInstances.size()); sortedBatches.push_back(b); sortedDist.push_back(m_TranslucentScratch.batchDistSum[i]); - // 按 (原 first, count) 切片搬运原扁平数组 - for (u32 j = 0; j < tb[i].count; ++j) { + // 链表逐实例搬运;新布局按 batch 连续 → next = 顺序+1。 + // 新实例下标 = b.first + n(n = 本 batch 已搬数量), + // 前一个实例的 next 指向它 + u32 prev = ViewScratch::kNoInstance; + u32 n = 0; + for (u32 idx = m_TranslucentScratch.batchHead[i]; + idx != ViewScratch::kNoInstance; + idx = m_TranslucentScratch.instanceNext[idx]) { sortedInstances.push_back(std::move( - m_TranslucentScratch.instances[tb[i].first + j])); + m_TranslucentScratch.instances[idx])); + sortedNext.push_back(ViewScratch::kNoInstance); + if (prev != ViewScratch::kNoInstance) { + sortedNext[prev] = b.first + n; + } + prev = b.first + n; + ++n; } + sortedHead.push_back( + n > 0 ? b.first : ViewScratch::kNoInstance); } m_TranslucentScratch.result.batches = std::move(sortedBatches); + m_TranslucentScratch.batchHead = std::move(sortedHead); + m_TranslucentScratch.instanceNext = std::move(sortedNext); m_TranslucentScratch.batchDistSum = std::move(sortedDist); m_TranslucentScratch.instances = std::move(sortedInstances); // instanceCount 不变(重排不丢实例) @@ -734,7 +761,7 @@ namespace Engine //(不透明 + 透明队列都查——选中玻璃也要有描边)。 // 无选中时保持空表,SelectionMask pass 只 clear 不绘制 m_Frame.selectionDraws.clear(); - if (m_Frame.selectedEntityBits != 0) { + if (m_Frame.selectedEntityValid) { auto collect = [&](const CullResult& view) { for (const auto& b : view.batches) { for (u32 i = 0; i < b.count; ++i) {