Compare commits
3
Commits
bfbb18f1a4
...
47dd879977
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47dd879977 | ||
|
|
1449bea86d | ||
|
|
1c4b6f37ff |
@@ -1,4 +1,8 @@
|
||||
.idea/
|
||||
.trae/
|
||||
|
||||
# 运行日志(工作目录生成)
|
||||
HelloVulkan.log
|
||||
|
||||
cmake-build-*/
|
||||
out/
|
||||
|
||||
@@ -26,3 +26,6 @@
|
||||
[submodule "Lib/meshoptimizer"]
|
||||
path = Lib/meshoptimizer
|
||||
url = https://github.com/zeux/meshoptimizer.git
|
||||
[submodule "Lib/JoltPhysics"]
|
||||
path = Lib/JoltPhysics
|
||||
url = https://github.com/jrouwe/JoltPhysics.git
|
||||
|
||||
@@ -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
|
||||
|
||||
+55
-38
@@ -14,6 +14,7 @@
|
||||
//
|
||||
|
||||
#include <Engine/Core/Application.h>
|
||||
#include <Engine/Core/Time.h>
|
||||
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Core/Stats.h>
|
||||
@@ -67,15 +68,22 @@ namespace
|
||||
// Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
|
||||
InitEditor();
|
||||
|
||||
SetEnv();
|
||||
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级),
|
||||
// 其余环境在 Stats 面板里按需 lazy 加载后切换
|
||||
const auto env = GetAssets().LoadEnvironment(kEnvPaths[0]);
|
||||
if (env.Valid()) {
|
||||
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env));
|
||||
}
|
||||
|
||||
const auto suntemple = GetAssets().LoadModel("assets/models/demo/asset.fbx");
|
||||
/*
|
||||
const auto suntemple = GetAssets().LoadModel(kTemplePath);
|
||||
auto entityhandle = scene.CreateEntity("SunTemple");
|
||||
entityhandle.AddComponent<MeshRendererComponent>(MeshRendererComponent{suntemple});
|
||||
*/
|
||||
|
||||
scene.CreateEntity();
|
||||
scene.CreateEntity();
|
||||
scene.CreateEntity();
|
||||
scene.CreateEntity("test entity");
|
||||
scene.CreateEntity("test entity");
|
||||
scene.CreateEntity("test entity");
|
||||
|
||||
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
|
||||
if (gun.Valid()) {
|
||||
@@ -84,10 +92,23 @@ namespace
|
||||
m_Gun.AddComponent<AnimationComponent>();
|
||||
auto& gt = m_Gun.GetComponent<TransformComponent>();
|
||||
gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
|
||||
// 物理验证:Dynamic 刚体(默认 Box halfExtents 0.5),
|
||||
// 出生位置悬空 → 应下落至地面静止(Transform 由物理写回)
|
||||
m_Gun.AddComponent<RigidBodyComponent>();
|
||||
} else {
|
||||
ENGINE_WARN("m1911 model not loaded, animation test skipped");
|
||||
}
|
||||
|
||||
// 地面:程序化纯色平面(半边长 15,中心 y=-0.5)+ 静态盒刚体
|
||||
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
|
||||
m_Ground = scene.CreateEntity("Ground");
|
||||
m_Ground.GetComponent<TransformComponent>()
|
||||
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose
|
||||
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
|
||||
auto& groundRb = m_Ground.AddComponent<RigidBodyComponent>();
|
||||
groundRb.type = RigidBodyComponent::BodyType::Static;
|
||||
groundRb.boxHalfExtents = glm::vec3(15.0f, 0.1f, 15.0f); // 平面顶点烘焙 ±15,不能用默认 0.5
|
||||
|
||||
/*
|
||||
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
|
||||
if (gun.Valid()) {
|
||||
@@ -177,26 +198,24 @@ namespace
|
||||
*/
|
||||
}
|
||||
|
||||
void OnUpdate(f32 deltaTime) override
|
||||
void OnUpdate(const FrameTime& frameTime) override
|
||||
{
|
||||
// 暂停时冻结动画时钟(帧历史验证:暂停后 Temporal Diff
|
||||
// 视图应在 2 帧内整屏变黑;相机移动同样会被 diff 捕捉到)
|
||||
if (!m_PauseAnim)
|
||||
{
|
||||
m_Time += deltaTime;
|
||||
}
|
||||
|
||||
// 相机(WASD/Space/Shift 移动;位姿读写实体 Transform)
|
||||
// 相机(WASD/Space/Shift 移动;位姿读写实体 Transform)。
|
||||
// 编辑器相机走真实轨:暂停(timeScale=0)时仍可飞行观察
|
||||
m_Camera.GetComponent<CameraComponent>()
|
||||
.camera.Update(GetWindow(),
|
||||
m_Camera.GetComponent<TransformComponent>(),
|
||||
deltaTime);
|
||||
frameTime.UnscaledDt());
|
||||
|
||||
// 旋转动画:编辑 Transform 分量后 Compose 重算 world
|
||||
// 旋转动画:编辑 Transform 分量后 Compose 重算 world。
|
||||
// 动画时钟 = Time::Elapsed()(scaled 累计,暂停自动冻结——
|
||||
// 帧历史验证:暂停后 Temporal Diff 视图 2 帧内整屏变黑)
|
||||
for (size_t i = 0; i < m_Spinners.size(); ++i)
|
||||
{
|
||||
auto& t = m_Spinners[i].GetComponent<TransformComponent>();
|
||||
t.SetRotation(glm::vec3(0.0f, m_Time * 60.0f + static_cast<f32>(i) * 40.0f, 0.0f));
|
||||
t.SetRotation(glm::vec3(
|
||||
0.0f, static_cast<f32>(Time::Elapsed()) * 60.0f +
|
||||
static_cast<f32>(i) * 40.0f, 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,15 +253,6 @@ namespace
|
||||
ENGINE_INFO("App: window resized to {}x{}", event.width, event.height);
|
||||
}
|
||||
|
||||
void SetEnv(int index = 0) {
|
||||
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级),
|
||||
// 其余环境在 Stats 面板里按需 lazy 加载后切换
|
||||
const auto env = GetAssets().LoadEnvironment(kEnvPaths[index]);
|
||||
if (env.Valid()) {
|
||||
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// Editor 注入:门面指针 + 反向能力回调(面板 → 沙盒)
|
||||
void InitEditor()
|
||||
@@ -254,7 +264,8 @@ namespace
|
||||
ctx.app = this;
|
||||
ctx.getEnvIndex = [this] { return m_EnvIndex; };
|
||||
ctx.setEnvIndex = [this](int index) { m_EnvIndex = index; };
|
||||
ctx.pauseAnim = &m_PauseAnim;
|
||||
// 继承原沙盒默认:初始暂停(StatsPanel 的 Pause Simulation 开关恢复)
|
||||
Time::SetTimeScale(0.0f);
|
||||
m_Editor->Init(ctx);
|
||||
|
||||
// 输入归属:覆盖 EnableImGui 的全局 WantCapture* 谓词
|
||||
@@ -275,8 +286,6 @@ namespace
|
||||
EntityHandle m_Ground{};
|
||||
EntityHandle m_Gun{}; // 骨骼动画验证实体(M1911)
|
||||
std::vector<EntityHandle> m_Spinners;
|
||||
f32 m_Time = 0.0f;
|
||||
bool m_PauseAnim = true; // 冻结动画时钟(Editor 面板可开关)
|
||||
int m_EnvIndex = kDefaultEnv;
|
||||
|
||||
// ---- 异步模型加载(SunTemple)----
|
||||
@@ -287,17 +296,25 @@ namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SandboxApp app;
|
||||
Logger::Initialize();
|
||||
|
||||
try
|
||||
{
|
||||
app.Run();
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
ENGINE_FATAL("Exception: {}", e.what());
|
||||
return -1;
|
||||
}
|
||||
SandboxApp app;
|
||||
|
||||
try
|
||||
{
|
||||
app.Run();
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
ENGINE_FATAL("Exception: {}", e.what());
|
||||
Logger::Shutdown();
|
||||
return -1;
|
||||
}
|
||||
} // app 析构:成员(Device/Renderer/Window…)全部销毁完毕
|
||||
|
||||
// 日志必须最后关:Device 析构期间 Vulkan validation 回调仍在
|
||||
// 打日志(销毁资源时报 VUID),logger 先亡 = 悬空调用 SIGSEGV
|
||||
Logger::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,23 @@ add_subdirectory(Lib/glfw)
|
||||
add_subdirectory(Lib/glm)
|
||||
add_subdirectory(Lib/spdlog)
|
||||
add_subdirectory(Lib/stb)
|
||||
# meshoptimizer(submodule,LOD 网格简化;demo/gltfpack 默认关闭,install 不需要)
|
||||
set(MESHOPT_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(Lib/meshoptimizer)
|
||||
|
||||
# ---- Jolt Physics(submodule v5.6,刚体/软体/角色/载具模拟)----
|
||||
# 顶层 CMake 在 Build/ 子目录;只编 jolt 静态库,示例/测试目标默认全关
|
||||
set(JPH_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE) # LTO 拖慢开发迭代
|
||||
set(OVERRIDE_CXX_FLAGS OFF CACHE BOOL "" FORCE) # 不动全局优化参数
|
||||
set(FLOATING_POINT_EXCEPTIONS_ENABLED OFF CACHE BOOL "" FORCE) # NaN 进物理不炸,仅报警
|
||||
# JPH_USE_CPU_COMPUTE 默认 ON,需要 DXC 把 Shaders/*.hlsl 编成 .spv(本机无
|
||||
# 工具链);我们不使用 Jolt 的 GPU compute 路径,连同后端 shader 一并关掉
|
||||
set(JPH_USE_CPU_COMPUTE OFF CACHE BOOL "" FORCE)
|
||||
set(JPH_USE_DX12 OFF CACHE BOOL "" FORCE)
|
||||
set(JPH_USE_VK OFF CACHE BOOL "" FORCE)
|
||||
set(JPH_USE_MTL OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(Lib/JoltPhysics/Build)
|
||||
|
||||
# ---- Assimp(vendored submodule,模型导入;只编译需要的 importer) ----
|
||||
set(BUILD_SHARED_LIBS OFF) # 静态链接
|
||||
|
||||
+35
-31
@@ -1,10 +1,8 @@
|
||||
file(GLOB_RECURSE ENGINE_SOURCES
|
||||
src/*.cpp
|
||||
)
|
||||
# ============ 第三方库目标(Engine 的依赖) ============
|
||||
|
||||
# ImGui(docking 分支,vendored submodule)
|
||||
set(IMGUI_DIR ${CMAKE_SOURCE_DIR}/Lib/imgui)
|
||||
set(IMGUI_SOURCES
|
||||
add_library(imgui STATIC
|
||||
${IMGUI_DIR}/imgui.cpp
|
||||
${IMGUI_DIR}/imgui_draw.cpp
|
||||
${IMGUI_DIR}/imgui_tables.cpp
|
||||
@@ -13,44 +11,50 @@ set(IMGUI_SOURCES
|
||||
${IMGUI_DIR}/backends/imgui_impl_glfw.cpp
|
||||
${IMGUI_DIR}/backends/imgui_impl_vulkan.cpp
|
||||
)
|
||||
target_include_directories(imgui PUBLIC
|
||||
${IMGUI_DIR}
|
||||
${IMGUI_DIR}/backends)
|
||||
# 两个 backend 头文件分别依赖 GLFW / Vulkan 头
|
||||
target_link_libraries(imgui PUBLIC glfw Vulkan::Vulkan)
|
||||
|
||||
# 编译成静态库
|
||||
add_library(Engine STATIC ${ENGINE_SOURCES} ${IMGUI_SOURCES})
|
||||
# Tracy(CPU/GPU 帧分析)
|
||||
add_library(tracy STATIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public/TracyClient.cpp)
|
||||
target_include_directories(tracy PUBLIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public)
|
||||
target_compile_definitions(tracy PUBLIC
|
||||
TRACY_ENABLE
|
||||
# 按需剖析:GPU context 宣告(GpuNewContext)与连接绑定——GUI 永远晚于
|
||||
# 程序启动连接,没有这个宏宣告消息被丢弃,GPU 时间线永远空白
|
||||
TRACY_ON_DEMAND)
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(tracy PUBLIC Threads::Threads)
|
||||
|
||||
# ============ Engine ============
|
||||
|
||||
file(GLOB_RECURSE ENGINE_SOURCES CONFIGURE_DEPENDS src/*.cpp)
|
||||
add_library(Engine STATIC ${ENGINE_SOURCES})
|
||||
|
||||
# PhysicsDebugDraw 派生 Jolt::DebugRendererSimple:Jolt 以 -fno-rtti 编译
|
||||
#(有 key function 的基类 typeinfo 符号不存在),该 TU 必须同步关闭 RTTI,
|
||||
# 否则派生类 typeinfo 引用悬空符号(链接期 undefined reference)
|
||||
set_source_files_properties(src/Physics/PhysicsDebugDraw.cpp PROPERTIES
|
||||
COMPILE_OPTIONS "-fno-rtti")
|
||||
|
||||
target_include_directories(Engine PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
# header-only 库:纯 include 路径
|
||||
${CMAKE_SOURCE_DIR}/Lib/entt/src
|
||||
${CMAKE_SOURCE_DIR}/Lib/VulkanMemoryAllocator/include
|
||||
${IMGUI_DIR}
|
||||
${IMGUI_DIR}/backends
|
||||
)
|
||||
${CMAKE_SOURCE_DIR}/Lib/VulkanMemoryAllocator/include)
|
||||
|
||||
# 链接第三方库
|
||||
target_link_libraries(Engine PUBLIC
|
||||
glfw
|
||||
glm::glm
|
||||
Vulkan::Vulkan
|
||||
spdlog::spdlog
|
||||
stb
|
||||
assimp::assimp
|
||||
)
|
||||
meshoptimizer
|
||||
Jolt::Jolt
|
||||
imgui
|
||||
tracy)
|
||||
|
||||
# Tracy(CPU/GPU 帧分析):宏关闭时全部退化空操作,零开销
|
||||
set(TRACY_DIR ${CMAKE_SOURCE_DIR}/Lib/tracy)
|
||||
target_include_directories(Engine PUBLIC ${TRACY_DIR}/public)
|
||||
target_sources(Engine PRIVATE ${TRACY_DIR}/public/TracyClient.cpp)
|
||||
target_compile_definitions(Engine PUBLIC TRACY_ENABLE)
|
||||
# 按需剖析:GPU context 宣告(GpuNewContext)与连接绑定——GUI 永远晚于
|
||||
# 程序启动连接,没有这个宏宣告消息被丢弃,GPU 时间线永远空白
|
||||
target_compile_definitions(Engine PUBLIC TRACY_ON_DEMAND)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(Engine PUBLIC Threads::Threads)
|
||||
|
||||
# meshoptimizer(vendored,LOD 网格简化;submodule 机制故障改为直接 vendor)
|
||||
file(GLOB MESHOPT_SOURCES ${CMAKE_SOURCE_DIR}/Lib/meshoptimizer/src/*.cpp)
|
||||
add_library(meshopt STATIC ${MESHOPT_SOURCES})
|
||||
target_include_directories(meshopt PUBLIC ${CMAKE_SOURCE_DIR}/Lib/meshoptimizer/src)
|
||||
target_link_libraries(Engine PUBLIC meshopt)
|
||||
|
||||
target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
|
||||
target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
// 和事件钩子(OnEvent + 类型化事件)定义程序行为;
|
||||
// runtime、editor 等不同程序形态只是不同的派生类。
|
||||
//
|
||||
// 主循环固定为:PollEvents → OnUpdate(dt) → [NewFrame → OnRenderOverlay →
|
||||
// 主循环固定为:PollEvents → OnUpdate(FrameTime) → [NewFrame → OnRenderOverlay →
|
||||
// BuildDrawData] → 主渲染(Renderer.Render)→ 判退出。
|
||||
// UI 构建与 GPU 提交分离:OnRenderOverlay 只"声明"控件,真正的 draw data
|
||||
// 提交发生在主渲染管线中的 ImGui pass(Forward 之后、Present 之前,
|
||||
// loadOp=Load 叠加)。主渲染是框架行为,派生类不可覆盖也不会丢失;
|
||||
// 框架自身永远不绘制任何 ImGui 控件——UI 内容全部由派生类定义。
|
||||
// dt 由框架统一计时(含首帧与卡顿钳制),派生层无需自己管理。
|
||||
// 时间由框架单源采集:帧首 Time::BeginFrame 落定 FrameTime(含卡顿钳制),
|
||||
// 派生层经 OnUpdate(frame) 或 Time 门面只读,不再自己碰 chrono。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_APPLICATION_H
|
||||
@@ -23,6 +24,7 @@
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Core/Event.h>
|
||||
#include <Engine/Core/Time.h>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
@@ -33,6 +35,7 @@ namespace Engine {
|
||||
class Window;
|
||||
class Renderer;
|
||||
class ImGuiLayer;
|
||||
class PhysicsWorld;
|
||||
|
||||
namespace ECS { class Scene; }
|
||||
namespace Assets { class AssetManager; }
|
||||
@@ -69,7 +72,7 @@ namespace Engine {
|
||||
Renderer& GetRenderer() const { return *m_Renderer; }
|
||||
ECS::Scene& GetScene() const { return *m_Scene; }
|
||||
Assets::AssetManager& GetAssets() const { return *m_Assets; }
|
||||
f32 GetDeltaTime() const { return m_DeltaTime; }
|
||||
PhysicsWorld& GetPhysics() const { return *m_Physics; }
|
||||
u64 GetFrameCount() const { return m_FrameCount; }
|
||||
|
||||
// ImGui相关
|
||||
@@ -83,8 +86,9 @@ namespace Engine {
|
||||
// 框架装配完成(Window/Renderer 可用),派生类做初始化(建场景等)
|
||||
virtual void OnInit() {}
|
||||
|
||||
// 每帧逻辑更新(输入、动画、物理……)
|
||||
virtual void OnUpdate(f32 deltaTime) {}
|
||||
// 每帧逻辑更新(输入、动画、物理……)。frame 当帧只读:
|
||||
// Dt() = scaled(游戏逻辑),UnscaledDt() = 真实(编辑器相机/UI)
|
||||
virtual void OnUpdate(const FrameTime& frame) {}
|
||||
|
||||
// 主渲染之后、当帧提交之前的叠加层钩子(ImGui/调试视图/编辑器 UI)。
|
||||
// 主渲染由框架固定执行;这里只负责"额外的"渲染,不覆写则无叠加层。
|
||||
@@ -115,12 +119,12 @@ namespace Engine {
|
||||
Scope<ECS::Scene> m_Scene;
|
||||
Scope<Renderer> m_Renderer;
|
||||
Scope<Assets::AssetManager> m_Assets;
|
||||
Scope<PhysicsWorld> m_Physics; // Jolt 物理(Run 起循环时创建)
|
||||
// 注意成员序:ImGuiLayer 必须后声明(先于 Renderer/Window 析构,
|
||||
// 其后端销毁需要 Device 与 GLFWwindow 存活)
|
||||
Scope<ImGuiLayer> m_ImGuiLayer;
|
||||
|
||||
bool m_Running = false;
|
||||
f32 m_DeltaTime = 0.0f;
|
||||
u64 m_FrameCount = 0;
|
||||
|
||||
// 帧率控制
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// Time:帧时间系统(单源采集 → 帧契约 → 静态门面)。
|
||||
//
|
||||
// 数据流(镜像 FrameSceneData 的落帧模式):
|
||||
// Application::Run 帧首采集真实 dt(steady_clock 单源,钳制卡顿)→
|
||||
// Time::BeginFrame 落定当帧 FrameTime(业务当帧只读)→ 三轨消费:
|
||||
// 游戏轨 Dt() = unscaledDt × timeScale(OnUpdate/动画/粒子)
|
||||
// 真实轨 UnscaledDt() (编辑器相机/UI/FPS,暂停不受影响)
|
||||
// 物理轨 accumulator (吃 Dt(),scale=0 天然冻结,编辑器对账仍跑)
|
||||
//
|
||||
// 对齐 UE/Unity:
|
||||
// Time::Delta/UnscaledDelta ≈ Unity Time.deltaTime / unscaledDeltaTime
|
||||
// Time::SetTimeScale(0) ≈ Unity timeScale=0 / UE GlobalTimeDilation=0
|
||||
// FrameTime.elapsed ≈ Unity Time.time(scaled 累计)
|
||||
// Time::Realtime() ≈ Unity realtimeSinceStartup
|
||||
// TimerManager(UE SetTimer 语义)留二期,有真实消费场景再上。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_TIME_H
|
||||
#define HELLOVULKAN_TIME_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
// 帧时间契约:Run 帧首落定,当帧只读
|
||||
struct FrameTime {
|
||||
f64 unscaledDt = 0.0; // 真实帧时长(秒,f64 防 long-run 误差)
|
||||
f32 timeScale = 1.0f; // 全局缩放(0 = 暂停)
|
||||
u64 frameCount = 0;
|
||||
f64 elapsed = 0.0; // 游戏累计(scaled)
|
||||
f64 unscaledElapsed = 0.0; // 真实累计
|
||||
|
||||
f32 Dt() const { return static_cast<f32>(unscaledDt) * timeScale; }
|
||||
f32 UnscaledDt() const { return static_cast<f32>(unscaledDt); }
|
||||
f32 Milliseconds() const { return Dt() * 1000.0f; }
|
||||
f32 UnscaledMilliseconds() const { return UnscaledDt() * 1000.0f; }
|
||||
};
|
||||
|
||||
// 静态门面:任意深度代码免穿引用拿 Application(Unity Time 风格)
|
||||
class Time {
|
||||
public:
|
||||
// ---- 消费端 ----
|
||||
static const FrameTime& Current();
|
||||
static f32 Delta(); // scaled dt(当帧)
|
||||
static f32 UnscaledDelta();
|
||||
static f64 Elapsed(); // scaled 累计(秒)
|
||||
static f64 Realtime(); // 进程启动至今(真实秒,性能测量统一入口)
|
||||
static f32 TimeScale();
|
||||
static u64 FrameCount();
|
||||
|
||||
// ---- 控制意图(下一帧生效)----
|
||||
static void SetTimeScale(f32 scale); // 0 = 暂停;负值钳为 0
|
||||
|
||||
// ---- 框架内部:Application::Run 帧首调用 ----
|
||||
static void BeginFrame(f64 unscaledDt, u64 frameCount);
|
||||
|
||||
private:
|
||||
static FrameTime s_Current;
|
||||
static f32 s_PendingScale;
|
||||
static const std::chrono::steady_clock::time_point s_Startup;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_TIME_H
|
||||
@@ -134,6 +134,45 @@ namespace Engine::ECS {
|
||||
bool castShadow = false; // 方向光可投影(引擎取第一个 castShadow 方向光为 CSM 光源)
|
||||
};
|
||||
|
||||
// 刚体(Jolt 后端)。纯数据 POD——头文件零 Jolt 依赖,
|
||||
// bodyId 存 JPH::BodyID 的裸位模式,翻译在 PhysicsWorld。
|
||||
// 生命周期:PhysicsWorld::SyncScene 每帧对账(创建/销毁/参数同步),
|
||||
// 动态体的 Transform 由物理写回(Writeback 标脏),静态/运动学的
|
||||
// Transform 由编辑器驱动同步进 body。
|
||||
// 扩展路线:软体/布料 = SoftBodyComponent(Jolt SoftBodySharedSettings),
|
||||
// 角色 = CharacterControllerComponent(CharacterVirtual),载具 = VehicleComponent
|
||||
// (VehicleConstraint)——都走 PhysicsWorld 门面,不复用本组件。
|
||||
struct RigidBodyComponent {
|
||||
enum class BodyType : u8 { Static = 0, Dynamic = 1, Kinematic = 2 };
|
||||
// Mesh:用实体 MeshRenderer 的视觉网格建碰撞(Unity MeshCollider
|
||||
// 语义)——Static → Jolt MeshShape(精确三角网格,不可移动);
|
||||
// Dynamic/Kinematic → ConvexHullShape(凸包近似,MeshShape 太贵
|
||||
// 不能动)。数据源 Mesh 的 CPU 位置副本,无 MeshRenderer 回退 Box
|
||||
enum class ColliderType : u8 { Box = 0, Sphere = 1, Mesh = 2 };
|
||||
|
||||
BodyType type = BodyType::Dynamic; // Unity 语义:加 Rigidbody 即受物理驱动
|
||||
ColliderType collider = ColliderType::Box;
|
||||
|
||||
// 碰撞体尺寸(× Transform 缩放,Unity BoxCollider.size 语义)——
|
||||
// 程序化模型顶点烘焙尺寸,不能从 scale 反推
|
||||
glm::vec3 boxHalfExtents{0.5f, 0.5f, 0.5f};
|
||||
f32 sphereRadius = 0.5f;
|
||||
|
||||
// 动态体参数
|
||||
f32 mass = 1.0f;
|
||||
f32 gravityScale = 1.0f;
|
||||
f32 friction = 0.5f;
|
||||
f32 restitution = 0.2f;
|
||||
|
||||
// 传感器(Unity isTrigger 语义):只产生 Added/Removed 事件,无碰撞解算
|
||||
bool isSensor = false;
|
||||
|
||||
// 运行时(PhysicsWorld 维护,勿手改)
|
||||
u32 bodyId = 0; // JPH::BodyID 裸位
|
||||
bool bodyValid = false;
|
||||
glm::vec3 velocity{0.0f}; // 只读调试视图:线速度(m/s)
|
||||
};
|
||||
|
||||
} // namespace Engine::ECS
|
||||
|
||||
#endif //HELLOVULKAN_COMPONENTS_H
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// PhysicsDebugDraw:Jolt 调试图元收集器(JPH::DebugRendererSimple 派生)。
|
||||
//
|
||||
// 职责只有"收集":Jolt 各调试源(BodyManager::Draw 的形状线框/速度箭头、
|
||||
// ConstraintManager::DrawConstraints、PhysicsWorld 手绘的重力/加速度箭头)
|
||||
// 触发的 DrawLine/DrawTriangle 回调全部收进顶点数组;渲染侧
|
||||
// (PhysicsDebugPass)每帧上传到 ring VB 绘制。收集器不理解 Vulkan,
|
||||
// 渲染 pass 不理解 Jolt——图元契约见 PhysicsDebugTypes.h。
|
||||
//
|
||||
// 生命周期:PhysicsWorld 持有并安装为 JPH::DebugRenderer::sInstance;
|
||||
// DebugRendererSimple 的构造即 DebugRenderer::Initialize()(预烘焙
|
||||
// box/sphere/cylinder 等通用几何 batch),NextFrame() 每帧释放不再
|
||||
// 引用的缓存几何。
|
||||
//
|
||||
// 扩展性(物理调试的唯一图元出口,新调试源零新通道):
|
||||
// 软体布料 → DrawSettings 的 mDrawSoftBody* 开关原生生效
|
||||
// 角色控制器 → CharacterVirtual::DebugDraw 走同一 sInstance
|
||||
// 载具控制器 → VehicleConstraint::Draw 走同一 sInstance
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PHYSICSDEBUGDRAW_H
|
||||
#define HELLOVULKAN_PHYSICSDEBUGDRAW_H
|
||||
|
||||
#include <Engine/Physics/PhysicsDebugTypes.h>
|
||||
|
||||
#ifdef JPH_DEBUG_RENDERER
|
||||
|
||||
#include <Jolt/Jolt.h>
|
||||
#include <Jolt/Renderer/DebugRendererSimple.h>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class PhysicsDebugDraw final : public JPH::DebugRendererSimple {
|
||||
public:
|
||||
// 析构交给基类:~DebugRenderer 自己断言并清 sInstance(JPH_ASSERT(sInstance == this)),
|
||||
// 派生类手动清会让基类断言看到 nullptr 而失败
|
||||
|
||||
PhysicsDebugFrame& Frame() { return m_Frame; }
|
||||
|
||||
// 每次收集前调用:清空上一帧图元(容量保留,稳态零堆分配)
|
||||
void BeginCollect() { m_Frame.Clear(); }
|
||||
|
||||
// ---- DebugRenderer 回调(基类其余绘制原语全部路由到这里)----
|
||||
void DrawLine(JPH::RVec3Arg inFrom, JPH::RVec3Arg inTo,
|
||||
JPH::ColorArg inColor) override;
|
||||
void DrawTriangle(JPH::RVec3Arg inV1, JPH::RVec3Arg inV2,
|
||||
JPH::RVec3Arg inV3, JPH::ColorArg inColor,
|
||||
JPH::DebugRenderer::ECastShadow inCastShadow) override;
|
||||
void DrawText3D(JPH::RVec3Arg, const JPH::string_view&,
|
||||
JPH::ColorArg, float) override {
|
||||
// 3D 文本暂不支持(遮挡 UI 噪音,需要时再挂 ImGui 调试层)
|
||||
}
|
||||
|
||||
private:
|
||||
static glm::vec3 ToGlm(JPH::RVec3Arg v)
|
||||
{
|
||||
return {static_cast<f32>(v.GetX()), static_cast<f32>(v.GetY()),
|
||||
static_cast<f32>(v.GetZ())};
|
||||
}
|
||||
static glm::vec4 ToGlm(JPH::ColorArg c)
|
||||
{
|
||||
constexpr f32 kInv = 1.0f / 255.0f;
|
||||
return {c.r * kInv, c.g * kInv, c.b * kInv, c.a * kInv};
|
||||
}
|
||||
|
||||
PhysicsDebugFrame m_Frame;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#else // !JPH_DEBUG_RENDERER
|
||||
|
||||
// Distribution 构建:Jolt 调试渲染被整体裁剪(DEBUG_RENDERER_IN_* 均关),
|
||||
// 收集器退化为空实现——所有消费方照常编译,零绘制零开销
|
||||
namespace Engine {
|
||||
|
||||
class PhysicsDebugDraw {
|
||||
public:
|
||||
PhysicsDebugFrame& Frame() { return m_Frame; }
|
||||
void BeginCollect() { m_Frame.Clear(); }
|
||||
void NextFrame() {}
|
||||
|
||||
private:
|
||||
PhysicsDebugFrame m_Frame;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif // JPH_DEBUG_RENDERER
|
||||
|
||||
#endif //HELLOVULKAN_PHYSICSDEBUGDRAW_H
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// PhysicsDebugTypes:物理调试可视化的数据契约(Jolt 零依赖)。
|
||||
//
|
||||
// PhysicsDebugDraw(收集器,Physics 层)生产、PhysicsDebugPass(渲染)
|
||||
// 消费;渲染侧只 include 本头文件,不接触任何 Jolt 类型——分层约束
|
||||
// 与 RigidBodyComponent 的"零 Jolt 依赖"一致。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PHYSICSDEBUGTYPES_H
|
||||
#define HELLOVULKAN_PHYSICSDEBUGTYPES_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
// 图元顶点(28B):世界空间位置 + 线性 HDR 颜色(可 >1,过 tonemap
|
||||
// 依旧醒目)。线段/三角形共用同一顶点格式,各走一条动态 VB
|
||||
struct PhysicsDebugVertex {
|
||||
glm::vec3 pos;
|
||||
glm::vec4 color;
|
||||
};
|
||||
|
||||
// 单帧收集结果:PhysicsWorld 每个 Step 调用尾按 debugBits 重新收集
|
||||
//(渲染帧率,收集间隔内数据保持稳定,多渲染帧复用同一份图元)
|
||||
struct PhysicsDebugFrame {
|
||||
std::vector<PhysicsDebugVertex> lines; // 线段顶点(成对)
|
||||
std::vector<PhysicsDebugVertex> triangles; // 三角形顶点(三张一副)
|
||||
|
||||
void Clear() { lines.clear(); triangles.clear(); }
|
||||
bool Empty() const { return lines.empty() && triangles.empty(); }
|
||||
};
|
||||
|
||||
// 可视化位掩码(ImGui 经 PhysicsWorld 门面读写;0 = 全关)
|
||||
enum PhysicsDebugBits : u32 {
|
||||
kPhyDbgColliders = 1 << 0, // collider 线框(Jolt BodyManager::Draw)
|
||||
kPhyDbgConstraints = 1 << 1, // 约束(关节/软体约束,接入后生效)
|
||||
kPhyDbgGravity = 1 << 2, // 重力箭头(手绘,Jolt 无现成 API)
|
||||
kPhyDbgVelocity = 1 << 3, // 速度箭头(Jolt DrawSettings.mDrawVelocity)
|
||||
kPhyDbgAccel = 1 << 4, // 加速度箭头(手绘,速度差分/定步长)
|
||||
kPhyDbgDefault = kPhyDbgColliders | kPhyDbgGravity |
|
||||
kPhyDbgVelocity | kPhyDbgAccel,
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_PHYSICSDEBUGTYPES_H
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// PhysicsEvents:碰撞事件数据契约(Jolt 零依赖)。
|
||||
//
|
||||
// PhysicsWorld 内部的 ContactListenerImpl 在步进中收集、帧尾入队;
|
||||
// 消费方(App/游戏逻辑)只 include 本头文件,不接触任何 Jolt 类型
|
||||
// ——分层约束与 RigidBodyComponent 的"零 Jolt 依赖"一致。
|
||||
//
|
||||
// 事件语义 = 最近一次物理步进(Step 开头清空队列,消费方每帧读取)。
|
||||
// a/b 存实体位模式(entt::to_integral 的裸值),消费方用
|
||||
// static_cast<entt::entity>(bits) 还原,并自行 registry.valid() 校验
|
||||
// ——实体可能在事件产生后、消费前被删除。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PHYSICSEVENTS_H
|
||||
#define HELLOVULKAN_PHYSICSEVENTS_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
struct CollisionEvent {
|
||||
enum class Kind : u8 { Added = 0, Persisted = 1, Removed = 2 };
|
||||
|
||||
u32 a = 0; // 实体位模式(Body1;Jolt 保证 BodyID 1 < 2)
|
||||
u32 b = 0; // 实体位模式(Body2)
|
||||
Kind kind = Kind::Added;
|
||||
glm::vec3 point{0.0f}; // 世界空间首个接触点(Removed 时无效)
|
||||
glm::vec3 normal{0.0f}; // 世界空间法线(body2 沿此方向推出;Removed 时无效)
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_PHYSICSEVENTS_H
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// PhysicsRay:射线查询数据契约(Jolt 零依赖)。
|
||||
//
|
||||
// PhysicsWorld::RayCast(BodyInterface/NarrowPhaseQuery 门面)生产;
|
||||
// 消费方(游戏查询/编辑器拾取)只 include 本头文件,不接触任何 Jolt
|
||||
// 类型——分层约束与 PhysicsEvents.h 一致。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PHYSICSRAY_H
|
||||
#define HELLOVULKAN_PHYSICSRAY_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
// 射线命中结果(最近命中单结果;编辑器拾取/点击选择够用,
|
||||
// 穿透列表走 CastRayCollector 扩展,本契约保持最小)
|
||||
struct RayHit {
|
||||
u32 entityBits = 0; // 命中实体的位模式(消费方自行 valid 校验)
|
||||
glm::vec3 point{0.0f}; // 世界空间命中点
|
||||
glm::vec3 normal{0.0f}; // 世界空间表面法线(命中子形状)
|
||||
f32 distance = 0.0f; // 命中距离(origin 起算)
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_PHYSICSRAY_H
|
||||
@@ -0,0 +1,140 @@
|
||||
//
|
||||
// PhysicsWorld:物理世界门面(Jolt 后端)。
|
||||
//
|
||||
// 镜像 SceneRenderer 的单向数据流:
|
||||
// SyncScene(读 ECS 对账 body)→ PhysicsSystem::Update → Writeback(动态体 → Transform 标脏)
|
||||
// 定步长 accumulator(1/60,最多 4 子步,积压丢弃防螺旋死亡)。
|
||||
//
|
||||
// 分层约束:
|
||||
// - 组件层(Components.h)零 Jolt 依赖:bodyId 存 JPH::BodyID 裸位模式
|
||||
// - App/编辑器只经 Application::GetPhysics() 访问开关,不接触 Jolt 头
|
||||
//
|
||||
// 可扩展路线(本门面是唯一入口,feature 各自分文件演进,禁止互相耦合):
|
||||
// 软体/布料 → Jolt SoftBodySharedSettings/SoftBodyCreationSettings(原生支持),
|
||||
// 扩展为 SyncSoftBodies/WritebackSoft(网格引用走 AssetRef)
|
||||
// 角色控制器 → CharacterVirtual(运动学扫掠,非刚体,独立 Controller 类持有)
|
||||
// 载具控制器 → VehicleConstraint(挂动态刚体 chassis 上,独立 Controller 类)
|
||||
// 碰撞事件 → ContactListenerImpl(本类嵌套,已实现)+ GetEvents 帧尾队列
|
||||
// 射线查询 → BodyInterface::RayCast 门面(编辑器拾取/游戏查询)
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PHYSICSWORLD_H
|
||||
#define HELLOVULKAN_PHYSICSWORLD_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Physics/PhysicsDebugDraw.h>
|
||||
#include <Engine/Physics/PhysicsEvents.h>
|
||||
#include <Engine/Physics/PhysicsRay.h>
|
||||
|
||||
#include <Jolt/Jolt.h>
|
||||
#include <Jolt/Physics/PhysicsSystem.h>
|
||||
#include <Jolt/Physics/Body/BodyID.h>
|
||||
#include <Jolt/Core/JobSystemThreadPool.h>
|
||||
#include <Jolt/Core/TempAllocator.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
namespace ECS { class Scene; struct RigidBodyComponent; struct TransformComponent; }
|
||||
namespace Assets { class AssetManager; }
|
||||
|
||||
class PhysicsWorld {
|
||||
public:
|
||||
// assets:Mesh 碰撞体解析 modelRef(MeshShape/ConvexHull 数据源)
|
||||
explicit PhysicsWorld(ECS::Scene& scene, Assets::AssetManager* assets);
|
||||
~PhysicsWorld();
|
||||
|
||||
PhysicsWorld(const PhysicsWorld&) = delete;
|
||||
PhysicsWorld& operator=(const PhysicsWorld&) = delete;
|
||||
|
||||
// 帧循环调用(OnUpdate 之前,Unity FixedUpdate→Update 语义)
|
||||
void Step(f32 dt);
|
||||
|
||||
void SetEnabled(bool enabled) { m_Enabled = enabled; }
|
||||
bool IsEnabled() const { return m_Enabled; }
|
||||
|
||||
// ---- 物理调试可视化(ImGui 开关;位掩码见 PhysicsDebugBits)----
|
||||
// Step 尾部按位收集图元(渲染帧率),Renderer 每帧上传绘制
|
||||
void SetDebugDrawBits(u32 bits) { m_DebugBits = bits; }
|
||||
u32 GetDebugDrawBits() const { return m_DebugBits; }
|
||||
PhysicsDebugFrame& GetDebugFrame() { return m_DebugDraw->Frame(); }
|
||||
|
||||
// ---- 碰撞事件(语义 = 最近一次步进;Step 开头清空)----
|
||||
// 主线程在 Step 之外读取,与多线程回调互不重叠,无需加锁
|
||||
const std::vector<CollisionEvent>& GetEvents() const {
|
||||
return m_PendingEvents;
|
||||
}
|
||||
|
||||
// ---- 射线查询(游戏查询/编辑器拾取;主线程调用,暂停时也可用)----
|
||||
// dir 会被内部归一化;maxDist 限制射程。命中返回 true 并填 outHit
|
||||
bool RayCast(const glm::vec3& origin, const glm::vec3& dir,
|
||||
f32 maxDist, RayHit& outHit) const;
|
||||
|
||||
private:
|
||||
// 创建时缓存的参数(变更检测 → 重建 body)
|
||||
struct BodyRecord {
|
||||
JPH::BodyID id{};
|
||||
u8 type = 0; // RigidBodyComponent::BodyType
|
||||
u8 collider = 0; // RigidBodyComponent::ColliderType
|
||||
glm::vec3 colliderSize{0.0f}; // 碰撞体尺寸缓存(box 半尺寸)
|
||||
f32 radius = 0.0f; // 球半径缓存
|
||||
f32 mass = 0.0f;
|
||||
bool isSensor = false; // 传感器标记(变更 → 重建 body)
|
||||
glm::vec3 lastPos{0.0f}; // 静态/运动体编辑器拖动检测
|
||||
glm::vec3 lastRot{0.0f};
|
||||
// 加速度箭头用:上一步速度(定步长差分)与平滑后的加速度
|
||||
glm::vec3 prevVel{0.0f};
|
||||
glm::vec3 lastAccel{0.0f};
|
||||
};
|
||||
|
||||
void SyncScene(); // 对账:孤儿清理 / 创建 / 参数同步
|
||||
void Writeback() const; // 动态体 → TransformComponent(标脏)
|
||||
void UpdateAccels(f32 dt); // 速度差分 → BodyRecord.lastAccel(step 率)
|
||||
void CollectDebugData(); // 按 m_DebugBits 收集调试图元(渲染帧率)
|
||||
void CreateBody(u32 entityBits, ECS::RigidBodyComponent& rb,
|
||||
ECS::TransformComponent& tr);
|
||||
// Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape /
|
||||
// Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box)
|
||||
JPH::RefConst<JPH::Shape> BuildMeshShape(u32 entityBits,
|
||||
ECS::RigidBodyComponent& rb,
|
||||
ECS::TransformComponent& tr);
|
||||
void RemoveBody(u32 entityBits);
|
||||
void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁)
|
||||
bool TryBitsByBodyId(const JPH::BodyID& id, u32& outBits) const;
|
||||
|
||||
ECS::Scene& m_Scene;
|
||||
Assets::AssetManager* m_Assets = nullptr; // Mesh 碰撞体解析 modelRef
|
||||
bool m_Enabled = true;
|
||||
f32 m_Accumulator = 0.0f;
|
||||
|
||||
// 层配置实现类在 cpp;接口必须比 PhysicsSystem 活得久
|
||||
Scope<JPH::BroadPhaseLayerInterface> m_BPLayerInterface;
|
||||
Scope<JPH::ObjectVsBroadPhaseLayerFilter> m_ObjectVsBPLayerFilter;
|
||||
Scope<JPH::ObjectLayerPairFilter> m_ObjectVsObjectLayerFilter;
|
||||
|
||||
Scope<JPH::TempAllocator> m_TempAllocator;
|
||||
Scope<JPH::JobSystem> m_JobSystem;
|
||||
Scope<JPH::PhysicsSystem> m_System;
|
||||
|
||||
std::unordered_map<u32, BodyRecord> m_Bodies; // entity 位模式 → body
|
||||
|
||||
// ---- 碰撞事件收集(多线程回调入队,主线程消费)----
|
||||
class ContactListenerImpl; // cpp 定义(JPH::ContactListener 派生)
|
||||
Scope<ContactListenerImpl> m_ContactListener;
|
||||
std::mutex m_EventMutex; // 保护 m_PendingEvents(回调并发)
|
||||
std::vector<CollisionEvent> m_PendingEvents; // 最近一次步进的事件
|
||||
|
||||
// ---- 物理调试可视化 ----
|
||||
// 堆上延迟构造:DebugRendererSimple 构造即预烘焙通用几何(走 JPH
|
||||
// 分配器),必须等 ctor 内 call_once 注册完分配器,见 PhysicsWorld.cpp
|
||||
Scope<PhysicsDebugDraw> m_DebugDraw; // Jolt 全局调试渲染后端(收集器)
|
||||
u32 m_DebugBits = 0; // PhysicsDebugBits(默认关,StatsPanel 开关)
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_PHYSICSWORLD_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 = 空闲镜像列表
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -67,7 +67,16 @@ namespace Engine
|
||||
};
|
||||
const Bounds& GetBounds() const { return m_Bounds; }
|
||||
|
||||
// ---- CPU 位置副本(物理 MeshShape/ConvexHull 数据源)----
|
||||
// GPU 上传后完整 Vertex 即弃,但物理碰撞仍需三角形:构造时抽
|
||||
// 位置-only(12B/顶点,约为完整 Vertex 的 1/5)+ LOD0 索引。
|
||||
const std::vector<glm::vec3>& GetCpuPositions() const { return m_CpuPositions; }
|
||||
const std::vector<u32>& GetCpuIndices() const { return m_CpuIndices; }
|
||||
|
||||
private:
|
||||
// 物理碰撞数据源(MeshShape 三角网格 / ConvexHull 点云)
|
||||
std::vector<glm::vec3> m_CpuPositions;
|
||||
std::vector<u32> m_CpuIndices; // LOD0(原始精度)
|
||||
// 同一 index buffer 内连续排布的各级索引区间
|
||||
struct LodRange {
|
||||
u32 firstIndex = 0;
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// PhysicsDebugPass:物理调试图元叠加(collider 线框/约束/重力/速度/
|
||||
// 加速度箭头)。
|
||||
//
|
||||
// 画在 HDRColor(Forward 之后、TAA 之前):调试图元进入场景主链路——
|
||||
// TAA 时域累积抗锯齿、Bloom/Tonemap 正常消费,kDebugTransparent 诊断
|
||||
// 模式下也可见。深度只测不写(被不透明几何正确遮挡),无混合直写
|
||||
// (fullscreen pass 状态泄漏教训)。
|
||||
//
|
||||
// 数据源 = PhysicsWorld 每帧收集的 PhysicsDebugFrame(Application 把
|
||||
// PhysicsWorld 的帧缓冲指针注入 Renderer)。空帧零绘制;debug 开关
|
||||
// 全关时 PhysicsWorld 不收集 → 顶点数为 0 → 本 pass 自然零开销。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_PHYSICSDEBUGPASS_H
|
||||
#define HELLOVULKAN_PHYSICSDEBUGPASS_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
#include <Engine/Vulkan/UniformBuffer.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
namespace Engine {
|
||||
struct PhysicsDebugFrame; // PhysicsDebugTypes.h(Jolt 零依赖契约)
|
||||
|
||||
class Device;
|
||||
class ShaderManager;
|
||||
class DescriptorManager;
|
||||
|
||||
namespace Passes {
|
||||
|
||||
class PhysicsDebugPass final : public RenderPass {
|
||||
public:
|
||||
// 顶点预算(每帧槽位,按顶点数):物理调试几十个 body 的
|
||||
// 线框 + 箭头远用不满;溢出截断并告警一次
|
||||
static constexpr u32 kMaxLineVerts = 1 << 16;
|
||||
static constexpr u32 kMaxTriVerts = 1 << 16;
|
||||
|
||||
PhysicsDebugPass(Device& device, MemoryAllocator& allocator,
|
||||
ShaderManager& shaders,
|
||||
DescriptorManager& descriptors,
|
||||
FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor,
|
||||
RG::TextureHandle depth,
|
||||
UniformBuffer& lineVB, UniformBuffer& triVB);
|
||||
~PhysicsDebugPass();
|
||||
|
||||
PhysicsDebugPass(const PhysicsDebugPass&) = delete;
|
||||
PhysicsDebugPass& operator=(const PhysicsDebugPass&) = delete;
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
|
||||
// 每帧 Renderer::Render 里(该槽位 fence 等待后)上传当帧图元
|
||||
void PrepareFrame(u32 frameSlot, const PhysicsDebugFrame& src);
|
||||
|
||||
private:
|
||||
Device& m_Device;
|
||||
DescriptorManager& m_Descriptors;
|
||||
FrameSceneData& m_Frame;
|
||||
RG::TextureHandle m_HDRColor;
|
||||
RG::TextureHandle m_Depth;
|
||||
UniformBuffer& m_LineVB; // per-slot ring(Renderer 持有生命周期)
|
||||
UniformBuffer& m_TriVB;
|
||||
|
||||
vk::PipelineLayout m_Layout; // 无描述符,push = viewProj 64B
|
||||
vk::ShaderEXT m_Vert{}, m_Frag{};
|
||||
|
||||
std::array<u32, kMaxFramesInFlight> m_LineCounts{};
|
||||
std::array<u32, kMaxFramesInFlight> m_TriCounts{};
|
||||
bool m_TruncationWarned = false;
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_PHYSICSDEBUGPASS_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
|
||||
@@ -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,影子不会被剔除裁掉)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <Engine/Core/Window.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
#include <Engine/Physics/PhysicsWorld.h>
|
||||
#include <Engine/Renderer/Renderer.h>
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
@@ -22,7 +23,6 @@ namespace Engine {
|
||||
|
||||
Application::Application(const std::string& title, u32 width, u32 height)
|
||||
{
|
||||
Logger::Initialize();
|
||||
|
||||
m_Window = CreateScope<Window>(title, width, height);
|
||||
|
||||
@@ -56,7 +56,10 @@ namespace Engine {
|
||||
|
||||
if (m_ImGuiLayer) m_ImGuiLayer.reset();
|
||||
|
||||
Logger::Shutdown();
|
||||
// 注意:这里不能 Logger::Shutdown()——成员析构(Device 销毁等)
|
||||
// 在函数体之后才执行,期间 Vulkan validation 回调仍会打日志,
|
||||
// logger 先亡会导致悬空调用 SIGSEGV。Shutdown 移到 main 的
|
||||
// app 作用域结束之后(所有成员析构完毕)。
|
||||
}
|
||||
|
||||
void Application::EnableImGui()
|
||||
@@ -94,18 +97,31 @@ namespace Engine {
|
||||
|
||||
OnInit();
|
||||
|
||||
// 物理世界(场景实体搭建完后创建)
|
||||
m_Physics = CreateScope<PhysicsWorld>(*m_Scene, m_Assets.get());
|
||||
// 物理调试图元 → 渲染侧(PhysicsDebugPass 每帧上传绘制)
|
||||
m_Renderer->SetPhysicsDebugSource(&m_Physics->GetDebugFrame());
|
||||
|
||||
while (m_Running) {
|
||||
const auto frameStart = std::chrono::steady_clock::now();
|
||||
|
||||
m_Window->PollEvents();
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
f32 dt = std::chrono::duration<f32>(now - m_LastFrameTime).count();
|
||||
// 钳制卡顿(断点/窗口拖动导致的超大 dt),避免物理/动画穿模
|
||||
m_DeltaTime = std::clamp(dt, 0.0f, 0.25f);
|
||||
const f64 dt = std::chrono::duration<f64>(now - m_LastFrameTime).count();
|
||||
// 钳制卡顿(断点/窗口拖动导致的超大 dt),避免物理/动画穿模;
|
||||
// 帧首落定 FrameTime(时间单源),业务当帧只读
|
||||
Time::BeginFrame(std::clamp(dt, 0.0, 0.25), m_FrameCount);
|
||||
const FrameTime& frame = Time::Current();
|
||||
m_LastFrameTime = now;
|
||||
|
||||
OnUpdate(m_DeltaTime);
|
||||
// 物理步进(OnUpdate 之前,Unity FixedUpdate→Update 语义):
|
||||
// 动态体姿态写回 Transform 并标脏,UpdateTransforms 统一重算;
|
||||
// 吃 scaled dt → timeScale=0(编辑器暂停)时 accumulator 冻结,
|
||||
// SyncScene 对账仍跑(拖静态体照常同步)
|
||||
m_Physics->Step(frame.Dt());
|
||||
|
||||
OnUpdate(frame);
|
||||
|
||||
// 延迟销毁在渲染前统一 flush,保证渲染遍历时池结构不变
|
||||
m_Scene->FlushDestroyQueue();
|
||||
@@ -130,7 +146,7 @@ namespace Engine {
|
||||
|
||||
// ---- 主渲染(框架行为):RG 顺序 Forward → ImGui(loadOp=Load) →
|
||||
// Present,draw data 在 ImGui pass 里才真正提交 GPU ----
|
||||
m_Renderer->Render(*m_Scene, *m_Assets, m_DeltaTime);
|
||||
m_Renderer->Render(*m_Scene, *m_Assets, frame.Dt());
|
||||
|
||||
// ---- 帧率控制:vsync 开启时 present 自带节奏;否则按目标限帧 ----
|
||||
// 策略:先 sleep 到剩 ~2ms,再自旋——精度高且不烧 CPU
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// Time.cpp:门面实现(静态快照 + 意图 pending,帧首落定)。
|
||||
//
|
||||
|
||||
#include <Engine/Core/Time.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
FrameTime Time::s_Current{};
|
||||
f32 Time::s_PendingScale = 1.0f;
|
||||
const std::chrono::steady_clock::time_point Time::s_Startup =
|
||||
std::chrono::steady_clock::now();
|
||||
|
||||
const FrameTime& Time::Current() { return s_Current; }
|
||||
f32 Time::Delta() { return s_Current.Dt(); }
|
||||
f32 Time::UnscaledDelta() { return s_Current.UnscaledDt(); }
|
||||
f64 Time::Elapsed() { return s_Current.elapsed; }
|
||||
u64 Time::FrameCount() { return s_Current.frameCount; }
|
||||
f32 Time::TimeScale() { return s_PendingScale; }
|
||||
|
||||
f64 Time::Realtime() {
|
||||
return std::chrono::duration<f64>(
|
||||
std::chrono::steady_clock::now() - s_Startup).count();
|
||||
}
|
||||
|
||||
void Time::SetTimeScale(const f32 scale) {
|
||||
s_PendingScale = std::max(scale, 0.0f); // 不允许负缩放
|
||||
}
|
||||
|
||||
void Time::BeginFrame(const f64 unscaledDt, const u64 frameCount) {
|
||||
s_Current.unscaledDt = unscaledDt;
|
||||
s_Current.timeScale = s_PendingScale; // 意图落帧(当帧只读)
|
||||
s_Current.frameCount = frameCount;
|
||||
s_Current.unscaledElapsed += unscaledDt;
|
||||
s_Current.elapsed += unscaledDt * s_PendingScale;
|
||||
}
|
||||
|
||||
} // namespace Engine
|
||||
@@ -127,7 +127,7 @@ namespace Engine
|
||||
glfwTerminate();
|
||||
return;
|
||||
}
|
||||
glfwSetWindowOpacity(m_Window, 0.0f);
|
||||
// glfwSetWindowOpacity(m_Window, 0.0f);
|
||||
|
||||
ENGINE_INFO("Window created: {} ({}x{})", title, width, height);
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// PhysicsDebugDraw.cpp:回调实现(图元收集)。
|
||||
//
|
||||
|
||||
#include <Engine/Physics/PhysicsDebugDraw.h>
|
||||
|
||||
#ifdef JPH_DEBUG_RENDERER
|
||||
|
||||
namespace Engine {
|
||||
|
||||
void PhysicsDebugDraw::DrawLine(JPH::RVec3Arg inFrom, JPH::RVec3Arg inTo,
|
||||
JPH::ColorArg inColor)
|
||||
{
|
||||
const glm::vec4 c = ToGlm(inColor);
|
||||
m_Frame.lines.push_back({ToGlm(inFrom), c});
|
||||
m_Frame.lines.push_back({ToGlm(inTo), c});
|
||||
}
|
||||
|
||||
void PhysicsDebugDraw::DrawTriangle(JPH::RVec3Arg inV1, JPH::RVec3Arg inV2,
|
||||
JPH::RVec3Arg inV3,
|
||||
JPH::ColorArg inColor,
|
||||
JPH::DebugRenderer::ECastShadow /*inCastShadow*/)
|
||||
{
|
||||
// 纯收集:实心面留待后续(线框模式下本回调不触发——
|
||||
// DebugRendererSimple::DrawGeometry 的 Wireframe 分支走 DrawLine)
|
||||
const glm::vec4 c = ToGlm(inColor);
|
||||
m_Frame.triangles.push_back({ToGlm(inV1), c});
|
||||
m_Frame.triangles.push_back({ToGlm(inV2), c});
|
||||
m_Frame.triangles.push_back({ToGlm(inV3), c});
|
||||
}
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif // JPH_DEBUG_RENDERER
|
||||
@@ -0,0 +1,634 @@
|
||||
//
|
||||
// PhysicsWorld.cpp:Jolt 后端实现。层配置/类型注册/对账/写回。
|
||||
//
|
||||
|
||||
#include <Engine/Physics/PhysicsWorld.h>
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
|
||||
#include <Jolt/RegisterTypes.h>
|
||||
#include <Jolt/Core/Factory.h>
|
||||
#include <Jolt/Physics/Body/BodyCreationSettings.h>
|
||||
#include <Jolt/Physics/Body/BodyInterface.h>
|
||||
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
|
||||
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
|
||||
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
||||
#include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
|
||||
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <Jolt/Physics/Collision/ContactListener.h>
|
||||
#include <Jolt/Physics/Collision/NarrowPhaseQuery.h>
|
||||
#include <Jolt/Physics/Collision/CastResult.h>
|
||||
#include <Jolt/Physics/Collision/RayCast.h>
|
||||
#include <Jolt/Physics/Body/BodyLock.h>
|
||||
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace Engine {
|
||||
namespace {
|
||||
// ---- 层配置:MVP 两层(非移动/移动),扩软体/传感器时再分层 ----
|
||||
constexpr JPH::ObjectLayer LAYER_NON_MOVING{0};
|
||||
constexpr JPH::ObjectLayer LAYER_MOVING{1};
|
||||
constexpr JPH::BroadPhaseLayer BP_NON_MOVING{0};
|
||||
constexpr JPH::BroadPhaseLayer BP_MOVING{1};
|
||||
|
||||
// ObjectLayer → BroadPhaseLayer 映射
|
||||
class BPLayerInterfaceImpl final : public JPH::BroadPhaseLayerInterface {
|
||||
public:
|
||||
JPH::uint GetNumBroadPhaseLayers() const override { return 2; }
|
||||
|
||||
JPH::BroadPhaseLayer GetBroadPhaseLayer(JPH::ObjectLayer layer) const override {
|
||||
return layer == LAYER_NON_MOVING ? BP_NON_MOVING : BP_MOVING;
|
||||
}
|
||||
#if defined(JPH_EXTERNAL_PROFILE) || defined(JPH_PROFILE_ENABLED)
|
||||
const char *GetBroadPhaseLayerName(JPH::BroadPhaseLayer) const override {
|
||||
return "BP";
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
class ObjectVsBPLayerFilterImpl final : public JPH::ObjectVsBroadPhaseLayerFilter {
|
||||
public:
|
||||
bool ShouldCollide(JPH::ObjectLayer, JPH::BroadPhaseLayer) const override {
|
||||
return true; // MVP 全放行
|
||||
}
|
||||
};
|
||||
|
||||
class ObjectLayerPairFilterImpl final : public JPH::ObjectLayerPairFilter {
|
||||
public:
|
||||
bool ShouldCollide(JPH::ObjectLayer a, JPH::ObjectLayer b) const override {
|
||||
switch (a) {
|
||||
case LAYER_NON_MOVING: return b == LAYER_MOVING;
|
||||
case LAYER_MOVING: return true; // 动体与静/动都碰
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 引擎 Compose() 的旋转合成是 Ry·Rx·Rz(YXZ 序,见 Components.h)——
|
||||
// 物理写回必须按同约定分解欧拉角,否则体素会随写回帧帧漂移。
|
||||
// R = Ry(y)·Rx(x)·Rz(z) 的矩阵展开(0 基):
|
||||
// M12 = -sx → x = asin(-M12);否则 y = atan2(M02, M22),z = atan2(M10, M11)
|
||||
glm::vec3 QuatToEulerYXZ(const JPH::Quat &q) {
|
||||
const glm::quat gq(q.GetW(), q.GetX(), q.GetY(), q.GetZ());
|
||||
const glm::mat3 m = glm::mat3(gq); // glm 列主序:m[col][row]
|
||||
// 数学记法 M[row][col]:M12 = m[2][1],M02 = m[2][0],M10 = m[0][1] …
|
||||
const f32 m12 = m[2][1];
|
||||
const f32 m02 = m[2][0];
|
||||
const f32 m22 = m[2][2];
|
||||
const f32 m10 = m[0][1];
|
||||
const f32 m11 = m[1][1];
|
||||
const f32 m01 = m[1][0];
|
||||
|
||||
const f32 sinX = std::clamp(-m12, -1.0f, 1.0f);
|
||||
const f32 x = std::asin(sinX);
|
||||
if (std::abs(sinX) < 0.9999f) {
|
||||
return {
|
||||
glm::degrees(x),
|
||||
glm::degrees(std::atan2(m02, m22)),
|
||||
glm::degrees(std::atan2(m10, m11))
|
||||
};
|
||||
}
|
||||
// 万向锁(x=±90°):z 并入 y,取 z=0,y = atan2(M01, M02)
|
||||
return {
|
||||
glm::degrees(x),
|
||||
glm::degrees(std::atan2(m01, m02)),
|
||||
0.0f
|
||||
};
|
||||
}
|
||||
|
||||
// 与 QuatToEulerYXZ 互逆:q = qy(y)·qx(x)·qz(z) ≡ Ry·Rx·Rz
|
||||
//(不能用 glm::quat(euler) ctor——那是 ZYX 航向序,语义不同)
|
||||
JPH::Quat EulerYXZToQuat(const glm::vec3 &eulerDeg) {
|
||||
const glm::quat qx = glm::angleAxis(glm::radians(eulerDeg.x), glm::vec3(1, 0, 0));
|
||||
const glm::quat qy = glm::angleAxis(glm::radians(eulerDeg.y), glm::vec3(0, 1, 0));
|
||||
const glm::quat qz = glm::angleAxis(glm::radians(eulerDeg.z), glm::vec3(0, 0, 1));
|
||||
const glm::quat q = qy * qx * qz;
|
||||
return {q.x, q.y, q.z, q.w};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ---- 碰撞事件监听:回调在 Update 内多线程并发,只读 body、加锁入队 ----
|
||||
|
||||
class PhysicsWorld::ContactListenerImpl final : public JPH::ContactListener {
|
||||
public:
|
||||
explicit ContactListenerImpl(PhysicsWorld &owner) : m_Owner(owner) {}
|
||||
|
||||
void OnContactAdded(const JPH::Body &b1, const JPH::Body &b2,
|
||||
const JPH::ContactManifold &manifold,
|
||||
JPH::ContactSettings &) override {
|
||||
Record(CollisionEvent::Kind::Added, b1, b2, manifold);
|
||||
}
|
||||
|
||||
void OnContactPersisted(const JPH::Body &b1, const JPH::Body &b2,
|
||||
const JPH::ContactManifold &manifold,
|
||||
JPH::ContactSettings &) override {
|
||||
Record(CollisionEvent::Kind::Persisted, b1, b2, manifold);
|
||||
}
|
||||
|
||||
void OnContactRemoved(const JPH::SubShapeIDPair &pair) override {
|
||||
// body 可能已销毁(GetUserData 不可用)→ BodyID 反查实体;
|
||||
// 主线程阻塞在 Update 内,m_Bodies 此刻稳定
|
||||
u32 a = 0, b = 0;
|
||||
if (!m_Owner.TryBitsByBodyId(pair.GetBody1ID(), a) ||
|
||||
!m_Owner.TryBitsByBodyId(pair.GetBody2ID(), b)) {
|
||||
return; // 一端已随实体移除,事件无意义
|
||||
}
|
||||
m_Owner.QueueEvent({a, b, CollisionEvent::Kind::Removed, {}, {}});
|
||||
}
|
||||
|
||||
private:
|
||||
void Record(CollisionEvent::Kind kind, const JPH::Body &b1,
|
||||
const JPH::Body &b2, const JPH::ContactManifold &manifold) {
|
||||
CollisionEvent ev;
|
||||
ev.kind = kind;
|
||||
ev.a = static_cast<u32>(b1.GetUserData());
|
||||
ev.b = static_cast<u32>(b2.GetUserData());
|
||||
if (!manifold.mRelativeContactPointsOn1.empty()) {
|
||||
const JPH::RVec3 p = manifold.GetWorldSpaceContactPointOn1(0);
|
||||
ev.point = {p.GetX(), p.GetY(), p.GetZ()};
|
||||
}
|
||||
const JPH::Vec3 n = manifold.mWorldSpaceNormal;
|
||||
ev.normal = {n.GetX(), n.GetY(), n.GetZ()};
|
||||
m_Owner.QueueEvent(std::move(ev));
|
||||
}
|
||||
|
||||
PhysicsWorld &m_Owner;
|
||||
};
|
||||
|
||||
// ---- 生命周期 ----
|
||||
|
||||
PhysicsWorld::PhysicsWorld(ECS::Scene &scene, Assets::AssetManager *assets)
|
||||
: m_Scene(scene), m_Assets(assets) {
|
||||
static std::once_flag s_JoltOnce;
|
||||
std::call_once(s_JoltOnce, [] {
|
||||
JPH::RegisterDefaultAllocator();
|
||||
JPH::Factory::sInstance = new JPH::Factory();
|
||||
JPH::RegisterTypes();
|
||||
});
|
||||
|
||||
// 调试收集器必须在分配器注册之后构造:DebugRendererSimple 构造即
|
||||
// Initialize() 预烘焙通用几何(STLAllocator 走 JPH 函数指针),若作
|
||||
// 直接成员会先于 call_once 构造 → 函数指针未注册 → SIGSEGV at 0x0
|
||||
m_DebugDraw = CreateScope<PhysicsDebugDraw>();
|
||||
|
||||
m_TempAllocator = CreateScope<JPH::TempAllocatorImpl>(16 * 1024 * 1024);
|
||||
const u32 threads = std::max(1u, std::thread::hardware_concurrency() - 1);
|
||||
m_JobSystem = CreateScope<JPH::JobSystemThreadPool>(
|
||||
JPH::cMaxPhysicsJobs, JPH::cMaxPhysicsBarriers, static_cast<int>(threads));
|
||||
|
||||
m_BPLayerInterface = CreateScope<BPLayerInterfaceImpl>();
|
||||
m_ObjectVsBPLayerFilter = CreateScope<ObjectVsBPLayerFilterImpl>();
|
||||
m_ObjectVsObjectLayerFilter = CreateScope<ObjectLayerPairFilterImpl>();
|
||||
|
||||
m_System = CreateScope<JPH::PhysicsSystem>();
|
||||
m_System->Init(4096, 0, 16384, 4096,
|
||||
*m_BPLayerInterface, *m_ObjectVsBPLayerFilter,
|
||||
*m_ObjectVsObjectLayerFilter);
|
||||
m_System->SetGravity(JPH::Vec3(0.0f, -9.81f, 0.0f));
|
||||
|
||||
// 碰撞事件监听(ctor 不走 JPH 分配器,仍保持"分配器后构造"纪律)
|
||||
m_ContactListener = CreateScope<ContactListenerImpl>(*this);
|
||||
m_System->SetContactListener(m_ContactListener.get());
|
||||
|
||||
#ifdef JPH_DEBUG_RENDERER
|
||||
// 调试收集器安装为 Jolt 全局渲染后端(DrawBodies/约束/软体全走这里)
|
||||
JPH::DebugRenderer::sInstance = m_DebugDraw.get();
|
||||
#endif
|
||||
|
||||
ENGINE_INFO("PhysicsWorld initialized (Jolt {} threads)", threads);
|
||||
}
|
||||
|
||||
PhysicsWorld::~PhysicsWorld() {
|
||||
// 先析构调试收集器:~DebugRenderer 会断言 sInstance==this 并清空、
|
||||
// 释放烘焙几何——必须先于 UnregisterTypes
|
||||
m_DebugDraw.reset();
|
||||
m_Bodies.clear();
|
||||
m_ContactListener.reset(); // 监听器先于 system 析构(持 m_Owner 引用)
|
||||
m_System.reset();
|
||||
// 单世界假设:进程退出时统一反注册(多世界时改为引用计数)
|
||||
JPH::UnregisterTypes();
|
||||
delete JPH::Factory::sInstance;
|
||||
JPH::Factory::sInstance = nullptr;
|
||||
}
|
||||
|
||||
// ---- 步进:定步长 accumulator ----
|
||||
|
||||
void PhysicsWorld::Step(f32 dt) {
|
||||
if (!m_Enabled) {
|
||||
m_Accumulator = 0.0f;
|
||||
m_DebugDraw->Frame().Clear(); // 物理关:清残留图元(防旧帧闪烁)
|
||||
m_PendingEvents.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// 事件队列每渲染帧清一次 → GetEvents 语义 = 最近一次步进的事件
|
||||
m_PendingEvents.clear();
|
||||
SyncScene(); // 每帧对账(编辑器改参数/加删实体即时生效)
|
||||
|
||||
constexpr f32 kFixedDt = 1.0f / 60.0f;
|
||||
constexpr int kMaxSubSteps = 4;
|
||||
|
||||
m_Accumulator += dt;
|
||||
int steps = static_cast<int>(m_Accumulator / kFixedDt);
|
||||
if (steps > 0) {
|
||||
steps = std::min(steps, kMaxSubSteps);
|
||||
m_Accumulator -= static_cast<f32>(steps) * kFixedDt;
|
||||
// 丢弃多余积压(卡顿后不追帧,防螺旋死亡)
|
||||
m_Accumulator = std::min(m_Accumulator, kFixedDt);
|
||||
|
||||
m_System->Update(kFixedDt, steps, m_TempAllocator.get(), m_JobSystem.get());
|
||||
Writeback();
|
||||
UpdateAccels(kFixedDt); // 速度差分 → 加速度(step 率采样一次)
|
||||
}
|
||||
|
||||
// 调试收集按渲染帧率跑(无步进帧也重画,箭头不闪);全关时清空
|
||||
if (m_DebugBits != 0) {
|
||||
CollectDebugData();
|
||||
} else {
|
||||
m_DebugDraw->Frame().Clear();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 物理调试可视化 ----
|
||||
|
||||
void PhysicsWorld::UpdateAccels(f32 dt) {
|
||||
auto &bi = m_System->GetBodyInterface();
|
||||
for (auto &[bits, rec]: m_Bodies) {
|
||||
if (rec.type != static_cast<u8>(ECS::RigidBodyComponent::BodyType::Dynamic)) {
|
||||
continue;
|
||||
}
|
||||
const JPH::Vec3 v = bi.GetLinearVelocity(rec.id);
|
||||
const glm::vec3 gv(v.GetX(), v.GetY(), v.GetZ());
|
||||
const glm::vec3 instAccel = (gv - rec.prevVel) / dt;
|
||||
// 轻度 EMA:积分噪声被滤掉,碰撞冲量的单步尖峰仍然保留
|
||||
rec.lastAccel = glm::mix(rec.lastAccel, instAccel, 0.35f);
|
||||
rec.prevVel = gv;
|
||||
}
|
||||
}
|
||||
|
||||
void PhysicsWorld::CollectDebugData() {
|
||||
m_DebugDraw->BeginCollect();
|
||||
|
||||
// 1) Jolt 原生:collider 线框 + 速度箭头(软体 mDrawSoftBody* 后续加这里)
|
||||
if ((m_DebugBits & (kPhyDbgColliders | kPhyDbgVelocity)) != 0) {
|
||||
JPH::BodyManager::DrawSettings ds;
|
||||
ds.mDrawShape = (m_DebugBits & kPhyDbgColliders) != 0;
|
||||
ds.mDrawShapeWireframe = true;
|
||||
ds.mDrawVelocity = (m_DebugBits & kPhyDbgVelocity) != 0;
|
||||
m_System->DrawBodies(ds, m_DebugDraw.get());
|
||||
}
|
||||
|
||||
// 2) 约束(关节;软体/载具约束接入后自动生效)
|
||||
if ((m_DebugBits & kPhyDbgConstraints) != 0) {
|
||||
m_System->DrawConstraints(m_DebugDraw.get());
|
||||
}
|
||||
|
||||
// 3) 手绘箭头(Jolt 无现成 API):重力 = 常量方向指示(长度固定),
|
||||
// 加速度 = 速度差分(方向/长度随值,静止噪声阈值截断)
|
||||
auto &bi = m_System->GetBodyInterface();
|
||||
const JPH::Vec3 gravity = m_System->GetGravity();
|
||||
constexpr f32 kGravityLen = 1.0f; // 世界单位
|
||||
constexpr f32 kAccelScale = 0.05f;
|
||||
constexpr f32 kAccelMaxLen = 2.0f;
|
||||
const JPH::Color kGravityColor(220, 60, 60); // 红:重力
|
||||
const JPH::Color kAccelColor(255, 170, 40); // 橙:加速度
|
||||
|
||||
for (auto &[bits, rec]: m_Bodies) {
|
||||
if (rec.type != static_cast<u8>(ECS::RigidBodyComponent::BodyType::Dynamic)) {
|
||||
continue;
|
||||
}
|
||||
const JPH::RVec3 com = bi.GetCenterOfMassPosition(rec.id);
|
||||
|
||||
if ((m_DebugBits & kPhyDbgGravity) != 0) {
|
||||
const JPH::Vec3 g = gravity * bi.GetGravityFactor(rec.id);
|
||||
const f32 len = g.Length();
|
||||
if (len > 1e-4f) {
|
||||
const JPH::RVec3 tip = com + JPH::RVec3(g * (kGravityLen / len));
|
||||
m_DebugDraw->DrawArrow(com, tip, kGravityColor, kGravityLen * 0.12f);
|
||||
}
|
||||
}
|
||||
|
||||
if ((m_DebugBits & kPhyDbgAccel) != 0) {
|
||||
const f32 mag = glm::length(rec.lastAccel);
|
||||
if (mag > 0.05f) {
|
||||
const glm::vec3 dir = rec.lastAccel / mag;
|
||||
const f32 len = std::min(mag * kAccelScale, kAccelMaxLen);
|
||||
const JPH::RVec3 tip =
|
||||
com + JPH::RVec3(dir.x, dir.y, dir.z) * len;
|
||||
m_DebugDraw->DrawArrow(
|
||||
com, tip, kAccelColor, std::max(len * 0.15f, 0.05f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 释放上一帧缓存不再引用的通用几何 batch(DebugRenderer 内部池)
|
||||
m_DebugDraw->NextFrame();
|
||||
}
|
||||
|
||||
// ---- 对账 ----
|
||||
|
||||
void PhysicsWorld::SyncScene() {
|
||||
auto ® = m_Scene.GetRegistry();
|
||||
auto &bi = m_System->GetBodyInterface();
|
||||
|
||||
// 1. 孤儿清理:实体销毁 / 组件被移除 → 移除 body
|
||||
for (auto it = m_Bodies.begin(); it != m_Bodies.end();) {
|
||||
const auto e = static_cast<entt::entity>(it->first);
|
||||
if (!reg.valid(e) ||
|
||||
!reg.all_of<ECS::RigidBodyComponent, ECS::TransformComponent>(e)) {
|
||||
bi.RemoveBody(it->second.id);
|
||||
bi.DestroyBody(it->second.id);
|
||||
it = m_Bodies.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 遍历:缺失 → 创建;参数变更 → 重建;静态/运动学拖动 → 同步进 body
|
||||
auto view = reg.view<ECS::RigidBodyComponent, ECS::TransformComponent>();
|
||||
for (auto [e, rb, tr]: view.each()) {
|
||||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||
const auto found = m_Bodies.find(bits);
|
||||
if (found == m_Bodies.end() || !rb.bodyValid) {
|
||||
if (found != m_Bodies.end()) {
|
||||
bi.RemoveBody(found->second.id);
|
||||
bi.DestroyBody(found->second.id);
|
||||
m_Bodies.erase(found);
|
||||
}
|
||||
CreateBody(bits, rb, tr);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto &rec = found->second; // 非 const:静态/运动体拖动时更新 lastPos/lastRot
|
||||
if (rec.type != static_cast<u8>(rb.type) ||
|
||||
rec.collider != static_cast<u8>(rb.collider) ||
|
||||
rec.colliderSize != rb.boxHalfExtents ||
|
||||
rec.radius != rb.sphereRadius ||
|
||||
rec.mass != rb.mass ||
|
||||
rec.isSensor != rb.isSensor) {
|
||||
// 类型/碰撞体/质量变更 → 重建(简单正确,重置速度可接受)
|
||||
bi.RemoveBody(rec.id);
|
||||
bi.DestroyBody(rec.id);
|
||||
m_Bodies.erase(found);
|
||||
rb.bodyValid = false;
|
||||
CreateBody(bits, rb, tr);
|
||||
continue;
|
||||
}
|
||||
|
||||
const JPH::BodyID id(rec.id);
|
||||
bi.SetFriction(id, rb.friction);
|
||||
bi.SetRestitution(id, rb.restitution);
|
||||
if (rb.type == ECS::RigidBodyComponent::BodyType::Dynamic) {
|
||||
bi.SetGravityFactor(id, rb.gravityScale);
|
||||
} else if (rec.lastPos != tr.GetPosition() ||
|
||||
rec.lastRot != tr.GetRotation()) {
|
||||
// 编辑器拖动静态/运动学体 → 传送 body
|
||||
bi.SetPositionAndRotation(id,
|
||||
JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z),
|
||||
EulerYXZToQuat(tr.GetRotation()),
|
||||
JPH::EActivation::DontActivate);
|
||||
rec.lastPos = tr.GetPosition();
|
||||
rec.lastRot = tr.GetRotation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Mesh 碰撞体:视觉网格 → Jolt 形状 ----
|
||||
|
||||
JPH::RefConst<JPH::Shape> PhysicsWorld::BuildMeshShape(
|
||||
u32 entityBits, ECS::RigidBodyComponent &rb, ECS::TransformComponent &tr) {
|
||||
// 同实体 MeshRenderer 提供网格;缺失 → 回退(CreateBody 判 null 走 Box)
|
||||
auto *mc = m_Scene.GetRegistry().try_get<ECS::MeshRendererComponent>(
|
||||
static_cast<entt::entity>(entityBits));
|
||||
if (m_Assets == nullptr || mc == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
const Model *model = m_Assets->GetModel(mc->model);
|
||||
if (model == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 缩放烘进顶点:Jolt shape 无缩放语义,body 只吃 pos/rot
|
||||
const glm::vec3 s = tr.GetScale();
|
||||
|
||||
// 动态/运动体 → ConvexHull(MeshShape 三角网格太贵,Jolt 不允许动)
|
||||
if (rb.type != ECS::RigidBodyComponent::BodyType::Static) {
|
||||
JPH::ConvexHullShapeSettings cs;
|
||||
for (const auto &mesh: model->GetMeshes()) {
|
||||
for (const auto &p: mesh->GetCpuPositions()) {
|
||||
cs.mPoints.emplace_back(p.x * s.x, p.y * s.y, p.z * s.z);
|
||||
}
|
||||
}
|
||||
if (cs.mPoints.size() < 4) {
|
||||
return nullptr; // 凸包至少 4 点
|
||||
}
|
||||
const JPH::Shape::ShapeResult res = cs.Create();
|
||||
if (res.HasError()) {
|
||||
ENGINE_WARN("[Physics] ConvexHull build failed: {}",
|
||||
res.GetError().c_str());
|
||||
return nullptr;
|
||||
}
|
||||
return res.Get();
|
||||
}
|
||||
|
||||
// 静态体 → MeshShape 精确三角网格(各 mesh 顶点拼接,索引加基址偏移)
|
||||
JPH::VertexList verts;
|
||||
JPH::IndexedTriangleList tris;
|
||||
u32 base = 0;
|
||||
for (const auto &mesh: model->GetMeshes()) {
|
||||
const auto &pos = mesh->GetCpuPositions();
|
||||
const auto &idx = mesh->GetCpuIndices();
|
||||
verts.reserve(verts.size() + pos.size());
|
||||
for (const auto &p: pos) {
|
||||
verts.push_back(JPH::Float3(p.x * s.x, p.y * s.y, p.z * s.z));
|
||||
}
|
||||
tris.reserve(tris.size() + idx.size() / 3);
|
||||
for (size_t i = 0; i + 2 < idx.size(); i += 3) {
|
||||
// 退化三角(两索引相同)剔除:MeshShape 构建会因退化报错
|
||||
if (idx[i] != idx[i + 1] && idx[i] != idx[i + 2] &&
|
||||
idx[i + 1] != idx[i + 2]) {
|
||||
tris.emplace_back(idx[i], idx[i + 1], idx[i + 2],
|
||||
static_cast<u32>(0), base);
|
||||
}
|
||||
}
|
||||
base += static_cast<u32>(pos.size());
|
||||
}
|
||||
if (tris.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
JPH::MeshShapeSettings ms(std::move(verts), std::move(tris));
|
||||
const JPH::Shape::ShapeResult res = ms.Create();
|
||||
if (res.HasError()) {
|
||||
ENGINE_WARN("[Physics] MeshShape build failed: {}",
|
||||
res.GetError().c_str());
|
||||
return nullptr;
|
||||
}
|
||||
return res.Get();
|
||||
}
|
||||
|
||||
void PhysicsWorld::CreateBody(u32 entityBits, ECS::RigidBodyComponent &rb,
|
||||
ECS::TransformComponent &tr) {
|
||||
auto &bi = m_System->GetBodyInterface();
|
||||
|
||||
// 碰撞体:组件显式尺寸 × Transform 缩放(形状按单位尺度烹饪 +
|
||||
// ScaledShape 的优化留给 ShapeCache 阶段;MVP 直接带缩放建形状)
|
||||
JPH::RefConst<JPH::Shape> shape;
|
||||
if (rb.collider == ECS::RigidBodyComponent::ColliderType::Sphere) {
|
||||
const f32 scale = std::max({tr.GetScale().x, tr.GetScale().y, tr.GetScale().z});
|
||||
shape = new JPH::SphereShape(std::max(rb.sphereRadius * scale, 0.01f));
|
||||
} else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Mesh) {
|
||||
// Mesh 碰撞体:实体 MeshRenderer 的视觉网格(Unity MeshCollider
|
||||
// 语义)。缩放烘进顶点(Jolt shape 无缩放);Static → MeshShape
|
||||
// 精确三角网格,Dynamic/Kinematic → ConvexHull(MeshShape 太贵
|
||||
// 不能动)。无 MeshRenderer/空网格回退 Box 并告警
|
||||
shape = BuildMeshShape(entityBits, rb, tr);
|
||||
if (shape == nullptr) {
|
||||
shape = new JPH::BoxShape(JPH::Vec3(
|
||||
std::max(rb.boxHalfExtents.x * tr.GetScale().x, 0.01f),
|
||||
std::max(rb.boxHalfExtents.y * tr.GetScale().y, 0.01f),
|
||||
std::max(rb.boxHalfExtents.z * tr.GetScale().z, 0.01f)));
|
||||
}
|
||||
} else {
|
||||
shape = new JPH::BoxShape(JPH::Vec3(
|
||||
std::max(rb.boxHalfExtents.x * tr.GetScale().x, 0.01f),
|
||||
std::max(rb.boxHalfExtents.y * tr.GetScale().y, 0.01f),
|
||||
std::max(rb.boxHalfExtents.z * tr.GetScale().z, 0.01f)));
|
||||
}
|
||||
|
||||
const bool dynamic = rb.type == ECS::RigidBodyComponent::BodyType::Dynamic;
|
||||
const bool kinematic = rb.type == ECS::RigidBodyComponent::BodyType::Kinematic;
|
||||
JPH::BodyCreationSettings settings(
|
||||
shape,
|
||||
JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z),
|
||||
EulerYXZToQuat(tr.GetRotation()),
|
||||
dynamic
|
||||
? JPH::EMotionType::Dynamic
|
||||
: (kinematic
|
||||
? JPH::EMotionType::Kinematic
|
||||
: JPH::EMotionType::Static),
|
||||
(dynamic || kinematic) ? LAYER_MOVING : LAYER_NON_MOVING);
|
||||
|
||||
if (dynamic) {
|
||||
settings.mAllowSleeping = true;
|
||||
settings.mGravityFactor = rb.gravityScale;
|
||||
settings.mLinearDamping = 0.05f;
|
||||
settings.mAngularDamping = 0.05f;
|
||||
settings.mOverrideMassProperties = JPH::EOverrideMassProperties::CalculateInertia;
|
||||
settings.mMassPropertiesOverride.mMass = std::max(rb.mass, 1e-3f);
|
||||
}
|
||||
settings.mFriction = rb.friction;
|
||||
settings.mRestitution = rb.restitution;
|
||||
settings.mUserData = entityBits; // 事件回调:Body → 实体位模式反查
|
||||
settings.mIsSensor = rb.isSensor; // 传感器:只收 Added/Removed,不解算
|
||||
|
||||
const JPH::BodyID id = bi.CreateAndAddBody(
|
||||
settings, dynamic ? JPH::EActivation::Activate : JPH::EActivation::DontActivate);
|
||||
|
||||
BodyRecord rec;
|
||||
rec.id = id;
|
||||
rec.type = static_cast<u8>(rb.type);
|
||||
rec.collider = static_cast<u8>(rb.collider);
|
||||
rec.colliderSize = rb.boxHalfExtents;
|
||||
rec.radius = rb.sphereRadius;
|
||||
rec.mass = rb.mass;
|
||||
rec.isSensor = rb.isSensor;
|
||||
rec.lastPos = tr.GetPosition();
|
||||
rec.lastRot = tr.GetRotation();
|
||||
m_Bodies[entityBits] = rec;
|
||||
|
||||
rb.bodyId = id.GetIndexAndSequenceNumber();
|
||||
rb.bodyValid = true;
|
||||
}
|
||||
|
||||
void PhysicsWorld::RemoveBody(const u32 entityBits) {
|
||||
const auto found = m_Bodies.find(entityBits);
|
||||
if (found == m_Bodies.end()) return;
|
||||
auto &bi = m_System->GetBodyInterface();
|
||||
bi.RemoveBody(found->second.id);
|
||||
bi.DestroyBody(found->second.id);
|
||||
m_Bodies.erase(found);
|
||||
}
|
||||
|
||||
// ---- 碰撞事件队列 ----
|
||||
|
||||
void PhysicsWorld::QueueEvent(CollisionEvent &&ev) {
|
||||
std::lock_guard lock(m_EventMutex);
|
||||
m_PendingEvents.push_back(std::move(ev));
|
||||
}
|
||||
|
||||
bool PhysicsWorld::TryBitsByBodyId(const JPH::BodyID &id, u32 &outBits) const {
|
||||
for (const auto &[bits, rec]: m_Bodies) {
|
||||
if (rec.id == id) {
|
||||
outBits = bits;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- 射线查询 ----
|
||||
|
||||
bool PhysicsWorld::RayCast(const glm::vec3 &origin, const glm::vec3 &dir,
|
||||
const f32 maxDist, RayHit &outHit) const {
|
||||
const f32 len = glm::length(dir);
|
||||
if (len < 1e-8f) return false;
|
||||
|
||||
// RRayCast 的 direction 长度 = 射线长度(超出部分不报告命中)
|
||||
const JPH::RRayCast ray(
|
||||
JPH::RVec3(origin.x, origin.y, origin.z),
|
||||
JPH::Vec3(dir.x / len, dir.y / len, dir.z / len) * maxDist);
|
||||
|
||||
JPH::RayCastResult hit;
|
||||
if (!m_System->GetNarrowPhaseQuery().CastRay(ray, hit)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 法线需读 body(locking 接口,主线程安全);body 已销毁则丢弃
|
||||
const JPH::RVec3 point = ray.GetPointOnRay(hit.mFraction);
|
||||
JPH::BodyLockRead lock(m_System->GetBodyLockInterface(), hit.mBodyID);
|
||||
if (!lock.Succeeded()) return false;
|
||||
const JPH::Vec3 n =
|
||||
lock.GetBody().GetWorldSpaceSurfaceNormal(hit.mSubShapeID2, point);
|
||||
|
||||
u32 bits = 0;
|
||||
if (!TryBitsByBodyId(hit.mBodyID, bits)) return false;
|
||||
|
||||
outHit.entityBits = bits;
|
||||
outHit.point = {point.GetX(), point.GetY(), point.GetZ()};
|
||||
outHit.normal = {n.GetX(), n.GetY(), n.GetZ()};
|
||||
outHit.distance = hit.mFraction * maxDist;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)----
|
||||
|
||||
void PhysicsWorld::Writeback() const {
|
||||
auto ® = m_Scene.GetRegistry();
|
||||
const auto &bi = m_System->GetBodyInterface();
|
||||
|
||||
const auto view = reg.view<ECS::RigidBodyComponent, ECS::TransformComponent>();
|
||||
for (auto [e, rb, tr]: view.each()) {
|
||||
if (!rb.bodyValid ||
|
||||
rb.type != ECS::RigidBodyComponent::BodyType::Dynamic) {
|
||||
continue; // 静态/运动学体的 Transform 由编辑器驱动
|
||||
}
|
||||
const JPH::BodyID id(rb.bodyId);
|
||||
const JPH::RVec3 pos = bi.GetPosition(id);
|
||||
const JPH::Quat rot = bi.GetRotation(id);
|
||||
tr.SetPosition(glm::vec3(pos.GetX(), pos.GetY(), pos.GetZ()));
|
||||
tr.SetRotation(QuatToEulerYXZ(rot));
|
||||
|
||||
const JPH::Vec3 vel = bi.GetLinearVelocity(id);
|
||||
rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ());
|
||||
}
|
||||
}
|
||||
} // namespace Engine
|
||||
@@ -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,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 翻转
|
||||
|
||||
@@ -35,6 +35,14 @@ namespace Engine
|
||||
return;
|
||||
}
|
||||
|
||||
// CPU 位置副本:物理 MeshShape/ConvexHull 数据源(GPU 上传后
|
||||
// 完整 Vertex 不再保留,见 Mesh.h)
|
||||
m_CpuPositions.reserve(vertices.size());
|
||||
for (const auto& v : vertices) {
|
||||
m_CpuPositions.push_back(v.position);
|
||||
}
|
||||
m_CpuIndices = lods[0];
|
||||
|
||||
u64 vbSize = sizeof(Vertex) * vertices.size();
|
||||
m_VertexBuffer = CreateScope<Buffer>(
|
||||
device, allocator, vbSize,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,250 @@
|
||||
//
|
||||
// PhysicsDebugPass.cpp:pass 装配(shader object + ring VB 上传)与录制。
|
||||
//
|
||||
|
||||
#include <Engine/Renderer/Passes/PhysicsDebugPass.h>
|
||||
#include <Engine/Physics/PhysicsDebugTypes.h>
|
||||
#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 <array>
|
||||
#include <cstddef>
|
||||
|
||||
namespace Engine::Passes
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// 顶点布局:location0 = pos(vec3),location1 = color(vec4)
|
||||
//(setVertexInputEXT 走 2EXT 结构,与 Vertex.h 同一套)
|
||||
vk::VertexInputBindingDescription2EXT MakeBinding()
|
||||
{
|
||||
vk::VertexInputBindingDescription2EXT b;
|
||||
b.setBinding(0)
|
||||
.setStride(sizeof(PhysicsDebugVertex))
|
||||
.setInputRate(vk::VertexInputRate::eVertex)
|
||||
.setDivisor(1); // 0 需 zeroDivisor feature(VUID-04798)
|
||||
return b;
|
||||
}
|
||||
|
||||
std::array<vk::VertexInputAttributeDescription2EXT, 2> MakeAttributes()
|
||||
{
|
||||
std::array<vk::VertexInputAttributeDescription2EXT, 2> a;
|
||||
a[0].setLocation(0).setBinding(0)
|
||||
.setFormat(vk::Format::eR32G32B32Sfloat)
|
||||
.setOffset(offsetof(PhysicsDebugVertex, pos));
|
||||
a[1].setLocation(1).setBinding(0)
|
||||
.setFormat(vk::Format::eR32G32B32A32Sfloat)
|
||||
.setOffset(offsetof(PhysicsDebugVertex, color));
|
||||
return a;
|
||||
}
|
||||
|
||||
// 直写方程:(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;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PhysicsDebugPass::PhysicsDebugPass(Device& device, MemoryAllocator& /*allocator*/,
|
||||
ShaderManager& shaders,
|
||||
DescriptorManager& descriptors,
|
||||
FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor,
|
||||
RG::TextureHandle depth,
|
||||
UniformBuffer& lineVB, UniformBuffer& triVB)
|
||||
: m_Device(device), m_Descriptors(descriptors), m_Frame(frame),
|
||||
m_HDRColor(hdrColor), m_Depth(depth), m_LineVB(lineVB), m_TriVB(triVB)
|
||||
{
|
||||
// 无描述符 set:viewProj 走 push constant(shader 侧声明一致)
|
||||
m_Layout = descriptors.CreatePipelineLayout(
|
||||
std::vector<vk::DescriptorSetLayout>{}, sizeof(glm::mat4));
|
||||
|
||||
const std::string shaderDir = "shaders/";
|
||||
auto vert = shaders.CreateFromFile(
|
||||
shaderDir + "debug_line.vert.spv",
|
||||
vk::ShaderStageFlagBits::eVertex,
|
||||
std::vector<vk::DescriptorSetLayout>{},
|
||||
sizeof(glm::mat4));
|
||||
auto frag = shaders.CreateFromFile(
|
||||
shaderDir + "debug_line.frag.spv",
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
std::vector<vk::DescriptorSetLayout>{},
|
||||
sizeof(glm::mat4));
|
||||
|
||||
m_Vert = vert.Handle;
|
||||
m_Frag = frag.Handle;
|
||||
if (!m_Vert || !m_Frag) {
|
||||
ENGINE_FATAL("PhysicsDebugPass: failed to create shader objects");
|
||||
}
|
||||
}
|
||||
|
||||
PhysicsDebugPass::~PhysicsDebugPass()
|
||||
{
|
||||
const auto& vkDevice = m_Device.GetDevice();
|
||||
|
||||
if (m_Vert) vkDevice.destroyShaderEXT(m_Vert);
|
||||
if (m_Frag) vkDevice.destroyShaderEXT(m_Frag);
|
||||
vkDevice.destroyPipelineLayout(m_Layout);
|
||||
}
|
||||
|
||||
void PhysicsDebugPass::PrepareFrame(u32 frameSlot,
|
||||
const PhysicsDebugFrame& src)
|
||||
{
|
||||
// 预算截断:物理调试不应打满(打满说明该收敛可视化了)
|
||||
const u32 lineVerts = std::min<u32>(
|
||||
static_cast<u32>(src.lines.size()), kMaxLineVerts);
|
||||
const u32 triVerts = std::min<u32>(
|
||||
static_cast<u32>(src.triangles.size()), kMaxTriVerts);
|
||||
if ((src.lines.size() > kMaxLineVerts ||
|
||||
src.triangles.size() > kMaxTriVerts) && !m_TruncationWarned) {
|
||||
m_TruncationWarned = true;
|
||||
ENGINE_WARN("PhysicsDebugPass: vertex budget exceeded "
|
||||
"(lines {} / tris {}), clamped", src.lines.size(),
|
||||
src.triangles.size());
|
||||
}
|
||||
|
||||
// fence 已等(Renderer::Render 的 WaitFence 之后调用),
|
||||
// 写-读竞争关闭——与 UBO/SSBO 上传同一窗口语义
|
||||
if (lineVerts > 0) {
|
||||
m_LineVB.Update(frameSlot, src.lines.data(),
|
||||
static_cast<u64>(lineVerts) *
|
||||
sizeof(PhysicsDebugVertex));
|
||||
}
|
||||
if (triVerts > 0) {
|
||||
m_TriVB.Update(frameSlot, src.triangles.data(),
|
||||
static_cast<u64>(triVerts) *
|
||||
sizeof(PhysicsDebugVertex));
|
||||
}
|
||||
m_LineCounts[frameSlot] = lineVerts;
|
||||
m_TriCounts[frameSlot] = triVerts;
|
||||
}
|
||||
|
||||
void PhysicsDebugPass::Register(RG::RenderGraph& rg)
|
||||
{
|
||||
rg.AddPass("PhysicsDebug", [this](RG::RenderGraphContext& ctx) {
|
||||
const u32 lineCount = m_LineCounts[ctx.frame.frameSlot];
|
||||
const u32 triCount = m_TriCounts[ctx.frame.frameSlot];
|
||||
if (lineCount == 0 && triCount == 0) return; // 空帧零开销
|
||||
|
||||
ZoneScopedN("PhysicsDebug");
|
||||
const vk::CommandBuffer cmd = ctx.cmd;
|
||||
|
||||
// HDR:Load 承接 Forward 结果叠加;深度:Load 承接不透明深度
|
||||
// 做测试(只测不写),DontCare——下帧 Forward 重新 Clear
|
||||
vk::RenderingAttachmentInfo color;
|
||||
color.setImageView(ctx.View(m_HDRColor))
|
||||
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore);
|
||||
|
||||
vk::RenderingAttachmentInfo depth;
|
||||
depth.setImageView(ctx.View(m_Depth))
|
||||
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eDontCare);
|
||||
|
||||
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);
|
||||
|
||||
// layout 的 push range 是 vertex|fragment(CreatePipelineLayout 默认),
|
||||
// vkCmdPushConstants 的 stageFlags 必须覆盖 range 全部 stage(VUID-01796)
|
||||
cmd.pushConstants<glm::mat4>(
|
||||
m_Layout,
|
||||
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment,
|
||||
0, m_Frame.ubo.proj * m_Frame.ubo.view);
|
||||
|
||||
// ---- 动态状态(shader object 无管线对象,全量显式设置)----
|
||||
cmd.setRasterizerDiscardEnable(VK_FALSE);
|
||||
|
||||
cmd.setDepthTestEnable(VK_TRUE);
|
||||
cmd.setDepthCompareOp(vk::CompareOp::eLessOrEqual);
|
||||
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.setPrimitiveRestartEnable(VK_FALSE);
|
||||
|
||||
const vk::VertexInputBindingDescription2EXT binding = MakeBinding();
|
||||
const std::array<vk::VertexInputAttributeDescription2EXT, 2>
|
||||
attributes = MakeAttributes();
|
||||
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<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));
|
||||
|
||||
// 无混合直写(ArrayProxyNoTemporaries:具名局部变量传入)
|
||||
constexpr vk::Bool32 blendEnable = VK_FALSE;
|
||||
const vk::ColorBlendEquationEXT direct = MakeDirectEquation();
|
||||
constexpr vk::ColorComponentFlags colorWriteMask =
|
||||
vk::ColorComponentFlagBits::eR |
|
||||
vk::ColorComponentFlagBits::eG |
|
||||
vk::ColorComponentFlagBits::eB |
|
||||
vk::ColorComponentFlagBits::eA;
|
||||
cmd.setColorBlendEnableEXT(0, blendEnable);
|
||||
cmd.setColorBlendEquationEXT(0, direct);
|
||||
cmd.setColorWriteMaskEXT(0, colorWriteMask);
|
||||
|
||||
// 线段与三角形各一条流:换流 = 换 VB 绑定 + 拓扑
|
||||
cmd.setLineWidth(1.0f); // 线拓扑必须显式设置(VUID-08617)
|
||||
if (lineCount > 0) {
|
||||
cmd.setPrimitiveTopology(vk::PrimitiveTopology::eLineList);
|
||||
const std::array<vk::Buffer, 1> bufs{m_LineVB.Get(ctx.frame.frameSlot)};
|
||||
const std::array<vk::DeviceSize, 1> offs{0};
|
||||
cmd.bindVertexBuffers(0, bufs, offs);
|
||||
cmd.draw(lineCount, 1, 0, 0);
|
||||
}
|
||||
if (triCount > 0) {
|
||||
cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList);
|
||||
const std::array<vk::Buffer, 1> bufs{m_TriVB.Get(ctx.frame.frameSlot)};
|
||||
const std::array<vk::DeviceSize, 1> offs{0};
|
||||
cmd.bindVertexBuffers(0, bufs, offs);
|
||||
cmd.draw(triCount, 1, 0, 0);
|
||||
}
|
||||
|
||||
cmd.endRendering();
|
||||
})
|
||||
// Write use(WAW 链):保证排在 Forward/ForwardTrans 之后、
|
||||
// TAA(Read HDRColor)之前——调试图元进入 TAA/Bloom/Tonemap 主链路
|
||||
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
|
||||
.Write(m_Depth, RG::ResourceUse::DepthAttachment);
|
||||
}
|
||||
|
||||
} // namespace Engine::Passes
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Submodule
+1
Submodule Lib/JoltPhysics added at 5830c342b9
@@ -0,0 +1,10 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec4 inColor;
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
// HDR 的 a = 窗口透过率语义(透明窗口与桌面合成):调试图元
|
||||
// 永远不透明(a=1),rgb 直写线性 HDR 值
|
||||
outColor = vec4(inColor.rgb, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#version 450
|
||||
|
||||
// 物理调试图元:世界空间线段/三角形,颜色透传(unlit)。
|
||||
// viewProj 经 push constant 下发(proj × view,与 Forward 同源),
|
||||
// 无描述符 set——调试图元不依赖任何纹理/缓冲。
|
||||
|
||||
layout(location = 0) in vec3 inPos;
|
||||
layout(location = 1) in vec4 inColor;
|
||||
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
layout(push_constant) uniform PC {
|
||||
mat4 viewProj;
|
||||
} pc;
|
||||
|
||||
void main() {
|
||||
gl_Position = pc.viewProj * vec4(inPos, 1.0);
|
||||
outColor = inColor;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user