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:
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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,8 +214,8 @@ void StatsPanel::Draw(const EditorContext& ctx)
|
||||
}
|
||||
}
|
||||
ImGui::Text("fps: %.1f frame: %llu",
|
||||
1.0f / app.GetDeltaTime(),
|
||||
static_cast<unsigned long long>(app.GetFrameCount()));
|
||||
1.0f / Time::Current().UnscaledDt(),
|
||||
static_cast<unsigned long long>(app.GetFrameCount()));
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
@@ -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 ® = 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 ® = 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user