// // 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 #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; } } }; // Jolt ↔ glm 四元数直转(TransformComponent 旋转存 glm::quat, // UE FTransform 语义):物理同步/写回不再经欧拉往返 → 姿态 // 零漂移(分量序:Jolt ctor (x,y,z,w),glm ctor (w,x,y,z)) JPH::Quat ToJolt(const glm::quat &q) { return {q.x, q.y, q.z, q.w}; } glm::quat ToGlm(const JPH::Quat &q) { return {q.GetW(), q.GetX(), q.GetY(), q.GetZ()}; } // 变更检测快照取值:collider 决定哪组参数生效(collider 本身变更 // 在外层先触发重建,到这里时语义已对齐)。Box/Mesh/Compound 不用 void ExtractShapeParams(const ECS::RigidBodyComponent &rb, f32 &radius, f32 &halfHeight) { using CT = ECS::RigidBodyComponent::ColliderType; switch (rb.collider) { case CT::Sphere: radius = rb.sphereRadius; break; case CT::Capsule: radius = rb.capsuleRadius; halfHeight = rb.capsuleHalfHeight; break; case CT::Cylinder: radius = rb.cylinderRadius; halfHeight = rb.cylinderHalfHeight; break; default: break; } } // 球/胶囊/圆柱类径向形状的缩放取 max 轴(不可各向异性) f32 UniformScale(const glm::vec3 &s) { return std::max({s.x, s.y, s.z}); } } // 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(); // 角色间碰撞互查表(内部 Array 走 JPH 分配器,须在 call_once 之后) m_VsCharacterCollision = 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, 65536, 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_Characters.clear(); // 角色析构回 m_VsCharacterCollision 摘除,先于 Jolt 反注册 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(); UpdateCharacters(kFixedDt); // 角色扫掠(Jolt 官方建议:物理更新后) 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* 后续加这里)。 // 速度箭头不交 Jolt 画——其 BodyManager::DrawBodies 无零速保 // 护,静止刚体的零向量箭头退化成 +X 方向(本次修复的突兀箭 // 头)→ 统一在下方手画并加阈值 if ((m_DebugBits & kPhyDbgColliders) != 0) { JPH::BodyManager::DrawSettings ds; ds.mDrawShape = true; ds.mDrawShapeWireframe = true; m_System->DrawBodies(ds, m_DebugDraw.get()); } // 2) 约束(关节;软体/载具约束接入后自动生效) if ((m_DebugBits & kPhyDbgConstraints) != 0) { m_System->DrawConstraints(m_DebugDraw.get()); } // 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; 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 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) { 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 f32 len = std::min(mag * kAccelScale, kAccelMaxLen); Debug::Arrow(com, com + (rec.lastAccel / mag) * len, kAccelColor, /*headSize*/ 0.12f, /*width*/ 2.0f, /*duration*/ kDbgDur, /*depthTest*/ false); } } } // 4) 角色控制器:CharacterVirtual 非 body,DrawBodies 画不到, // 手动画胶囊线框(绿 = 与普通 collider 区分) if ((m_DebugBits & kPhyDbgColliders) != 0) { for (auto &[bits, entry] : m_Characters) { entry.cc->DebugDraw(*m_DebugDraw); } } // 角色运动类线条(速度/加速度)一律归用户脚本画(Unity // CharacterController 同语义:引擎只画 collider,移动调试归 // 移动逻辑的拥有者);物理侧不保留任何角色箭头 // 释放上一帧缓存不再引用的通用几何 batch(DebugRenderer 内部池) m_DebugDraw->NextFrame(); } // ---- 对账 ---- void PhysicsWorld::SyncScene() { auto ® = m_Scene.GetRegistry(); auto &bi = m_System->GetBodyInterface(); std::unordered_set invalidated; // 本帧销毁/重建 body → 约束失效传染 // 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); m_BodyToEntity.erase(it->second.id.GetIndexAndSequenceNumber()); invalidated.insert(it->first); 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); // 禁用 = 视为组件不存在:销毁 body(对账自然消化),重启用即重建 if (!rb.active) { if (found != m_Bodies.end()) { bi.RemoveBody(found->second.id); bi.DestroyBody(found->second.id); m_BodyToEntity.erase(found->second.id.GetIndexAndSequenceNumber()); m_Bodies.erase(found); invalidated.insert(bits); } rb.bodyValid = false; continue; } if (found == m_Bodies.end() || !rb.bodyValid) { if (found != m_Bodies.end()) { bi.RemoveBody(found->second.id); bi.DestroyBody(found->second.id); m_BodyToEntity.erase(found->second.id.GetIndexAndSequenceNumber()); m_Bodies.erase(found); invalidated.insert(bits); } CreateBody(bits, rb, tr); continue; } auto &rec = found->second; // 非 const:静态/运动体拖动时更新 lastPos/lastRot f32 radius = 0.0f, halfHeight = 0.0f; ExtractShapeParams(rb, radius, halfHeight); if (rec.type != static_cast(rb.type) || rec.collider != static_cast(rb.collider) || rec.colliderSize != rb.boxHalfExtents || rec.radius != radius || rec.halfHeight != halfHeight || rec.compound != rb.compound || rec.mass != rb.mass || rec.isTrigger != rb.isTrigger) { // 类型/碰撞体/质量变更 → 重建(简单正确,重置速度可接受) bi.RemoveBody(rec.id); bi.DestroyBody(rec.id); m_BodyToEntity.erase(found->second.id.GetIndexAndSequenceNumber()); m_Bodies.erase(found); invalidated.insert(bits); 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); // 编辑器拖动 Dynamic 体(暂停期/物理未步进的帧)→ 传送 + // 清速度(UE 编辑器语义:编辑器操作权威于物理 body,拖动 = // 重新放置)。物理模拟中 Writeback 每步同步 lastPos/lastRot // → 本比对只在真正拖动帧为真,不会打断自由模拟。 // body 是世界系:读 world 缓存(层级实体取父链组合结果) if (rec.lastPos != tr.GetWorldPosition() || rec.lastRot != tr.GetWorldRotationQuat()) { bi.SetPositionAndRotation( id, JPH::RVec3(tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z), ToJolt(tr.GetWorldRotationQuat()), JPH::EActivation::Activate); bi.SetLinearVelocity(id, JPH::Vec3(0.0f, 0.0f, 0.0f)); bi.SetAngularVelocity(id, JPH::Vec3(0.0f, 0.0f, 0.0f)); rec.lastPos = tr.GetWorldPosition(); rec.lastRot = tr.GetWorldRotationQuat(); } } else if (rec.lastPos != tr.GetWorldPosition() || rec.lastRot != tr.GetWorldRotationQuat()) { // 编辑器拖动静态/运动学体 → 传送 body bi.SetPositionAndRotation(id, JPH::RVec3(tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z), ToJolt(tr.GetWorldRotationQuat()), JPH::EActivation::DontActivate); rec.lastPos = tr.GetWorldPosition(); rec.lastRot = tr.GetWorldRotationQuat(); } } // 3. 约束对账(body 循环之后:依赖新建 body 的最终 ID) SyncConstraints(invalidated); // 4. 角色对账(非刚体,不依赖 body 生命周期) SyncCharacters(); } // ---- 约束对账 ---- void PhysicsWorld::SyncConstraints(const std::unordered_set &invalidated) { auto ® = m_Scene.GetRegistry(); const auto tainted = [&invalidated](const u32 bits) { return invalidated.count(bits); }; // 1. owner 实体/组件没了(或被禁用),或任一端 body 本帧被销毁/重建 → 移除 for (auto it = m_Constraints.begin(); it != m_Constraints.end();) { const u32 owner = it->first; const auto ownerEnt = static_cast(owner); const ECS::JointComponent* jc = reg.valid(ownerEnt) ? reg.try_get(ownerEnt) : nullptr; const bool ownerGone = jc == nullptr || !jc->active; if (ownerGone || tainted(owner) || tainted(it->second.targetBits)) { m_System->RemoveConstraint(it->second.constraint.GetPtr()); it = m_Constraints.erase(it); } else { ++it; } } // 2. 遍历组件:快照变更 → 重建;缺失 → 创建;禁用 → 跳过(上面已清)。 // target 惰性按 UUID 反查(UUID 失联/body 未就绪 → 下帧再试) auto view = reg.view(); for (auto [e, jc]: view.each()) { if (!jc.active) { continue; } const u32 bits = static_cast(entt::to_integral(e)); const auto found = m_Constraints.find(bits); if (found != m_Constraints.end()) { if (found->second.snapshot == jc) { continue; } m_System->RemoveConstraint(found->second.constraint.GetPtr()); m_Constraints.erase(found); } const ECS::Entity target = m_Scene.FindEntityByUUID(jc.target); if (target == entt::null) { continue; } const u32 targetBits = static_cast(entt::to_integral(target)); if (bits == targetBits || !m_Bodies.contains(bits) || !m_Bodies.contains(targetBits)) { continue; } CreateConstraint(bits, targetBits, jc); } } void PhysicsWorld::CreateConstraint(u32 ownerBits, u32 targetBits, const ECS::JointComponent &jc) { auto ® = m_Scene.GetRegistry(); const auto &b1 = m_Bodies.find(ownerBits)->second; const auto &b2 = m_Bodies.find(targetBits)->second; // 锚点(Transform 局部系)× 本体缩放 → COM 局部系 //(EConstraintSpace::LocalToBodyCOM 要求减 Shape::GetCenterOfMass, // Compound 等形状 COM 会偏离原点;Box/Sphere/Capsule 为零) const auto *tr1 = reg.try_get( static_cast(ownerBits)); const auto *tr2 = reg.try_get( static_cast(targetBits)); if (tr1 == nullptr || tr2 == nullptr) { return; } const glm::vec3 s1 = tr1->GetWorldScale(); const glm::vec3 s2 = tr2->GetWorldScale(); // 双写锁读位姿/COM + 创建(SyncScene 在主线程,与 Update 不并发) JPH::BodyLockWrite lock1(m_System->GetBodyLockInterface(), b1.id); JPH::BodyLockWrite lock2(m_System->GetBodyLockInterface(), b2.id); if (!lock1.Succeeded() || !lock2.Succeeded()) { return; } JPH::Body &body1 = lock1.GetBody(); JPH::Body &body2 = lock2.GetBody(); const JPH::Quat r1 = body1.GetRotation(); const JPH::Quat r2 = body2.GetRotation(); const JPH::Vec3 com1 = body1.GetShape()->GetCenterOfMass(); const JPH::Vec3 com2 = body2.GetShape()->GetCenterOfMass(); const JPH::RVec3 p1(jc.anchorA.x * s1.x - com1.GetX(), jc.anchorA.y * s1.y - com1.GetY(), jc.anchorA.z * s1.z - com1.GetZ()); const JPH::RVec3 p2(jc.anchorB.x * s2.x - com2.GetX(), jc.anchorB.y * s2.y - com2.GetY(), jc.anchorB.z * s2.z - com2.GetZ()); JPH::Ref constraint; if (jc.type == ECS::JointComponent::Type::Fixed) { // 轴框架 = 同一世界姿态在各体局部系的表达 → 锁定创建时相对姿态 JPH::FixedConstraintSettings s; s.mSpace = JPH::EConstraintSpace::LocalToBodyCOM; s.mPoint1 = p1; s.mPoint2 = p2; s.mAxisX1 = r1.Conjugated() * JPH::Vec3::sAxisX(); s.mAxisY1 = r1.Conjugated() * JPH::Vec3::sAxisY(); s.mAxisX2 = r2.Conjugated() * JPH::Vec3::sAxisX(); s.mAxisY2 = r2.Conjugated() * JPH::Vec3::sAxisY(); constraint = s.Create(body1, body2); } else if (jc.type == ECS::JointComponent::Type::Hinge) { // 转轴:本体局部轴 → 世界(不吃缩放,归一化),再投到两体局部系; // 法线任取与轴垂直的世界方向,保证两体框架共轴共面 JPH::HingeConstraintSettings s; s.mSpace = JPH::EConstraintSpace::LocalToBodyCOM; s.mPoint1 = p1; s.mPoint2 = p2; const JPH::Vec3 localAxis = JPH::Vec3( jc.hingeAxis.x, jc.hingeAxis.y, jc.hingeAxis.z).Normalized(); const JPH::Vec3 worldAxis = r1 * localAxis; JPH::Vec3 helper = std::abs(worldAxis.GetY()) < 0.99f ? JPH::Vec3::sAxisY() : JPH::Vec3::sAxisX(); const JPH::Vec3 worldNormal = worldAxis.Cross(helper).Normalized(); s.mHingeAxis1 = r1.Conjugated() * worldAxis; s.mNormalAxis1 = r1.Conjugated() * worldNormal; s.mHingeAxis2 = r2.Conjugated() * worldAxis; s.mNormalAxis2 = r2.Conjugated() * worldNormal; if (jc.enableLimit) { s.mLimitsMin = std::clamp(glm::radians(jc.limitMin), -JPH::JPH_PI, 0.0f); s.mLimitsMax = std::clamp(glm::radians(jc.limitMax), 0.0f, JPH::JPH_PI); } constraint = s.Create(body1, body2); } else { // Ball = PointConstraint(球窝:三旋转自由度全放开) JPH::PointConstraintSettings s; s.mSpace = JPH::EConstraintSpace::LocalToBodyCOM; s.mPoint1 = p1; s.mPoint2 = p2; constraint = s.Create(body1, body2); } m_System->AddConstraint(constraint.GetPtr()); ConstraintRecord rec; rec.constraint = std::move(constraint); rec.snapshot = jc; rec.targetBits = targetBits; m_Constraints[ownerBits] = rec; } void PhysicsWorld::RemoveConstraint(const u32 ownerBits) { const auto found = m_Constraints.find(ownerBits); if (found == m_Constraints.end()) { return; } m_System->RemoveConstraint(found->second.constraint.GetPtr()); m_Constraints.erase(found); // Ref 析构 → 引用计数释放 } // ---- 角色控制器 ---- void PhysicsWorld::SyncCharacters() { auto ® = m_Scene.GetRegistry(); // 1. 孤儿清理:实体销毁 / 组件被移除或禁用(Ref 析构即清理,无 body 负担) for (auto it = m_Characters.begin(); it != m_Characters.end();) { const auto e = static_cast(it->first); const auto* cc = reg.valid(e) ? reg.try_get(e) : nullptr; if (cc == nullptr || !cc->active || !reg.all_of(e)) { it = m_Characters.erase(it); } else { ++it; } } // 2. 遍历:缺失 → 创建;参数变更 → 重建;编辑器拖动 → 传送;禁用 → 跳过 auto view = reg.view(); for (auto [e, cc, tr]: view.each()) { if (!cc.active) { continue; } const u32 bits = static_cast(entt::to_integral(e)); CharacterController::Params p; p.radius = cc.radius; p.halfHeight = cc.capsuleHalfHeight; p.mass = cc.mass; p.maxSlopeAngle = cc.maxSlopeAngle; p.stepHeight = cc.stepHeight; p.shapeOffset = cc.shapeOffset; auto found = m_Characters.find(bits); if (found == m_Characters.end()) { CharacterEntry entry; entry.cc = std::make_unique( *m_System, p, tr.GetWorldPosition()); entry.cc->SetCharacterCollision(*m_VsCharacterCollision); entry.lastPos = tr.GetWorldPosition(); m_Characters.emplace(bits, std::move(entry)); continue; } if (found->second.cc->GetParams() != p) { // 参数变更 → 重建(位置沿用当前 Transform);旧实例析构 // 自动摘除旧条目,新实例重新登记 found->second.cc = std::make_unique( *m_System, p, tr.GetWorldPosition()); found->second.cc->SetCharacterCollision(*m_VsCharacterCollision); found->second.lastPos = tr.GetWorldPosition(); continue; } if (found->second.lastPos != tr.GetWorldPosition()) { // 编辑器拖动实体 → 传送角色(角色是唯一位置源,反向覆盖) found->second.cc->Teleport(tr.GetWorldPosition()); found->second.lastPos = tr.GetWorldPosition(); } } } void PhysicsWorld::UpdateCharacters(const f32 dt) { auto ® = m_Scene.GetRegistry(); const JPH::Vec3 gravity = m_System->GetGravity(); for (auto &[bits, entry]: m_Characters) { auto *tr = reg.try_get( static_cast(bits)); if (tr == nullptr) { continue; } // 每角色重力倍率(组件直读: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) m_Scene.SetWorldPose( static_cast(bits), p, tr->GetWorldRotationQuat(), tr->GetWorldScale()); entry.lastPos = p; // ---- 角色碰撞事件合成(Enter/Exit)---- // CharacterVirtual 非刚体,Jolt 的 ContactListener 不会为角色 // 回调——脚本因此收不到"玩家撞墙"。这里读角色本步活跃接触, // 与上一步集合对比出边沿,合成与刚体同款的 CollisionEvent // (a = 角色实体,b = 被撞实体)。只处理刚体接触(mBodyB 有效): // 角色↔角色由 CharacterVsCharacterCollision 的物理互查处理, // 不发脚本事件(对方是角色时 mBodyB 无效,天然跳过) std::unordered_set now; now.reserve(entry.contacts.size() + 4); for (const JPH::CharacterContact& c : entry.cc->GetActiveContacts()) { if (c.mBodyB.IsInvalid()) continue; // 对方非刚体(角色) if (c.mWasDiscarded) continue; // 被丢弃/传感器 if (c.mDistance > 0.0f) continue; // >0 = 预测接触,未真相交 u32 other = 0; if (!TryBitsByBodyId(c.mBodyB, other)) continue; now.insert(other); if (entry.contacts.count(other) == 0) { const glm::vec3 pt( static_cast(c.mPosition.GetX()), static_cast(c.mPosition.GetY()), static_cast(c.mPosition.GetZ())); const glm::vec3 n(c.mContactNormal.GetX(), c.mContactNormal.GetY(), c.mContactNormal.GetZ()); QueueEvent({bits, other, CollisionEvent::Kind::Added, pt, n}); } } for (const u32 prev : entry.contacts) { if (now.count(prev) == 0) { QueueEvent( {bits, prev, CollisionEvent::Kind::Removed, {}, {}}); } } entry.contacts = std::move(now); } } // ---- 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 || !mc->model.Valid()) { return nullptr; // 无效句柄(未指派)不进 Get(对无效 FATAL) } const Model *model = m_Assets->GetModel(mc->model); if (model == nullptr) { return nullptr; } // 缩放烘进顶点:Jolt shape 无缩放语义,body 只吃 pos/rot。 // 取 world 缩放(层级实体 = 父链累计) const glm::vec3 s = tr.GetWorldScale(); // 动态/运动体 → ConvexHull(MeshShape 三角网格太贵,Jolt 不允许动) if (rb.type != ECS::RigidBodyComponent::BodyType::Static) { JPH::ConvexHullShapeSettings cs; const auto &meshes = model->GetMeshes(); for (size_t mi = 0; mi < meshes.size(); ++mi) { // 单 mesh 子引用:碰撞只取渲染可见的同一 mesh(语义跟随) if (mc->meshIndex >= 0 && static_cast(mi) != mc->meshIndex) { continue; } for (const auto &p: meshes[mi]->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; const auto &meshes = model->GetMeshes(); for (size_t mi = 0; mi < meshes.size(); ++mi) { // 单 mesh 子引用:碰撞只取渲染可见的同一 mesh(语义跟随) if (mc->meshIndex >= 0 && static_cast(mi) != mc->meshIndex) { continue; } const auto &pos = meshes[mi]->GetCpuPositions(); const auto &idx = meshes[mi]->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(); } JPH::RefConst PhysicsWorld::BuildCompoundShape( ECS::RigidBodyComponent &rb, ECS::TransformComponent &tr) { if (rb.compound.empty()) { return nullptr; } // 缩放烘焙:child 局部位置按轴缩放,尺寸按 max 轴(球/胶囊径向 // 不可各向异性)。rotation 存数据与序列化(首版 Inspector 不编辑) const glm::vec3 s = tr.GetScale(); JPH::StaticCompoundShapeSettings cs; for (const auto &child: rb.compound) { JPH::RefConst sub; const f32 us = UniformScale(s); switch (child.shape) { case ECS::RigidBodyComponent::CompoundChild::Shape::Sphere: sub = new JPH::SphereShape( std::max(child.radius * us, 0.01f)); break; case ECS::RigidBodyComponent::CompoundChild::Shape::Capsule: sub = new JPH::CapsuleShape( std::max(child.radius * us, 0.01f), std::max(child.capsuleHalfHeight * us, 0.01f)); break; default: // Box sub = new JPH::BoxShape(JPH::Vec3( std::max(child.boxHalfExtents.x * s.x, 0.01f), std::max(child.boxHalfExtents.y * s.y, 0.01f), std::max(child.boxHalfExtents.z * s.z, 0.01f))); break; } cs.AddShape(JPH::Vec3(child.position.x * s.x, child.position.y * s.y, child.position.z * s.z), ToJolt(child.rotation), sub); } const JPH::Shape::ShapeResult res = cs.Create(); if (res.HasError()) { ENGINE_WARN("[Physics] CompoundShape 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 直接带缩放建形状)。 // 注意 Jolt 两 ctor 参数序相反:CapsuleShape(半径, 圆柱段半高)、 // CylinderShape(圆柱段半高, 半径) JPH::RefConst shape; const f32 uniformScale = UniformScale(tr.GetScale()); if (rb.collider == ECS::RigidBodyComponent::ColliderType::Sphere) { shape = new JPH::SphereShape(std::max(rb.sphereRadius * uniformScale, 0.01f)); } else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Capsule) { shape = new JPH::CapsuleShape( std::max(rb.capsuleRadius * uniformScale, 0.01f), std::max(rb.capsuleHalfHeight * uniformScale, 0.01f)); } else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Cylinder) { shape = new JPH::CylinderShape( std::max(rb.cylinderHalfHeight * uniformScale, 0.01f), std::max(rb.cylinderRadius * uniformScale, 0.01f)); } else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Compound) { // 子形状列表 → StaticCompoundShape;空列表/构建失败回退 Box shape = BuildCompoundShape(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 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.GetWorldScale().x, 0.01f), std::max(rb.boxHalfExtents.y * tr.GetWorldScale().y, 0.01f), std::max(rb.boxHalfExtents.z * tr.GetWorldScale().z, 0.01f))); } } else { shape = new JPH::BoxShape(JPH::Vec3( std::max(rb.boxHalfExtents.x * tr.GetWorldScale().x, 0.01f), std::max(rb.boxHalfExtents.y * tr.GetWorldScale().y, 0.01f), std::max(rb.boxHalfExtents.z * tr.GetWorldScale().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.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z), ToJolt(tr.GetWorldRotationQuat()), 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.isTrigger; // 触发器:只收 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; ExtractShapeParams(rb, rec.radius, rec.halfHeight); rec.compound = rb.compound; rec.mass = rb.mass; rec.isTrigger = rb.isTrigger; rec.lastPos = tr.GetWorldPosition(); rec.lastRot = tr.GetWorldRotationQuat(); m_Bodies[entityBits] = rec; m_BodyToEntity[id.GetIndexAndSequenceNumber()] = entityBits; 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_BodyToEntity.erase(found->second.id.GetIndexAndSequenceNumber()); 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 { const auto& found = m_BodyToEntity.find(id.GetIndexAndSequenceNumber()); if (found == m_BodyToEntity.end()) return false; outBits = found->second; return true; } // ---- 射线查询 ---- 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; } // ---- 刚体驱动(游戏逻辑/编辑器调试;见头文件语义注释)---- const PhysicsWorld::BodyRecord *PhysicsWorld::FindRecord(const u32 entityBits) const { const auto found = m_Bodies.find(entityBits); return found != m_Bodies.end() ? &found->second : nullptr; } // 力/扭矩/冲量:仅动态体(Jolt 对静态/运动学体施加力无意义) void PhysicsWorld::AddForce(const u32 entityBits, const glm::vec3 &force) { const auto *rec = FindRecord(entityBits); if (!rec || rec->type != static_cast(ECS::RigidBodyComponent::BodyType::Dynamic)) return; m_System->GetBodyInterface().AddForce( rec->id, JPH::Vec3(force.x, force.y, force.z)); } void PhysicsWorld::AddImpulse(const u32 entityBits, const glm::vec3 &impulse) { const auto *rec = FindRecord(entityBits); if (!rec || rec->type != static_cast(ECS::RigidBodyComponent::BodyType::Dynamic)) return; m_System->GetBodyInterface().AddImpulse( rec->id, JPH::Vec3(impulse.x, impulse.y, impulse.z)); } void PhysicsWorld::AddTorque(const u32 entityBits, const glm::vec3 &torque) { const auto *rec = FindRecord(entityBits); if (!rec || rec->type != static_cast(ECS::RigidBodyComponent::BodyType::Dynamic)) return; m_System->GetBodyInterface().AddTorque( rec->id, JPH::Vec3(torque.x, torque.y, torque.z)); } // 速度直写:动态+运动学体都有效(运动学体速度即其运动方式) void PhysicsWorld::SetLinearVelocity(const u32 entityBits, const glm::vec3 &velocity) { const auto *rec = FindRecord(entityBits); if (!rec || rec->type == static_cast(ECS::RigidBodyComponent::BodyType::Static)) return; m_System->GetBodyInterface().SetLinearVelocity( rec->id, JPH::Vec3(velocity.x, velocity.y, velocity.z)); } void PhysicsWorld::SetAngularVelocity(const u32 entityBits, const glm::vec3 &angularVelocity) { const auto *rec = FindRecord(entityBits); if (!rec || rec->type == static_cast(ECS::RigidBodyComponent::BodyType::Static)) return; m_System->GetBodyInterface().SetAngularVelocity( rec->id, JPH::Vec3(angularVelocity.x, angularVelocity.y, angularVelocity.z)); } glm::vec3 PhysicsWorld::GetLinearVelocity(const u32 entityBits) const { const auto *rec = FindRecord(entityBits); if (!rec || rec->type == static_cast(ECS::RigidBodyComponent::BodyType::Static)) return {0.0f, 0.0f, 0.0f}; const JPH::Vec3 v = m_System->GetBodyInterface().GetLinearVelocity(rec->id); return {v.GetX(), v.GetY(), v.GetZ()}; } // ---- 角色控制器门面 ---- CharacterController *PhysicsWorld::FindCharacter(const u32 entityBits) { const auto found = m_Characters.find(entityBits); return found != m_Characters.end() ? found->second.cc.get() : nullptr; } void PhysicsWorld::SetCharacterVelocity(const u32 entityBits, const glm::vec3 &velocity) { if (CharacterController *cc = FindCharacter(entityBits)) { cc->SetLinearVelocity(velocity); } } void PhysicsWorld::Jump(const u32 entityBits, const f32 speed) { if (CharacterController *cc = FindCharacter(entityBits)) { cc->Jump(speed); } } bool PhysicsWorld::IsCharacterGrounded(const u32 entityBits) const { const auto found = m_Characters.find(entityBits); return found != m_Characters.end() && found->second.cc->IsGrounded(); } glm::vec3 PhysicsWorld::GetCharacterVelocity(const u32 entityBits) const { const auto found = m_Characters.find(entityBits); return found != m_Characters.end() ? found->second.cc->GetLinearVelocity() : 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}; } glm::vec3 PhysicsWorld::GetCharacterImpactVelocity(const u32 entityBits) const { const auto found = m_Characters.find(entityBits); return found != m_Characters.end() ? found->second.cc->GetSweepVelocity() : glm::vec3{0.0f}; } // ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)---- void PhysicsWorld::Writeback() { 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); const glm::vec3 worldPos(pos.GetX(), pos.GetY(), pos.GetZ()); const glm::quat worldRot = ToGlm(rot); // body 姿态是世界系:写回经 world→local 换算(挂父层级实体 // 不破坏相对位姿;根实体 = 直写)。标脏 → UpdateTransforms 重算 m_Scene.SetWorldPose( e, worldPos, worldRot, tr.GetWorldScale()); const JPH::Vec3 vel = bi.GetLinearVelocity(id); rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ()); // lastPos/lastRot 同步到 body 当前姿态:否则下帧 SyncScene 会把物理 // 位移误判为编辑器拖动 → teleport + 清零速度死循环。 // 注意:必须用 bi.GetPosition/GetRotation 的 fresh 值,不能读 // tr.GetWorldPosition()——SetWorldPose 只改 local+标脏,world cache // 要等本帧末尾 Scene::UpdateTransforms 才重算,此刻仍是旧值。 const u32 bits = static_cast(entt::to_integral(e)); if (auto it = m_Bodies.find(bits); it != m_Bodies.end()) { it->second.lastPos = worldPos; it->second.lastRot = worldRot; } } } } // namespace Engine