diff --git a/Engine/include/Engine/Renderer/Mesh.h b/Engine/include/Engine/Renderer/Mesh.h index f26e068..f04bbc1 100644 --- a/Engine/include/Engine/Renderer/Mesh.h +++ b/Engine/include/Engine/Renderer/Mesh.h @@ -47,6 +47,8 @@ namespace Engine { return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].indexCount; } + // 全局连续槽位(SceneRenderer 数组化 batch 查找用,避免逐实例 hash) + u32 GetSlot() const { return m_Slot; } // 局部空间包围球(构造时从顶点算出),视锥剔除用 struct Bounds { @@ -66,6 +68,7 @@ namespace Engine Scope m_IndexBuffer; std::array m_Lods{}; Material* m_Material; + u32 m_Slot = 0; Bounds m_Bounds; u64 m_VertexBytes = 0; diff --git a/Engine/include/Engine/Renderer/Model.h b/Engine/include/Engine/Renderer/Model.h index ad55e91..a7ef320 100644 --- a/Engine/include/Engine/Renderer/Model.h +++ b/Engine/include/Engine/Renderer/Model.h @@ -81,6 +81,9 @@ namespace Engine Model& operator=(const Model&) = delete; const std::vector>& GetMeshes() const { return m_Meshes; } + // 全模型联合包围球(各 mesh bounds 的并集,局部空间): + // 实体级粗剔除用——整个模型在视锥外直接跳过全部 mesh 循环 + const Mesh::Bounds& GetBounds() const { return m_Bounds; } private: Model(Device& device, MemoryAllocator& allocator, @@ -103,6 +106,9 @@ namespace Engine bool isGltf); void InitDefaultTextures(); + // 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除) + void RebuildUnionBounds(); + Texture* GetOrLoadTexture(const std::string& path, bool srgb); // 材质缓存入口:同贴图组合+因子共享同一 Material(一个描述符集), @@ -124,6 +130,7 @@ namespace Engine std::array, Material::kSlotCount> m_DefaultTextures; std::vector> m_Meshes; + Mesh::Bounds m_Bounds; // mesh 建完后聚合(实体级粗剔除) std::string m_ModelPath; bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV }; diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index c51f18d..ec9ce7d 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -111,6 +111,7 @@ namespace Engine { u32 GetMainInstanceCount() const; u32 GetShadowInstanceCount() const; bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字) + f32 GetCollectMs() const; // CPU 收集+剔除段耗时 // 垂直同步开关(present mode 切换会重建 swapchain), // 只应在帧 UI 构建段调用(Render 之前) diff --git a/Engine/include/Engine/Renderer/SceneRenderer.h b/Engine/include/Engine/Renderer/SceneRenderer.h index 5c0aaec..8b38d75 100644 --- a/Engine/include/Engine/Renderer/SceneRenderer.h +++ b/Engine/include/Engine/Renderer/SceneRenderer.h @@ -21,8 +21,10 @@ #define HELLOVULKAN_SCENERENDERER_H #include +#include #include #include +#include #include namespace Engine { @@ -67,23 +69,26 @@ namespace Engine { // ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)---- struct ViewScratch { + // cur + prev 合并写:两次随机堆访问 → 一次(cache miss 减半) + struct InstanceMatrices { + glm::mat4 cur; + glm::mat4 prev; + }; CullResult result; // 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 = // firstInstance + 连续区间,实体序收集不满足(多实体共享 mesh // 时实例分散各处),扁平化成连续数组推迟到 appendView - std::vector> batchWorlds; - std::vector> batchPrevWorlds; - // key = (Mesh*, LOD):同 mesh 不同 LOD 索引区间不同, - // 必须分开成 batch(低 8 位 LOD,高位指针,指针 8 字节对齐安全) - std::unordered_map lookup; + std::vector> batchMats; + // batch 查找:数组寻址(下标 = Mesh::GetSlot(),值 = 每 LOD 的 + // batchIdx+1,0 = 未用)——逐实例 hash 在大场景是毫秒级开销 + std::vector> lookup; void Clear() { result.batches.clear(); result.instanceCount = 0; - batchWorlds.clear(); - batchPrevWorlds.clear(); - lookup.clear(); + batchMats.clear(); + for (auto& e : lookup) e.fill(0); // 容量保留,置零复用 } }; ViewScratch m_MainScratch; diff --git a/Engine/src/Renderer/Mesh.cpp b/Engine/src/Renderer/Mesh.cpp index 8b0b07a..08f4bae 100644 --- a/Engine/src/Renderer/Mesh.cpp +++ b/Engine/src/Renderer/Mesh.cpp @@ -10,16 +10,24 @@ #include #include +#include #include "Engine/Core/Stats.h" namespace Engine { + namespace { + // 全局连续 mesh 槽位:SceneRenderer 的 batch 查找用数组寻址 + //(每实例每视图一次 hash 的代价在大场景下是 5ms 级) + std::atomic s_nextMeshSlot{0}; + } + Mesh::Mesh(Device& device, MemoryAllocator& allocator, const std::vector& vertices, const std::vector>& lods, Material* material) : m_Material(material) + , m_Slot(s_nextMeshSlot.fetch_add(1, std::memory_order_relaxed)) { if (vertices.empty() || lods.empty() || lods[0].empty()) { diff --git a/Engine/src/Renderer/Model.cpp b/Engine/src/Renderer/Model.cpp index 9be1986..ebf174e 100644 --- a/Engine/src/Renderer/Model.cpp +++ b/Engine/src/Renderer/Model.cpp @@ -45,6 +45,23 @@ Model::Model(Device& device, MemoryAllocator& allocator, Model::~Model() = default; +void Model::RebuildUnionBounds() +{ + if (m_Meshes.empty()) { + m_Bounds = {}; + return; + } + // 各 mesh 包围球 → AABB 并集 → 外接球(保守,宁多画不漏画) + glm::vec3 mn(1e9f), mx(-1e9f); + for (const auto& mesh : m_Meshes) { + const auto& b = mesh->GetBounds(); + mn = glm::min(mn, b.center - glm::vec3(b.radius)); + mx = glm::max(mx, b.center + glm::vec3(b.radius)); + } + m_Bounds.center = (mn + mx) * 0.5f; + m_Bounds.radius = 0.5f * glm::length(mx - mn); +} + void Model::InitDefaultTextures() { // 各材质槽位的兜底纹理(1x1,开销可忽略): @@ -91,6 +108,7 @@ Model::Model(Device& device, MemoryAllocator& allocator, m_Meshes.push_back(CreateScope( m_Device, m_Allocator, vertices, lodIndices, mat)); + RebuildUnionBounds(); } Scope Model::CreatePlane(Device& device, MemoryAllocator& allocator, @@ -165,6 +183,7 @@ Scope Model::FromParsed(Device& device, MemoryAllocator& allocator, } ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)", parsed.path, model->m_Meshes.size(), model->m_TextureCache.size()); + model->RebuildUnionBounds(); return model; } @@ -184,6 +203,7 @@ void Model::LoadModel(const std::string& path) } ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)", path, m_Meshes.size(), m_TextureCache.size()); + RebuildUnionBounds(); } void Model::ProcessNode(const aiNode* node, const aiScene* scene, diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index aa9c67c..d358862 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -225,6 +225,11 @@ namespace Engine return m_SceneRenderer->Frame().instancesTruncated; } + f32 Renderer::GetCollectMs() const + { + return m_SceneRenderer->GetCollectMs(); + } + u32 Renderer::GetShadowInstanceCount() const { return m_SceneRenderer->GetShadowInstanceCount(); diff --git a/Engine/src/Renderer/SceneRenderer.cpp b/Engine/src/Renderer/SceneRenderer.cpp index 31d23a0..00c54df 100644 --- a/Engine/src/Renderer/SceneRenderer.cpp +++ b/Engine/src/Renderer/SceneRenderer.cpp @@ -291,6 +291,23 @@ namespace Engine glm::max(glm::length(glm::vec3(t.World()[1])), glm::length(glm::vec3(t.World()[2])))); + // ---- 实体级粗剔除(联合包围球):四视图全部看不到 → + // 跳过整个 mesh 循环(多 mesh 模型的大头优化)---- + const Mesh::Bounds& mb = model->GetBounds(); + const glm::vec3 modelCenter = + glm::vec3(t.World() * glm::vec4(mb.center, 1.0f)); + const f32 modelRadius = mb.radius * worldScale; + const bool mainVis = + mainFrustum.IntersectsSphere(modelCenter, modelRadius); + bool cascVis[kMaxCascades]; + bool anyVis = mainVis; + for (u32 c = 0; c < kMaxCascades; ++c) { + cascVis[c] = + cascFrustum[c].IntersectsSphere(modelCenter, modelRadius); + anyVis |= cascVis[c]; + } + if (!anyVis) return; + for (auto& mesh : model->GetMeshes()) { const auto& bounds = mesh->GetBounds(); const glm::vec3 worldCenter = @@ -317,31 +334,32 @@ namespace Engine instanceTruncated = true; return; } - // lookup 值 = batches 索引(此前误存 worlds 索引: - // 单实体时两序号恰好恒等,多实体共享 mesh 后分叉 - // → batches[worldsIdx] 越界)。batch.first(GPU - // firstInstance 基址)才是 worlds 索引,语义别混 - const u64 key = - (reinterpret_cast(mesh.get()) << 8) | lod; - const auto [it, inserted] = s.lookup.emplace( - key, static_cast(s.result.batches.size())); - if (inserted) { + // batch 查找 = 数组寻址(Mesh::GetSlot 槽位 + LOD 槽) + // batchIdx 存储 +1 偏移(0 = 该槽位尚无 batch) + const u32 slot = mesh->GetSlot(); + if (slot >= s.lookup.size()) { + s.lookup.resize(slot + 1); + } + auto& lodSlots = s.lookup[slot]; + u32 batchIdx = lodSlots[lod]; + if (batchIdx == 0) { + batchIdx = static_cast(s.result.batches.size()) + 1; + lodSlots[lod] = batchIdx; s.result.batches.push_back({mesh.get(), 0, 0, lod}); - s.batchWorlds.emplace_back(); - s.batchPrevWorlds.emplace_back(); + s.batchMats.emplace_back(); } // 实例归入自己的 batch(mesh 分组),保证同 batch // 实例在扁平化后连续——firstInstance 语义的前提 - const u32 batchIdx = it->second; - s.result.batches[batchIdx].count++; - s.batchWorlds[batchIdx].push_back(t.World()); - s.batchPrevWorlds[batchIdx].push_back(prevWorld); + const u32 bi = batchIdx - 1; + s.result.batches[bi].count++; + s.batchMats[bi].push_back({t.World(), prevWorld}); s.result.instanceCount++; ++totalInstances; }; tryAppend(m_MainScratch, mainFrustum, mainLod); for (u32 c = 0; c < kMaxCascades; ++c) { + if (!cascVis[c]) continue; // 粗剔除已判不可见 // 级联降级:越远的级联覆盖范围越大,用越低的 LOD //(阴影本来就是低频信息,误差可接受) tryAppend(m_CascScratch[c], cascFrustum[c], @@ -360,13 +378,10 @@ namespace Engine b.first = base; // 扁平化后的连续区间基址 base += b.count; dst.batches.push_back(b); - m_Frame.instanceModels.insert(m_Frame.instanceModels.end(), - s.batchWorlds[i].begin(), - s.batchWorlds[i].end()); - m_Frame.prevInstanceModels.insert( - m_Frame.prevInstanceModels.end(), - s.batchPrevWorlds[i].begin(), - s.batchPrevWorlds[i].end()); + for (const auto& m : s.batchMats[i]) { + m_Frame.instanceModels.push_back(m.cur); + m_Frame.prevInstanceModels.push_back(m.prev); + } } dst.instanceCount = s.result.instanceCount; };