From 1c4b6f37ff00887d31d6cf5fa57d2135ecca0846 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Fri, 2 Oct 2026 20:08:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Jolt=20=E7=89=A9=E7=90=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E2=80=94=E2=80=94=E5=88=9A=E4=BD=93/=E7=A2=B0?= =?UTF-8?q?=E6=92=9E=E4=BD=93/=E7=89=A9=E7=90=86=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E5=9B=BE=E5=85=83=20+=20=E5=BC=95=E6=93=8E=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E5=8D=95=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PhysicsWorld:Jolt 集成,Dynamic/Kinematic/Static 刚体,姿态写回 Transform(Unity FixedUpdate 语义,吃 scaled dt,暂停自动冻结) - 碰撞体:Box(默认/手动 halfExtents)+ Mesh 碰撞体(Static→精确 MeshShape / Dynamic·Kinematic→ConvexHull,数据源 = Mesh ctor 缓存 的 CPU 位置副本,退化三角剔除,失败回退 Box 并告警) - PhysicsDebugPass:物理调试图元每帧上传绘制(debug_line shader) - Time:引擎时间单源(BeginFrame 落定 FrameTime,scaled/unscaled 分离) - JoltPhysics 以 submodule 接入(官方 url) --- .gitignore | 4 + .gitmodules | 3 + CMakeLists.txt | 17 + Engine/CMakeLists.txt | 66 +- Engine/include/Engine/Core/Time.h | 68 ++ Engine/include/Engine/ECS/Components.h | 39 ++ .../include/Engine/Physics/PhysicsDebugDraw.h | 91 +++ .../Engine/Physics/PhysicsDebugTypes.h | 48 ++ Engine/include/Engine/Physics/PhysicsEvents.h | 34 + Engine/include/Engine/Physics/PhysicsRay.h | 28 + Engine/include/Engine/Physics/PhysicsWorld.h | 140 ++++ Engine/include/Engine/Renderer/Mesh.h | 9 + .../Engine/Renderer/Passes/PhysicsDebugPass.h | 77 +++ Engine/src/Core/Time.cpp | 40 ++ Engine/src/Physics/PhysicsDebugDraw.cpp | 34 + Engine/src/Physics/PhysicsWorld.cpp | 634 ++++++++++++++++++ Engine/src/Renderer/Mesh.cpp | 8 + .../src/Renderer/Passes/PhysicsDebugPass.cpp | 250 +++++++ Lib/JoltPhysics | 1 + shaders/debug_line.frag | 10 + shaders/debug_line.vert | 19 + 21 files changed, 1589 insertions(+), 31 deletions(-) create mode 100644 Engine/include/Engine/Core/Time.h create mode 100644 Engine/include/Engine/Physics/PhysicsDebugDraw.h create mode 100644 Engine/include/Engine/Physics/PhysicsDebugTypes.h create mode 100644 Engine/include/Engine/Physics/PhysicsEvents.h create mode 100644 Engine/include/Engine/Physics/PhysicsRay.h create mode 100644 Engine/include/Engine/Physics/PhysicsWorld.h create mode 100644 Engine/include/Engine/Renderer/Passes/PhysicsDebugPass.h create mode 100644 Engine/src/Core/Time.cpp create mode 100644 Engine/src/Physics/PhysicsDebugDraw.cpp create mode 100644 Engine/src/Physics/PhysicsWorld.cpp create mode 100644 Engine/src/Renderer/Passes/PhysicsDebugPass.cpp create mode 160000 Lib/JoltPhysics create mode 100644 shaders/debug_line.frag create mode 100644 shaders/debug_line.vert diff --git a/.gitignore b/.gitignore index bdd6658..adf2646 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ .idea/ +.trae/ + +# 运行日志(工作目录生成) +HelloVulkan.log cmake-build-*/ out/ diff --git a/.gitmodules b/.gitmodules index 6a7fbcc..db1c060 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index c67b91f..9579b06 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) # 静态链接 diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 44987a8..6c6ee2d 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -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) \ No newline at end of file +target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1) diff --git a/Engine/include/Engine/Core/Time.h b/Engine/include/Engine/Core/Time.h new file mode 100644 index 0000000..1099d36 --- /dev/null +++ b/Engine/include/Engine/Core/Time.h @@ -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 + +#include + +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(unscaledDt) * timeScale; } + f32 UnscaledDt() const { return static_cast(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 diff --git a/Engine/include/Engine/ECS/Components.h b/Engine/include/Engine/ECS/Components.h index 793b349..cece77c 100644 --- a/Engine/include/Engine/ECS/Components.h +++ b/Engine/include/Engine/ECS/Components.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 diff --git a/Engine/include/Engine/Physics/PhysicsDebugDraw.h b/Engine/include/Engine/Physics/PhysicsDebugDraw.h new file mode 100644 index 0000000..af47836 --- /dev/null +++ b/Engine/include/Engine/Physics/PhysicsDebugDraw.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 + +#ifdef JPH_DEBUG_RENDERER + +#include +#include + +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(v.GetX()), static_cast(v.GetY()), + static_cast(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 diff --git a/Engine/include/Engine/Physics/PhysicsDebugTypes.h b/Engine/include/Engine/Physics/PhysicsDebugTypes.h new file mode 100644 index 0000000..16e8f5c --- /dev/null +++ b/Engine/include/Engine/Physics/PhysicsDebugTypes.h @@ -0,0 +1,48 @@ +// +// PhysicsDebugTypes:物理调试可视化的数据契约(Jolt 零依赖)。 +// +// PhysicsDebugDraw(收集器,Physics 层)生产、PhysicsDebugPass(渲染) +// 消费;渲染侧只 include 本头文件,不接触任何 Jolt 类型——分层约束 +// 与 RigidBodyComponent 的"零 Jolt 依赖"一致。 +// + +#ifndef HELLOVULKAN_PHYSICSDEBUGTYPES_H +#define HELLOVULKAN_PHYSICSDEBUGTYPES_H + +#include +#include +#include + +namespace Engine { + + // 图元顶点(28B):世界空间位置 + 线性 HDR 颜色(可 >1,过 tonemap + // 依旧醒目)。线段/三角形共用同一顶点格式,各走一条动态 VB + struct PhysicsDebugVertex { + glm::vec3 pos; + glm::vec4 color; + }; + + // 单帧收集结果:PhysicsWorld 每个 Step 调用尾按 debugBits 重新收集 + //(渲染帧率,收集间隔内数据保持稳定,多渲染帧复用同一份图元) + struct PhysicsDebugFrame { + std::vector lines; // 线段顶点(成对) + std::vector 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 diff --git a/Engine/include/Engine/Physics/PhysicsEvents.h b/Engine/include/Engine/Physics/PhysicsEvents.h new file mode 100644 index 0000000..6dc5aae --- /dev/null +++ b/Engine/include/Engine/Physics/PhysicsEvents.h @@ -0,0 +1,34 @@ +// +// PhysicsEvents:碰撞事件数据契约(Jolt 零依赖)。 +// +// PhysicsWorld 内部的 ContactListenerImpl 在步进中收集、帧尾入队; +// 消费方(App/游戏逻辑)只 include 本头文件,不接触任何 Jolt 类型 +// ——分层约束与 RigidBodyComponent 的"零 Jolt 依赖"一致。 +// +// 事件语义 = 最近一次物理步进(Step 开头清空队列,消费方每帧读取)。 +// a/b 存实体位模式(entt::to_integral 的裸值),消费方用 +// static_cast(bits) 还原,并自行 registry.valid() 校验 +// ——实体可能在事件产生后、消费前被删除。 +// + +#ifndef HELLOVULKAN_PHYSICSEVENTS_H +#define HELLOVULKAN_PHYSICSEVENTS_H + +#include +#include + +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 diff --git a/Engine/include/Engine/Physics/PhysicsRay.h b/Engine/include/Engine/Physics/PhysicsRay.h new file mode 100644 index 0000000..a9985d6 --- /dev/null +++ b/Engine/include/Engine/Physics/PhysicsRay.h @@ -0,0 +1,28 @@ +// +// PhysicsRay:射线查询数据契约(Jolt 零依赖)。 +// +// PhysicsWorld::RayCast(BodyInterface/NarrowPhaseQuery 门面)生产; +// 消费方(游戏查询/编辑器拾取)只 include 本头文件,不接触任何 Jolt +// 类型——分层约束与 PhysicsEvents.h 一致。 +// + +#ifndef HELLOVULKAN_PHYSICSRAY_H +#define HELLOVULKAN_PHYSICSRAY_H + +#include +#include + +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 diff --git a/Engine/include/Engine/Physics/PhysicsWorld.h b/Engine/include/Engine/Physics/PhysicsWorld.h new file mode 100644 index 0000000..c90fa45 --- /dev/null +++ b/Engine/include/Engine/Physics/PhysicsWorld.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 +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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& 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 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 m_BPLayerInterface; + Scope m_ObjectVsBPLayerFilter; + Scope m_ObjectVsObjectLayerFilter; + + Scope m_TempAllocator; + Scope m_JobSystem; + Scope m_System; + + std::unordered_map m_Bodies; // entity 位模式 → body + + // ---- 碰撞事件收集(多线程回调入队,主线程消费)---- + class ContactListenerImpl; // cpp 定义(JPH::ContactListener 派生) + Scope m_ContactListener; + std::mutex m_EventMutex; // 保护 m_PendingEvents(回调并发) + std::vector m_PendingEvents; // 最近一次步进的事件 + + // ---- 物理调试可视化 ---- + // 堆上延迟构造:DebugRendererSimple 构造即预烘焙通用几何(走 JPH + // 分配器),必须等 ctor 内 call_once 注册完分配器,见 PhysicsWorld.cpp + Scope m_DebugDraw; // Jolt 全局调试渲染后端(收集器) + u32 m_DebugBits = 0; // PhysicsDebugBits(默认关,StatsPanel 开关) + }; + +} // namespace Engine + +#endif //HELLOVULKAN_PHYSICSWORLD_H diff --git a/Engine/include/Engine/Renderer/Mesh.h b/Engine/include/Engine/Renderer/Mesh.h index 4df3fcb..1abd2fe 100644 --- a/Engine/include/Engine/Renderer/Mesh.h +++ b/Engine/include/Engine/Renderer/Mesh.h @@ -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& GetCpuPositions() const { return m_CpuPositions; } + const std::vector& GetCpuIndices() const { return m_CpuIndices; } + private: + // 物理碰撞数据源(MeshShape 三角网格 / ConvexHull 点云) + std::vector m_CpuPositions; + std::vector m_CpuIndices; // LOD0(原始精度) // 同一 index buffer 内连续排布的各级索引区间 struct LodRange { u32 firstIndex = 0; diff --git a/Engine/include/Engine/Renderer/Passes/PhysicsDebugPass.h b/Engine/include/Engine/Renderer/Passes/PhysicsDebugPass.h new file mode 100644 index 0000000..ec2bbdf --- /dev/null +++ b/Engine/include/Engine/Renderer/Passes/PhysicsDebugPass.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 +#include +#include +#include +#include + +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 m_LineCounts{}; + std::array m_TriCounts{}; + bool m_TruncationWarned = false; + }; + + } // namespace Passes +} // namespace Engine + +#endif //HELLOVULKAN_PHYSICSDEBUGPASS_H diff --git a/Engine/src/Core/Time.cpp b/Engine/src/Core/Time.cpp new file mode 100644 index 0000000..384eae2 --- /dev/null +++ b/Engine/src/Core/Time.cpp @@ -0,0 +1,40 @@ +// +// Time.cpp:门面实现(静态快照 + 意图 pending,帧首落定)。 +// + +#include + +#include + +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( + 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 diff --git a/Engine/src/Physics/PhysicsDebugDraw.cpp b/Engine/src/Physics/PhysicsDebugDraw.cpp new file mode 100644 index 0000000..98e0353 --- /dev/null +++ b/Engine/src/Physics/PhysicsDebugDraw.cpp @@ -0,0 +1,34 @@ +// +// PhysicsDebugDraw.cpp:回调实现(图元收集)。 +// + +#include + +#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 diff --git a/Engine/src/Physics/PhysicsWorld.cpp b/Engine/src/Physics/PhysicsWorld.cpp new file mode 100644 index 0000000..7b57d72 --- /dev/null +++ b/Engine/src/Physics/PhysicsWorld.cpp @@ -0,0 +1,634 @@ +// +// PhysicsWorld.cpp:Jolt 后端实现。层配置/类型注册/对账/写回。 +// + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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(b1.GetUserData()); + ev.b = static_cast(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(); + + m_TempAllocator = CreateScope(16 * 1024 * 1024); + const u32 threads = std::max(1u, std::thread::hardware_concurrency() - 1); + m_JobSystem = CreateScope( + JPH::cMaxPhysicsJobs, JPH::cMaxPhysicsBarriers, static_cast(threads)); + + m_BPLayerInterface = CreateScope(); + m_ObjectVsBPLayerFilter = CreateScope(); + m_ObjectVsObjectLayerFilter = CreateScope(); + + m_System = CreateScope(); + 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(*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(m_Accumulator / kFixedDt); + if (steps > 0) { + steps = std::min(steps, kMaxSubSteps); + m_Accumulator -= static_cast(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(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(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(it->first); + if (!reg.valid(e) || + !reg.all_of(e)) { + bi.RemoveBody(it->second.id); + bi.DestroyBody(it->second.id); + it = m_Bodies.erase(it); + } else { + ++it; + } + } + + // 2. 遍历:缺失 → 创建;参数变更 → 重建;静态/运动学拖动 → 同步进 body + auto view = reg.view(); + for (auto [e, rb, tr]: view.each()) { + const u32 bits = static_cast(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(rb.type) || + rec.collider != static_cast(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 PhysicsWorld::BuildMeshShape( + u32 entityBits, ECS::RigidBodyComponent &rb, ECS::TransformComponent &tr) { + // 同实体 MeshRenderer 提供网格;缺失 → 回退(CreateBody 判 null 走 Box) + auto *mc = m_Scene.GetRegistry().try_get( + static_cast(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(0), base); + } + } + base += static_cast(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 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(rb.type); + rec.collider = static_cast(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(); + 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 diff --git a/Engine/src/Renderer/Mesh.cpp b/Engine/src/Renderer/Mesh.cpp index 08f4bae..7beabd7 100644 --- a/Engine/src/Renderer/Mesh.cpp +++ b/Engine/src/Renderer/Mesh.cpp @@ -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( device, allocator, vbSize, diff --git a/Engine/src/Renderer/Passes/PhysicsDebugPass.cpp b/Engine/src/Renderer/Passes/PhysicsDebugPass.cpp new file mode 100644 index 0000000..89085ae --- /dev/null +++ b/Engine/src/Renderer/Passes/PhysicsDebugPass.cpp @@ -0,0 +1,250 @@ +// +// PhysicsDebugPass.cpp:pass 装配(shader object + ring VB 上传)与录制。 +// + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include + +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 MakeAttributes() + { + std::array 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{}, sizeof(glm::mat4)); + + const std::string shaderDir = "shaders/"; + auto vert = shaders.CreateFromFile( + shaderDir + "debug_line.vert.spv", + vk::ShaderStageFlagBits::eVertex, + std::vector{}, + sizeof(glm::mat4)); + auto frag = shaders.CreateFromFile( + shaderDir + "debug_line.frag.spv", + vk::ShaderStageFlagBits::eFragment, + std::vector{}, + 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( + static_cast(src.lines.size()), kMaxLineVerts); + const u32 triVerts = std::min( + static_cast(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(lineVerts) * + sizeof(PhysicsDebugVertex)); + } + if (triVerts > 0) { + m_TriVB.Update(frameSlot, src.triangles.data(), + static_cast(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 stages = { + vk::ShaderStageFlagBits::eVertex, + vk::ShaderStageFlagBits::eFragment + }; + const std::array shaders = {m_Vert, m_Frag}; + cmd.bindShadersEXT(stages, shaders); + + // layout 的 push range 是 vertex|fragment(CreatePipelineLayout 默认), + // vkCmdPushConstants 的 stageFlags 必须覆盖 range 全部 stage(VUID-01796) + cmd.pushConstants( + 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 + 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(ctx.extent.width), + static_cast(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 bufs{m_LineVB.Get(ctx.frame.frameSlot)}; + const std::array offs{0}; + cmd.bindVertexBuffers(0, bufs, offs); + cmd.draw(lineCount, 1, 0, 0); + } + if (triCount > 0) { + cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList); + const std::array bufs{m_TriVB.Get(ctx.frame.frameSlot)}; + const std::array 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 diff --git a/Lib/JoltPhysics b/Lib/JoltPhysics new file mode 160000 index 0000000..5830c34 --- /dev/null +++ b/Lib/JoltPhysics @@ -0,0 +1 @@ +Subproject commit 5830c342b90fa087f118aa0f086d541a4950b1d9 diff --git a/shaders/debug_line.frag b/shaders/debug_line.frag new file mode 100644 index 0000000..5a3ca9b --- /dev/null +++ b/shaders/debug_line.frag @@ -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); +} diff --git a/shaders/debug_line.vert b/shaders/debug_line.vert new file mode 100644 index 0000000..f81de13 --- /dev/null +++ b/shaders/debug_line.vert @@ -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; +}