feat: HitProxy GPU 拾取 + UE 选中描边 + 编辑器拾取交互

- 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 本身无状态变更)
This commit is contained in:
2026-10-02 20:09:05 +08:00
parent 1c4b6f37ff
commit 1449bea86d
29 changed files with 1360 additions and 43 deletions
+6 -1
View File
@@ -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();
+14 -1
View File
@@ -16,6 +16,8 @@
#include "EditorContext.h"
#include "ViewportPanel.h" // ViewportInputState(成员按值持有)
#include <Engine/Core/Types.h> // u32(SelectEntity 位模式转换)
#include <entt/entt.hpp>
#include <memory>
@@ -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<Engine::u32>(entt::to_integral(e)));
}
}
entt::entity GetSelected() const { return m_Selected; }
private:
-3
View File
@@ -40,9 +40,6 @@ struct EditorContext {
std::function<int()> getEnvIndex; // 当前环境表下标
std::function<void(int)> setEnvIndex;
// 冻结动画时钟(沙盒 spinners 用;保留原 Scene Debug 行为)
bool *pauseAnim = nullptr;
};
#endif //HELLOVULKAN_EDITORCONTEXT_H
+2 -1
View File
@@ -70,6 +70,7 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
if (reg.all_of<Engine::ECS::AnimationComponent>(e)) badge += "[A]";
if (reg.all_of<Engine::ECS::CameraComponent>(e)) badge += "[C]";
if (reg.all_of<Engine::ECS::LightComponent>(e)) badge += "[L]";
if (reg.all_of<Engine::ECS::RigidBodyComponent>(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");
}
}
});
+49
View File
@@ -76,6 +76,11 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
reg.emplace<Engine::ECS::NameComponent>(selected);
}
if (!reg.all_of<Engine::ECS::RigidBodyComponent>(selected) &&
ImGui::MenuItem("RigidBody"))
{
reg.emplace<Engine::ECS::RigidBodyComponent>(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<Engine::ECS::RigidBodyComponent>(selected))
{
if (ImGui::CollapsingHeader("RigidBody", ImGuiTreeNodeFlags_DefaultOpen))
{
int type = static_cast<int>(rb->type);
if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0"))
{
rb->type = static_cast<Engine::ECS::RigidBodyComponent::BodyType>(type);
}
int collider = static_cast<int>(rb->collider);
if (ImGui::Combo("collider", &collider, "Box\0Sphere\0Mesh\0"))
{
rb->collider = static_cast<Engine::ECS::RigidBodyComponent::ColliderType>(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<Engine::ECS::AnimationComponent>(selected))
{
+53 -4
View File
@@ -4,6 +4,8 @@
#include "StatsPanel.h"
#include "Engine/ECS/Components.h"
#include "Engine/Physics/PhysicsWorld.h"
#include "Engine/Core/Time.h"
#include <imgui.h>
#include <glm/gtc/type_ptr.hpp>
@@ -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,7 +214,7 @@ void StatsPanel::Draw(const EditorContext& ctx)
}
}
ImGui::Text("fps: %.1f frame: %llu",
1.0f / app.GetDeltaTime(),
1.0f / Time::Current().UnscaledDt(),
static_cast<unsigned long long>(app.GetFrameCount()));
}
+84 -4
View File
@@ -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 <imgui.h>
#include <vulkan/vulkan.h> // vk::Extent2D 经 Renderer 头已带入,显式无妨
#include <Engine/ECS/Components.h> // MeshRenderer / Name / RigidBody(日志对照)
#include <Engine/Renderer/Renderer.h> // RequestPick / TakePickResult 门面
#include <Engine/Renderer/SceneTypes.h> // kPickMiss
#include <Engine/Core/Log.h> // 命中打印
#include <glm/glm.hpp>
using namespace Engine; // u32 / f32(Engine/Core/Types.h)
void ViewportPanel::Draw(const EditorContext &ctx, ViewportInputState &out) {
std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
ViewportInputState &out) const {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
std::optional<entt::entity> 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 &reg = ctx.scene->GetRegistry();
const entt::entity hit = static_cast<entt::entity>(*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 &reg = ctx.scene->GetRegistry();
const auto &name = reg.get<ECS::NameComponent>(e).name;
if (const auto *rb = reg.try_get<ECS::RigidBodyComponent>(e)) {
const auto &tr = reg.get<ECS::TransformComponent>(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);
}
}
+18 -1
View File
@@ -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 <imgui.h> // ImVec2(RequestPickAt 签名)
#include <entt/entt.hpp>
#include <optional>
// 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<entt::entity> 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
@@ -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 = 空闲镜像列表
+2 -2
View File
@@ -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:
@@ -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 <Engine/Core/Types.h>
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/Vulkan/Buffer.h>
#include <vulkan/vulkan.hpp>
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<Buffer> m_Staging;
bool m_ResultValid = false;
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_HITPROXYPASS_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 <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <vulkan/vulkan.hpp>
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<vk::DescriptorSet> m_Sets; // per-frame slot
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_OUTLINEPASS_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)
@@ -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 <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <vulkan/vulkan.hpp>
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
+42 -1
View File
@@ -26,6 +26,7 @@
#include <Engine/RenderGraph/RenderGraph.h>
#include <vulkan/vulkan.hpp>
#include <array>
#include <optional>
#include <vector>
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<u32> 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<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity)
Scope<UniformBuffer> m_SkinOffsetBuffer; // 实例→palette 基址 ring(SSBO,与实例序平行)
Scope<UniformBuffer> m_SkinPaletteBuffer; // 骨骼蒙皮矩阵 palette ring(SSBO,帧首 bump)
Scope<UniformBuffer> m_EntityIdBuffer; // 实例→实体位模式 ring(SSBO,HitProxy 拾取)
Scope<Passes::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配
Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配
// 物理调试图元 ring VB(eVertexBuffer;per-slot fence 语义复用
// UniformBuffer——PhysicsDebugPass::PrepareFrame 上传,pass 内绑定)
Scope<UniformBuffer> m_PhysicsLineVB;
Scope<UniformBuffer> m_PhysicsTriVB;
// 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造
Scope<IblGenerator> m_Ibl;
// 中性兜底环境(IBL 四件套):Application 未加载/加载失败时保底;
@@ -189,6 +223,8 @@ namespace Engine {
std::array<RG::TextureHandle, kMaxCascades> 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
@@ -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 语义 =
@@ -63,6 +63,14 @@ namespace Engine {
std::vector<IndirectGroup> 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<glm::mat4> skinPalettes;
// per-instance 实体 id(与 instanceModels 平行对齐;
// HitProxy id pass 按 gl_InstanceIndex 取,片元写入 R32Uint 目标)
std::vector<u32> instanceEntityIds;
// 本帧拾取请求(UI 段点击,SceneRenderer 落帧后自动清除;
// HitProxy/HitProxyCopy 两 pass 据此决定是否绘制/拷贝)
PickRequest pickRequest{};
// 选中描边(UE 编辑器语义):选中实体的位模式(0 = 无选中)。
// BuildFrame 据此从实例表筛出 selectionDraws,SelectionMask pass
// 只画选中实体 → Outline pass 在 mask 边缘叠加描边色
u32 selectedEntityBits = 0;
std::vector<SelectionDraw> selectionDraws;
CullResult mainView; // 相机视锥 → Forward pass
// 每级联独立剔除结果(Shadow pass 按 region 各画各的——
// 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉)
+7
View File
@@ -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);
+12 -13
View File
@@ -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::mat3>(
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 翻转
+246
View File
@@ -0,0 +1,246 @@
//
// HitProxyPass.cpp:pass 装配(shader object + 暂存 buffer)与录制。
//
// 拾取管线与 Forward 同构(Set0 描述符、顶点输入、MDI batch 结构全部
// 复用),差异只有:R32Uint 目标、无光照 id 片元、深度重清自写。
//
#include <Engine/Renderer/Passes/HitProxyPass.h>
#include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Renderer/Vertex.h>
#include <Engine/Renderer/Mesh.h> // batch.mesh->Draw(完整类型)
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/DescriptorManager.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <Engine/Core/Log.h>
#include <glm/glm.hpp>
#include <tracy/Tracy.hpp>
#include <algorithm>
#include <array>
#include <cstring>
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<vk::DescriptorSetLayout>{gpu.uboSetLayout}, 0);
const std::string shaderDir = "shaders/";
auto vert = shaders.CreateFromFile(
shaderDir + "hitproxy.vert.spv",
vk::ShaderStageFlagBits::eVertex,
std::vector<vk::DescriptorSetLayout>{gpu.uboSetLayout}, 0);
auto frag = shaders.CreateFromFile(
shaderDir + "hitproxy.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{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<Buffer>(
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<u32, 4>{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<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment};
const std::array<vk::ShaderEXT, 2> 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<f32>(ctx.extent.width),
static_cast<f32>(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<i32>((m_Frame.pickRequest.ndc.x * 0.5f + 0.5f) *
static_cast<f32>(ext.width)),
0, static_cast<i32>(ext.width) - 1);
const i32 py = std::clamp(
static_cast<i32>((m_Frame.pickRequest.ndc.y * 0.5f + 0.5f) *
static_cast<f32>(ext.height)),
0, static_cast<i32>(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
+142
View File
@@ -0,0 +1,142 @@
//
// OutlinePass.cpp:pass 装配与录制(见头文件注释)。
//
// 录制段复制 FullscreenGpu::RecordFullscreen 的动态状态骨架,仅两处
// 不同:颜色混合改为 alpha 叠加(其余像素 alpha=0 不改画面)、
// PresentTarget 附件 loadOp=eLoad(保留 Tonemap 输出)。
//
#include <Engine/Renderer/Passes/OutlinePass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Core/FrameContext.h>
#include <glm/glm.hpp>
#include <array>
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<vk::DescriptorSetLayout>{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<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment};
std::array<vk::ShaderEXT, 2> 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<float>(ctx.extent.width),
static_cast<float>(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
+8 -1
View File
@@ -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<DescriptorBinding> 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);
}
@@ -0,0 +1,138 @@
//
// SelectionMaskPass.cpp:pass 装配与录制(见头文件注释)。
//
#include <Engine/Renderer/Passes/SelectionMaskPass.h>
#include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Renderer/Vertex.h>
#include <Engine/Renderer/Mesh.h>
#include <array>
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<vk::DescriptorSetLayout>{
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<float, 4>{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<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment};
std::array<vk::ShaderEXT, 2> 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<float>(ctx.extent.width),
static_cast<float>(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
+120 -6
View File
@@ -15,6 +15,11 @@
#include <Engine/Renderer/Passes/SkyboxPass.h>
#include <Engine/Renderer/Passes/ShadowPass.h>
#include <Engine/Renderer/Passes/ForwardPass.h>
#include <Engine/Renderer/Passes/PhysicsDebugPass.h>
#include <Engine/Renderer/Passes/HitProxyPass.h>
#include <Engine/Renderer/Passes/SelectionMaskPass.h>
#include <Engine/Renderer/Passes/OutlinePass.h>
#include <Engine/Physics/PhysicsDebugTypes.h>
#include <Engine/Renderer/Cubemap.h>
#include <Engine/Renderer/IblGenerator.h>
#include <Engine/Renderer/TracyGpuProfiler.h>
@@ -80,15 +85,32 @@ namespace Engine
*m_Device, *m_Allocator,
sizeof(glm::mat4) * kMaxSkinMatrices, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
m_EntityIdBuffer = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator,
sizeof(u32) * kMaxInstances, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
// 3. pass 装配体:布局/shader/采样器/per-slot 描述符
m_SceneGpu = CreateScope<Passes::SceneGpu>(
*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<Passes::FullscreenGpu>(
*m_Device, *m_DescriptorManager, *m_ShaderManager);
// 3.2 物理调试图元 ring VB:eVertexBuffer 用法,复用 UniformBuffer
// 的 per-slot fence 语义(PrepareFrame 在 WaitFence 后上传)
m_PhysicsLineVB = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator,
u64(Passes::PhysicsDebugPass::kMaxLineVerts) *
sizeof(PhysicsDebugVertex),
kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer);
m_PhysicsTriVB = CreateScope<UniformBuffer>(
*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<u32> 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<Passes::ForwardPass>(
*m_SceneGpu, frame, m_HDRColor, m_Velocity, m_Depth, m_ShadowMaps));
// 物理调试叠加:写 HDRColor(WAW 链排 Forward 之后,RAW 链在
// TAA 之前)——调试图元进 TAA/Bloom/Tonemap 主链路
m_Passes.push_back(CreateScope<Passes::PhysicsDebugPass>(
*m_Device, *m_Allocator, *m_ShaderManager, *m_DescriptorManager,
frame, m_HDRColor, m_Depth, *m_PhysicsLineVB, *m_PhysicsTriVB));
m_PhysicsDebugPass =
static_cast<Passes::PhysicsDebugPass*>(m_Passes.back().get());
// UE 式 GPU 拾取:请求帧把 id 画进 R32Uint + 拷贝请求像素进
// host 暂存(深度 WAW 链尾,排在 Forward/ForwardTrans/PhysicsDebug
// 之后;App 层经 RequestPick/TakePickResult 消费)
m_Passes.push_back(CreateScope<Passes::HitProxyPass>(
*m_Device, *m_Allocator, *m_ShaderManager, *m_DescriptorManager,
*m_SceneGpu, frame, m_HitProxyId, m_Depth));
m_HitProxyPass =
static_cast<Passes::HitProxyPass*>(m_Passes.back().get());
// 选中描边链:mask(只画选中实体,无深度测试=完整投影轮廓,
// UE 透墙可见语义)→ Outline(叠加在 Tonemap 后的 PresentTarget
// 上,恒定 LDR 描边色)
m_Passes.push_back(CreateScope<Passes::SelectionMaskPass>(
*m_Device, *m_ShaderManager, *m_SceneGpu, frame, m_SelectionMask));
m_Passes.push_back(CreateScope<Passes::TAAPass>(
*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<Passes::OutlinePass>(
*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<u64> ticks(m_QueriesPerFrame, 0);
// 只读上一轮该槽位实际写入的查询数:被剔除 pass 的时间戳永远
// 不会写入,按满额 eWait 阻塞读会等一个永不可用的查询(死锁)
const u32 written = m_RenderGraph->GetWrittenQueries(frameSlot);
if (written == 0) return;
// fence 已等待本槽位上一轮完成,结果必然可用(阻塞式 + 64bit)
std::vector<u64> 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 {
+34 -2
View File
@@ -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<f32>(targetExtent.width) /
static_cast<f32>(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();
+13
View File
@@ -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);
}
+57
View File
@@ -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];
}
+61
View File
@@ -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);
}
+17
View File
@@ -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);
}