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
@@ -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,影子不会被剔除裁掉)