diff --git a/App/scripts/ScriptDebug.h b/App/scripts/ScriptDebug.h new file mode 100644 index 0000000..9bc2ff0 --- /dev/null +++ b/App/scripts/ScriptDebug.h @@ -0,0 +1,52 @@ +// +// ScriptDebug.h:脚本侧力学调试封装(Unity Gizmos/Debug.DrawRay 的 +// 游戏内同位物)。把调试惯例固化成默认值,脚本不用记参数序: +// - width = 像素(≥1),默认 3px +// - duration 恒 0(OnUpdate 每帧重画,事件类需求再裸调 Debug::) +// - depthTest 恒 false(跟角色跑的线被模型遮挡就失去意义) +// - headSize = 世界单位(箭头翼头空间尺寸),默认 0.08 +// + +#pragma once + +#include + +#include + +namespace ScriptDbg { + + // 基础箭头:colorHex 语义色见下(重力红/支撑黄/意图蓝/真实绿) + inline void Arrow(const glm::vec3& from, const glm::vec3& to, + Engine::u32 colorHex, float width = 3.0f) { + Engine::Debug::Arrow(from, to, Engine::Debug::Rgb(colorHex), + /*headSize*/ 0.08f, /*width*/ width, + /*duration*/ 0.0f, /*depthTest*/ false); + } + + // 重力:anchor=脚底,len=视觉长度。建议 ∝ gravityScale,且与支撑 + // 力同长——静置时红(下)黄(上)等长 = 力平衡,一眼可读 + inline void Gravity(const glm::vec3& anchor, float len) { + Arrow(anchor, anchor - glm::vec3(0.0f, len, 0.0f), 0xDC3C3C); + } + + // 接触反力(法向支撑):只在有接触时存在——空中传 len=0 内部直接 + // 不画(力学语义:离地即失支撑) + inline void Support(const glm::vec3& anchor, float len) { + if (len > 1e-3f) { + Arrow(anchor, anchor + glm::vec3(0.0f, len, 0.0f), 0xD9C33C); + } + } + + // 加速度(深橙):脚本侧差分出的测量加速度(F=ma 的 a)。scale = + // m/s² → 世界长度换算;mag 低于 minMag 视为静置/匀速("没有线"= + // 没有加速度,本身就是信息),len 钳到 maxLen 防冲击尖峰爆屏 + inline void Accel(const glm::vec3& anchor, const glm::vec3& accel, + float scale = 0.05f, float maxLen = 2.0f) { + const float mag = glm::length(accel); + if (mag > 0.1f) { + const float len = glm::min(mag * scale, maxLen); + Arrow(anchor, anchor + (accel / mag) * len, 0xE0640A); + } + } + +} // namespace ScriptDbg diff --git a/App/scripts/WASDMove.cpp b/App/scripts/WASDMove.cpp index 9ea3888..1dbf35f 100644 --- a/App/scripts/WASDMove.cpp +++ b/App/scripts/WASDMove.cpp @@ -1,10 +1,13 @@ // // WASDMove.cpp:WASD 控制实体移动(相机水平朝向参考,UE gameplay 语义)。 // -// 移动语义分派: +// 移动语义分派(按实体配置自动选择,优先级从上到下): +// - CharacterControllerComponent → 角色控制器路径(扫掠运动学: +// 水平喂意图速度 / 重力自动积分 / Space 跳跃,Unity +// CharacterController.Move 同构) // - Dynamic 刚体 → 走物理速度门面(每帧喂水平速度,Y 归物理—— // 重力/跳跃不受影响;松键喂零速 = 立即停,Unity velocity 语义) -// - 无刚体 → 直接平移 Transform +// - 都没有 → 直接平移 Transform // // 编辑器形态键位天然不冲突:RMB 按住 = 相机吃 WASD 飞行(脚本也读 // 到按键 → 同时在动,测移动时松开 RMB 即可);游戏形态 WASD 常开。 @@ -12,14 +15,18 @@ #include #include +#include #include +#include +#include "ScriptDebug.h" #include #include -#include #include #include +#include + using namespace Engine; // Input 门面(统一输入入口) namespace { @@ -28,10 +35,27 @@ namespace { public: const char* Name() const override { return "WASDMove"; } + void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { + pb.Field("Speed", &m_Speed, 3.0f); // 走路 m/s + pb.Field("RunSpeed", &m_RunSpeed, 6.0f); // 跑步 m/s + pb.Field("TurnResponse", &m_TurnResponse, 12.0f); // 转向阻尼 + pb.Field("AccelResponse", &m_AccelResponse, 8.0f); // 加速/转向响应 + pb.Field("StopResponse", &m_StopResponse, 14.0f); // 松手制动响应 + pb.Field("JumpSpeed", &m_JumpSpeed, 6.0f); // 起跳初速 m/s + // clip 按名引用(Unity anim.Play("Walk") 语义)——名字在 + // Inspector 里改,attach 顺序变了脚本零改动 + pb.Field("IdleClip", &m_IdleClip, std::string("idle")); + pb.Field("WalkClip", &m_WalkClip, std::string("walk")); + pb.Field("RunClip", &m_RunClip, std::string("run")); + } + void OnUpdate(float dt) override { // 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene // view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)。 - // 键鼠查询统一走 Input 门面(脚本层零 GLFW 依赖) + // 输入直读按键(Unity Input.GetKey(KeyCode.W) 同形);UI 捕获 + // 键盘时门面静默。要手柄/运行时改键再换动作层 + //(Input::ActionValue2("Move")——Unity6 新 Input System / + // UE5 Enhanced Input 对应物),本示例保持最简 const glm::quat cq = App().GetRenderer() .GetEditorCamera() .GetTransform() @@ -39,32 +63,167 @@ namespace { const glm::vec3 fwd = cq * glm::vec3(0.0f, 0.0f, -1.0f); const glm::vec3 right = cq * glm::vec3(1.0f, 0.0f, 0.0f); - glm::vec3 move(0.0f); - if (Input::IsKeyDown(Input::Key::W)) move += fwd; - if (Input::IsKeyDown(Input::Key::S)) move -= fwd; - if (Input::IsKeyDown(Input::Key::D)) move += right; - if (Input::IsKeyDown(Input::Key::A)) move -= right; + // 约定:X+ = 右(D),Y- = 前(W);方向合成 = fwd*(-Y) + right*(+X) + glm::vec2 mv(0.0f); + if (Input::IsKeyDown(Input::Key::W)) mv.y -= 1.0f; + if (Input::IsKeyDown(Input::Key::S)) mv.y += 1.0f; + if (Input::IsKeyDown(Input::Key::A)) mv.x -= 1.0f; + if (Input::IsKeyDown(Input::Key::D)) mv.x += 1.0f; + if (glm::length(mv) > 1.0f) mv = glm::normalize(mv); + + glm::vec3 move = fwd * (-mv.y) + right * mv.x; move.y = 0.0f; if (glm::length(move) > 1e-4f) { move = glm::normalize(move); } - const glm::vec3 vel = move * m_Speed; + // 走/跑 + const bool moving = glm::length(mv) > 0.1f; + const bool running = + moving && Input::IsKeyDown(Input::Key::LeftShift); + const float speed = running ? m_RunSpeed : m_Speed; + const glm::vec3 desired = move * speed; // 期望速度(输入意图) - if (auto* rb = TryGetComponent(); + auto& phy = App().GetPhysics(); + const bool hasCC = + TryGetComponent() != + nullptr; + const bool grounded = + hasCC ? phy.IsCharacterGrounded(EntityBits()) : true; + + // ---- 动量整形(惯性的本体,纯脚本):真实速度逐帧逼近期 + // 望速度——起步渐快、转向沿弧线漂移、折返经零减速再反向、 + // 松手滑行刹车。加速/转向用 AccelResponse,松手用更硬的 + // StopResponse(角色要"想停就停");空中操纵性降到 1/4 + //(起跳后不宜急转弯)。指数逼近,帧率无关 + float response = moving ? m_AccelResponse : m_StopResponse; + if (!grounded) { + response *= 0.25f; + } + m_PlanarVel += + (desired - m_PlanarVel) * (1.0f - std::exp(-response * dt)); + if (glm::length(desired - m_PlanarVel) < 0.02f) { + m_PlanarVel = desired; // 贴齐,防低速蠕动 + } + + // ---- 路径 1:角色控制器(挂了 CharacterControllerComponent)---- + // 水平喂动量整形后的真实速度;Y 保留现值——重力由控制器每 + // 步自动积分,每帧覆写 Y 会把下落速度清零(空中变悬浮) + if (hasCC) { + const glm::vec3 cur = phy.GetCharacterVelocity(EntityBits()); + phy.SetCharacterVelocity( + EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z}); + + // 跳跃:着地 + 上升沿触发。IsKeyDown 是电平量(按住 + // 每帧都真),不设边沿会变成贴地火箭;IsGrounded 防 + // 空中连跳 + const bool jumpDown = Input::IsKeyDown(Input::Key::Space); + if (jumpDown && !m_JumpWasDown && + phy.IsCharacterGrounded(EntityBits())) { + phy.Jump(EntityBits(), m_JumpSpeed); + } + m_JumpWasDown = jumpDown; + } else if (auto* rb = + TryGetComponent(); rb != nullptr && rb->bodyValid && rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic) { // 物理驱动:只覆写水平速度,Y 保留物理积分离散结果 //(整体喂 0 会抵消重力 → 空中物体悬浮) - auto& phy = App().GetPhysics(); const glm::vec3 cur = phy.GetLinearVelocity(EntityBits()); - phy.SetLinearVelocity(EntityBits(), {vel.x, cur.y, vel.z}); + phy.SetLinearVelocity( + EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z}); } else { - Transform().SetPosition(Transform().GetPosition() + vel * dt); + Transform().SetPosition(Transform().GetPosition() + + m_PlanarVel * dt); } + + // ---- 朝向:面向"真实速度"方向(不是输入方向,模型默认 +Z)---- + // 这是消"平移感"的关键:动量整形后速度滞后于输入,若面朝 + // 输入方向,身体朝前脚却在侧滑——正是割裂感的来源。面朝 + // 真实速度 = 脚底朝向与位移一致,转向呈现弧线漂移。速度 + // 低于阈值(起步瞬间/折返过零)锁定上帧朝向:折返表现成 + // U 弯回转而不是原地瞬转。只取水平面(角色不俯仰翻滚) + if (glm::length(m_PlanarVel) > 0.3f) { + const float targetYaw = + std::atan2(m_PlanarVel.x, m_PlanarVel.z); + const glm::vec3 modelFwd = + Transform().GetRotationQuat() * glm::vec3(0.0f, 0.0f, 1.0f); + const float curYaw = std::atan2(modelFwd.x, modelFwd.z); + const float dyaw = std::atan2(std::sin(targetYaw - curYaw), + std::cos(targetYaw - curYaw)); + const float k = 1.0f - std::exp(-m_TurnResponse * dt); + Transform().SetRotationQuat( + glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0))); + } + + // ---- 动画:idle/walk/run 三态切换(Direct 模式 crossfade)---- + // 目标 clip 是"电平量":Shift 中途按下/松开都要能换,所以 + // 每帧重算目标,只在目标与上帧不同才 Play(边沿)。每帧无 + // 条件 Play 会不停重置播放头,动画永远播不完 + const std::string& target = !moving ? m_IdleClip : (running ? m_RunClip : m_WalkClip); + if (target != m_CurClip) { + if (auto* anim = TryGetComponent(); + anim != nullptr && anim->Play(target, 0.35f)) { + // fade 0.35s:locomotion 混合的经验值(Unity 默认 + // 0.25 太短,0.5+ 显拖沓);相位同步在引擎侧做 + m_CurClip = target; // 播放成功才记账(模型未就绪下帧重试) + } + } + + const glm::vec3 anchor = + (hasCC ? phy.GetCharacterPosition(EntityBits()) + : Transform().GetPosition()) + + glm::vec3(0.0f, 0.05f, 0.0f); + + // ---- 运动线(ScriptDbg 封装:像素线宽 3px / 当帧瞬时 / + // 透视直显已固化为默认值,不用记参数序):蓝 = 输入期 + // 望速度,绿 = 动量整形后的真实速度——同一起点,夹角 + // 即惯性滞量(转向弧线/U 弯/刹车可视化) + ScriptDbg::Arrow(anchor, anchor + desired * 0.2f, 0x2B5FD0); + ScriptDbg::Arrow(anchor, anchor + m_PlanarVel * 0.2f, 0x1E9E42); + + // ---- 力学线:重力(红,下)+ 接触反力(黄,上,仅着地)。 + // 长度 ∝ gravityScale:静置时红黄等长 = 力平衡;起跳 + // 离地黄箭头消失 = 离地即失支撑;gravityScale 调大两 + // 箭同步变长(重量感可视化) + float gs = 1.0f; + if (auto* cc = TryGetComponent< + Engine::ECS::CharacterControllerComponent>()) { + gs = cc->gravityScale; + } + const float gLen = 0.5f * gs; + ScriptDbg::Gravity(anchor, gLen); + ScriptDbg::Support(anchor, grounded ? gLen : 0.0f); + + const glm::vec3 vel = hasCC + ? phy.GetCharacterVelocity(EntityBits()) + : m_PlanarVel; + if (dt > 1e-5f) { + m_AccelS = glm::mix(m_AccelS, (vel - m_PrevVel) / dt, 0.35f); + } + m_PrevVel = vel; + ScriptDbg::Accel(anchor, m_AccelS); + + } + + void DebugShow() { + } private: - float m_Speed = 5.0f; // m/s + float m_Speed = 3.0f; // 走路 m/s(声明默认;存档值覆盖) + float m_RunSpeed = 6.0f; // 跑步 m/s(Shift 按住) + float m_TurnResponse = 12.0f; // 转向阻尼强度(越大越跟手) + float m_AccelResponse = 8.0f; // 加速/转向响应(动量整形,越小越"重") + float m_StopResponse = 14.0f; // 松手制动响应(越大刹车越硬) + glm::vec3 m_PlanarVel{0.0f}; // 平滑后的水平真实速度(动量状态) + float m_JumpSpeed = 6.0f; // 起跳初速(配合组件 gravityScale 调手感) + std::string m_IdleClip = "idle"; // clip 名(Inspector 可改) + std::string m_WalkClip = "walk"; + std::string m_RunClip = "run"; + std::string m_CurClip = "idle"; // 当前目标 clip(边沿检测基准) + bool m_JumpWasDown = false; // 上帧跳跃键(上升沿检测) + glm::vec3 m_PrevVel{0.0f}; // 上帧测量速度(加速度差分基准) + glm::vec3 m_AccelS{0.0f}; // 平滑测量加速度(EMA 0.35,调试图) }; ENGINE_REGISTER_SCRIPT(WASDMove) diff --git a/Engine/include/Engine/Debug/DebugDraw.h b/Engine/include/Engine/Debug/DebugDraw.h new file mode 100644 index 0000000..7b3f49e --- /dev/null +++ b/Engine/include/Engine/Debug/DebugDraw.h @@ -0,0 +1,139 @@ +// +// DebugDraw:引擎级通用调试图元服务(Unity Debug.DrawLine / UE +// DrawDebugHelpers 的对应物)。 +// +// 分层定位:全局静态 API,任何层(脚本/物理/动画/AI)调用即收集; +// Renderer 每帧取走缓冲,PhysicsDebugPass 统一上传叠加渲染。引擎侧 +// 只写这一次,之后一切新可视化 = 调用方加几行,不再碰引擎代码。 +// +// 语义对齐 Unity/UE: +// duration <= 0 → 只画当前帧(调用方每帧重调,Unity DrawLine 同) +// duration > 0 → 存活 duration 秒(内部持久列表,期间无需重调; +// 固定步长系统用 2×步长 即可跨渲染帧无缝续接) +// depthTest=false → 透视遮挡直显(UE DrawDebugLine bPersistent 位) +// width → 线宽(需 wideLines 特性,设备不支持时钳回 1.0) +// +// 线程模型:主线程单写单读(脚本 OnUpdate 收集 → Renderer 上传 → +// EndFrame 翻页)。 +// 预算:kMaxVerts 截断 + 一次告警(调试图元打满是滥用信号)。 +// + +#ifndef HELLOVULKAN_DEBUGDRAW_H +#define HELLOVULKAN_DEBUGDRAW_H + +#include + +#include +#include + +#include + +namespace Engine::Debug { + + // 顶点(28B):与 PhysicsDebugVertex 同布局——渲染 pass 复用同一 + // 顶点输入态(pos vec3 + color vec4) + struct Vertex { + glm::vec3 pos; + glm::vec4 color; + }; + + // 渲染批次:同 (拓扑, 线宽, 深度测试) 的连续顶点段。调用方按 + // 参数自动分组,draw 侧每批一次状态切换(动态状态零管线对象) + struct Batch { + bool tris; // true = triangleList / false = lineList + f32 width; + bool depthTest; + u32 first; // 顶点偏移(顶点数,非字节) + u32 count; + }; + + // 单帧收集结果(Renderer 每帧消费,勿手动修改) + struct DebugFrame { + std::vector verts; + std::vector batches; + + void Clear() { verts.clear(); batches.clear(); } + bool Empty() const { return batches.empty(); } + }; + + // 顶点预算(每渲染帧):调试线框足够用,溢出截断 + 告警一次 + inline constexpr u32 kMaxVerts = 1 << 16; + + // ---- 全局开关 ---- + void SetEnabled(bool on); + bool IsEnabled(); + void Clear(); // 清空瞬时 + 持久图元(PIE 停止/场景切换时调用) + + // ---- 线类(全部世界空间)---- + void Line(const glm::vec3& a, const glm::vec3& b, + const glm::vec4& color, + f32 width = 1.0f, f32 duration = 0.0f, + bool depthTest = true); + + // Ray:from 出发沿单位方向 dirNorm 画 length 长(Unity DrawRay 同形) + void Ray(const glm::vec3& from, const glm::vec3& dirNorm, f32 length, + const glm::vec4& color, + f32 width = 1.0f, f32 duration = 0.0f, bool depthTest = true); + + // 箭头:主干 + 两翼头(正交基自动构建,零向量方向安全退化成点) + void Arrow(const glm::vec3& from, const glm::vec3& to, + const glm::vec4& color, f32 headSize = 0.12f, + f32 width = 1.0f, f32 duration = 0.0f, + bool depthTest = true); + + // 十字标记(Unity Debug.DrawCross 同位) + void Cross(const glm::vec3& pos, f32 size, const glm::vec4& color, + f32 width = 1.0f, f32 duration = 0.0f, + bool depthTest = true); + + // ---- 形状线框 ---- + + // 三大圆经线圈(UE DrawDebugSphere 同形) + void WireSphere(const glm::vec3& center, f32 radius, + const glm::vec4& color, int segments = 16, + f32 width = 1.0f, f32 duration = 0.0f, + bool depthTest = true); + + // 朝向 normal 的平面圆 + void WireCircle(const glm::vec3& center, f32 radius, + const glm::vec3& normal, const glm::vec4& color, + int segments = 24, + f32 width = 1.0f, f32 duration = 0.0f, + bool depthTest = true); + + // 轴对齐 / 旋转盒(rot 单位四元数 = AABB;UE DrawDebugBox 同形) + void WireBox(const glm::vec3& center, const glm::vec3& halfExtents, + const glm::vec4& color, + const glm::quat& rot = glm::quat(1.0f, 0.0f, 0.0f, 0.0f), + f32 width = 1.0f, f32 duration = 0.0f, + bool depthTest = true); + + // 胶囊线框:轴向 local +Y(Jolt 惯例),halfHeight = 圆柱段半长 + //(总高 = 2×(halfHeight+radius),与引擎 CharacterController 参数一致) + void WireCapsule(const glm::vec3& center, f32 radius, f32 halfHeight, + const glm::vec4& color, + const glm::quat& rot = glm::quat(1.0f, 0.0f, 0.0f, 0.0f), + f32 width = 1.0f, f32 duration = 0.0f, + bool depthTest = true); + + // ---- 便捷色:0xRRGGBB → 线性 HDR vec4(可 >1 过 tonemap 依旧醒目)---- + constexpr glm::vec4 Rgb(u32 hex, f32 alpha = 1.0f) + { + constexpr f32 kInv = 1.0f / 255.0f; + return {(static_cast((hex >> 16) & 0xFF)) * kInv, + (static_cast((hex >> 8) & 0xFF)) * kInv, + (static_cast(hex & 0xFF)) * kInv, + alpha}; + } + + // ---- Renderer 消费端(业务代码勿调用)---- + + // 取当前帧缓冲(禁用/空 = Empty)。Renderer 上传窗口调用 + const DebugFrame& Frame(); + + // 翻页:清瞬时缓冲 + 清过期持久项。Renderer 上传完成后调用 + void EndFrame(); + +} // namespace Engine::Debug + +#endif //HELLOVULKAN_DEBUGDRAW_H diff --git a/Engine/include/Engine/Physics/PhysicsWorld.h b/Engine/include/Engine/Physics/PhysicsWorld.h index f686800..85bf9f1 100644 --- a/Engine/include/Engine/Physics/PhysicsWorld.h +++ b/Engine/include/Engine/Physics/PhysicsWorld.h @@ -99,6 +99,9 @@ namespace Engine { void Jump(u32 entityBits, f32 speed); // 垂直速度置 speed bool IsCharacterGrounded(u32 entityBits) const; glm::vec3 GetCharacterVelocity(u32 entityBits) const; + // 角色脚底位置(物理权威世界坐标):调试图元/特效锚点用它, + // 而不是 Transform().GetPosition()——后者在有父级时是本地坐标 + glm::vec3 GetCharacterPosition(u32 entityBits) const; private: // 创建时缓存的参数(变更检测 → 重建 body) diff --git a/Engine/include/Engine/Renderer/Passes/PhysicsDebugPass.h b/Engine/include/Engine/Renderer/Passes/PhysicsDebugPass.h index 40e9457..21fb6c1 100644 --- a/Engine/include/Engine/Renderer/Passes/PhysicsDebugPass.h +++ b/Engine/include/Engine/Renderer/Passes/PhysicsDebugPass.h @@ -1,25 +1,28 @@ // -// PhysicsDebugPass:物理调试图元叠加(collider 线框/约束/重力/速度/ -// 加速度箭头)。 +// PhysicsDebugPass:调试图元叠加 pass(双源)。 +// 物理源 = PhysicsWorld 收集的 PhysicsDebugFrame(collider 线框/ +// 约束,Application 注入帧缓冲指针) +// 通用源 = Engine::Debug 调试服务(脚本/任意层的 Line/Arrow/Wire*, +// Unity Debug.DrawLine 同位;批次自带线宽/深度测试/拓扑) // // 画在 HDRColor(Forward 之后、TAA 之前):调试图元进入场景主链路—— // TAA 时域累积抗锯齿、Bloom/Tonemap 正常消费,kDebugTransparent 诊断 // 模式下也可见。深度只测不写(被不透明几何正确遮挡),无混合直写 // (fullscreen pass 状态泄漏教训)。 // -// 数据源 = PhysicsWorld 每帧收集的 PhysicsDebugFrame(Application 把 -// PhysicsWorld 的帧缓冲指针注入 Renderer)。空帧零绘制;debug 开关 -// 全关时 PhysicsWorld 不收集 → 顶点数为 0 → 本 pass 自然零开销。 +// 空帧零绘制:两源顶点数均 0 时提前短路。 // #ifndef HELLOVULKAN_PHYSICSDEBUGPASS_H #define HELLOVULKAN_PHYSICSDEBUGPASS_H #include +#include #include #include #include #include +#include #include namespace Engine { @@ -44,7 +47,8 @@ namespace Engine { FrameSceneData& frame, RG::TextureHandle hdrColor, RG::TextureHandle depth, - UniformBuffer& lineVB, UniformBuffer& triVB); + UniformBuffer& lineVB, UniformBuffer& triVB, + UniformBuffer& debugVB); ~PhysicsDebugPass(); PhysicsDebugPass(const PhysicsDebugPass&) = delete; @@ -52,23 +56,36 @@ namespace Engine { void Register(RG::RenderGraph& rg) override; - // 每帧 Renderer::Render 里(该槽位 fence 等待后)上传当帧图元 - void PrepareFrame(u32 frameSlot, const PhysicsDebugFrame& src); + // 每帧 Renderer::Render 里(该槽位 fence 等待后)上传当帧 + // 图元。dbg 为 nullptr = 通用调试源禁用/空 + void PrepareFrame(u32 frameSlot, const PhysicsDebugFrame& src, + const Debug::DebugFrame* dbg); private: + // 一个可绘制段:独立拓扑/线宽/深度测试(动态状态逐批设置) + struct Span { + bool tris; + f32 width; + bool depthTest; + u32 first; // 顶点偏移(对应 VB 内顶点序号) + u32 count; + }; + 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_LineVB; // per-slot ring(物理源,Renderer 持有) UniformBuffer& m_TriVB; + UniformBuffer& m_DebugVB; // 通用调试源 ring(Renderer 持有) vk::PipelineLayout m_Layout; // 无描述符,push = viewProj 64B ShaderManager::ShaderRef m_Vert, m_Frag; // 析构自动销毁 std::array m_LineCounts{}; std::array m_TriCounts{}; + std::array, kMaxFramesInFlight> m_Spans; bool m_TruncationWarned = false; }; diff --git a/Engine/include/Engine/Vulkan/Device.h b/Engine/include/Engine/Vulkan/Device.h index 2143e33..a6ad627 100644 --- a/Engine/include/Engine/Vulkan/Device.h +++ b/Engine/include/Engine/Vulkan/Device.h @@ -48,6 +48,8 @@ public: // 各向异性过滤:纹理采样器清晰度(掠射角材质锐利度)的硬件开关 bool IsSamplerAnisotropySupported() const { return m_SamplerAnisotropySupported; } float MaxSamplerAnisotropy() const { return m_MaxSamplerAnisotropy; } + bool IsWideLinesSupported() const { return m_WideLinesSupported; } + float MaxLineWidth() const { return m_MaxLineWidth; } private: void CreateInstance(); @@ -81,6 +83,8 @@ private: bool m_DrawIndirectCountSupported = false; bool m_SamplerAnisotropySupported = false; float m_MaxSamplerAnisotropy = 1.0f; + bool m_WideLinesSupported = false; + float m_MaxLineWidth = 1.0f; // 设备线宽上限(像素,limits.lineWidthRange[1]) std::vector m_ValidationLayers = { "VK_LAYER_KHRONOS_validation" }; std::vector m_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; diff --git a/Engine/src/Debug/DebugDraw.cpp b/Engine/src/Debug/DebugDraw.cpp new file mode 100644 index 0000000..4190f0a --- /dev/null +++ b/Engine/src/Debug/DebugDraw.cpp @@ -0,0 +1,359 @@ +// +// DebugDraw.cpp:通用调试图元服务实现。 +// +// 结构:瞬时缓冲(duration=0,调用方每帧重画)+ 持久列表(duration>0, +// 按到期时间淘汰)→ Frame() 按 (拓扑,线宽,深度测试) 键聚合为连续顶点段 +// → Renderer 上传 → EndFrame() 翻页。聚合器不缓存跨帧顶点,持久项每帧 +// 重拷贝(调试工具量级,< 数千顶点,非热路径)。 +// + +#include +#include + +#include +#include +#include + +namespace Engine::Debug { + + namespace { + + constexpr f32 kPi = 3.14159265358979f; + + // 瞬时批次:Draw 调用直接追加(批次键匹配复用,保留容量) + struct TransientBatch { + f32 width; + bool depthTest; + std::vector verts; + }; + + // 持久项:快照顶点 + 到期时刻(steady_clock 秒) + struct Persistent { + f32 width; + bool depthTest; + std::vector verts; + f64 expiry; + }; + + bool g_Enabled = true; + bool g_Warned = false; + std::vector g_Transient; + std::vector g_Persistent; + DebugFrame g_Frame; + + using Clock = std::chrono::steady_clock; + + f64 NowSec() + { + return std::chrono::duration(Clock::now().time_since_epoch()) + .count(); + } + + TransientBatch& BatchFor(f32 width, bool depthTest) + { + for (auto& b : g_Transient) { + if (b.depthTest == depthTest && + std::abs(b.width - width) < 1e-3f) { + return b; + } + } + g_Transient.push_back(TransientBatch{width, depthTest, {}}); + return g_Transient.back(); + } + + void LineImpl(const glm::vec3& a, const glm::vec3& b, + const glm::vec4& color, f32 width, f32 duration, + bool depthTest) + { + if (!g_Enabled) return; + if (duration > 0.0f) { + Persistent p; + p.width = width; + p.depthTest = depthTest; + p.expiry = NowSec() + static_cast(duration); + p.verts.push_back({a, color}); + p.verts.push_back({b, color}); + g_Persistent.push_back(std::move(p)); + } else { + auto& batch = BatchFor(width, depthTest); + batch.verts.push_back({a, color}); + batch.verts.push_back({b, color}); + } + } + + // 圆折线发射器:center + u·cosθ + v·sinθ(u/v 已含半径) + void CircleImpl(const glm::vec3& center, const glm::vec3& u, + const glm::vec3& v, const glm::vec4& color, + int segments, f32 width, f32 duration, + bool depthTest) + { + glm::vec3 prev = center + u; + for (int i = 1; i <= segments; ++i) { + const f32 t = + static_cast(i) / static_cast(segments) * 2.0f * kPi; + const glm::vec3 p = center + u * std::cos(t) + v * std::sin(t); + LineImpl(prev, p, color, width, duration, depthTest); + prev = p; + } + } + + } // namespace + + void SetEnabled(bool on) + { + g_Enabled = on; + if (!on) { + Clear(); + } + } + + bool IsEnabled() { return g_Enabled; } + + void Clear() + { + g_Transient.clear(); + g_Persistent.clear(); + g_Frame.Clear(); + } + + void Line(const glm::vec3& a, const glm::vec3& b, const glm::vec4& color, + f32 width, f32 duration, bool depthTest) + { + LineImpl(a, b, color, width, duration, depthTest); + } + + void Ray(const glm::vec3& from, const glm::vec3& dirNorm, f32 length, + const glm::vec4& color, f32 width, f32 duration, bool depthTest) + { + LineImpl(from, from + dirNorm * length, color, width, duration, + depthTest); + } + + void Arrow(const glm::vec3& from, const glm::vec3& to, + const glm::vec4& color, f32 headSize, f32 width, f32 duration, + bool depthTest) + { + const glm::vec3 d = to - from; + const f32 lenSq = glm::dot(d, d); + if (lenSq < 1e-12f) { + // 零长退化:画十字点标(而不是未定义方向的箭头) + Cross(from, headSize * 0.5f, color, width, duration, depthTest); + return; + } + LineImpl(from, to, color, width, duration, depthTest); + + // 箭头四翼:轴周围 90° 均布两对 V(60° 张角)。单 V 在视线与 + // 翼面共面时退化成一条线;四翼保证任意视角至少一对正对相机 + const glm::vec3 dir = d * (1.0f / std::sqrt(lenSq)); + const glm::vec3 up = + std::abs(dir.y) < 0.99f ? glm::vec3(0.0f, 1.0f, 0.0f) + : glm::vec3(1.0f, 0.0f, 0.0f); + const glm::vec3 side = glm::normalize(glm::cross(dir, up)); + const glm::vec3 bit = glm::normalize(glm::cross(dir, side)); + const glm::vec3 heads[4] = { + glm::normalize(dir + side * 0.6f), + glm::normalize(dir - side * 0.6f), + glm::normalize(dir + bit * 0.6f), + glm::normalize(dir - bit * 0.6f), + }; + for (const glm::vec3& h : heads) { + LineImpl(to, to - h * headSize, color, width, duration, depthTest); + } + } + + void Cross(const glm::vec3& pos, f32 size, const glm::vec4& color, + f32 width, f32 duration, bool depthTest) + { + const glm::vec3 s(size, size, size); + LineImpl(pos - glm::vec3(size, 0, 0), pos + glm::vec3(size, 0, 0), + color, width, duration, depthTest); + LineImpl(pos - glm::vec3(0, size, 0), pos + glm::vec3(0, size, 0), + color, width, duration, depthTest); + LineImpl(pos - glm::vec3(0, 0, size), pos + glm::vec3(0, 0, size), + color, width, duration, depthTest); + (void)s; + } + + void WireSphere(const glm::vec3& center, f32 radius, + const glm::vec4& color, int segments, f32 width, + f32 duration, bool depthTest) + { + // 三大圆:XY / XZ / YZ 平面 + CircleImpl(center, {radius, 0, 0}, {0, radius, 0}, color, segments, + width, duration, depthTest); + CircleImpl(center, {radius, 0, 0}, {0, 0, radius}, color, segments, + width, duration, depthTest); + CircleImpl(center, {0, radius, 0}, {0, 0, radius}, color, segments, + width, duration, depthTest); + } + + void WireCircle(const glm::vec3& center, f32 radius, + const glm::vec3& normal, const glm::vec4& color, + int segments, f32 width, f32 duration, bool depthTest) + { + const f32 nSq = glm::dot(normal, normal); + if (nSq < 1e-12f) return; + const glm::vec3 n = normal * (1.0f / std::sqrt(nSq)); + const glm::vec3 up = std::abs(n.y) < 0.99f + ? glm::vec3(0.0f, 1.0f, 0.0f) + : glm::vec3(1.0f, 0.0f, 0.0f); + const glm::vec3 u = glm::normalize(glm::cross(n, up)) * radius; + const glm::vec3 v = glm::normalize(glm::cross(n, u / radius)) * radius; + CircleImpl(center, u, v, color, segments, width, duration, depthTest); + } + + void WireBox(const glm::vec3& center, const glm::vec3& halfExtents, + const glm::vec4& color, const glm::quat& rot, f32 width, + f32 duration, bool depthTest) + { + glm::vec3 corner[8]; + int ci = 0; + for (int i = 0; i < 8; ++i) { + const glm::vec3 local( + (i & 1) ? halfExtents.x : -halfExtents.x, + (i & 2) ? halfExtents.y : -halfExtents.y, + (i & 4) ? halfExtents.z : -halfExtents.z); + corner[ci++] = center + rot * local; + } + // 12 条边 + static constexpr int kEdges[12][2] = { + {0, 1}, {2, 3}, {4, 5}, {6, 7}, // X 向 + {0, 2}, {1, 3}, {4, 6}, {5, 7}, // Y 向 + {0, 4}, {1, 5}, {2, 6}, {3, 7}, // Z 向 + }; + for (const auto& e : kEdges) { + LineImpl(corner[e[0]], corner[e[1]], color, width, duration, + depthTest); + } + } + + void WireCapsule(const glm::vec3& center, f32 radius, f32 halfHeight, + const glm::vec4& color, const glm::quat& rot, f32 width, + f32 duration, bool depthTest) + { + // 轴向 local +Y:两端圆 + 4 侧线 + 两盖各 2 半圆(X-Y / Z-Y 平面) + auto W = [&](const glm::vec3& local) { return center + rot * local; }; + + auto CircleY = [&](f32 y) { + glm::vec3 prev; + for (int i = 0; i <= 16; ++i) { + const f32 t = static_cast(i) / 16.0f * 2.0f * kPi; + const glm::vec3 p = W({radius * std::cos(t), y, + radius * std::sin(t)}); + if (i > 0) { + LineImpl(prev, p, color, width, duration, depthTest); + } + prev = p; + } + }; + CircleY(halfHeight); + CircleY(-halfHeight); + + LineImpl(W({radius, halfHeight, 0}), W({radius, -halfHeight, 0}), + color, width, duration, depthTest); + LineImpl(W({-radius, halfHeight, 0}), W({-radius, -halfHeight, 0}), + color, width, duration, depthTest); + LineImpl(W({0, halfHeight, radius}), W({0, -halfHeight, radius}), + color, width, duration, depthTest); + LineImpl(W({0, halfHeight, -radius}), W({0, -halfHeight, -radius}), + color, width, duration, depthTest); + + auto CapArc = [&](f32 cap, f32 xz) { + // xz: 半圆所在竖直平面(1 = X-Y 面,0 = Z-Y 面) + glm::vec3 prev; + for (int i = 0; i <= 8; ++i) { + const f32 a = static_cast(i) / 8.0f * kPi; + const f32 radial = radius * std::cos(a); + const f32 axial = cap * (halfHeight + radius * std::sin(a)); + const glm::vec3 p = + W({radial * xz, axial, radial * (1.0f - xz)}); + if (i > 0) { + LineImpl(prev, p, color, width, duration, depthTest); + } + prev = p; + } + }; + CapArc(1.0f, 1.0f); + CapArc(-1.0f, 1.0f); + CapArc(1.0f, 0.0f); + CapArc(-1.0f, 0.0f); + } + + const DebugFrame& Frame() + { + g_Frame.Clear(); + if (!g_Enabled) { + return g_Frame; + } + + // ---- 按 (拓扑=线, 线宽, 深度) 聚合:瞬时 + 未过期持久项 ---- + // groups = 聚合组暂存(键少量,线性扫)→ 展开为连续顶点段 + std::vector>> groups; + auto findGroup = [&](f32 w, bool d) -> std::vector& { + for (auto& [b, vs] : groups) { + if (!b.tris && b.depthTest == d && + std::abs(b.width - w) < 1e-3f) { + return vs; + } + } + groups.push_back({Batch{false, w, d, 0, 0}, {}}); + return groups.back().second; + }; + + for (const auto& t : g_Transient) { + findGroup(t.width, t.depthTest) + .insert(findGroup(t.width, t.depthTest).end(), + t.verts.begin(), t.verts.end()); + } + const f64 now = NowSec(); + for (const auto& p : g_Persistent) { + if (p.expiry > now) { + findGroup(p.width, p.depthTest) + .insert(findGroup(p.width, p.depthTest).end(), + p.verts.begin(), p.verts.end()); + } + } + + // ---- 展开 + 预算截断 ---- + u64 total = 0; + for (const auto& [b, vs] : groups) { + total += vs.size(); + } + const u64 cap = kMaxVerts; + if (total > cap && !g_Warned) { + g_Warned = true; + ENGINE_WARN("DebugDraw: vertex budget exceeded ({} > {}), " + "clamped", + total, cap); + } + for (auto& [b, vs] : groups) { + if (g_Frame.verts.size() >= cap) break; + b.first = static_cast(g_Frame.verts.size()); + const u64 room = cap - g_Frame.verts.size(); + const u64 n = std::min(vs.size(), room); + g_Frame.verts.insert(g_Frame.verts.end(), vs.begin(), + vs.begin() + static_cast(n)); + b.count = static_cast(n); + if (b.count > 0) { + g_Frame.batches.push_back(b); + } + } + return g_Frame; + } + + void EndFrame() + { + g_Frame.Clear(); + const f64 now = NowSec(); + g_Persistent.erase(std::remove_if(g_Persistent.begin(), + g_Persistent.end(), + [now](const Persistent& p) { + return p.expiry <= now; + }), + g_Persistent.end()); + for (auto& t : g_Transient) { + t.verts.clear(); // 保留批次与容量(稳态零重分配) + } + } + +} // namespace Engine::Debug diff --git a/Engine/src/Physics/PhysicsWorld.cpp b/Engine/src/Physics/PhysicsWorld.cpp index 1f171bf..5f94351 100644 --- a/Engine/src/Physics/PhysicsWorld.cpp +++ b/Engine/src/Physics/PhysicsWorld.cpp @@ -3,6 +3,7 @@ // #include +#include #include #include @@ -274,12 +275,14 @@ namespace Engine { void PhysicsWorld::CollectDebugData() { m_DebugDraw->BeginCollect(); - // 1) Jolt 原生:collider 线框 + 速度箭头(软体 mDrawSoftBody* 后续加这里) - if ((m_DebugBits & (kPhyDbgColliders | kPhyDbgVelocity)) != 0) { + // 1) Jolt 原生:collider 线框(软体 mDrawSoftBody* 后续加这里)。 + // 速度箭头不交 Jolt 画——其 BodyManager::DrawBodies 无零速保 + // 护,静止刚体的零向量箭头退化成 +X 方向(本次修复的突兀箭 + // 头)→ 统一在下方手画并加阈值 + if ((m_DebugBits & kPhyDbgColliders) != 0) { JPH::BodyManager::DrawSettings ds; - ds.mDrawShape = (m_DebugBits & kPhyDbgColliders) != 0; + ds.mDrawShape = true; ds.mDrawShapeWireframe = true; - ds.mDrawVelocity = (m_DebugBits & kPhyDbgVelocity) != 0; m_System->DrawBodies(ds, m_DebugDraw.get()); } @@ -289,39 +292,86 @@ namespace Engine { } // 3) 手绘箭头(Jolt 无现成 API):重力 = 常量方向指示(长度固定), - // 加速度 = 速度差分(方向/长度随值,静止噪声阈值截断) + // 加速度 = 速度差分(方向/长度随值,静止噪声阈值截断)。 + // 走引擎通用 DebugDraw 服务。收集按渲染帧率跑(Step 末尾 + // 无步进帧也重画,每帧全量重建)→ duration 必须 0(当帧瞬 + // 时):此前沿用 step 率时代的 2×步长续期,高帧率下每根箭 + // 头叠 4-5 个滞后副本,物体一动副本脱锚拖尾——表现为断断 + // 续续乱跳 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); // 橙:加速度 + constexpr f32 kVelScale = 0.2f; // 速度箭头比例(单位/速度) + constexpr f32 kDbgDur = 0.0f; // 渲染帧率收集 → 瞬时图元 + constexpr glm::vec4 kGravityColor = Debug::Rgb(0xDC3C3C); // 红:重力 + constexpr glm::vec4 kAccelColor = Debug::Rgb(0xE0640A); // 深橙:加速度 + constexpr glm::vec4 kVelColor = Debug::Rgb(0x5ADC5A); // 绿:速度 + constexpr glm::vec4 kContactColor = Debug::Rgb(0xD9C33C); // 黄:接触反力 + auto ToG = [](const JPH::RVec3Arg v) { + return glm::vec3{static_cast(v.GetX()), + static_cast(v.GetY()), + static_cast(v.GetZ())}; + }; for (auto &[bits, rec]: m_Bodies) { if (rec.type != static_cast(ECS::RigidBodyComponent::BodyType::Dynamic)) { continue; } - const JPH::RVec3 com = bi.GetCenterOfMassPosition(rec.id); + const glm::vec3 com = ToG(bi.GetCenterOfMassPosition(rec.id)); + + // 速度箭头(绿):零速阈值——Jolt 原生 DrawBodies 无零速保 + // 护(静止零向量箭头退化成 +X),此处阈值手画 + if ((m_DebugBits & kPhyDbgVelocity) != 0) { + const JPH::Vec3 v = bi.GetLinearVelocity(rec.id); + if (v.LengthSq() > 1e-4f) { // >0.01 m/s 才画 + Debug::Arrow(com, com + ToG(JPH::RVec3(v * kVelScale)), + kVelColor, + /*headSize*/ 0.12f, /*width*/ 2.0f, + /*duration*/ kDbgDur, /*depthTest*/ false); + } + } 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); + Debug::Arrow( + com, + com + ToG(JPH::RVec3(g * (kGravityLen / len))), + kGravityColor, + /*headSize*/ 0.12f, /*width*/ 2.0f, + /*duration*/ kDbgDur, /*depthTest*/ false); } } if ((m_DebugBits & kPhyDbgAccel) != 0) { + // 接触合力(比力 F/m = a − g,从测量加速度反推):静置 + // = +g 纯法向(与重力箭头等长反向 = 力平衡);滑动出现 + // 水平分量(摩擦);自由落体 ≈ 0 不画。长度按重力箭头 + // 同一比例归一 → 静置红下黄上视觉等长 + const JPH::Vec3 gB = gravity * bi.GetGravityFactor(rec.id); + const glm::vec3 spec = rec.lastAccel - + glm::vec3(gB.GetX(), gB.GetY(), gB.GetZ()); + const f32 smag = glm::length(spec); + if (smag > 0.2f && gravity.Length() > 1e-4f) { + const f32 slen = std::min( + smag * kGravityLen / gravity.Length(), kAccelMaxLen); + Debug::Arrow(com, com + (spec / smag) * slen, + kContactColor, + /*headSize*/ 0.12f, /*width*/ 2.0f, + /*duration*/ kDbgDur, /*depthTest*/ false); + } + 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)); + Debug::Arrow(com, com + (rec.lastAccel / mag) * len, + kAccelColor, + /*headSize*/ 0.12f, + /*width*/ 2.0f, + /*duration*/ kDbgDur, /*depthTest*/ false); } } } @@ -334,6 +384,10 @@ namespace Engine { } } + // 角色运动类线条(速度/加速度)一律归用户脚本画(Unity + // CharacterController 同语义:引擎只画 collider,移动调试归 + // 移动逻辑的拥有者);物理侧不保留任何角色箭头 + // 释放上一帧缓存不再引用的通用几何 batch(DebugRenderer 内部池) m_DebugDraw->NextFrame(); } @@ -675,7 +729,12 @@ namespace Engine { if (tr == nullptr) { continue; } - entry.cc->Update(dt, gravity, *m_TempAllocator); + // 每角色重力倍率(组件直读:Inspector 调完下个物理步即生效, + // 不触发重建——重力只是 Update 的入参,不是形状参数) + const auto *cc = reg.try_get( + static_cast(bits)); + const f32 gscale = cc != nullptr ? cc->gravityScale : 1.0f; + entry.cc->Update(dt, gravity * gscale, *m_TempAllocator); const glm::vec3 p = entry.cc->GetPosition(); // 原点 = 脚底(世界系) // 写回走 world→local 换算(层级实体挂父下仍正确;标脏 → // UpdateTransforms 重算 world) @@ -1047,6 +1106,12 @@ namespace Engine { : glm::vec3{0.0f}; } + glm::vec3 PhysicsWorld::GetCharacterPosition(const u32 entityBits) const { + const auto found = m_Characters.find(entityBits); + return found != m_Characters.end() ? found->second.cc->GetPosition() + : glm::vec3{0.0f}; + } + // ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)---- void PhysicsWorld::Writeback() { diff --git a/Engine/src/Renderer/Passes/PhysicsDebugPass.cpp b/Engine/src/Renderer/Passes/PhysicsDebugPass.cpp index 09fa6b2..d6097f6 100644 --- a/Engine/src/Renderer/Passes/PhysicsDebugPass.cpp +++ b/Engine/src/Renderer/Passes/PhysicsDebugPass.cpp @@ -64,9 +64,11 @@ namespace Engine::Passes FrameSceneData& frame, RG::TextureHandle hdrColor, RG::TextureHandle depth, - UniformBuffer& lineVB, UniformBuffer& triVB) + UniformBuffer& lineVB, UniformBuffer& triVB, + UniformBuffer& debugVB) : m_Device(device), m_Descriptors(descriptors), m_Frame(frame), - m_HDRColor(hdrColor), m_Depth(depth), m_LineVB(lineVB), m_TriVB(triVB) + m_HDRColor(hdrColor), m_Depth(depth), m_LineVB(lineVB), m_TriVB(triVB), + m_DebugVB(debugVB) { // 无描述符 set:viewProj 走 push constant(shader 侧声明一致) m_Layout = descriptors.CreatePipelineLayout( @@ -98,7 +100,8 @@ namespace Engine::Passes } void PhysicsDebugPass::PrepareFrame(u32 frameSlot, - const PhysicsDebugFrame& src) + const PhysicsDebugFrame& src, + const Debug::DebugFrame* dbg) { // 预算截断:物理调试不应打满(打满说明该收敛可视化了) const u32 lineVerts = std::min( @@ -115,18 +118,40 @@ namespace Engine::Passes // fence 已等(Renderer::Render 的 WaitFence 之后调用), // 写-读竞争关闭——与 UBO/SSBO 上传同一窗口语义 + m_Spans[frameSlot].clear(); if (lineVerts > 0) { m_LineVB.Update(frameSlot, src.lines.data(), static_cast(lineVerts) * sizeof(PhysicsDebugVertex)); + m_Spans[frameSlot].push_back({false, 1.0f, true, 0, lineVerts}); } if (triVerts > 0) { m_TriVB.Update(frameSlot, src.triangles.data(), static_cast(triVerts) * sizeof(PhysicsDebugVertex)); + m_Spans[frameSlot].push_back({true, 1.0f, true, 0, triVerts}); } m_LineCounts[frameSlot] = lineVerts; m_TriCounts[frameSlot] = triVerts; + + // 通用调试源(独立 ring VB,与物理源隔离):批次按收集侧的 + // (拓扑, 线宽, 深度) 原样展开为绘制段;超预算部分截断 + if (dbg != nullptr && !dbg->Empty() && !dbg->verts.empty()) { + const u32 total = static_cast(dbg->verts.size()); + const u32 n = std::min(total, Debug::kMaxVerts); + m_DebugVB.Update(frameSlot, dbg->verts.data(), + static_cast(n) * sizeof(Debug::Vertex)); + u32 consumed = 0; + for (const Debug::Batch& b : dbg->batches) { + if (consumed >= n) break; + const u32 take = std::min(b.count, n - consumed); + if (take > 0) { + m_Spans[frameSlot].push_back( + {b.tris, b.width, b.depthTest, consumed, take}); + } + consumed += take; + } + } } void PhysicsDebugPass::Register(RG::RenderGraph& rg) @@ -134,7 +159,10 @@ namespace Engine::Passes 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; // 空帧零开销 + const std::vector& spans = m_Spans[ctx.frame.frameSlot]; + if (lineCount == 0 && triCount == 0 && spans.empty()) { + return; // 空帧零开销 + } ZoneScopedN("PhysicsDebug"); const vk::CommandBuffer cmd = ctx.cmd; @@ -236,6 +264,29 @@ namespace Engine::Passes cmd.draw(triCount, 1, 0, 0); } + // 通用调试源批次:逐批动态状态(拓扑/线宽/深度测试)。 + // 线宽 >1 需 wideLines 特性,不支持时钳回 1.0(VUID-07937) + if (!spans.empty()) { + const std::array bufs{m_DebugVB.Get(ctx.frame.frameSlot)}; + const std::array offs{0}; + cmd.bindVertexBuffers(0, bufs, offs); + for (const Span& s : spans) { + cmd.setPrimitiveTopology( + s.tris ? vk::PrimitiveTopology::eTriangleList + : vk::PrimitiveTopology::eLineList); + // 线宽单位 = 像素,设备范围 [1.0, lineWidthRange[1]]: + // 低于下限的值(如 0.18)是校验错误,驱动按 1px 处 + // 理——必须钳进范围;wideLines 不支持则恒 1px + cmd.setLineWidth(m_Device.IsWideLinesSupported() + ? std::clamp(s.width, 1.0f, + m_Device.MaxLineWidth()) + : 1.0f); + cmd.setDepthTestEnable(s.depthTest ? VK_TRUE : VK_FALSE); + cmd.draw(s.count, 1, s.first, 0); + } + cmd.setLineWidth(1.0f); // 恢复默认,防状态外泄 + } + cmd.endRendering(); }) // Write use(WAW 链):保证排在 Forward/ForwardTrans 之后、 diff --git a/Engine/src/Vulkan/Device.cpp b/Engine/src/Vulkan/Device.cpp index 749a169..5a11a85 100644 --- a/Engine/src/Vulkan/Device.cpp +++ b/Engine/src/Vulkan/Device.cpp @@ -170,6 +170,12 @@ void Device::PickPhysicalDevice() m_SamplerAnisotropySupported = features2.features.samplerAnisotropy == VK_TRUE; m_MaxSamplerAnisotropy = props.limits.maxSamplerAnisotropy; + // 宽线(core 特性位;调试图元 >1 线宽必需,桌面 GPU 普遍支持) + m_WideLinesSupported = features2.features.wideLines == VK_TRUE; + // 线宽设备范围(像素):[0]=下限(普遍 1.0),[1]=上限——debug 线 + // 宽钳制用。wideLines 不支持时只有 1.0 合法 + m_MaxLineWidth = props.limits.lineWidthRange[1]; + // core 特性:MDI(drawCount>1)必需 m_MultiDrawIndirectSupported = features2.features.multiDrawIndirect == VK_TRUE; // 1.2 特性:drawIndexedIndirectCount 必需(VUID-04445) @@ -192,6 +198,8 @@ void Device::PickPhysicalDevice() ENGINE_INFO(" Dynamic Rendering Local Read (1.4): {}", localReadSupported ? "Yes" : "No"); ENGINE_INFO(" Push Descriptor (1.4): {}", pushDescriptorSupported ? "Yes" : "No"); ENGINE_INFO(" Shader Object (EXT): {}", m_ShaderObjectSupported ? "Yes" : "No"); + ENGINE_INFO(" Wide Lines (1.0 core): {}", + m_WideLinesSupported ? "Yes" : "No"); ENGINE_INFO(" Multi Draw Indirect (1.0 core): {}", m_MultiDrawIndirectSupported ? "Yes" : "No"); ENGINE_INFO(" Draw Indirect Count (1.2): {}", @@ -261,6 +269,8 @@ void Device::CreateLogicalDevice() features.setMultiDrawIndirect(m_MultiDrawIndirectSupported ? VK_TRUE : VK_FALSE); // 纹理各向异性过滤(材质掠射角清晰度) features.setSamplerAnisotropy(m_SamplerAnisotropySupported ? VK_TRUE : VK_FALSE); + // 宽线(调试图元线宽 >1;不支持时 pass 侧钳回 1.0) + features.setWideLines(m_WideLinesSupported ? VK_TRUE : VK_FALSE); // ======================================================== // 启用 1.4 特性链