From 1449bea86d324ce8599b5eff8c2c9c47d9303094 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Fri, 2 Oct 2026 20:09:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20HitProxy=20GPU=20=E6=8B=BE=E5=8F=96=20+?= =?UTF-8?q?=20UE=20=E9=80=89=E4=B8=AD=E6=8F=8F=E8=BE=B9=20+=20=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E6=8B=BE=E5=8F=96=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HitProxy 拾取:RG 管线渲染实体 id → host readback,点击帧 PickEntity 精确命中;拾取按视觉几何(Model 联合包围球射线求交), 与 Collider 解耦(UE/Unity 编辑器语义),命中打印实体名+碰撞体参数 - 选中描边(UE 语义):SelectionMask pass(R8Unorm 完整投影轮廓, 无深度测试透墙可见)→ Outline pass(边缘检测 + alpha 叠加, 必须注册在 Tonemap 后);edgeCut 贴视口边缘;widthPx=6 可调 - shader object 实证修复:VUID-08879 同程序 set layouts 超集一致、 VUID-10065 push block 4×float 平铺、新 pass 析构销毁 VkShaderEXT - RG:SideEffect() 保活"产出无 RG 内消费者"的 pass 链 - Editor:层次面板点击选中、Inspector 姿态编辑、Stats 面板拾取信息 - 相机 Update 改 const(姿态写入 Transform 引用,Camera 本身无状态变更) --- App/editor/Editor.cpp | 7 +- App/editor/Editor.h | 15 +- App/editor/EditorContext.h | 3 - App/editor/HierarchyPanel.cpp | 3 +- App/editor/InspectorPanel.cpp | 49 ++++ App/editor/StatsPanel.cpp | 59 ++++- App/editor/ViewportPanel.cpp | 88 ++++++- App/editor/ViewportPanel.h | 19 +- .../include/Engine/RenderGraph/RenderGraph.h | 9 + Engine/include/Engine/Renderer/Camera.h | 4 +- .../Engine/Renderer/Passes/HitProxyPass.h | 74 ++++++ .../Engine/Renderer/Passes/OutlinePass.h | 54 ++++ .../include/Engine/Renderer/Passes/SceneGpu.h | 5 +- .../Renderer/Passes/SelectionMaskPass.h | 51 ++++ Engine/include/Engine/Renderer/Renderer.h | 43 ++- .../include/Engine/Renderer/SceneRenderer.h | 10 + Engine/include/Engine/Renderer/SceneTypes.h | 33 +++ Engine/src/RenderGraph/RenderGraph.cpp | 7 + Engine/src/Renderer/Camera.cpp | 25 +- Engine/src/Renderer/Passes/HitProxyPass.cpp | 246 ++++++++++++++++++ Engine/src/Renderer/Passes/OutlinePass.cpp | 142 ++++++++++ Engine/src/Renderer/Passes/SceneGpu.cpp | 9 +- .../src/Renderer/Passes/SelectionMaskPass.cpp | 138 ++++++++++ Engine/src/Renderer/Renderer.cpp | 126 ++++++++- Engine/src/Renderer/SceneRenderer.cpp | 36 ++- shaders/hitproxy.frag | 13 + shaders/hitproxy.vert | 57 ++++ shaders/outline.frag | 61 +++++ shaders/sel_mask.frag | 17 ++ 29 files changed, 1360 insertions(+), 43 deletions(-) create mode 100644 Engine/include/Engine/Renderer/Passes/HitProxyPass.h create mode 100644 Engine/include/Engine/Renderer/Passes/OutlinePass.h create mode 100644 Engine/include/Engine/Renderer/Passes/SelectionMaskPass.h create mode 100644 Engine/src/Renderer/Passes/HitProxyPass.cpp create mode 100644 Engine/src/Renderer/Passes/OutlinePass.cpp create mode 100644 Engine/src/Renderer/Passes/SelectionMaskPass.cpp create mode 100644 shaders/hitproxy.frag create mode 100644 shaders/hitproxy.vert create mode 100644 shaders/outline.frag create mode 100644 shaders/sel_mask.frag diff --git a/App/editor/Editor.cpp b/App/editor/Editor.cpp index 8218d84..5b2d832 100644 --- a/App/editor/Editor.cpp +++ b/App/editor/Editor.cpp @@ -63,7 +63,12 @@ void Editor::Draw() // 常规面板窗口(窗口名 = docking 身份) if (m_ShowHierarchy) m_Hierarchy->Draw(m_Ctx, m_Selected); if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected); - if (m_ShowViewport) m_Viewport->Draw(m_Ctx, m_VpInput); + if (m_ShowViewport) + { + if (auto picked = m_Viewport->Draw(m_Ctx, m_VpInput)) { + SelectEntity(*picked); // Viewport 点击拾取 → 选中联动 Inspector/Hierarchy + } + } if (m_ShowStats) m_Stats->Draw(m_Ctx); DrawQuitModal(); diff --git a/App/editor/Editor.h b/App/editor/Editor.h index e032091..2b17a09 100644 --- a/App/editor/Editor.h +++ b/App/editor/Editor.h @@ -16,6 +16,8 @@ #include "EditorContext.h" #include "ViewportPanel.h" // ViewportInputState(成员按值持有) +#include // u32(SelectEntity 位模式转换) + #include #include @@ -53,7 +55,18 @@ public: void RequestQuit() { m_ShowQuitConfirm = true; } // ---- 选中状态 ---- - void SelectEntity(entt::entity e) { m_Selected = e; } + // 同步描边:Renderer 落帧后 SelectionMask/Outline 只画选中实体 + //(bits = 实体位模式,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))); + } + } entt::entity GetSelected() const { return m_Selected; } private: diff --git a/App/editor/EditorContext.h b/App/editor/EditorContext.h index 4c6f066..8e47825 100644 --- a/App/editor/EditorContext.h +++ b/App/editor/EditorContext.h @@ -40,9 +40,6 @@ struct EditorContext { std::function getEnvIndex; // 当前环境表下标 std::function setEnvIndex; - - // 冻结动画时钟(沙盒 spinners 用;保留原 Scene Debug 行为) - bool *pauseAnim = nullptr; }; #endif //HELLOVULKAN_EDITORCONTEXT_H diff --git a/App/editor/HierarchyPanel.cpp b/App/editor/HierarchyPanel.cpp index c4aae6c..31f067f 100644 --- a/App/editor/HierarchyPanel.cpp +++ b/App/editor/HierarchyPanel.cpp @@ -70,6 +70,7 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected) if (reg.all_of(e)) badge += "[A]"; if (reg.all_of(e)) badge += "[C]"; if (reg.all_of(e)) badge += "[L]"; + if (reg.all_of(e)) badge += "[P]"; if (!badge.empty()) { ImGui::SameLine(); @@ -77,7 +78,7 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected) if (ImGui::IsItemHovered()) { ImGui::SetTooltip( - "M=Mesh A=Animation C=Camera L=Light"); + "M=Mesh A=Animation C=Camera L=Light P=Physics"); } } }); diff --git a/App/editor/InspectorPanel.cpp b/App/editor/InspectorPanel.cpp index af23193..6c24aa6 100644 --- a/App/editor/InspectorPanel.cpp +++ b/App/editor/InspectorPanel.cpp @@ -76,6 +76,11 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) { reg.emplace(selected); } + if (!reg.all_of(selected) && + ImGui::MenuItem("RigidBody")) + { + reg.emplace(selected); + } ImGui::EndPopup(); } ImGui::Separator(); @@ -172,6 +177,50 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) } } + // ---- RigidBody(Jolt 刚体:动态体 Transform 由物理每帧写回)---- + // type/collider/mass 变更 → PhysicsWorld 重建 body;其余参数即时生效 + if (auto* rb = reg.try_get(selected)) + { + if (ImGui::CollapsingHeader("RigidBody", ImGuiTreeNodeFlags_DefaultOpen)) + { + int type = static_cast(rb->type); + if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0")) + { + rb->type = static_cast(type); + } + int collider = static_cast(rb->collider); + if (ImGui::Combo("collider", &collider, "Box\0Sphere\0Mesh\0")) + { + rb->collider = static_cast(collider); + } + if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Box) + { + ImGui::DragFloat3("halfExtents", &rb->boxHalfExtents.x, 0.05f, 0.01f, 500.0f); + } + else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Sphere) + { + ImGui::DragFloat("radius", &rb->sphereRadius, 0.05f, 0.01f, 500.0f); + } + else + { + // Mesh:跟随 MeshRenderer 视觉网格(Static=精确网格, + // Dynamic=凸包近似),无尺寸可调 + ImGui::TextDisabled("follows MeshRenderer geometry"); + } + ImGui::DragFloat("mass", &rb->mass, 0.1f, 0.1f, 1000.0f, "%.1f"); + ImGui::SliderFloat("gravityScale", &rb->gravityScale, -1.0f, 5.0f); + ImGui::SliderFloat("friction", &rb->friction, 0.0f, 2.0f); + ImGui::SliderFloat("restitution", &rb->restitution, 0.0f, 1.0f); + ImGui::Checkbox("isSensor", &rb->isSensor); + if (rb->bodyValid && + rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic) + { + ImGui::Text("vel: %.2f, %.2f, %.2f m/s", + rb->velocity.x, rb->velocity.y, rb->velocity.z); + } + } + } + // ---- Animation ---- if (auto* anim = reg.try_get(selected)) { diff --git a/App/editor/StatsPanel.cpp b/App/editor/StatsPanel.cpp index fc67e43..ae58bf2 100644 --- a/App/editor/StatsPanel.cpp +++ b/App/editor/StatsPanel.cpp @@ -4,6 +4,8 @@ #include "StatsPanel.h" #include "Engine/ECS/Components.h" +#include "Engine/Physics/PhysicsWorld.h" +#include "Engine/Core/Time.h" #include #include @@ -33,10 +35,25 @@ void StatsPanel::Draw(const EditorContext& ctx) ImGui::Separator(); } - // ---- Scene:动画时钟冻结(沙盒 spinners 用)---- - if (ctx.pauseAnim != nullptr) + ImGui::Separator(); + static const char* displayTarget[] = { + "Fullscreen", + "Viewport", + }; + int mode = renderer.GetDisplayMode(); + if (ImGui::Combo("##DisplayTarget", &mode, displayTarget, IM_ARRAYSIZE(displayTarget))) { + renderer.SetDisplayMode(mode); + } + + ImGui::Separator(); + + // ---- Scene:暂停模拟(引擎 Time 门面:动画/物理统一冻结)---- { - ImGui::Checkbox("Pause Animation", ctx.pauseAnim); + bool paused = Time::TimeScale() == 0.0f; + if (ImGui::Checkbox("Pause Simulation", &paused)) + { + Time::SetTimeScale(paused ? 0.0f : 1.0f); + } } // ---- Rendering:渲染管线参数与开关 ---- @@ -124,6 +141,38 @@ void StatsPanel::Draw(const EditorContext& ctx) } } + // ---- Physics Debug:物理调试可视化(collider/约束/重力/速度/加速度)---- + if (ImGui::CollapsingHeader("Physics Debug")) + { + auto& physics = app.GetPhysics(); + u32 bits = physics.GetDebugDrawBits(); + bool master = bits != 0; + if (ImGui::Checkbox("Enable##PhysDebug", &master)) + { + bits = master ? kPhyDbgDefault : 0; + } + if (master) + { + ImGui::Indent(); + auto toggle = [&bits](const char* label, u32 bit) { + bool on = (bits & bit) != 0; + if (ImGui::Checkbox(label, &on)) + { + bits = on ? (bits | bit) : (bits & ~bit); + } + }; + toggle("Colliders", kPhyDbgColliders); + toggle("Constraints", kPhyDbgConstraints); + toggle("Gravity", kPhyDbgGravity); + toggle("Velocity", kPhyDbgVelocity); + toggle("Acceleration", kPhyDbgAccel); + ImGui::Unindent(); + } + physics.SetDebugDrawBits(bits); + // 碰撞事件计数(最近一次步进;掉落物触地时应 > 0) + ImGui::Text("contacts (frame): %zu", physics.GetEvents().size()); + } + // ---- GPU Timings:只读遥测(默认折叠,不占面板高度)---- if (ImGui::CollapsingHeader("GPU Timings")) { @@ -165,8 +214,8 @@ void StatsPanel::Draw(const EditorContext& ctx) } } ImGui::Text("fps: %.1f frame: %llu", - 1.0f / app.GetDeltaTime(), - static_cast(app.GetFrameCount())); + 1.0f / Time::Current().UnscaledDt(), + static_cast(app.GetFrameCount())); } ImGui::End(); diff --git a/App/editor/ViewportPanel.cpp b/App/editor/ViewportPanel.cpp index 19a5d6c..1460377 100644 --- a/App/editor/ViewportPanel.cpp +++ b/App/editor/ViewportPanel.cpp @@ -1,18 +1,33 @@ // -// ViewportPanel.cpp:显示模式单选 + ImGui::Image + 分辨率上报。 -// 逻辑照搬原 main.cpp 的 Viewport 窗口(行为不变)。 +// ViewportPanel:场景渲染输出面板。 +// +// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供 +// Editor Viewport / Fullscreen 显示模式切换。面板可用区域 +// (物理像素)上报为渲染分辨率 → 画面精确填满面板。 +// +// 点击拾取(UE 式 GPU HitProxy):左键单击 → 鼠标 NDC 经 +// Renderer::RequestPick 请求下一帧 id 渲染 + 像素拷贝;Draw 每帧 +// 轮询 TakePickResult 取走结果。像素级精度,与视觉画面一致 +//(视觉几何语义:与 Collider 无关,无 collider 实体同样可选中)。 // #include "ViewportPanel.h" #include -#include // vk::Extent2D 经 Renderer 头已带入,显式无妨 +#include // MeshRenderer / Name / RigidBody(日志对照) +#include // RequestPick / TakePickResult 门面 +#include // kPickMiss +#include // 命中打印 + +#include using namespace Engine; // u32 / f32(Engine/Core/Types.h) -void ViewportPanel::Draw(const EditorContext &ctx, ViewportInputState &out) { +std::optional ViewportPanel::Draw(const EditorContext &ctx, + ViewportInputState &out) const { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + std::optional picked; { ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoCollapse); @@ -23,6 +38,26 @@ void ViewportPanel::Draw(const EditorContext &ctx, ViewportInputState &out) { auto &renderer = *ctx.renderer; const int mode = renderer.GetDisplayMode(); + // ---- 拾取结果轮询(先于新请求:先取走上帧结果再发新请求)。 + // 拷贝随上一帧命令缓冲提交,TakePickResult 内 waitIdle 保证 + // 暂存可见(仅拷贝就绪帧阻塞,点击低频不可感知)---- + if (auto bits = renderer.TakePickResult()) { + auto ® = ctx.scene->GetRegistry(); + const entt::entity hit = static_cast(*bits); + if (*bits != kPickMiss && reg.valid(hit)) { + picked = hit; + LogPick(ctx, hit); + } else { + // 空白(kPickMiss)或悬空位模式:清选中(与旧语义一致) + picked = entt::null; + if (*bits == kPickMiss) { + ENGINE_INFO("[pick] miss (sky/empty)"); + } else { + ENGINE_WARN("[pick] dangling entity bits {:#x}", *bits); + } + } + } + const void *sceneTex = renderer.GetSceneTexture(); if (sceneTex != nullptr && mode == kDisplayViewport) { // 面板可用区域(物理像素)上报为渲染分辨率 → 画面精确填满 @@ -43,6 +78,12 @@ void ViewportPanel::Draw(const EditorContext &ctx, ViewportInputState &out) { if (avail.x / avail.y > aspect) size.x = avail.y * aspect; else size.y = avail.x / aspect; ImGui::Image((ImTextureID) (intptr_t) sceneTex, size); + + // 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。 + // 结果下一帧经轮询到达(本帧不改变选中) + if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) { + RequestPickAt(ctx, size); + } } else { ImGui::TextDisabled("(scene texture not ready)"); } @@ -50,4 +91,43 @@ void ViewportPanel::Draw(const EditorContext &ctx, ViewportInputState &out) { ImGui::End(); } ImGui::PopStyleVar(); + return picked; +} + +void ViewportPanel::RequestPickAt(const EditorContext &ctx, + const ImVec2 &size) const { + // 鼠标在 Image 内的位置 → NDC。Vulkan NDC y 与屏幕 v 同向 + //(向下为正,与渲染同构;skybox.frag 反投影交叉验证过方向) + const ImVec2 mouse = ImGui::GetMousePos(); + const ImVec2 mn = ImGui::GetItemRectMin(); + const glm::vec2 ndc{ + (mouse.x - mn.x) / size.x * 2.0f - 1.0f, + (mouse.y - mn.y) / size.y * 2.0f - 1.0f, + }; + + // UE 式 GPU HitProxy:下一帧 id 渲染 + 请求像素 1×1 拷贝 + ctx.renderer->RequestPick(ndc); +} + +void ViewportPanel::LogPick(const EditorContext &ctx, entt::entity e) { + // 命中详情:实体名;有物理体则附 collider 对照(诊断"物理盒 ≠ + // 视觉"的错位,如 M1911 的默认 0.5 半尺寸盒) + auto ® = ctx.scene->GetRegistry(); + const auto &name = reg.get(e).name; + if (const auto *rb = reg.try_get(e)) { + const auto &tr = reg.get(e); + ENGINE_INFO("[pick] '{}' (hitproxy) | collider={} type={} " + "boxHalf=({:.2f},{:.2f},{:.2f}) sphereR={:.2f} " + "bodyPos=({:.2f},{:.2f},{:.2f})", + name, + rb->collider == ECS::RigidBodyComponent::ColliderType::Box + ? "box" : "sphere", + rb->type == ECS::RigidBodyComponent::BodyType::Static + ? "static" : "dynamic", + rb->boxHalfExtents.x, rb->boxHalfExtents.y, + rb->boxHalfExtents.z, rb->sphereRadius, + tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z); + } else { + ENGINE_INFO("[pick] '{}' (hitproxy) | (no collider)", name); + } } diff --git a/App/editor/ViewportPanel.h b/App/editor/ViewportPanel.h index b366fa1..6019843 100644 --- a/App/editor/ViewportPanel.h +++ b/App/editor/ViewportPanel.h @@ -5,12 +5,19 @@ // Editor Viewport / Fullscreen 显示模式切换。面板可用区域 // (物理像素)上报为渲染分辨率 → 画面精确填满面板。 // +// 点击拾取(UE 式 GPU HitProxy):单击 → NDC 请求 id 渲染;结果 +// 由 Draw 每帧轮询取走(像素级,与视觉画面一致)。 +// #ifndef HELLOVULKAN_VIEWPORTPANEL_H #define HELLOVULKAN_VIEWPORTPANEL_H #include "EditorContext.h" +#include // ImVec2(RequestPickAt 签名) +#include +#include + // Viewport 面板每帧上报的输入焦点状态(Hazel 模式:编辑器据此决定 // 游戏侧输入是否放行——悬停/持焦 Viewport 才拥有 WASD/鼠标) struct ViewportInputState { @@ -20,7 +27,17 @@ struct ViewportInputState { class ViewportPanel { public: - void Draw(const EditorContext& ctx, ViewportInputState& out); + // 返回值:本帧拾取命中的实体(无点击/未命中 = nullopt 或 null); + // Editor 据此 SelectEntity(面板不持有选中状态) + std::optional Draw(const EditorContext& ctx, ViewportInputState& out) const; + +private: + // Image 区域内的鼠标位置 → NDC → Renderer::RequestPick + //(结果下一帧由 Draw 轮询 TakePickResult 取走) + void RequestPickAt(const EditorContext& ctx, const ImVec2& size) const; + + // 命中打印:实体名 + collider 参数对照(诊断物理盒 ≠ 视觉用) + static void LogPick(const EditorContext& ctx, entt::entity e); }; #endif //HELLOVULKAN_VIEWPORTPANEL_H diff --git a/Engine/include/Engine/RenderGraph/RenderGraph.h b/Engine/include/Engine/RenderGraph/RenderGraph.h index 94ef54b..1447002 100644 --- a/Engine/include/Engine/RenderGraph/RenderGraph.h +++ b/Engine/include/Engine/RenderGraph/RenderGraph.h @@ -296,6 +296,14 @@ namespace Engine::RG { u32 GetBarrierCount() const; u32 GetPooledImageCount() const; + // GPU timestamp:指定槽位上一轮实际写入的查询数(active pass × 2)。 + // 剔除使 active 数 < 总 pass 数——读侧必须按此数取结果,按满额 + // 阻塞读会等永远无人写入的查询(死锁) + u32 GetWrittenQueries(u32 frameSlot) const + { + return frameSlot < 4 ? m_WrittenQueries[frameSlot] : 0; + } + private: friend struct RenderGraphContext; @@ -340,6 +348,7 @@ namespace Engine::RG { // GPU timestamp(可选注入) vk::QueryPool m_QueryPool{}; u32 m_QueriesPerFrame = 0; + u32 m_WrittenQueries[4] = {}; // 每槽位上一轮实际写入数(active×2) IGpuProfiler* m_GpuProfiler = nullptr; // 非拥有(Renderer 持有) // 物理资源池:key = (format, extent, usage),value = 空闲镜像列表 diff --git a/Engine/include/Engine/Renderer/Camera.h b/Engine/include/Engine/Renderer/Camera.h index a708797..4ba71a7 100644 --- a/Engine/include/Engine/Renderer/Camera.h +++ b/Engine/include/Engine/Renderer/Camera.h @@ -24,7 +24,7 @@ namespace Engine { public: // 输入驱动:WASD/Space/Shift 移动 + 按住右键鼠标视角。 // 全部写回传入的 transform,有变更才 Compose。 - void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime); + void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime) const; // 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose) glm::mat4 GetView(const ECS::TransformComponent& transform) const; @@ -37,7 +37,7 @@ namespace Engine { // 鼠标视角总开关(默认开)。交互 = 按住右键自由视角(光标捕获), // 松开释放光标;右键落在 ImGui 面板上时归 UI 不触发相机 - void SetMouseLookEnabled(bool enabled) { m_MouseLookEnabled = enabled; } + void SetMouseLookEnabled(const bool enabled) { m_MouseLookEnabled = enabled; } bool IsMouseLookEnabled() const { return m_MouseLookEnabled; } private: diff --git a/Engine/include/Engine/Renderer/Passes/HitProxyPass.h b/Engine/include/Engine/Renderer/Passes/HitProxyPass.h new file mode 100644 index 0000000..407752f --- /dev/null +++ b/Engine/include/Engine/Renderer/Passes/HitProxyPass.h @@ -0,0 +1,74 @@ +// +// HitProxyPass:UE 式 GPU 拾取(像素级 id pass + 点击 readback)。 +// +// 两个 RG pass: +// "HitProxy" —— 拾取请求帧把不透明 + 透明 batch 用 id 管线画进 +// R32Uint 目标(片元值 = 实体位模式)。深度自测自写 +// (重清主深度):遮挡语义 = 可见表面胜出;运行在 +// Forward/ForwardTrans/PhysicsDebug 之后的 WAW 链尾, +// 主链下游无人再读深度,下帧 Forward 照常重新 Clear +// "HitProxyCopy" —— 请求像素 1×1 CopyImageToBuffer 进 host-visible +// 暂存。TransferSrc use 让 RG 自动完成 +// eColorAttachmentOptimal → eTransferSrcOptimal 迁移 +// +// 无请求帧零绘制零拷贝(barrier 照常插入,GPU 无实际工作)。 +// 结果流:Renderer::TakePickResult(waitIdle 保证提交完成)→ 映射暂存。 +// + +#ifndef HELLOVULKAN_HITPROXYPASS_H +#define HELLOVULKAN_HITPROXYPASS_H + +#include +#include +#include +#include +#include + +namespace Engine { + + class Device; + class ShaderManager; + class DescriptorManager; + + namespace Passes { + + class SceneGpu; + + class HitProxyPass final : public RenderPass { + public: + HitProxyPass(Device& device, MemoryAllocator& allocator, + ShaderManager& shaders, DescriptorManager& descriptors, + SceneGpu& gpu, FrameSceneData& frame, + RG::TextureHandle idTarget, RG::TextureHandle depth); + ~HitProxyPass(); + + HitProxyPass(const HitProxyPass&) = delete; + HitProxyPass& operator=(const HitProxyPass&) = delete; + + void Register(RG::RenderGraph& rg) override; + + // 有待取结果(拷贝已录制进当帧命令缓冲) + bool HasPendingResult() const { return m_ResultValid; } + // 取走拷贝结果(一次性)。调用方保证 GPU 已 idle + //(Renderer::TakePickResult 内 waitIdle) + bool TakeResult(u32& outBits); + + private: + Device& m_Device; + SceneGpu& m_Gpu; + FrameSceneData& m_Frame; + RG::TextureHandle m_IdTarget; + RG::TextureHandle m_Depth; + + vk::PipelineLayout m_Layout; // Set0 = uboSetLayout,无 push + vk::ShaderEXT m_Vert{}, m_Frag{}; + + // 1×1 像素暂存(host visible,持久映射);拷贝录制时置位 + Scope m_Staging; + bool m_ResultValid = false; + }; + + } // namespace Passes +} // namespace Engine + +#endif //HELLOVULKAN_HITPROXYPASS_H diff --git a/Engine/include/Engine/Renderer/Passes/OutlinePass.h b/Engine/include/Engine/Renderer/Passes/OutlinePass.h new file mode 100644 index 0000000..85c5253 --- /dev/null +++ b/Engine/include/Engine/Renderer/Passes/OutlinePass.h @@ -0,0 +1,54 @@ +// +// OutlinePass:选中描边后处理(UE 编辑器选中轮廓语义)。 +// +// 对 SelectionMask 做边缘检测,alpha 混合叠加在 Tonemap 之后的 +// PresentTarget 上——描边是恒定 LDR UI 色,不进 bloom/曝光/TAA。 +// 无选中帧跳过绘制(SideEffect 保活,见 HitProxy 教训)。 +// + +#ifndef HELLOVULKAN_OUTLINEPASS_H +#define HELLOVULKAN_OUTLINEPASS_H + +#include +#include +#include +#include + +namespace Engine { + + namespace Passes { + + class FullscreenGpu; + + class OutlinePass final : public RenderPass { + public: + // color:描边色(LDR);widthPx:描边宽度(物理像素) + OutlinePass(Device& device, FullscreenGpu& gpu, ShaderManager& shaders, + FrameSceneData& frame, + RG::TextureHandle mask, RG::TextureHandle present, + const glm::vec3& color = glm::vec3(1.0f, 0.62f, 0.05f), + f32 widthPx = 6.0f); + ~OutlinePass() override; + + OutlinePass(const OutlinePass&) = delete; + OutlinePass& operator=(const OutlinePass&) = delete; + + void Register(RG::RenderGraph& rg) override; + void OnTargetsRebuilt(RG::RenderGraph& rg) override; + + private: + Device& m_Device; + FullscreenGpu& m_Gpu; + FrameSceneData& m_Frame; + RG::TextureHandle m_Mask; + RG::TextureHandle m_Present; + glm::vec3 m_Color; + f32 m_WidthPx; + ShaderObject m_Frag{}; + std::vector m_Sets; // per-frame slot + }; + + } // namespace Passes +} // namespace Engine + +#endif //HELLOVULKAN_OUTLINEPASS_H diff --git a/Engine/include/Engine/Renderer/Passes/SceneGpu.h b/Engine/include/Engine/Renderer/Passes/SceneGpu.h index cd7732d..698c928 100644 --- a/Engine/include/Engine/Renderer/Passes/SceneGpu.h +++ b/Engine/include/Engine/Renderer/Passes/SceneGpu.h @@ -38,7 +38,8 @@ namespace Engine { UniformBuffer& ubo, UniformBuffer& objectBuffer, UniformBuffer& prevObjectBuffer, UniformBuffer& skinOffsetBuffer, - UniformBuffer& skinPaletteBuffer); + UniformBuffer& skinPaletteBuffer, + UniformBuffer& entityIdBuffer); ~SceneGpu(); SceneGpu(const SceneGpu&) = delete; @@ -54,7 +55,7 @@ namespace Engine { // ---- set 布局(shader 契约,改绑定必须同步 shaders/)---- vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer, - // SkinOffsets, SkinPalettes} + // SkinOffsets, SkinPalettes, EntityIds} vk::DescriptorSetLayout textureSetLayout; // Set1 = {材质 4 槽 + 参数 UBO} vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube} vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO) diff --git a/Engine/include/Engine/Renderer/Passes/SelectionMaskPass.h b/Engine/include/Engine/Renderer/Passes/SelectionMaskPass.h new file mode 100644 index 0000000..8e8bd27 --- /dev/null +++ b/Engine/include/Engine/Renderer/Passes/SelectionMaskPass.h @@ -0,0 +1,51 @@ +// +// SelectionMaskPass:选中描边 mask pass(UE CustomStencil 语义)。 +// +// 只绘制选中实体(FrameSceneData.selectionDraws,BuildFrame 从实例表 +// 筛出)到 R8Unorm mask:复用 SceneGpu 的 forwardVert(静态/蒙皮同源 +// 变换)+ 恒 1 片元。无深度测试 → mask = 完整投影轮廓(透墙可见, +// 且前方遮挡物不会在 mask 上挖洞——挖洞会让边缘检测把洞描出来)。 +// +// 无选中帧只 clear mask 不绘制(SideEffect 保活——拷贝终点在 RG 外 +// 的 pass 会被引用计数剔除,见 HitProxy 教训)。 +// + +#ifndef HELLOVULKAN_SELECTIONMASKPASS_H +#define HELLOVULKAN_SELECTIONMASKPASS_H + +#include +#include +#include +#include + +namespace Engine { + + namespace Passes { + + class SceneGpu; + + class SelectionMaskPass final : public RenderPass { + public: + SelectionMaskPass(Device& device, ShaderManager& shaders, + SceneGpu& gpu, FrameSceneData& frame, + RG::TextureHandle mask); + ~SelectionMaskPass() override; + + SelectionMaskPass(const SelectionMaskPass&) = delete; + SelectionMaskPass& operator=(const SelectionMaskPass&) = delete; + + void Register(RG::RenderGraph& rg) override; + void OnTargetsRebuilt(RG::RenderGraph& rg) override {} + + private: + Device& m_Device; + SceneGpu& m_Gpu; + FrameSceneData& m_Frame; + RG::TextureHandle m_Mask; + ShaderObject m_Frag{}; + }; + + } // namespace Passes +} // namespace Engine + +#endif //HELLOVULKAN_SELECTIONMASKPASS_H diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index 7ca4e49..25916f7 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -26,6 +26,7 @@ #include #include #include +#include #include namespace Engine { @@ -45,10 +46,14 @@ namespace Engine { class IblGenerator; class ImGuiLayer; class TracyGpuProfiler; + struct PhysicsDebugFrame; // PhysicsDebugTypes.h(物理调试图元契约) namespace ECS { class Scene; } namespace Assets { class AssetManager; class IAssetUploader; } - namespace Passes { class RenderPass; class SceneGpu; class FullscreenGpu; class PostProcessParams; } + namespace Passes { + class RenderPass; class SceneGpu; class FullscreenGpu; + class PostProcessParams; class PhysicsDebugPass; class HitProxyPass; + } class Renderer { public: @@ -78,6 +83,13 @@ namespace Engine { if (layer) RefreshSceneTextures(); // 后端就绪:注册场景纹理 } + // 物理调试数据源(PhysicsWorld 每帧收集的图元缓冲;不拥有)。 + // Application 装配时注入;nullptr = 无物理调试 + void SetPhysicsDebugSource(const PhysicsDebugFrame* src) + { + m_PhysicsDebug = src; + } + // ---- 场景显示纹理(tonemap 输出,ImGui::Image 可采样显示)---- // 按 m_CurrentFrame 取当帧槽位的描述符(UI 构建段与紧随的 // Render 同槽位)。无 ImGui 层时返回 nullptr @@ -129,12 +141,29 @@ namespace Engine { // ---- 显示模式(Editor 视口 ↔ 全屏直出;帧首同步生效)---- int GetDisplayMode() const { return m_DesiredDisplayMode; } void SetDisplayMode(int mode); // cpp 内定义(SceneRenderer 前置声明) + // 当帧场景数据(只读直通)。CPU 消费如视口拾取:反投影矩阵必须 + // 与渲染同一台相机同一投影(含 [0,1] 深度语义),禁止自行重算 + const FrameSceneData& GetFrameSceneData() const; // cpp 内定义 // Editor 视口的期望渲染尺寸(像素;UI 面板每帧上报,帧首统一生效) void SetViewportSize(u32 w, u32 h) { m_DesiredViewport = vk::Extent2D{w, h}; } + // ---- Editor 视口拾取(UE 式 GPU HitProxy:id pass + 点击 readback)---- + // UI 段点击时调用:下一帧 HitProxy 渲染 + 请求像素拷贝(Vulkan + // NDC,y 向下,与渲染同构) + void RequestPick(const glm::vec2& ndc); + // UI 段轮询:拷贝结果就绪则取走(一次性;nullopt = 暂无结果)。 + // 返回 kPickMiss = 未命中(天空/空隙),有效值 = 实体位模式。 + // 内部 waitIdle 保证提交完成(点击低频,单帧 hitch 不可感知) + std::optional TakePickResult(); + + // ---- Editor 选中描边(UE 编辑器选中轮廓)---- + // UI 段选中变化时调用(bits = 实体位模式,0 = 清选中); + // 下一帧 SelectionMask 只画该实体,Outline 在其轮廓叠加描边色 + void SetSelectedEntity(u32 entityBits); + // 垂直同步开关(present mode 切换会重建 swapchain), // 只应在帧 UI 构建段调用(Render 之前) void SetVSync(bool vsync) { m_DesiredVSync = vsync; } // 帧首同步生效 @@ -165,8 +194,13 @@ namespace Engine { Scope m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity) Scope m_SkinOffsetBuffer; // 实例→palette 基址 ring(SSBO,与实例序平行) Scope m_SkinPaletteBuffer; // 骨骼蒙皮矩阵 palette ring(SSBO,帧首 bump) + Scope m_EntityIdBuffer; // 实例→实体位模式 ring(SSBO,HitProxy 拾取) Scope m_SceneGpu; // 场景类 pass 共享装配 Scope m_FullscreenGpu; // 全屏 pass 共享装配 + // 物理调试图元 ring VB(eVertexBuffer;per-slot fence 语义复用 + // UniformBuffer——PhysicsDebugPass::PrepareFrame 上传,pass 内绑定) + Scope m_PhysicsLineVB; + Scope m_PhysicsTriVB; // 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造 Scope m_Ibl; // 中性兜底环境(IBL 四件套):Application 未加载/加载失败时保底; @@ -189,6 +223,8 @@ namespace Engine { std::array m_ShadowMaps{}; // CSM 级联 RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一 RG::TextureHandle m_Velocity{}; // Forward 输出(clip 空间运动向量) + RG::TextureHandle m_HitProxyId{}; // 拾取 id 目标(R32Uint,片元写实体位模式) + RG::TextureHandle m_SelectionMask{}; // 选中描边 mask(R8Unorm,选中像素=1) RG::TextureHandle m_TAAColor{}; // TAA 累积输出(自引用历史),后处理源 RG::TextureHandle m_BloomA{}; // 半分辨率:提取 + 垂直模糊结果 RG::TextureHandle m_BloomB{}; // 半分辨率:水平模糊中间结果 @@ -200,6 +236,11 @@ namespace Engine { // 可选叠加层(不拥有;Application 持有) ImGuiLayer* m_ImGuiLayer = nullptr; + // 物理调试(非拥有指针:pass 在 m_Passes 内,数据源由 Application 注入) + Passes::PhysicsDebugPass* m_PhysicsDebugPass = nullptr; + const PhysicsDebugFrame* m_PhysicsDebug = nullptr; + // HitProxy 拾取(非拥有指针:pass 在 m_Passes 内) + Passes::HitProxyPass* m_HitProxyPass = nullptr; // GPU timestamp(pass 级耗时,ImGui 可读) vk::QueryPool m_QueryPool; u32 m_QueriesPerFrame = 0; // 每帧槽位的查询数 = pass 数 × 2 diff --git a/Engine/include/Engine/Renderer/SceneRenderer.h b/Engine/include/Engine/Renderer/SceneRenderer.h index eea0677..1d2a5de 100644 --- a/Engine/include/Engine/Renderer/SceneRenderer.h +++ b/Engine/include/Engine/Renderer/SceneRenderer.h @@ -62,6 +62,13 @@ namespace Engine { void SetLodEnabled(bool b) { m_LodEnabled = b; } void SetDebugView(int v) { m_DebugView = v; } void SetDisplayMode(int m) { m_DisplayMode = m; } // kDisplayDirect / kDisplayViewport + // Editor 视口拾取请求(UI 段写入,下一次 BuildFrame 落帧后清除) + void SetPickRequest(const glm::vec2& ndc) + { + m_PendingPick = PickRequest{true, ndc}; + } + // Editor 选中实体(描边):UI 段写入,BuildFrame 落帧并筛出绘制 + void SetSelectedEntity(u32 entityBits) { m_PendingSelected = entityBits; } private: // CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影 @@ -76,6 +83,8 @@ namespace Engine { static void PackIndirect(CullResult& view, bool byMaterial); FrameSceneData m_Frame; + PickRequest m_PendingPick{}; // 拾取请求缓冲(BuildFrame 落帧后清除) + u32 m_PendingSelected = 0; // 选中实体缓冲(描边) // ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)---- struct ViewScratch { @@ -84,6 +93,7 @@ namespace Engine { glm::mat4 cur; glm::mat4 prev; u32 skinBase = 0; // palette 区段基址(0 = identity 槽) + u32 entityBits = 0; // 实体位模式(HitProxy id pass 消费) }; CullResult result; // 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 = diff --git a/Engine/include/Engine/Renderer/SceneTypes.h b/Engine/include/Engine/Renderer/SceneTypes.h index 88be9e8..838231a 100644 --- a/Engine/include/Engine/Renderer/SceneTypes.h +++ b/Engine/include/Engine/Renderer/SceneTypes.h @@ -63,6 +63,14 @@ namespace Engine { std::vector indirectGroups; }; + // 选中描边:单实例绘制指令(SelectionMask pass 逐条 draw)。 + // instance = 该实例在实例矩阵数组中的绝对下标 + struct SelectionDraw { + Mesh* mesh; + u32 instance; + u32 lod; + }; + constexpr u32 kMaxLights = 4; // 实例 SSBO 容量(矩阵数)。四视图(main + 3 级联)共享同一预算: // 需求 = Σ各视图剔除后实例数,大场景斜光时阴影实例可膨胀到主视图 @@ -79,6 +87,17 @@ namespace Engine { // 直通它 → 静态模型蒙皮零影响 constexpr u32 kIdentityPaletteSlot = 0; + // ---- Editor 视口拾取(UE 式 GPU HitProxy)---- + // R32Uint id 目标的清屏色 = 未命中(天空/空隙);恰为 entt::null + // 位模式,永不与有效实体冲突 + inline constexpr u32 kPickMiss = 0xFFFFFFFFu; + + // 点击拾取请求(UI 段写入 → SceneRenderer 落帧 → HitProxy pass 只读) + struct PickRequest { + bool active = false; + glm::vec2 ndc{0.0f}; // 点击点 NDC(Vulkan y 向下,与渲染同构) + }; + // 镜像 shader 侧 UBO 布局的灯光项(std140,每灯 64B)。 // 字段未用部分置零——shader 按类型分支读取 struct LightData { @@ -140,6 +159,20 @@ namespace Engine { // [skinBase .. skinBase+boneCount) = 该实体全部骨骼的蒙皮矩阵 std::vector skinPalettes; + // per-instance 实体 id(与 instanceModels 平行对齐; + // HitProxy id pass 按 gl_InstanceIndex 取,片元写入 R32Uint 目标) + std::vector instanceEntityIds; + + // 本帧拾取请求(UI 段点击,SceneRenderer 落帧后自动清除; + // HitProxy/HitProxyCopy 两 pass 据此决定是否绘制/拷贝) + PickRequest pickRequest{}; + + // 选中描边(UE 编辑器语义):选中实体的位模式(0 = 无选中)。 + // BuildFrame 据此从实例表筛出 selectionDraws,SelectionMask pass + // 只画选中实体 → Outline pass 在 mask 边缘叠加描边色 + u32 selectedEntityBits = 0; + std::vector selectionDraws; + CullResult mainView; // 相机视锥 → Forward pass // 每级联独立剔除结果(Shadow pass 按 region 各画各的—— // 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉) diff --git a/Engine/src/RenderGraph/RenderGraph.cpp b/Engine/src/RenderGraph/RenderGraph.cpp index 6baeb8a..405961f 100644 --- a/Engine/src/RenderGraph/RenderGraph.cpp +++ b/Engine/src/RenderGraph/RenderGraph.cpp @@ -729,6 +729,7 @@ namespace Engine::RG { // 时间戳(BOTTOM_OF_PIPE),索引按执行序排布 u32 tsPos = 0; if (m_QueryPool) { + m_WrittenQueries[frame.frameSlot] = 0; // 上轮残留清零 frame.cmd.resetQueryPool(m_QueryPool, frame.frameSlot * m_QueriesPerFrame, m_QueriesPerFrame); @@ -820,6 +821,12 @@ namespace Engine::RG { ++tsPos; } + // 记录本槽位实际写入的查询数:读侧(Renderer::ReadGpuTimes)按此 + // 数取结果——被剔除 pass 的时间戳永远不写,按满额阻塞读会死锁 + if (m_QueryPool) { + m_WrittenQueries[frame.frameSlot] = tsPos * 2; + } + // 帧尾收集:profiler 在当帧 cmd 内解析查询并上报(提交前录制) if (m_GpuProfiler) { m_GpuProfiler->EndFrame(frame.cmd); diff --git a/Engine/src/Renderer/Camera.cpp b/Engine/src/Renderer/Camera.cpp index 747bee3..093b3cb 100644 --- a/Engine/src/Renderer/Camera.cpp +++ b/Engine/src/Renderer/Camera.cpp @@ -12,8 +12,7 @@ namespace Engine { - void Camera::Update(Window& window, ECS::TransformComponent& t, f32 deltaTime) - { + void Camera::Update(Window& window, ECS::TransformComponent& transform, const f32 deltaTime) const { // 鼠标视角(编辑器习惯):按住右键捕获光标进入自由视角, // 松开释放光标(ImGui 面板可正常交互)。m_MouseLookEnabled 总开关 const bool wantLook = m_MouseLookEnabled && @@ -30,14 +29,14 @@ namespace Engine { if (mouseDX != 0 || mouseDY != 0) { - glm::vec3 rot = t.GetRotation(); + glm::vec3 rot = transform.GetRotation(); // 右手系绕 Y 正角度 = 向左转,鼠标右移(DX>0)应看向右 // → yaw 取负。pitch 正角度 = 抬头,鼠标下移(DY>0)低头 // → rot.x 直接用负号,两者符号已核对一致 rot.y -= mouseDX * m_MouseSensitivity; rot.x = glm::clamp( rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f); - t.SetRotation(rot); + transform.SetRotation(rot); } } else if (window.IsCursorCaptured()) @@ -47,7 +46,7 @@ namespace Engine { // 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X, // 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机) - const glm::vec3& rot = t.GetRotation(); + const glm::vec3& rot = transform.GetRotation(); const glm::mat3 r = static_cast( glm::rotate(glm::mat4(1.0f), glm::radians(rot.y), glm::vec3(0, 1, 0)) * @@ -56,13 +55,13 @@ namespace Engine { const glm::vec3 forward = r * glm::vec3(0, 0, -1); const glm::vec3 right = r * glm::vec3(1, 0, 0); - f32 speed = m_MoveSpeed * deltaTime; - if (window.IsKeyPressed(GLFW_KEY_W)) { t.SetPosition(t.GetPosition() + forward * speed); } - if (window.IsKeyPressed(GLFW_KEY_S)) { t.SetPosition(t.GetPosition() - forward * speed); } - if (window.IsKeyPressed(GLFW_KEY_A)) { t.SetPosition(t.GetPosition() - right * speed); } - if (window.IsKeyPressed(GLFW_KEY_D)) { t.SetPosition(t.GetPosition() + right * speed); } - if (window.IsKeyPressed(GLFW_KEY_SPACE)) { t.SetPosition(t.GetPosition() + glm::vec3(0, speed, 0)); } - if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT)) { t.SetPosition(t.GetPosition() - glm::vec3(0, speed, 0)); } + const f32 speed = m_MoveSpeed * deltaTime; + if (window.IsKeyPressed(GLFW_KEY_W)) { transform.SetPosition(transform.GetPosition() + forward * speed); } + if (window.IsKeyPressed(GLFW_KEY_S)) { transform.SetPosition(transform.GetPosition() - forward * speed); } + if (window.IsKeyPressed(GLFW_KEY_A)) { transform.SetPosition(transform.GetPosition() - right * speed); } + if (window.IsKeyPressed(GLFW_KEY_D)) { transform.SetPosition(transform.GetPosition() + right * speed); } + if (window.IsKeyPressed(GLFW_KEY_SPACE)) { transform.SetPosition(transform.GetPosition() + glm::vec3(0, speed, 0)); } + if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT)) { transform.SetPosition(transform.GetPosition() - glm::vec3(0, speed, 0)); } } glm::mat4 Camera::GetView(const ECS::TransformComponent& t) const @@ -71,7 +70,7 @@ namespace Engine { return glm::inverse(t.World()); } - glm::mat4 Camera::GetProjection(f32 aspect, f32 nearZ, f32 farZ) const + glm::mat4 Camera::GetProjection(const f32 aspect, const f32 nearZ, const f32 farZ) const { auto proj = glm::perspective(glm::radians(45.0f), aspect, nearZ, farZ); proj[1][1] *= -1.0f; // Vulkan Y 翻转 diff --git a/Engine/src/Renderer/Passes/HitProxyPass.cpp b/Engine/src/Renderer/Passes/HitProxyPass.cpp new file mode 100644 index 0000000..015641f --- /dev/null +++ b/Engine/src/Renderer/Passes/HitProxyPass.cpp @@ -0,0 +1,246 @@ +// +// HitProxyPass.cpp:pass 装配(shader object + 暂存 buffer)与录制。 +// +// 拾取管线与 Forward 同构(Set0 描述符、顶点输入、MDI batch 结构全部 +// 复用),差异只有:R32Uint 目标、无光照 id 片元、深度重清自写。 +// + +#include +#include +#include +#include // batch.mesh->Draw(完整类型) +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Engine::Passes +{ + namespace + { + // 直写方程:(ONE, ZERO) → dst = src,数学上等价于无混合 + vk::ColorBlendEquationEXT MakeDirectEquation() + { + vk::ColorBlendEquationEXT eq; + eq.setSrcColorBlendFactor(vk::BlendFactor::eOne) + .setDstColorBlendFactor(vk::BlendFactor::eZero) + .setColorBlendOp(vk::BlendOp::eAdd) + .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) + .setDstAlphaBlendFactor(vk::BlendFactor::eZero) + .setAlphaBlendOp(vk::BlendOp::eAdd); + return eq; + } + + constexpr vk::ColorComponentFlags kColorWriteMask = + vk::ColorComponentFlagBits::eR | + vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | + vk::ColorComponentFlagBits::eA; + } // namespace + + HitProxyPass::HitProxyPass(Device& device, MemoryAllocator& allocator, + ShaderManager& shaders, + DescriptorManager& descriptors, + SceneGpu& gpu, FrameSceneData& frame, + RG::TextureHandle idTarget, + RG::TextureHandle depth) + : m_Device(device), m_Gpu(gpu), m_Frame(frame), + m_IdTarget(idTarget), m_Depth(depth) + { + // Set0 = uboSetLayout(UBO + 实例矩阵 + 蒙皮 + 实体 id),无 push + m_Layout = descriptors.CreatePipelineLayout( + std::vector{gpu.uboSetLayout}, 0); + + const std::string shaderDir = "shaders/"; + auto vert = shaders.CreateFromFile( + shaderDir + "hitproxy.vert.spv", + vk::ShaderStageFlagBits::eVertex, + std::vector{gpu.uboSetLayout}, 0); + auto frag = shaders.CreateFromFile( + shaderDir + "hitproxy.frag.spv", + vk::ShaderStageFlagBits::eFragment, + std::vector{gpu.uboSetLayout}, 0); + + m_Vert = vert.Handle; + m_Frag = frag.Handle; + if (!m_Vert || !m_Frag) { + ENGINE_FATAL("HitProxyPass: failed to create shader objects"); + } + + // 1×1 像素暂存(256B 对齐 minMemoryMapAlignment 余量),持久映射 + m_Staging = CreateScope( + device, allocator, 256, + vk::BufferUsageFlagBits::eTransferDst, + vk::MemoryPropertyFlagBits::eHostVisible | + vk::MemoryPropertyFlagBits::eHostCoherent); + } + + HitProxyPass::~HitProxyPass() + { + const auto& vkDevice = m_Device.GetDevice(); + + if (m_Vert) vkDevice.destroyShaderEXT(m_Vert); + if (m_Frag) vkDevice.destroyShaderEXT(m_Frag); + vkDevice.destroyPipelineLayout(m_Layout); + } + + bool HitProxyPass::TakeResult(u32& outBits) + { + if (!m_ResultValid || !m_Staging || !m_Staging->MappedPtr()) { + return false; + } + m_ResultValid = false; + std::memcpy(&outBits, m_Staging->MappedPtr(), sizeof(u32)); + return true; + } + + void HitProxyPass::Register(RG::RenderGraph& rg) + { + // ---- id 渲染:拾取请求帧才绘制(无请求帧零 GPU 开销)---- + rg.AddPass("HitProxy", [this](RG::RenderGraphContext& ctx) { + if (!m_Frame.pickRequest.active) return; + + ZoneScopedN("HitProxy"); + const vk::CommandBuffer cmd = ctx.cmd; + + // id 目标清 kPickMiss(天空/空隙 = 未命中;0xFFFFFFFF 永不 + // 与有效实体冲突) + vk::ClearValue idClear; + idClear.color = vk::ClearColorValue( + std::array{kPickMiss, kPickMiss, kPickMiss, kPickMiss}); + + vk::RenderingAttachmentInfo color; + color.setImageView(ctx.View(m_IdTarget)) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(idClear); + + // 主深度重清自写:拾取遮挡 = 本 pass 内深度解算,最近表面 + // 胜出。本 pass 是深度 WAW 链尾,主链下游无人再读深度 + vk::RenderingAttachmentInfo depth; + depth.setImageView(ctx.View(m_Depth)) + .setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eDontCare) + .setClearValue( + vk::ClearValue(vk::ClearDepthStencilValue(1.0f, 0))); + + vk::RenderingInfo renderingInfo; + renderingInfo.setRenderArea({{0, 0}, ctx.extent}) + .setLayerCount(1) + .setColorAttachments(color) + .setPDepthAttachment(&depth); + + cmd.beginRendering(renderingInfo); + + const std::array stages = { + vk::ShaderStageFlagBits::eVertex, + vk::ShaderStageFlagBits::eFragment}; + const std::array shaders = {m_Vert, m_Frag}; + cmd.bindShadersEXT(stages, shaders); + + // Set0 = uboSets(UBO + 实例矩阵 + 蒙皮 + 实体 id) + cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + m_Layout, 0, + m_Gpu.uboSets[ctx.frame.frameSlot], {}); + + // ---- 动态状态(shader object 无管线对象,全量显式设置)---- + cmd.setRasterizerDiscardEnable(VK_FALSE); + + cmd.setDepthTestEnable(VK_TRUE); + cmd.setDepthCompareOp(vk::CompareOp::eLess); + cmd.setDepthWriteEnable(VK_TRUE); + cmd.setDepthBoundsTestEnable(VK_FALSE); + cmd.setStencilTestEnable(VK_FALSE); + cmd.setDepthBiasEnable(VK_FALSE); + + cmd.setPolygonModeEXT(vk::PolygonMode::eFill); + cmd.setCullMode(vk::CullModeFlagBits::eNone); + cmd.setFrontFace(vk::FrontFace::eCounterClockwise); + cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList); + cmd.setPrimitiveRestartEnable(VK_FALSE); + + const auto binding = Vertex::GetBindingDescription(); + const auto attributes = Vertex::GetAttributeDescriptions(); + cmd.setVertexInputEXT(binding, attributes); + + cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1); + constexpr vk::SampleMask sampleMask = 0xFFFFFFFF; + cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask); + cmd.setAlphaToCoverageEnableEXT(VK_FALSE); + cmd.setAlphaToOneEnableEXT(VK_FALSE); + + const vk::Viewport viewport( + 0.0f, 0.0f, static_cast(ctx.extent.width), + static_cast(ctx.extent.height), 0.0f, 1.0f); + cmd.setViewportWithCount(viewport); + cmd.setScissorWithCount(vk::Rect2D({0, 0}, ctx.extent)); + + // 无混合直写(id 是整数载荷,混合无意义) + constexpr vk::Bool32 blendEnable = VK_FALSE; + const vk::ColorBlendEquationEXT direct = MakeDirectEquation(); + cmd.setColorBlendEnableEXT(0, blendEnable); + cmd.setColorBlendEquationEXT(0, direct); + cmd.setColorWriteMaskEXT(0, kColorWriteMask); + + // 不透明 + 透明 batch 全画:id 语义下透明按不透明处理 + //(玻璃可拾取且遮挡其后图元,与 UE HitProxy 一致) + for (const auto& batch : m_Frame.mainView.batches) { + batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod); + } + for (const auto& batch : m_Frame.translucentView.batches) { + batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod); + } + + cmd.endRendering(); + }) + .Write(m_IdTarget, RG::ResourceUse::ColorAttachment) + .Write(m_Depth, RG::ResourceUse::DepthAttachment) + // 拷贝终点是 RG 外的 host 暂存 buffer:从数据流视角 idTarget + // 无 RG 内消费者,两 pass 会被引用计数剔除——显式保活 + .SideEffect(); + + // ---- 像素拷贝:请求像素 1×1 → host 暂存。TransferSrc use 让 + // RG 自动做 eColorAttachmentOptimal → eTransferSrcOptimal 迁移 + //(RAW 边保证排在本 pass 的 id 写入之后)---- + rg.AddPass("HitProxyCopy", [this](RG::RenderGraphContext& ctx) { + if (!m_Frame.pickRequest.active) return; + + const vk::CommandBuffer cmd = ctx.cmd; + + // NDC → 请求像素(钳到目标内,防面板边缘点击越界) + const vk::Extent2D ext = ctx.Extent(m_IdTarget); + const i32 px = std::clamp( + static_cast((m_Frame.pickRequest.ndc.x * 0.5f + 0.5f) * + static_cast(ext.width)), + 0, static_cast(ext.width) - 1); + const i32 py = std::clamp( + static_cast((m_Frame.pickRequest.ndc.y * 0.5f + 0.5f) * + static_cast(ext.height)), + 0, static_cast(ext.height) - 1); + + vk::BufferImageCopy region; + region.setImageSubresource(vk::ImageSubresourceLayers( + vk::ImageAspectFlagBits::eColor, + 0, 0, 1)) + .setImageOffset(vk::Offset3D(px, py, 0)) + .setImageExtent(vk::Extent3D(1, 1, 1)); + + cmd.copyImageToBuffer( + ctx.Image(m_IdTarget), vk::ImageLayout::eTransferSrcOptimal, + m_Staging->Get(), region); + + m_ResultValid = true; // 拷贝已录制:结果可待取 + }) + .Read(m_IdTarget, RG::ResourceUse::TransferSrc) + .SideEffect(); + } + +} // namespace Engine::Passes diff --git a/Engine/src/Renderer/Passes/OutlinePass.cpp b/Engine/src/Renderer/Passes/OutlinePass.cpp new file mode 100644 index 0000000..b5f9372 --- /dev/null +++ b/Engine/src/Renderer/Passes/OutlinePass.cpp @@ -0,0 +1,142 @@ +// +// OutlinePass.cpp:pass 装配与录制(见头文件注释)。 +// +// 录制段复制 FullscreenGpu::RecordFullscreen 的动态状态骨架,仅两处 +// 不同:颜色混合改为 alpha 叠加(其余像素 alpha=0 不改画面)、 +// PresentTarget 附件 loadOp=eLoad(保留 Tonemap 输出)。 +// + +#include +#include +#include +#include +#include + +#include + +#include + +namespace Engine::Passes +{ + OutlinePass::OutlinePass(Device& device, FullscreenGpu& gpu, ShaderManager& shaders, + FrameSceneData& frame, + RG::TextureHandle mask, RG::TextureHandle present, + const glm::vec3& color, f32 widthPx) + : m_Device(device), m_Gpu(gpu), m_Frame(frame), m_Mask(mask), m_Present(present), + m_Color(color), m_WidthPx(widthPx) { + m_Frag = shaders.CreateFromFile( + "shaders/outline.frag.spv", + vk::ShaderStageFlagBits::eFragment, + std::vector{gpu.setLayout}, + sizeof(FullscreenPush)); + + m_Sets.resize(kMaxFramesInFlight); + for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) { + m_Sets[slot] = m_Gpu.AllocateSet(); + } + } + + OutlinePass::~OutlinePass() + { + // VkShaderEXT 是 Device 子对象,必须在 vkDestroyDevice 前销毁 + if (m_Frag.Handle) m_Device.GetDevice().destroyShaderEXT(m_Frag.Handle); + } + + void OutlinePass::Register(RG::RenderGraph& rg) + { + rg.AddPass("Outline", [this](RG::RenderGraphContext& ctx) { + // 无选中零 GPU(mask 已由 SelectionMask pass 清 0,无描边) + if (m_Frame.selectedEntityBits == 0) return; + + const vk::CommandBuffer cmd = ctx.cmd; + + // PresentTarget:eLoad 保留 Tonemap 输出,只叠加描边 + vk::RenderingAttachmentInfo colorAttachment; + colorAttachment.setImageView(ctx.View(m_Present)) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eLoad) + .setStoreOp(vk::AttachmentStoreOp::eStore); + + vk::RenderingInfo renderingInfo; + renderingInfo.setRenderArea({{0, 0}, ctx.extent}) + .setLayerCount(1) + .setColorAttachments(colorAttachment); + cmd.beginRendering(renderingInfo); + + std::array stages = { + vk::ShaderStageFlagBits::eVertex, + vk::ShaderStageFlagBits::eFragment}; + std::array shaders = {m_Gpu.vert, m_Frag.Handle}; + cmd.bindShadersEXT(stages, shaders); + + cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + m_Gpu.layout, 0, + m_Sets[ctx.frame.frameSlot], {}); + + cmd.setRasterizerDiscardEnable(VK_FALSE); + cmd.setDepthTestEnable(VK_FALSE); + cmd.setDepthWriteEnable(VK_FALSE); + cmd.setDepthBoundsTestEnable(VK_FALSE); + cmd.setStencilTestEnable(VK_FALSE); + cmd.setDepthBiasEnable(VK_FALSE); + cmd.setPolygonModeEXT(vk::PolygonMode::eFill); + cmd.setCullMode(vk::CullModeFlagBits::eNone); + cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList); + cmd.setPrimitiveRestartEnable(VK_FALSE); + + cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1); + vk::SampleMask sampleMask = 0xFFFFFFFF; + cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask); + + vk::Viewport viewport(0.0f, 0.0f, + static_cast(ctx.extent.width), + static_cast(ctx.extent.height), + 0.0f, 1.0f); + cmd.setViewportWithCount(viewport); + cmd.setScissorWithCount(vk::Rect2D({0, 0}, ctx.extent)); + + // alpha 叠加:描边像素 src 上色,其余(alpha=0)不改画面。 + // alpha 通道必须保持 dst(Zero,One)——PresentTarget 的 + // alpha 归 Tonemap/ImGui 链所有:ImGui 后端按 (SRC_ALPHA, + // ONE_MINUS_SRC_ALPHA) 混合,若把 alpha 覆写成 0,视口图 + // 整体被乘 0 = 黑屏(只有描边像素 alpha=1 可见) + cmd.setColorBlendEnableEXT(0, VK_TRUE); + vk::ColorBlendEquationEXT blend; + blend.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) + .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha) + .setColorBlendOp(vk::BlendOp::eAdd) + .setSrcAlphaBlendFactor(vk::BlendFactor::eZero) + .setDstAlphaBlendFactor(vk::BlendFactor::eOne) + .setAlphaBlendOp(vk::BlendOp::eAdd); + cmd.setColorBlendEquationEXT(0, blend); + cmd.setColorWriteMaskEXT(0, + vk::ColorComponentFlagBits::eR | + vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | + vk::ColorComponentFlagBits::eA); + + // 复用全屏 push(16B):exposure 槽位=宽度,param0-2=描边色 + const FullscreenPush push{m_WidthPx, m_Color.r, m_Color.g, m_Color.b}; + cmd.pushConstants(m_Gpu.layout, + vk::ShaderStageFlagBits::eVertex | + vk::ShaderStageFlagBits::eFragment, + 0, sizeof(FullscreenPush), &push); + + cmd.draw(3, 1, 0, 0); + cmd.endRendering(); + }) + .Write(m_Present, RG::ResourceUse::ColorAttachment) + .Read(m_Mask, RG::ResourceUse::ShaderRead, + vk::ShaderStageFlagBits::eFragment) + // 无选中帧跳过绘制:显式保活(HitProxy 教训) + .SideEffect(); + } + + void OutlinePass::OnTargetsRebuilt(RG::RenderGraph& rg) + { + for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) { + m_Gpu.UpdateSetInput(m_Sets[slot], 0, rg, m_Mask, slot); + } + } + +} // namespace Engine::Passes diff --git a/Engine/src/Renderer/Passes/SceneGpu.cpp b/Engine/src/Renderer/Passes/SceneGpu.cpp index 75c5317..fe3ae22 100644 --- a/Engine/src/Renderer/Passes/SceneGpu.cpp +++ b/Engine/src/Renderer/Passes/SceneGpu.cpp @@ -23,12 +23,14 @@ namespace Engine::Passes UniformBuffer& ubo, UniformBuffer& objectBuffer, UniformBuffer& prevObjectBuffer, UniformBuffer& skinOffsetBuffer, - UniformBuffer& skinPaletteBuffer) + UniformBuffer& skinPaletteBuffer, + UniformBuffer& entityIdBuffer) : m_Device(device), m_Allocator(allocator), m_Descriptors(descriptors) { // Set 0: UBO + GPUDriven 实例数据(SSBO,vertex 读取) // + 上一帧实例矩阵(TAA velocity,vertex 读取) // + 骨骼蒙皮:SkinOffsets(实例→palette 基址)+ SkinPalettes(骨骼矩阵) + // + EntityIds(实例→实体位模式,HitProxy 拾取) std::vector uboBindings = { {0, vk::DescriptorType::eUniformBuffer, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment}, @@ -40,6 +42,8 @@ namespace Engine::Passes vk::ShaderStageFlagBits::eVertex}, {4, vk::DescriptorType::eStorageBuffer, vk::ShaderStageFlagBits::eVertex}, + {5, vk::DescriptorType::eStorageBuffer, + vk::ShaderStageFlagBits::eVertex}, }; uboSetLayout = descriptors.CreateLayout(uboBindings); @@ -155,6 +159,9 @@ namespace Engine::Passes descriptors.UpdateStorageBuffer( uboSets[slot], 4, skinPaletteBuffer.Get(slot), skinPaletteBuffer.GetSize()); + descriptors.UpdateStorageBuffer( + uboSets[slot], 5, entityIdBuffer.Get(slot), + entityIdBuffer.GetSize()); shadowSets[slot] = descriptors.AllocateDescriptorSet(shadowSetLayout); } diff --git a/Engine/src/Renderer/Passes/SelectionMaskPass.cpp b/Engine/src/Renderer/Passes/SelectionMaskPass.cpp new file mode 100644 index 0000000..84a2d22 --- /dev/null +++ b/Engine/src/Renderer/Passes/SelectionMaskPass.cpp @@ -0,0 +1,138 @@ +// +// SelectionMaskPass.cpp:pass 装配与录制(见头文件注释)。 +// + +#include +#include +#include +#include +#include +#include + +#include + +namespace Engine::Passes +{ + SelectionMaskPass::SelectionMaskPass(Device& device, ShaderManager& shaders, + SceneGpu& gpu, FrameSceneData& frame, + RG::TextureHandle mask) + : m_Device(device), m_Gpu(gpu), m_Frame(frame), m_Mask(mask) + { + // 恒 1 片元;布局数组必须与 forwardVert 完全一致(shader object + // 要求同程序各 shader 的 set layouts 逐项相同,VUID-08879), + // 本 shader 只用 Set0,多声明是合法超集 + m_Frag = shaders.CreateFromFile( + "shaders/sel_mask.frag.spv", + vk::ShaderStageFlagBits::eFragment, + std::vector{ + gpu.uboSetLayout, gpu.textureSetLayout, gpu.shadowSetLayout}, + 0); + } + + SelectionMaskPass::~SelectionMaskPass() + { + // VkShaderEXT 是 Device 子对象,必须在 vkDestroyDevice 前销毁 + if (m_Frag.Handle) m_Device.GetDevice().destroyShaderEXT(m_Frag.Handle); + } + + void SelectionMaskPass::Register(RG::RenderGraph& rg) + { + rg.AddPass("SelectionMask", [this](RG::RenderGraphContext& ctx) { + const vk::CommandBuffer cmd = ctx.cmd; + const bool hasDraws = !m_Frame.selectionDraws.empty(); + + // mask 附件:每帧清 0(无选中帧残留自动消失) + vk::ClearValue maskClear; + maskClear.color = vk::ClearColorValue(std::array{0.f, 0.f, 0.f, 0.f}); + vk::RenderingAttachmentInfo colorAttachment; + colorAttachment.setImageView(ctx.View(m_Mask)) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(maskClear); + + // 无深度附件——mask = 选中实体完整投影轮廓(不做遮挡判定, + // UE 编辑器选中轮廓透墙可见同语义)。若做深度测试,前方 + // 遮挡物会在 mask 上挖洞,边缘检测会把洞的轮廓也描出来 + // (表现为"选地面却给枪描了边")。 + vk::RenderingInfo renderingInfo; + renderingInfo.setRenderArea({{0, 0}, ctx.extent}) + .setLayerCount(1) + .setColorAttachments(colorAttachment); + cmd.beginRendering(renderingInfo); + + if (hasDraws) { + // 复用 Forward 顶点着色(静态/蒙皮同源变换)+ 恒 1 片元 + std::array stages = { + vk::ShaderStageFlagBits::eVertex, + vk::ShaderStageFlagBits::eFragment}; + std::array shaders = {m_Gpu.forwardVert, + m_Frag.Handle}; + cmd.bindShadersEXT(stages, shaders); + + // Set0:本帧槽位 UBO + 实例/蒙皮 SSBO(Set1 材质、Set2 + // 阴影不采样不绑,合法) + cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + m_Gpu.forwardLayout, 0, + m_Gpu.uboSets[ctx.frame.frameSlot], {}); + + // ---- 动态状态(与 Forward 对齐,无深度测试)---- + cmd.setRasterizerDiscardEnable(VK_FALSE); + cmd.setDepthTestEnable(VK_FALSE); + cmd.setDepthWriteEnable(VK_FALSE); + cmd.setDepthBoundsTestEnable(VK_FALSE); + cmd.setStencilTestEnable(VK_FALSE); + cmd.setDepthBiasEnable(VK_FALSE); + cmd.setPolygonModeEXT(vk::PolygonMode::eFill); + cmd.setCullMode(vk::CullModeFlagBits::eNone); + cmd.setFrontFace(vk::FrontFace::eCounterClockwise); + cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList); + cmd.setPrimitiveRestartEnable(VK_FALSE); + + cmd.setVertexInputEXT(Vertex::GetBindingDescription(), + Vertex::GetAttributeDescriptions()); + + cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1); + vk::SampleMask sampleMask = 0xFFFFFFFF; + cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask); + cmd.setAlphaToCoverageEnableEXT(VK_FALSE); + cmd.setAlphaToOneEnableEXT(VK_FALSE); + + vk::Viewport viewport(0.0f, 0.0f, + static_cast(ctx.extent.width), + static_cast(ctx.extent.height), + 0.0f, 1.0f); + cmd.setViewportWithCount(viewport); + cmd.setScissorWithCount(vk::Rect2D({0, 0}, ctx.extent)); + + // 无混合直写(mask 任何非零 = 选中) + cmd.setColorBlendEnableEXT(0, VK_FALSE); + vk::ColorBlendEquationEXT direct; + direct.setSrcColorBlendFactor(vk::BlendFactor::eOne) + .setDstColorBlendFactor(vk::BlendFactor::eZero) + .setColorBlendOp(vk::BlendOp::eAdd) + .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) + .setDstAlphaBlendFactor(vk::BlendFactor::eZero) + .setAlphaBlendOp(vk::BlendOp::eAdd); + cmd.setColorBlendEquationEXT(0, direct); + cmd.setColorWriteMaskEXT(0, + vk::ColorComponentFlagBits::eR | + vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | + vk::ColorComponentFlagBits::eA); + + // 逐选中实例绘制(一次一实例,选中开销可忽略) + for (const auto& d : m_Frame.selectionDraws) { + d.mesh->Draw(cmd, 1, d.instance, d.lod); + } + } + + cmd.endRendering(); + }) + .Write(m_Mask, RG::ResourceUse::ColorAttachment) + // 无选中帧只 clear 不绘制:显式保活,否则整条描边链被 + // 引用计数剔除(HitProxy 教训) + .SideEffect(); + } + +} // namespace Engine::Passes diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index c85c2bf..161ba21 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -15,6 +15,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -80,15 +85,32 @@ namespace Engine *m_Device, *m_Allocator, sizeof(glm::mat4) * kMaxSkinMatrices, kMaxFramesInFlight, vk::BufferUsageFlagBits::eStorageBuffer); + m_EntityIdBuffer = CreateScope( + *m_Device, *m_Allocator, + sizeof(u32) * kMaxInstances, kMaxFramesInFlight, + vk::BufferUsageFlagBits::eStorageBuffer); // 3. pass 装配体:布局/shader/采样器/per-slot 描述符 m_SceneGpu = CreateScope( *m_Device, *m_Allocator, *m_DescriptorManager, *m_ShaderManager, *m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer, - *m_SkinOffsetBuffer, *m_SkinPaletteBuffer); + *m_SkinOffsetBuffer, *m_SkinPaletteBuffer, *m_EntityIdBuffer); m_FullscreenGpu = CreateScope( *m_Device, *m_DescriptorManager, *m_ShaderManager); + // 3.2 物理调试图元 ring VB:eVertexBuffer 用法,复用 UniformBuffer + // 的 per-slot fence 语义(PrepareFrame 在 WaitFence 后上传) + m_PhysicsLineVB = CreateScope( + *m_Device, *m_Allocator, + u64(Passes::PhysicsDebugPass::kMaxLineVerts) * + sizeof(PhysicsDebugVertex), + kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer); + m_PhysicsTriVB = CreateScope( + *m_Device, *m_Allocator, + u64(Passes::PhysicsDebugPass::kMaxTriVerts) * + sizeof(PhysicsDebugVertex), + kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer); + // 3.5 环境预积分生成端(compute 链)+ 中性兜底四件套。 // 真实环境由 Application 经 AssetManager 加载后 SetEnvironment // (资产路径归资产层,Renderer 不感知具体文件) @@ -211,6 +233,41 @@ namespace Engine m_SceneRenderer->SetDisplayMode(mode); } + const FrameSceneData &Renderer::GetFrameSceneData() const + { + return m_SceneRenderer->Frame(); + } + + // ---- Editor 视口拾取(UE 式 GPU HitProxy)---- + + void Renderer::RequestPick(const glm::vec2& ndc) + { + m_SceneRenderer->SetPickRequest(ndc); + } + + void Renderer::SetSelectedEntity(u32 entityBits) + { + m_SceneRenderer->SetSelectedEntity(entityBits); + } + + std::optional Renderer::TakePickResult() + { + // 无待取结果直接返回(绝大多数帧的路径,零开销) + if (!m_HitProxyPass || !m_HitProxyPass->HasPendingResult()) { + return std::nullopt; + } + + // 拷贝记录在上一帧的命令缓冲里:waitIdle 保证提交完成、 + // 暂存可见(点击低频路径,单帧 hitch 不可感知) + m_Device->GetDevice().waitIdle(); + + u32 bits = 0; + if (!m_HitProxyPass->TakeResult(bits)) { + return std::nullopt; + } + return bits; + } + void Renderer::SetEnvironment(const Cubemap* env) { if (!env) env = m_Environment.get(); @@ -305,6 +362,18 @@ namespace Engine "Velocity", RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat}); + // HitProxy 拾取 id 目标:R32Uint,片元写实体位模式(UE 式 GPU + // 拾取;请求帧才绘制,见 HitProxyPass) + m_HitProxyId = m_RenderGraph->CreateTexture( + "HitProxyId", + RG::TextureDesc{vk::Format::eR32Uint}); + + // 选中描边 mask:R8Unorm,选中实体像素 = 1(SelectionMask pass + // 写,Outline pass 读;UE CustomStencil 语义) + m_SelectionMask = m_RenderGraph->CreateTexture( + "SelectionMask", + RG::TextureDesc{vk::Format::eR8Unorm}); + // TAA 累积输出:既是 resolve 结果也是下一帧的历史(自引用) m_TAAColor = m_RenderGraph->CreateTexture( "TAAColor", @@ -335,6 +404,26 @@ namespace Engine m_HDRColor, m_Environment.get())); m_Passes.push_back(CreateScope( *m_SceneGpu, frame, m_HDRColor, m_Velocity, m_Depth, m_ShadowMaps)); + // 物理调试叠加:写 HDRColor(WAW 链排 Forward 之后,RAW 链在 + // TAA 之前)——调试图元进 TAA/Bloom/Tonemap 主链路 + m_Passes.push_back(CreateScope( + *m_Device, *m_Allocator, *m_ShaderManager, *m_DescriptorManager, + frame, m_HDRColor, m_Depth, *m_PhysicsLineVB, *m_PhysicsTriVB)); + m_PhysicsDebugPass = + static_cast(m_Passes.back().get()); + // UE 式 GPU 拾取:请求帧把 id 画进 R32Uint + 拷贝请求像素进 + // host 暂存(深度 WAW 链尾,排在 Forward/ForwardTrans/PhysicsDebug + // 之后;App 层经 RequestPick/TakePickResult 消费) + m_Passes.push_back(CreateScope( + *m_Device, *m_Allocator, *m_ShaderManager, *m_DescriptorManager, + *m_SceneGpu, frame, m_HitProxyId, m_Depth)); + m_HitProxyPass = + static_cast(m_Passes.back().get()); + // 选中描边链:mask(只画选中实体,无深度测试=完整投影轮廓, + // UE 透墙可见语义)→ Outline(叠加在 Tonemap 后的 PresentTarget + // 上,恒定 LDR 描边色) + m_Passes.push_back(CreateScope( + *m_Device, *m_ShaderManager, *m_SceneGpu, frame, m_SelectionMask)); m_Passes.push_back(CreateScope( *m_FullscreenGpu, frame, /*historyAlpha*/ 0.9f, m_HDRColor, m_Velocity, m_TAAColor)); @@ -347,6 +436,12 @@ namespace Engine *m_FullscreenGpu, *m_DescriptorManager, *m_PostParams, frame, m_TAAColor, m_BloomA, m_TemporalDiff, m_PresentTarget, m_HDRColor, m_Velocity, m_ShadowMaps[0], m_ShadowMaps[1], m_ShadowMaps[2])); + // Outline 必须在 Tonemap 之后:PresentTarget 上的 alpha 叠加若 + // 排在 Tonemap 前,会被 Tonemap 整帧覆盖(WAW 序 = 注册序)。 + // Blit/ImGui 之前——它们把 PresentTarget 送去 backbuffer/UI。 + m_Passes.push_back(CreateScope( + *m_Device, *m_FullscreenGpu, *m_ShaderManager, frame, + m_SelectionMask, m_PresentTarget)); // Direct 模式适配器:PresentTarget → backbuffer(Viewport 模式空转)。 // 必须注册在 ImGui 之前:两者都写 backbuffer,WAW 边按声明序 // 连接(前 writer → 后 writer),反序会让 blit 盖掉 UI @@ -419,6 +514,9 @@ namespace Engine for (auto& pass : m_Passes) { pass->OnTargetsRebuilt(*m_RenderGraph); } + // active pass 数可能随重编译变化 → 旧写入数对不上新读取数, + // 每槽位重新走一次 primed 跳读 + m_TimesPrimed[0] = m_TimesPrimed[1] = false; RefreshSceneTextures(); } @@ -457,18 +555,23 @@ namespace Engine return; } - const u32 passCount = m_QueriesPerFrame / 2; - std::vector ticks(m_QueriesPerFrame, 0); + // 只读上一轮该槽位实际写入的查询数:被剔除 pass 的时间戳永远 + // 不会写入,按满额 eWait 阻塞读会等一个永不可用的查询(死锁) + const u32 written = m_RenderGraph->GetWrittenQueries(frameSlot); + if (written == 0) return; - // fence 已等待本槽位上一轮完成,结果必然可用(阻塞式 + 64bit) + std::vector ticks(written, 0); + + // fence 已等待本槽位上一轮完成,written 范围内结果必然可用 const vk::Result r = m_Device->GetDevice().getQueryPoolResults( m_QueryPool, - frameSlot * m_QueriesPerFrame, m_QueriesPerFrame, - sizeof(u64) * m_QueriesPerFrame, + frameSlot * m_QueriesPerFrame, written, + sizeof(u64) * written, ticks.data(), sizeof(u64), vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait); if (r != vk::Result::eSuccess) return; + const u32 passCount = written / 2; for (u32 i = 0; i < passCount; ++i) { const u64 start = ticks[i * 2]; const u64 end = ticks[i * 2 + 1]; @@ -511,6 +614,12 @@ namespace Engine m_SkinOffsetBuffer->Update(frameSlot, frame.skinOffsets.data(), frame.skinOffsets.size() * sizeof(u32)); } + // 实体 id 与实例矩阵同序(HitProxy id pass 按 gl_InstanceIndex 取) + if (!frame.instanceEntityIds.empty()) { + m_EntityIdBuffer->Update(frameSlot, frame.instanceEntityIds.data(), + frame.instanceEntityIds.size() * + sizeof(u32)); + } if (!frame.skinPalettes.empty()) { m_SkinPaletteBuffer->Update(frameSlot, frame.skinPalettes.data(), frame.skinPalettes.size() * @@ -519,6 +628,11 @@ namespace Engine // MDI 命令/count 同窗口落槽位 ring(fence 已等,写-读竞争关闭) m_SceneGpu->UploadIndirect(frameSlot, frame); + // 物理调试图元同窗口上传(fence 已等);空帧计数归零,pass 零开销 + if (m_PhysicsDebug && m_PhysicsDebugPass) { + m_PhysicsDebugPass->PrepareFrame(frameSlot, *m_PhysicsDebug); + } + // 3. acquire swapchain image u32 imageIndex = 0; try { diff --git a/Engine/src/Renderer/SceneRenderer.cpp b/Engine/src/Renderer/SceneRenderer.cpp index 90880c0..c45d0ec 100644 --- a/Engine/src/Renderer/SceneRenderer.cpp +++ b/Engine/src/Renderer/SceneRenderer.cpp @@ -263,10 +263,17 @@ namespace Engine auto* cameraComp = scene.GetPrimaryCamera(&camTransform); if (!cameraComp || !camTransform) { // 无相机:本帧跳过(正常场景必有相机,安全网) - return false; + return false; // 拾取请求未消费,留待下一帧重试 } const Camera& camera = cameraComp->camera; + // 拾取请求落帧(一次性消费:本帧 HitProxy 渲染 + 像素拷贝)。 + // 放在相机校验之后:跳帧时请求保留,不丢点击 + m_Frame.pickRequest = m_PendingPick; + m_PendingPick.active = false; + // 选中实体落帧(持续状态,不做一次性清除) + m_Frame.selectedEntityBits = m_PendingSelected; + const f32 aspect = static_cast(targetExtent.width) / static_cast(targetExtent.height); ubo.view = camera.GetView(*camTransform); @@ -481,6 +488,7 @@ namespace Engine m_Frame.instanceModels.clear(); m_Frame.prevInstanceModels.clear(); m_Frame.skinOffsets.clear(); + m_Frame.instanceEntityIds.clear(); m_MainScratch.Clear(); for (auto& s : m_CascScratch) s.Clear(); m_TranslucentScratch.Clear(); // 透明队列每帧重排批次,必须整帧重建 @@ -586,7 +594,8 @@ namespace Engine // 实例在扁平化后连续——firstInstance 语义的前提 const u32 bi = batchIdx - 1; s.result.batches[bi].count++; - s.batchMats[bi].push_back({t.World(), prevWorld, skinBase}); + s.batchMats[bi].push_back( + {t.World(), prevWorld, skinBase, entKey}); s.batchDistSum[bi] += dist; // 透明排序用 s.result.instanceCount++; ++totalInstances; @@ -627,6 +636,8 @@ namespace Engine m_Frame.prevInstanceModels.push_back(m.prev); // 与实例序平行对齐(shader 按 gl_InstanceIndex 取) m_Frame.skinOffsets.push_back(m.skinBase); + // 实体位模式同序对齐(HitProxy id pass 消费) + m_Frame.instanceEntityIds.push_back(m.entityBits); } } dst.instanceCount = s.result.instanceCount; @@ -680,6 +691,27 @@ namespace Engine } appendView(m_Frame.translucentView, m_TranslucentScratch); + // ---- 5.5 选中描边:从扁平实例表筛出选中实体的单实例绘制 ---- + //(不透明 + 透明队列都查——选中玻璃也要有描边)。 + // 无选中时保持空表,SelectionMask pass 只 clear 不绘制 + m_Frame.selectionDraws.clear(); + if (m_Frame.selectedEntityBits != 0) { + auto collect = [&](const CullResult& view) { + for (const auto& b : view.batches) { + for (u32 i = 0; i < b.count; ++i) { + const u32 idx = b.first + i; + if (m_Frame.instanceEntityIds[idx] == + m_Frame.selectedEntityBits) { + m_Frame.selectionDraws.push_back( + SelectionDraw{b.mesh, idx, b.lod}); + } + } + } + }; + collect(m_Frame.mainView); + collect(m_Frame.translucentView); + } + // 帧末滚动时域状态:cur → prev(每帧重建,销毁的实体自动清出) m_PrevWorlds.swap(m_CurWorlds); m_CurWorlds.clear(); diff --git a/shaders/hitproxy.frag b/shaders/hitproxy.frag new file mode 100644 index 0000000..d8aa4a2 --- /dev/null +++ b/shaders/hitproxy.frag @@ -0,0 +1,13 @@ +#version 450 + +// HitProxy 片元:把顶点传来的实体 id 原样写进 R32Uint 目标。 +// flat 限定 = 无插值,像素值即命中图元的实体位模式; +// 清屏色 0xFFFFFFFF(kPickMiss)= 天空/空隙未命中。 + +layout(location = 0) flat in uint inEntityId; + +layout(location = 0) out uvec4 outColor; + +void main() { + outColor = uvec4(inEntityId, 0u, 0u, 0u); +} diff --git a/shaders/hitproxy.vert b/shaders/hitproxy.vert new file mode 100644 index 0000000..7f1d59d --- /dev/null +++ b/shaders/hitproxy.vert @@ -0,0 +1,57 @@ +#version 450 + +// HitProxy 顶点:UE 式 GPU 拾取——Forward 同结构画一遍(无光照无纹理), +// 输出 per-instance 实体 id。位置变换与 simple_shader.vert 完全同源 +//(模型矩阵 + 蒙皮),保证拾取像素与可见画面像素一致。 +// +// Set0 与 simple_shader 共用同一 uboSetLayout(SceneGpu 契约); +// 本 shader 只声明用到的绑定,描述符集合里未用的绑定合法。 + +layout(location = 0) in vec3 inPosition; +layout(location = 4) in uvec4 inBoneIds; // 骨骼槽(palette 下标) +layout(location = 5) in vec4 inWeights; // 无骨骼顶点 = (1,0,0,0) + +layout(location = 0) flat out uint outEntityId; // 整数 varying 必须 flat + +layout(std430, set = 0, binding = 1) readonly buffer ObjectBuffer { + mat4 models[]; +} objects; + +layout(std430, set = 0, binding = 3) readonly buffer SkinOffsetBuffer { + uint skinBases[]; +} skinOffsets; + +layout(std430, set = 0, binding = 4) readonly buffer SkinPaletteBuffer { + mat4 bones[]; +} skinPalettes; + +// per-instance 实体 id(与 models 平行对齐,SceneRenderer 收集时写入) +layout(std430, set = 0, binding = 5) readonly buffer EntityIdBuffer { + uint ids[]; +} entityIds; + +// 只用 view/proj:std140 前两个 mat4 与完整 UBO 的布局前缀一致 +layout(set = 0, binding = 0) uniform Ubo { + mat4 view; + mat4 proj; +} ubo; + +// 4 权重混合的蒙皮矩阵(与 simple_shader.vert 同逻辑) +mat4 SkinMatrix() { + uint base = skinOffsets.skinBases[gl_InstanceIndex]; + return skinPalettes.bones[base + inBoneIds.x] * inWeights.x + + skinPalettes.bones[base + inBoneIds.y] * inWeights.y + + skinPalettes.bones[base + inBoneIds.z] * inWeights.z + + skinPalettes.bones[base + inBoneIds.w] * inWeights.w; +} + +void main() { + mat4 model = objects.models[gl_InstanceIndex]; + mat4 skin = SkinMatrix(); + + vec3 skinnedPos = (skin * vec4(inPosition, 1.0)).xyz; + vec4 worldPos = model * vec4(skinnedPos, 1.0); + gl_Position = ubo.proj * ubo.view * worldPos; + + outEntityId = entityIds.ids[gl_InstanceIndex]; +} diff --git a/shaders/outline.frag b/shaders/outline.frag new file mode 100644 index 0000000..cd93e54 --- /dev/null +++ b/shaders/outline.frag @@ -0,0 +1,61 @@ +#version 450 + +// UE 编辑器选中描边:对 SelectionMask 做 16 方向圆周边缘检测, +// alpha 叠加在 Tonemap 之后的 PresentTarget 上(LDR 恒定 UI 色, +// 不进 bloom/曝光/TAA——与 UE 编辑器选中轮廓一致)。 +// +// 规则: +// 外描边 自身 mask=0 且邻域有 1 → 描边(物体外圈一圈) +// 贴边线 自身 mask=1 且邻域采样越出视口(视为 0)→ 描边——物体 +// 延伸出视口时 mask 在边缘截断,描边自动贴着渲染视口边缘 +// 完全出视野 mask 全 0 → 无描边 +// 描边不做深度遮挡判定(透墙可见)——UE 编辑器同行为,便于找到 +// 被挡住的选中物体。 + +layout(location = 0) in vec2 fragUV; +layout(location = 0) out vec4 outColor; + +layout(set = 0, binding = 0) uniform sampler2D maskImage; + +// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878)。 +// 注意:必须用 4×float 平铺——"float + vec3" 会被 GLSL 对齐成 28B +// (vec3 落 16 偏移),超出 FullscreenPush 的 [0,16] 声明范围 +// (VUID-10065)。语义与 FullscreenPush 对齐:exposure 槽位=描边宽度, +// param0-2=描边色。 +layout(push_constant) uniform PushConstants { + float widthPx; // 描边宽度(物理像素) + float colorR; // 描边色 R + float colorG; // 描边色 G + float colorB; // 描边色 B +} pc; + +void main() { + const float m = texture(maskImage, fragUV).r; + + const vec2 texel = vec2(pc.widthPx) / vec2(textureSize(maskImage, 0)); + float ringMax = 0.0; + bool anyOutside = false; + const int N = 16; + for (int i = 0; i < N; ++i) { + const float a = float(i) * (6.2831853 / float(N)); + const vec2 suv = fragUV + vec2(cos(a), sin(a)) * texel; + // 越界 = 视口截断(视为无 mask)→ 贴边像素产生描边 + const bool outside = suv.x < 0.0 || suv.x > 1.0 || + suv.y < 0.0 || suv.y > 1.0; + anyOutside = anyOutside || outside; + if (!outside) { + ringMax = max(ringMax, texture(maskImage, suv).r); + } + } + + // 外描边:自身 mask=0 且邻域有 1(真轮廓;mask 无洞,不会描出 + // 前方遮挡物的轮廓) + // 贴边内描边:自身 mask=1 且邻域越出视口(仅视口截断,物体延伸 + // 出视野时描边贴着渲染视口边缘) + const bool outer = m < 0.5 && ringMax > 0.5; + const bool edgeCut = m > 0.5 && anyOutside; + const float edge = (outer || edgeCut) ? 1.0 : 0.0; + + // LDR 恒定色直出,alpha 混合叠加在 Tonemap 输出上 + outColor = vec4(pc.colorR, pc.colorG, pc.colorB, edge); +} diff --git a/shaders/sel_mask.frag b/shaders/sel_mask.frag new file mode 100644 index 0000000..eaae127 --- /dev/null +++ b/shaders/sel_mask.frag @@ -0,0 +1,17 @@ +#version 450 + +// 选中描边 mask 片元:恒输出 1(选中标记)。 +// +// 与 forwardVert 配对(复用 SceneGpu 的 Set0:UBO + 蒙皮 SSBO,静态/ +// 蒙皮同一 shader 内分支)。写入 R8Unorm SelectionMask;任何非零 = 该 +// 像素属于选中实体(黑色物体也可靠——颜色无关,恒 1)。 + +layout(location = 0) out vec4 outColor; + +// 仅接口对齐(simple_shader.vert 同槽位输出 float,未消费会刷 +// validation 警告),mask 不需要视图深度 +layout(location = 3) in float fragViewDepth; + +void main() { + outColor = vec4(1.0); +}