From 82910c41e86cf1b8f6b5f98ca33f8f2ea285940b Mon Sep 17 00:00:00 2001 From: atdunbg Date: Sat, 3 Oct 2026 16:26:20 +0800 Subject: [PATCH] physics: joint constraints (fixed/hinge/ball) - JointComponent: type, target UUID, per-body local anchors, hinge axis + angle limits (data only, zero Jolt deps) - PhysicsWorld::SyncConstraints: create/remove/rebuild on snapshot change; body rebuild contagion via invalidated-bits list; target resolved lazily by UUID each sync (no two-pass load needed) - Jolt FixedConstraint/HingeConstraint/PointConstraint in LocalToBodyCOM space (anchors minus shape COM); fixed frames lock creation-time relative pose; hinge axis aligned across bodies - Inspector: Joint section with target entity name dropdown - Serialization: Joint map with UUID target, as fallbacks --- App/editor/InspectorPanel.cpp | 57 +++++++ Engine/include/Engine/ECS/Components.h | 23 +++ Engine/include/Engine/Physics/PhysicsWorld.h | 16 ++ Engine/src/Physics/PhysicsWorld.cpp | 161 +++++++++++++++++++ Engine/src/Serialization/SceneSerializer.cpp | 39 +++++ 5 files changed, 296 insertions(+) diff --git a/App/editor/InspectorPanel.cpp b/App/editor/InspectorPanel.cpp index b3b1e64..17e9481 100644 --- a/App/editor/InspectorPanel.cpp +++ b/App/editor/InspectorPanel.cpp @@ -116,6 +116,11 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) { reg.emplace(selected); } + if (!reg.all_of(selected) && + ImGui::MenuItem("Joint")) + { + reg.emplace(selected); + } ImGui::EndPopup(); } ImGui::Separator(); @@ -334,6 +339,58 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) } } + // ---- Joint(约束:两端 body 就绪后由 PhysicsWorld 对账创建)---- + if (auto* jc = reg.try_get(selected)) + { + if (ImGui::CollapsingHeader("Joint", ImGuiTreeNodeFlags_DefaultOpen)) + { + int type = static_cast(jc->type); + if (ImGui::Combo("type", &type, "Fixed\0Hinge\0Ball\0")) + { + jc->type = static_cast(type); + } + + // target 实体下拉:实体名列表,选中态按 UUID 匹配(变更写回) + std::string items; + std::vector uuids; + int current = -1; + reg.view().each( + [&](entt::entity e, const Engine::ECS::IdComponent& id, + const Engine::ECS::NameComponent& n) + { + if (e == selected) + { + return; // 自连无意义 + } + if (id.uuid == jc->target) + { + current = static_cast(uuids.size()); + } + uuids.push_back(id.uuid); + items.append(n.name); + items.push_back('\0'); + }); + items.push_back('\0'); // ImGui Combo 双 \0 结尾 + if (ImGui::Combo("target", ¤t, items.c_str()) && current >= 0) + { + jc->target = uuids[static_cast(current)]; + } + + ImGui::DragFloat3("anchor A", &jc->anchorA.x, 0.05f); + ImGui::DragFloat3("anchor B", &jc->anchorB.x, 0.05f); + if (jc->type == Engine::ECS::JointComponent::Type::Hinge) + { + ImGui::DragFloat3("hinge axis", &jc->hingeAxis.x, 0.05f); + ImGui::Checkbox("enable limit", &jc->enableLimit); + if (jc->enableLimit) + { + ImGui::DragFloat("min deg", &jc->limitMin, 1.0f, -180.0f, 0.0f); + ImGui::DragFloat("max deg", &jc->limitMax, 1.0f, 0.0f, 180.0f); + } + } + } + } + // ---- Animation ---- if (auto* anim = reg.try_get(selected)) { diff --git a/Engine/include/Engine/ECS/Components.h b/Engine/include/Engine/ECS/Components.h index bde24b7..abe3dbc 100644 --- a/Engine/include/Engine/ECS/Components.h +++ b/Engine/include/Engine/ECS/Components.h @@ -236,6 +236,29 @@ namespace Engine::ECS { glm::vec3 velocity{0.0f}; // 只读调试视图:线速度(m/s) }; + // 约束/关节(Jolt TwoBodyConstraint 家族)。纯数据 POD 零 Jolt 依赖; + // 创建/对账在 PhysicsWorld::SyncConstraints(kPhyDbgConstraints 出图元)。 + // target 存 UUID(跨存档稳定),惰性解析——SyncScene 每帧按 UUID 反查 + // 实体:序列化无需两遍解析,UUID 再生后自然失联,加载顺序无关 + struct JointComponent { + enum class Type : u8 { Fixed = 0, Hinge = 1, Ball = 2 }; + Type type = Type::Fixed; + UUID target{}; // 目标实体(0 = 未设置,不建约束) + + // 锚点:各体 Transform 局部系(Unity connectedAnchor 语义, + // × 本体缩放烘焙)。Fixed/Hinge/Ball 都连这两点 + glm::vec3 anchorA{0.0f}; // 本体(挂组件的实体) + glm::vec3 anchorB{0.0f}; // 目标体 + + // Hinge 专用:转轴(本体局部系,方向量不吃缩放)+ 角度限位(度) + glm::vec3 hingeAxis{1.0f, 0.0f, 0.0f}; + bool enableLimit = false; + f32 limitMin = -90.0f; // Jolt 语义 min∈[-180,0] max∈[0,180] + f32 limitMax = 90.0f; + + bool operator==(const JointComponent&) const = default; + }; + } // namespace Engine::ECS #endif //HELLOVULKAN_COMPONENTS_H diff --git a/Engine/include/Engine/Physics/PhysicsWorld.h b/Engine/include/Engine/Physics/PhysicsWorld.h index d6498c6..bba3732 100644 --- a/Engine/include/Engine/Physics/PhysicsWorld.h +++ b/Engine/include/Engine/Physics/PhysicsWorld.h @@ -30,6 +30,7 @@ #include #include #include +#include // JPH::Ref #include #include @@ -114,6 +115,12 @@ namespace Engine { void CollectDebugData(); // 按 m_DebugBits 收集调试图元(渲染帧率) void CreateBody(u32 entityBits, ECS::RigidBodyComponent& rb, ECS::TransformComponent& tr); + // 约束对账:owner 孤儿清理 / body 重建传染失效 / 建缺失与变更重建 + // invalidated = 本帧 SyncScene 中被销毁或重建 body 的实体位模式 + void SyncConstraints(const std::vector& invalidated); + void CreateConstraint(u32 ownerBits, u32 targetBits, + const ECS::JointComponent& jc); + void RemoveConstraint(u32 ownerBits); // Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape / // Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box) JPH::RefConst BuildMeshShape(u32 entityBits, @@ -128,6 +135,15 @@ namespace Engine { void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁) bool TryBitsByBodyId(const JPH::BodyID& id, u32& outBits) const; + // ---- 约束(JointComponent → Jolt TwoBodyConstraint;owner 实体寻址)---- + // target 惰性解析 UUID;body 重建/销毁 → 引用它的约束失效重建 + struct ConstraintRecord { + JPH::Ref constraint; + ECS::JointComponent snapshot; // 变更检测 → 移除重建 + u32 targetBits = 0; + }; + std::unordered_map m_Constraints; + ECS::Scene& m_Scene; Assets::AssetManager* m_Assets = nullptr; // Mesh 碰撞体解析 modelRef bool m_Enabled = true; diff --git a/Engine/src/Physics/PhysicsWorld.cpp b/Engine/src/Physics/PhysicsWorld.cpp index 8fb7f38..83a8046 100644 --- a/Engine/src/Physics/PhysicsWorld.cpp +++ b/Engine/src/Physics/PhysicsWorld.cpp @@ -19,6 +19,9 @@ #include #include #include +#include +#include +#include #include #include @@ -331,6 +334,7 @@ namespace Engine { void PhysicsWorld::SyncScene() { auto ® = m_Scene.GetRegistry(); auto &bi = m_System->GetBodyInterface(); + std::vector invalidated; // 本帧销毁/重建 body → 约束失效传染 // 1. 孤儿清理:实体销毁 / 组件被移除 → 移除 body for (auto it = m_Bodies.begin(); it != m_Bodies.end();) { @@ -339,6 +343,7 @@ namespace Engine { !reg.all_of(e)) { bi.RemoveBody(it->second.id); bi.DestroyBody(it->second.id); + invalidated.push_back(it->first); it = m_Bodies.erase(it); } else { ++it; @@ -355,6 +360,7 @@ namespace Engine { bi.RemoveBody(found->second.id); bi.DestroyBody(found->second.id); m_Bodies.erase(found); + invalidated.push_back(bits); } CreateBody(bits, rb, tr); continue; @@ -375,6 +381,7 @@ namespace Engine { bi.RemoveBody(rec.id); bi.DestroyBody(rec.id); m_Bodies.erase(found); + invalidated.push_back(bits); rb.bodyValid = false; CreateBody(bits, rb, tr); continue; @@ -396,6 +403,160 @@ namespace Engine { rec.lastRot = tr.GetRotationQuat(); } } + + // 3. 约束对账(body 循环之后:依赖新建 body 的最终 ID) + SyncConstraints(invalidated); + } + + // ---- 约束对账 ---- + + void PhysicsWorld::SyncConstraints(const std::vector &invalidated) { + auto ® = m_Scene.GetRegistry(); + const auto tainted = [&invalidated](u32 bits) { + return std::find(invalidated.begin(), invalidated.end(), bits) + != invalidated.end(); + }; + + // 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 bool ownerGone = !reg.valid(ownerEnt) || + !reg.all_of(ownerEnt); + 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()) { + 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->GetScale(); + const glm::vec3 s2 = tr2->GetScale(); + + // 双写锁读位姿/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 析构 → 引用计数释放 } // ---- Mesh 碰撞体:视觉网格 → Jolt 形状 ---- diff --git a/Engine/src/Serialization/SceneSerializer.cpp b/Engine/src/Serialization/SceneSerializer.cpp index ff91d8b..de25e94 100644 --- a/Engine/src/Serialization/SceneSerializer.cpp +++ b/Engine/src/Serialization/SceneSerializer.cpp @@ -307,6 +307,27 @@ namespace Engine::Serialization { out << YAML::EndMap; } + if (const auto* jc = reg.try_get(e)) { + out << YAML::Key << "Joint" << YAML::Value + << YAML::BeginMap; + out << YAML::Key << "Type" << YAML::Value + << static_cast(jc->type); + // 实体引用存 UUID(跨存档稳定;target=0 未设置) + out << YAML::Key << "Target" << YAML::Value + << jc->target.Value(); + out << YAML::Key << "AnchorA" << YAML::Value; + EmitVec3(out, jc->anchorA); + out << YAML::Key << "AnchorB" << YAML::Value; + EmitVec3(out, jc->anchorB); + out << YAML::Key << "HingeAxis" << YAML::Value; + EmitVec3(out, jc->hingeAxis); + out << YAML::Key << "EnableLimit" << YAML::Value + << jc->enableLimit; + out << YAML::Key << "LimitMin" << YAML::Value << jc->limitMin; + out << YAML::Key << "LimitMax" << YAML::Value << jc->limitMax; + out << YAML::EndMap; + } + out << YAML::EndMap; // Components out << YAML::EndMap; // Entity } @@ -562,6 +583,24 @@ namespace Engine::Serialization { rb.isSensor = n["IsSensor"].as(rb.isSensor); // bodyId/bodyValid/velocity 保持默认(对账重建) } + + // Joint:target 只存 UUID,加载后由 PhysicsWorld 惰性反查 + //(两遍解析不需要——UUID 再生/加载顺序均由逐帧解析消化) + if (const YAML::Node n = comps["Joint"]) { + auto& jc = handle.AddComponent(); + if (const int type = n["Type"].as(0); + type >= 0 && type <= 2) { + jc.type = + static_cast(type); + } + jc.target = UUID{n["Target"].as(0)}; + jc.anchorA = ReadVec3(n["AnchorA"], jc.anchorA); + jc.anchorB = ReadVec3(n["AnchorB"], jc.anchorB); + jc.hingeAxis = ReadVec3(n["HingeAxis"], jc.hingeAxis); + jc.enableLimit = n["EnableLimit"].as(jc.enableLimit); + jc.limitMin = n["LimitMin"].as(jc.limitMin); + jc.limitMax = n["LimitMax"].as(jc.limitMax); + } } // 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms: