diff --git a/App/editor/InspectorPanel.cpp b/App/editor/InspectorPanel.cpp index 5c88694..b3b1e64 100644 --- a/App/editor/InspectorPanel.cpp +++ b/App/editor/InspectorPanel.cpp @@ -236,7 +236,8 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) rb->type = static_cast(type); } int collider = static_cast(rb->collider); - if (ImGui::Combo("collider", &collider, "Box\0Sphere\0Mesh\0")) + if (ImGui::Combo("collider", &collider, + "Box\0Sphere\0Mesh\0Capsule\0Cylinder\0Compound\0")) { rb->collider = static_cast(collider); } @@ -248,6 +249,61 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) { ImGui::DragFloat("radius", &rb->sphereRadius, 0.05f, 0.01f, 500.0f); } + else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Capsule) + { + // 半高 = 圆柱段半高(不含半球端盖),总高 = 2*(halfHeight + radius) + ImGui::DragFloat("radius", &rb->capsuleRadius, 0.05f, 0.01f, 500.0f); + ImGui::DragFloat("halfHeight", &rb->capsuleHalfHeight, 0.05f, 0.0f, 500.0f); + } + else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Cylinder) + { + ImGui::DragFloat("radius", &rb->cylinderRadius, 0.05f, 0.01f, 500.0f); + ImGui::DragFloat("halfHeight", &rb->cylinderHalfHeight, 0.05f, 0.01f, 500.0f); + } + else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Compound) + { + // 子形状列表:尺寸/位置变更 → PhysicsWorld 重建 body(既有模式) + ImGui::Text("children: %d", static_cast(rb->compound.size())); + ImGui::SameLine(); + if (ImGui::Button("+ add")) + { + rb->compound.emplace_back(); + } + int remove = -1; + for (int i = 0; i < static_cast(rb->compound.size()); ++i) + { + auto& c = rb->compound[i]; + ImGui::PushID(i); + ImGui::Separator(); + int sh = static_cast(c.shape); + if (ImGui::Combo("shape", &sh, "Box\0Sphere\0Capsule\0")) + { + c.shape = static_cast(sh); + } + if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Box) + { + ImGui::DragFloat3("halfExtents", &c.boxHalfExtents.x, 0.05f, 0.01f, 500.0f); + } + else + { + ImGui::DragFloat("radius", &c.radius, 0.05f, 0.01f, 500.0f); + if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Capsule) + { + ImGui::DragFloat("halfHeight", &c.capsuleHalfHeight, 0.05f, 0.0f, 500.0f); + } + } + ImGui::DragFloat3("offset", &c.position.x, 0.05f); + if (ImGui::Button("remove")) + { + remove = i; + } + ImGui::PopID(); + } + if (remove >= 0) + { + rb->compound.erase(rb->compound.begin() + remove); + } + } else { // Mesh:跟随 MeshRenderer 视觉网格(Static=精确网格, diff --git a/Engine/include/Engine/ECS/Components.h b/Engine/include/Engine/ECS/Components.h index 3cb3d4e..bde24b7 100644 --- a/Engine/include/Engine/ECS/Components.h +++ b/Engine/include/Engine/ECS/Components.h @@ -19,6 +19,7 @@ #include #include #include +#include namespace Engine { class Model; @@ -187,15 +188,38 @@ namespace Engine::ECS { // 语义)——Static → Jolt MeshShape(精确三角网格,不可移动); // Dynamic/Kinematic → ConvexHullShape(凸包近似,MeshShape 太贵 // 不能动)。数据源 Mesh 的 CPU 位置副本,无 MeshRenderer 回退 Box - enum class ColliderType : u8 { Box = 0, Sphere = 1, Mesh = 2 }; + enum class ColliderType : u8 { + Box = 0, Sphere = 1, Mesh = 2, + Capsule = 3, Cylinder = 4, Compound = 5 + }; + + // Compound 子形状:局部 pos/rot 相对刚体原点,不递归嵌套 + //(child 不能再是 Compound)。尺寸语义同顶层(× Transform 缩放烘焙) + struct CompoundChild { + enum class Shape : u8 { Box = 0, Sphere = 1, Capsule = 2 }; + Shape shape = Shape::Box; + glm::vec3 boxHalfExtents{0.25f, 0.25f, 0.25f}; + f32 radius = 0.25f; // Sphere 半径 / Capsule 半径 + f32 capsuleHalfHeight = 0.25f; // 仅 Capsule:圆柱段半高 + glm::vec3 position{0.0f}; + glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f}; + bool operator==(const CompoundChild&) const = default; + }; BodyType type = BodyType::Dynamic; // Unity 语义:加 Rigidbody 即受物理驱动 ColliderType collider = ColliderType::Box; // 碰撞体尺寸(× Transform 缩放,Unity BoxCollider.size 语义)—— - // 程序化模型顶点烘焙尺寸,不能从 scale 反推 + // 程序化模型顶点烘焙尺寸,不能从 scale 反推。 + // Capsule/Cylinder 的"半高"指圆柱段半高(不含半球端盖),总高 = + // 2*(halfHeight + radius);Jolt 两 ctor 参数序相反见 PhysicsWorld glm::vec3 boxHalfExtents{0.5f, 0.5f, 0.5f}; f32 sphereRadius = 0.5f; + f32 capsuleRadius = 0.5f; // Unity CapsuleCollider 默认值同构 + f32 capsuleHalfHeight = 0.5f; + f32 cylinderRadius = 0.5f; + f32 cylinderHalfHeight = 0.5f; + std::vector compound; // 仅 ColliderType::Compound // 动态体参数 f32 mass = 1.0f; diff --git a/Engine/include/Engine/Physics/PhysicsWorld.h b/Engine/include/Engine/Physics/PhysicsWorld.h index 5b967c7..d6498c6 100644 --- a/Engine/include/Engine/Physics/PhysicsWorld.h +++ b/Engine/include/Engine/Physics/PhysicsWorld.h @@ -25,6 +25,7 @@ #include #include #include +#include // BodyRecord 快照存 CompoundChild(完整类型) #include #include @@ -95,7 +96,9 @@ namespace Engine { u8 type = 0; // RigidBodyComponent::BodyType u8 collider = 0; // RigidBodyComponent::ColliderType glm::vec3 colliderSize{0.0f}; // 碰撞体尺寸缓存(box 半尺寸) - f32 radius = 0.0f; // 球半径缓存 + f32 radius = 0.0f; // 球/Capsule/Cylinder 半径(collider 决定语义) + f32 halfHeight = 0.0f; // Capsule/Cylinder 圆柱段半高 + std::vector compound; // 子形状快照 f32 mass = 0.0f; bool isSensor = false; // 传感器标记(变更 → 重建 body) glm::vec3 lastPos{0.0f}; // 静态/运动体编辑器拖动检测 @@ -116,6 +119,10 @@ namespace Engine { JPH::RefConst BuildMeshShape(u32 entityBits, ECS::RigidBodyComponent& rb, ECS::TransformComponent& tr); + // Compound 碰撞体:子形状列表 → Jolt StaticCompoundShape。 + // 空 children 或构建失败返回 null(回退 Box) + JPH::RefConst BuildCompoundShape(ECS::RigidBodyComponent& rb, + ECS::TransformComponent& tr); void RemoveBody(u32 entityBits); const BodyRecord* FindRecord(u32 entityBits) const; // 刚体驱动寻址 void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁) diff --git a/Engine/src/Physics/PhysicsWorld.cpp b/Engine/src/Physics/PhysicsWorld.cpp index e880ccf..8fb7f38 100644 --- a/Engine/src/Physics/PhysicsWorld.cpp +++ b/Engine/src/Physics/PhysicsWorld.cpp @@ -16,6 +16,9 @@ #include #include #include +#include +#include +#include #include #include @@ -83,6 +86,26 @@ namespace Engine { 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、加锁入队 ---- @@ -338,10 +361,14 @@ namespace Engine { } 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 != rb.sphereRadius || + rec.radius != radius || + rec.halfHeight != halfHeight || + rec.compound != rb.compound || rec.mass != rb.mass || rec.isSensor != rb.isSensor) { // 类型/碰撞体/质量变更 → 重建(简单正确,重置速度可接受) @@ -444,16 +471,80 @@ namespace Engine { 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 直接带缩放建形状) + // ScaledShape 的优化留给 ShapeCache 阶段;MVP 直接带缩放建形状)。 + // 注意 Jolt 两 ctor 参数序相反:CapsuleShape(半径, 圆柱段半高)、 + // CylinderShape(圆柱段半高, 半径) JPH::RefConst shape; + const f32 uniformScale = UniformScale(tr.GetScale()); 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)); + 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 @@ -507,7 +598,8 @@ namespace Engine { rec.type = static_cast(rb.type); rec.collider = static_cast(rb.collider); rec.colliderSize = rb.boxHalfExtents; - rec.radius = rb.sphereRadius; + ExtractShapeParams(rb, rec.radius, rec.halfHeight); + rec.compound = rb.compound; rec.mass = rb.mass; rec.isSensor = rb.isSensor; rec.lastPos = tr.GetPosition(); diff --git a/Engine/src/Serialization/SceneSerializer.cpp b/Engine/src/Serialization/SceneSerializer.cpp index 527da35..ff91d8b 100644 --- a/Engine/src/Serialization/SceneSerializer.cpp +++ b/Engine/src/Serialization/SceneSerializer.cpp @@ -268,6 +268,34 @@ namespace Engine::Serialization { EmitVec3(out, rb->boxHalfExtents); out << YAML::Key << "SphereRadius" << YAML::Value << rb->sphereRadius; + out << YAML::Key << "CapsuleRadius" << YAML::Value + << rb->capsuleRadius; + out << YAML::Key << "CapsuleHalfHeight" << YAML::Value + << rb->capsuleHalfHeight; + out << YAML::Key << "CylinderRadius" << YAML::Value + << rb->cylinderRadius; + out << YAML::Key << "CylinderHalfHeight" << YAML::Value + << rb->cylinderHalfHeight; + if (!rb->compound.empty()) { + out << YAML::Key << "Compound" << YAML::Value + << YAML::BeginSeq; + for (const auto& c : rb->compound) { + out << YAML::BeginMap; + out << YAML::Key << "Shape" << YAML::Value + << static_cast(c.shape); + out << YAML::Key << "BoxHalfExtents" << YAML::Value; + EmitVec3(out, c.boxHalfExtents); + out << YAML::Key << "Radius" << YAML::Value << c.radius; + out << YAML::Key << "CapsuleHalfHeight" << YAML::Value + << c.capsuleHalfHeight; + out << YAML::Key << "Position" << YAML::Value; + EmitVec3(out, c.position); + out << YAML::Key << "Rotation" << YAML::Value; + EmitQuat(out, c.rotation); // (w,x,y,z) + out << YAML::EndMap; + } + out << YAML::EndSeq; + } out << YAML::Key << "Mass" << YAML::Value << rb->mass; out << YAML::Key << "GravityScale" << YAML::Value << rb->gravityScale; @@ -491,7 +519,7 @@ namespace Engine::Serialization { static_cast(type); } if (const int col = n["Collider"].as(0); - col >= 0 && col <= 2) { + col >= 0 && col <= 5) { rb.collider = static_cast( col); @@ -499,6 +527,34 @@ namespace Engine::Serialization { rb.boxHalfExtents = ReadVec3(n["BoxHalfExtents"], rb.boxHalfExtents); rb.sphereRadius = n["SphereRadius"].as(rb.sphereRadius); + rb.capsuleRadius = n["CapsuleRadius"].as(rb.capsuleRadius); + rb.capsuleHalfHeight = + n["CapsuleHalfHeight"].as(rb.capsuleHalfHeight); + rb.cylinderRadius = + n["CylinderRadius"].as(rb.cylinderRadius); + rb.cylinderHalfHeight = + n["CylinderHalfHeight"].as(rb.cylinderHalfHeight); + // 僵尸节点铁律:拷贝初始化 + 判空再遍历(缺 Compound 键安全) + if (const YAML::Node cs = n["Compound"]) { + for (const auto& item : cs) { + ECS::RigidBodyComponent::CompoundChild c; + if (const int sh = item["Shape"].as(0); + sh >= 0 && sh <= 2) { + c.shape = static_cast< + ECS::RigidBodyComponent::CompoundChild::Shape>( + sh); + } + c.boxHalfExtents = ReadVec3(item["BoxHalfExtents"], + c.boxHalfExtents); + c.radius = item["Radius"].as(c.radius); + c.capsuleHalfHeight = + item["CapsuleHalfHeight"].as( + c.capsuleHalfHeight); + c.position = ReadVec3(item["Position"], c.position); + c.rotation = ReadQuat(item["Rotation"]); + rb.compound.push_back(std::move(c)); + } + } rb.mass = n["Mass"].as(rb.mass); rb.gravityScale = n["GravityScale"].as(rb.gravityScale); rb.friction = n["Friction"].as(rb.friction);