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<u32> + 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.
This commit is contained in:
2026-10-05 00:39:48 +08:00
parent e038cb39fa
commit dec171cd1e
8 changed files with 97 additions and 40 deletions
+49 -22
View File
@@ -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<f32>(targetExtent.width) /
static_cast<f32>(targetExtent.height);
@@ -613,18 +615,23 @@ namespace Engine
if (batchIdx == 0) {
batchIdx = static_cast<u32>(s.result.batches.size()) + 1;
lodSlots[lod] = batchIdx;
// first = 当前 instances 数组大小(batch 还没实例)
// push_back 实例后区间自然连续
s.result.batches.push_back(
{mesh.get(), static_cast<u32>(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<u32>(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<InstanceBatch> sortedBatches;
std::vector<ViewScratch::InstanceMatrices> sortedInstances;
std::vector<u32> sortedNext;
std::vector<u32> sortedHead;
std::vector<f32> 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<u32>(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) {