diff --git a/App/main.cpp b/App/main.cpp index 6535b10..98947e9 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -169,6 +169,9 @@ protected: // Pass 级 GPU 耗时(RG timestamp) ImGui::Separator(); ImGui::Text("GPU Timings"); + ImGui::Text("instances: %u main / %u shadow", + renderer.GetMainInstanceCount(), + renderer.GetShadowInstanceCount()); const auto& names = renderer.GetPassNames(); const auto& times = renderer.GetPassGpuMs(); for (size_t i = 0; i < names.size() && i < times.size(); ++i) diff --git a/Engine/include/Engine/Renderer/Frustum.h b/Engine/include/Engine/Renderer/Frustum.h new file mode 100644 index 0000000..5114ca0 --- /dev/null +++ b/Engine/include/Engine/Renderer/Frustum.h @@ -0,0 +1,64 @@ +// +// Created by atdunbg on 2026/9/23. +// +// Frustum:Gribb-Hartmann 视锥平面提取 + 包围球相交测试。 +// +// 平面从 view-proj 矩阵的行组合中直接抽出(无需分解参数), +// 用于按视图剔除(相机视图 / 光源视图各自独立一份)。 +// 注意:near/far 平面按 Vulkan [0,1] 深度约定提取 +//(z_clip ∈ [0,1]:near = row2,far = row3 - row2)。 +// + +#ifndef HELLOVULKAN_FRUSTUM_H +#define HELLOVULKAN_FRUSTUM_H + +#include +#include +#include + +namespace Engine { + + struct Frustum { + // 依次:left, right, bottom, top, near, far + // xyz = 平面法线(未归一化,仅做符号/点积测试),w = 偏移 + std::array planes{}; + + // 从 view-proj 矩阵提取 6 平面(列主序 glm 矩阵的"行"组合)。 + // 注意必须传未做 [0,1] 深度重映射的矩阵,提取公式才成立。 + static Frustum FromMatrix(const glm::mat4& m) + { + Frustum f; + // 列主序下的"行 i": (m[0][i], m[1][i], m[2][i], m[3][i]) + const auto row = [&m](u32 i) { + return glm::vec4(m[0][i], m[1][i], m[2][i], m[3][i]); + }; + const glm::vec4 r0 = row(0); + const glm::vec4 r1 = row(1); + const glm::vec4 r2 = row(2); + const glm::vec4 r3 = row(3); + + f.planes[0] = r3 + r0; // left: x' >= -w + f.planes[1] = r3 - r0; // right: x' <= w + f.planes[2] = r3 + r1; // bottom: y' >= -w + f.planes[3] = r3 - r1; // top: y' <= w + f.planes[4] = r2; // near: z' >= 0(Vulkan [0,1]) + f.planes[5] = r3 - r2; // far: z' <= w + return f; + } + + // 包围球与视锥相交测试(外接球近似,略保守——宁多画不漏画)。 + // 对每个平面:球心在平面负侧且距离超过 radius 才算外。 + bool IntersectsSphere(const glm::vec3& center, f32 radius) const + { + for (const auto& p : planes) { + if (glm::dot(glm::vec3(p), center) + p.w < -radius) { + return false; + } + } + return true; + } + }; + +} // namespace Engine + +#endif //HELLOVULKAN_FRUSTUM_H diff --git a/Engine/include/Engine/Renderer/Mesh.h b/Engine/include/Engine/Renderer/Mesh.h index cb0e1df..427946d 100644 --- a/Engine/include/Engine/Renderer/Mesh.h +++ b/Engine/include/Engine/Renderer/Mesh.h @@ -37,11 +37,19 @@ namespace Engine Material* GetMaterial() const { return m_Material.get(); } u32 GetIndexCount() const { return m_IndexCount; } + // 局部空间包围球(构造时从顶点算出),视锥剔除用 + struct Bounds { + glm::vec3 center{0.0f}; + f32 radius = 0.0f; + }; + const Bounds& GetBounds() const { return m_Bounds; } + private: Scope m_VertexBuffer; Scope m_IndexBuffer; Scope m_Material; u32 m_IndexCount = 0; + Bounds m_Bounds; u64 m_VertexBytes = 0; u64 m_IndexBytes = 0; diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index c175d9d..e3b7f0a 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -138,8 +139,17 @@ namespace Engine { glm::mat4 m_LightSpace{1.0f}; // 阴影光源 VP(每帧由 Render 算好) // GPUDriven:当帧实例数据(Render 期收集,Forward pass 消费) - std::vector m_InstanceBatches; - std::vector m_InstanceModels; + // 双视图剔除:相机视锥与光源视锥各出一份可见列表—— + // 相机外的实体仍可能投影子进视野,所以 Shadow 用光源视锥单独剔除 + struct CullResult { + std::vector batches; + u32 instanceCount = 0; + }; + CullResult m_MainView; // 相机视锥 + CullResult m_ShadowView; // 光源视锥 + std::unordered_map m_MainBatchLookup; + std::unordered_map m_ShadowBatchLookup; + std::vector m_InstanceModels; // [主视图段 | 阴影视图段] static constexpr u32 kMaxInstances = 16384; // 可选叠加层(不拥有;Application 持有) @@ -174,6 +184,10 @@ namespace Engine { const std::vector& GetPassNames() const { return m_PassNames; } const std::vector& GetPassGpuMs() const { return m_PassGpuMs; } + // 双视图剔除后的实例数(遥测) + u32 GetMainInstanceCount() const { return m_MainView.instanceCount; } + u32 GetShadowInstanceCount() const { return m_ShadowView.instanceCount; } + // 垂直同步开关(Mailbox ↔ Immediate):切换会重建 swapchain, // 只应在帧 UI 构建段调用(Render 之前) void SetVSync(bool vsync); diff --git a/Engine/src/Renderer/Mesh.cpp b/Engine/src/Renderer/Mesh.cpp index c1e028b..e8ee7f2 100644 --- a/Engine/src/Renderer/Mesh.cpp +++ b/Engine/src/Renderer/Mesh.cpp @@ -9,6 +9,8 @@ #include #include +#include + #include "Engine/Core/Stats.h" namespace Engine @@ -43,6 +45,22 @@ namespace Engine m_IndexCount = static_cast(indices.size()); + // 局部空间包围球:顶点包围盒 → 中心 + 最大半径(视锥剔除用) + { + glm::vec3 mn(std::numeric_limits::max()); + glm::vec3 mx(std::numeric_limits::lowest()); + for (const auto& v : vertices) { + mn = glm::min(mn, v.position); + mx = glm::max(mx, v.position); + } + m_Bounds.center = (mn + mx) * 0.5f; + m_Bounds.radius = 0.0f; + for (const auto& v : vertices) { + m_Bounds.radius = glm::max(m_Bounds.radius, + glm::length(v.position - m_Bounds.center)); + } + } + ENGINE_INFO("Mesh created: {} vertices, {} indices", vertices.size(), indices.size()); m_VertexBytes = sizeof(Vertex) * vertices.size(); diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index ae171c4..688695c 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -446,9 +447,8 @@ namespace Engine vk::Rect2D scissor({0, 0}, {kShadowMapSize, kShadowMapSize}); cmd.setScissorWithCount(scissor); - // ---- 绘制:GPUDriven——与 Forward 共用同一批 batch ---- - // lightVP 全 pass 只 push 一次;模型矩阵经实例索引从 - // ObjectBuffer 取(与 Forward 同一份数据) + // ---- 绘制:GPUDriven——与 Forward 共用实例数据,但用 + // 光源视锥剔除后的 batch(相机外实体仍投影子)---- cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, m_ShadowPipelineLayout, 0, m_UBODescriptorSets[ctx.frame.frameSlot], {}); @@ -459,7 +459,7 @@ namespace Engine vk::ShaderStageFlagBits::eFragment, 0, sizeof(glm::mat4), &m_LightSpace); - for (const auto& batch : m_InstanceBatches) { + for (const auto& batch : m_ShadowView.batches) { batch.mesh->Draw(cmd, batch.count, batch.first); } @@ -475,7 +475,7 @@ namespace Engine vk::ClearValue clearValue; clearValue.color = vk::ClearColorValue( - std::array{0.0f, 0.0f, 0.0f, 0.0f}); + std::array{0.1f, 0.1f, 0.1f, 0.0f}); vk::RenderingAttachmentInfo colorAttachment; colorAttachment.setImageView(ctx.View(m_HDRColor)) @@ -570,11 +570,11 @@ namespace Engine vk::ColorComponentFlagBits::eA; cmd.setColorWriteMaskEXT(0, colorWriteMask); - // ---- 绘制:GPUDriven 按 mesh 分 batch ---- + // ---- 绘制:GPUDriven 按 mesh 分 batch(相机视锥剔除后)---- // 实例矩阵已在 Render() 里写入 ObjectBuffer(per-slot SSBO); - // 同 mesh 的 N 个实体合并为一次 drawIndexed(instanceCount=N), - // draw 数 = 唯一 mesh 数,与实体数量解耦 - for (const auto& batch : m_InstanceBatches) { + // 同 mesh 的 N 个可见实体合并为一次 drawIndexed(instanceCount=N), + // draw 数 = 可见唯一 mesh 数,与实体数量解耦 + for (const auto& batch : m_MainView.batches) { cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, m_PipelineLayout, kTextureSetIndex, batch.mesh->GetMaterial()->GetDescriptorSet(), {}); @@ -945,49 +945,69 @@ namespace Engine m_UniformBuffer->Update(frameSlot, &ubo, sizeof(ubo)); m_SyncManager->ResetFence(frameSlot); - // ---- GPUDriven:收集实例数据(按 mesh 聚合 batch)---- - // 两趟遍历:先收集唯一 mesh(保持首现序),再逐 batch 追加实例矩阵, - // 保证每个 batch 的矩阵区间连续(draw 用 firstInstance 指向起点)。 - // TODO: 实体量大时改哈希聚合 + 单趟填充 + // ---- GPUDriven:单趟收集 + 双视图剔除 ---- + // 每个实例对相机视锥、光源视锥分别做包围球测试: + // 进相机视锥 → 主视图 batch(Forward 画) + // 进光源视锥 → 阴影 batch(Shadow 画)——相机外实体仍可投影子 + // 实例矩阵数组 = [主视图段 | 阴影视图段],batch.first 为全局下标。 + // 哈希聚合保证各视图内 batch 区间连续。 const auto collectStart = std::chrono::steady_clock::now(); - m_InstanceBatches.clear(); m_InstanceModels.clear(); + m_MainView.batches.clear(); + m_ShadowView.batches.clear(); + m_MainView.instanceCount = 0; + m_ShadowView.instanceCount = 0; + m_MainBatchLookup.clear(); + m_ShadowBatchLookup.clear(); bool instanceTruncated = false; - std::vector uniqueMeshes; + const Frustum mainFrustum = Frustum::FromMatrix(ubo.proj * ubo.view); + // 注意:剔除平面提取按 [0,1] 深度约定——相机 proj 与 m_LightSpace + // 都已做 [0,1] 重映射;原始 [-1,1] 矩阵的 near 平面公式不同,勿用 + const Frustum shadowFrustum = Frustum::FromMatrix(m_LightSpace); + scene.Each( - [&](Entity, TransformComponent&, MeshRendererComponent& mr) { + [&](Entity, TransformComponent& t, MeshRendererComponent& mr) { Model* model = assets.GetModel(mr.model); if (!model) return; - for (auto& mesh : model->GetMeshes()) { - if (std::find(uniqueMeshes.begin(), uniqueMeshes.end(), - mesh.get()) == uniqueMeshes.end()) { - uniqueMeshes.push_back(mesh.get()); - } - } - }); - for (Mesh* meshPtr : uniqueMeshes) { - InstanceBatch batch{meshPtr, - static_cast(m_InstanceModels.size()), 0}; - scene.Each( - [&](Entity, TransformComponent& t, MeshRendererComponent& mr) { - Model* model = assets.GetModel(mr.model); - if (!model) return; - for (auto& m : model->GetMeshes()) { - if (m.get() != meshPtr) continue; + // 实例世界缩放(矩阵列向量最大长度,包围球半径随缩放) + const f32 worldScale = glm::max( + glm::length(glm::vec3(t.world[0])), + glm::max(glm::length(glm::vec3(t.world[1])), + glm::length(glm::vec3(t.world[2])))); + + for (auto& mesh : model->GetMeshes()) { + const auto& bounds = mesh->GetBounds(); + const glm::vec3 worldCenter = + glm::vec3(t.world * glm::vec4(bounds.center, 1.0f)); + const f32 worldRadius = bounds.radius * worldScale; + + auto tryAppend = [&](CullResult& view, const Frustum& frustum, + std::unordered_map& lookup) { + if (!frustum.IntersectsSphere(worldCenter, worldRadius)) { + return; + } if (m_InstanceModels.size() >= kMaxInstances) { instanceTruncated = true; return; } + const auto [it, inserted] = lookup.emplace( + mesh.get(), static_cast(view.batches.size())); + if (inserted) { + view.batches.push_back({mesh.get(), + static_cast(m_InstanceModels.size()), 0}); + } + view.batches[it->second].count++; + view.instanceCount++; m_InstanceModels.push_back(t.world); - ++batch.count; - } - }); - if (batch.count > 0) { - m_InstanceBatches.push_back(batch); - } - } + }; + + tryAppend(m_MainView, mainFrustum, m_MainBatchLookup); + tryAppend(m_ShadowView, shadowFrustum, m_ShadowBatchLookup); + } + }); + if (instanceTruncated) { // 只告警一次(每帧告警会刷屏) static bool warned = false;