physics: capsule/cylinder/compound collider shapes

- ColliderType + Capsule/Cylinder/Compound; CompoundChild list (Box/
  Sphere/Capsule, local pos/quat, non-recursive)
- CreateBody: CapsuleShape/CylinderShape (ctor param orders differ) +
  StaticCompoundShape; empty/failed compound falls back to Box
- BodyRecord caches radius/halfHeight/compound; SyncScene rebuilds on
  change (per-collider param extraction)
- Inspector: combo entries + shape params + compound child list editor
- Serialization: new fields with as<T> fallback, compound seq round-trip
This commit is contained in:
2026-10-03 16:17:44 +08:00
parent 6c81bfd70d
commit a744b332eb
5 changed files with 245 additions and 10 deletions
+57 -1
View File
@@ -236,7 +236,8 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
rb->type = static_cast<Engine::ECS::RigidBodyComponent::BodyType>(type); rb->type = static_cast<Engine::ECS::RigidBodyComponent::BodyType>(type);
} }
int collider = static_cast<int>(rb->collider); int collider = static_cast<int>(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<Engine::ECS::RigidBodyComponent::ColliderType>(collider); rb->collider = static_cast<Engine::ECS::RigidBodyComponent::ColliderType>(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); 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<int>(rb->compound.size()));
ImGui::SameLine();
if (ImGui::Button("+ add"))
{
rb->compound.emplace_back();
}
int remove = -1;
for (int i = 0; i < static_cast<int>(rb->compound.size()); ++i)
{
auto& c = rb->compound[i];
ImGui::PushID(i);
ImGui::Separator();
int sh = static_cast<int>(c.shape);
if (ImGui::Combo("shape", &sh, "Box\0Sphere\0Capsule\0"))
{
c.shape = static_cast<Engine::ECS::RigidBodyComponent::CompoundChild::Shape>(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 else
{ {
// Mesh:跟随 MeshRenderer 视觉网格(Static=精确网格, // Mesh:跟随 MeshRenderer 视觉网格(Static=精确网格,
+26 -2
View File
@@ -19,6 +19,7 @@
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <string> #include <string>
#include <vector>
namespace Engine { namespace Engine {
class Model; class Model;
@@ -187,15 +188,38 @@ namespace Engine::ECS {
// 语义)——Static → Jolt MeshShape(精确三角网格,不可移动); // 语义)——Static → Jolt MeshShape(精确三角网格,不可移动);
// Dynamic/Kinematic → ConvexHullShape(凸包近似,MeshShape 太贵 // Dynamic/Kinematic → ConvexHullShape(凸包近似,MeshShape 太贵
// 不能动)。数据源 Mesh 的 CPU 位置副本,无 MeshRenderer 回退 Box // 不能动)。数据源 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 即受物理驱动 BodyType type = BodyType::Dynamic; // Unity 语义:加 Rigidbody 即受物理驱动
ColliderType collider = ColliderType::Box; ColliderType collider = ColliderType::Box;
// 碰撞体尺寸(× Transform 缩放,Unity BoxCollider.size 语义)—— // 碰撞体尺寸(× Transform 缩放,Unity BoxCollider.size 语义)——
// 程序化模型顶点烘焙尺寸,不能从 scale 反推 // 程序化模型顶点烘焙尺寸,不能从 scale 反推。
// Capsule/Cylinder 的"半高"指圆柱段半高(不含半球端盖),总高 =
// 2*(halfHeight + radius);Jolt 两 ctor 参数序相反见 PhysicsWorld
glm::vec3 boxHalfExtents{0.5f, 0.5f, 0.5f}; glm::vec3 boxHalfExtents{0.5f, 0.5f, 0.5f};
f32 sphereRadius = 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<CompoundChild> compound; // 仅 ColliderType::Compound
// 动态体参数 // 动态体参数
f32 mass = 1.0f; f32 mass = 1.0f;
+8 -1
View File
@@ -25,6 +25,7 @@
#include <Engine/Physics/PhysicsDebugDraw.h> #include <Engine/Physics/PhysicsDebugDraw.h>
#include <Engine/Physics/PhysicsEvents.h> #include <Engine/Physics/PhysicsEvents.h>
#include <Engine/Physics/PhysicsRay.h> #include <Engine/Physics/PhysicsRay.h>
#include <Engine/ECS/Components.h> // BodyRecord 快照存 CompoundChild(完整类型)
#include <Jolt/Jolt.h> #include <Jolt/Jolt.h>
#include <Jolt/Physics/PhysicsSystem.h> #include <Jolt/Physics/PhysicsSystem.h>
@@ -95,7 +96,9 @@ namespace Engine {
u8 type = 0; // RigidBodyComponent::BodyType u8 type = 0; // RigidBodyComponent::BodyType
u8 collider = 0; // RigidBodyComponent::ColliderType u8 collider = 0; // RigidBodyComponent::ColliderType
glm::vec3 colliderSize{0.0f}; // 碰撞体尺寸缓存(box 半尺寸) 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<ECS::RigidBodyComponent::CompoundChild> compound; // 子形状快照
f32 mass = 0.0f; f32 mass = 0.0f;
bool isSensor = false; // 传感器标记(变更 → 重建 body) bool isSensor = false; // 传感器标记(变更 → 重建 body)
glm::vec3 lastPos{0.0f}; // 静态/运动体编辑器拖动检测 glm::vec3 lastPos{0.0f}; // 静态/运动体编辑器拖动检测
@@ -116,6 +119,10 @@ namespace Engine {
JPH::RefConst<JPH::Shape> BuildMeshShape(u32 entityBits, JPH::RefConst<JPH::Shape> BuildMeshShape(u32 entityBits,
ECS::RigidBodyComponent& rb, ECS::RigidBodyComponent& rb,
ECS::TransformComponent& tr); ECS::TransformComponent& tr);
// Compound 碰撞体:子形状列表 → Jolt StaticCompoundShape。
// 空 children 或构建失败返回 null(回退 Box)
JPH::RefConst<JPH::Shape> BuildCompoundShape(ECS::RigidBodyComponent& rb,
ECS::TransformComponent& tr);
void RemoveBody(u32 entityBits); void RemoveBody(u32 entityBits);
const BodyRecord* FindRecord(u32 entityBits) const; // 刚体驱动寻址 const BodyRecord* FindRecord(u32 entityBits) const; // 刚体驱动寻址
void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁) void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁)
+97 -5
View File
@@ -16,6 +16,9 @@
#include <Jolt/Physics/Collision/Shape/SphereShape.h> #include <Jolt/Physics/Collision/Shape/SphereShape.h>
#include <Jolt/Physics/Collision/Shape/MeshShape.h> #include <Jolt/Physics/Collision/Shape/MeshShape.h>
#include <Jolt/Physics/Collision/Shape/ConvexHullShape.h> #include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
#include <Engine/Assets/AssetManager.h> #include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Model.h> #include <Engine/Renderer/Model.h>
@@ -83,6 +86,26 @@ namespace Engine {
glm::quat ToGlm(const JPH::Quat &q) { glm::quat ToGlm(const JPH::Quat &q) {
return {q.GetW(), q.GetX(), q.GetY(), q.GetZ()}; 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 } // namespace
// ---- 碰撞事件监听:回调在 Update 内多线程并发,只读 body、加锁入队 ---- // ---- 碰撞事件监听:回调在 Update 内多线程并发,只读 body、加锁入队 ----
@@ -338,10 +361,14 @@ namespace Engine {
} }
auto &rec = found->second; // 非 const:静态/运动体拖动时更新 lastPos/lastRot auto &rec = found->second; // 非 const:静态/运动体拖动时更新 lastPos/lastRot
f32 radius = 0.0f, halfHeight = 0.0f;
ExtractShapeParams(rb, radius, halfHeight);
if (rec.type != static_cast<u8>(rb.type) || if (rec.type != static_cast<u8>(rb.type) ||
rec.collider != static_cast<u8>(rb.collider) || rec.collider != static_cast<u8>(rb.collider) ||
rec.colliderSize != rb.boxHalfExtents || rec.colliderSize != rb.boxHalfExtents ||
rec.radius != rb.sphereRadius || rec.radius != radius ||
rec.halfHeight != halfHeight ||
rec.compound != rb.compound ||
rec.mass != rb.mass || rec.mass != rb.mass ||
rec.isSensor != rb.isSensor) { rec.isSensor != rb.isSensor) {
// 类型/碰撞体/质量变更 → 重建(简单正确,重置速度可接受) // 类型/碰撞体/质量变更 → 重建(简单正确,重置速度可接受)
@@ -444,16 +471,80 @@ namespace Engine {
return res.Get(); return res.Get();
} }
JPH::RefConst<JPH::Shape> 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<JPH::Shape> 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, void PhysicsWorld::CreateBody(u32 entityBits, ECS::RigidBodyComponent &rb,
ECS::TransformComponent &tr) { ECS::TransformComponent &tr) {
auto &bi = m_System->GetBodyInterface(); auto &bi = m_System->GetBodyInterface();
// 碰撞体:组件显式尺寸 × Transform 缩放(形状按单位尺度烹饪 + // 碰撞体:组件显式尺寸 × Transform 缩放(形状按单位尺度烹饪 +
// ScaledShape 的优化留给 ShapeCache 阶段;MVP 直接带缩放建形状) // ScaledShape 的优化留给 ShapeCache 阶段;MVP 直接带缩放建形状)。
// 注意 Jolt 两 ctor 参数序相反:CapsuleShape(半径, 圆柱段半高)、
// CylinderShape(圆柱段半高, 半径)
JPH::RefConst<JPH::Shape> shape; JPH::RefConst<JPH::Shape> shape;
const f32 uniformScale = UniformScale(tr.GetScale());
if (rb.collider == ECS::RigidBodyComponent::ColliderType::Sphere) { 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 * uniformScale, 0.01f));
shape = new JPH::SphereShape(std::max(rb.sphereRadius * scale, 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) { } else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Mesh) {
// Mesh 碰撞体:实体 MeshRenderer 的视觉网格(Unity MeshCollider // Mesh 碰撞体:实体 MeshRenderer 的视觉网格(Unity MeshCollider
// 语义)。缩放烘进顶点(Jolt shape 无缩放);Static → MeshShape // 语义)。缩放烘进顶点(Jolt shape 无缩放);Static → MeshShape
@@ -507,7 +598,8 @@ namespace Engine {
rec.type = static_cast<u8>(rb.type); rec.type = static_cast<u8>(rb.type);
rec.collider = static_cast<u8>(rb.collider); rec.collider = static_cast<u8>(rb.collider);
rec.colliderSize = rb.boxHalfExtents; rec.colliderSize = rb.boxHalfExtents;
rec.radius = rb.sphereRadius; ExtractShapeParams(rb, rec.radius, rec.halfHeight);
rec.compound = rb.compound;
rec.mass = rb.mass; rec.mass = rb.mass;
rec.isSensor = rb.isSensor; rec.isSensor = rb.isSensor;
rec.lastPos = tr.GetPosition(); rec.lastPos = tr.GetPosition();
+57 -1
View File
@@ -268,6 +268,34 @@ namespace Engine::Serialization {
EmitVec3(out, rb->boxHalfExtents); EmitVec3(out, rb->boxHalfExtents);
out << YAML::Key << "SphereRadius" << YAML::Value out << YAML::Key << "SphereRadius" << YAML::Value
<< rb->sphereRadius; << 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<int>(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 << "Mass" << YAML::Value << rb->mass;
out << YAML::Key << "GravityScale" << YAML::Value out << YAML::Key << "GravityScale" << YAML::Value
<< rb->gravityScale; << rb->gravityScale;
@@ -491,7 +519,7 @@ namespace Engine::Serialization {
static_cast<ECS::RigidBodyComponent::BodyType>(type); static_cast<ECS::RigidBodyComponent::BodyType>(type);
} }
if (const int col = n["Collider"].as<int>(0); if (const int col = n["Collider"].as<int>(0);
col >= 0 && col <= 2) { col >= 0 && col <= 5) {
rb.collider = rb.collider =
static_cast<ECS::RigidBodyComponent::ColliderType>( static_cast<ECS::RigidBodyComponent::ColliderType>(
col); col);
@@ -499,6 +527,34 @@ namespace Engine::Serialization {
rb.boxHalfExtents = ReadVec3(n["BoxHalfExtents"], rb.boxHalfExtents = ReadVec3(n["BoxHalfExtents"],
rb.boxHalfExtents); rb.boxHalfExtents);
rb.sphereRadius = n["SphereRadius"].as<f32>(rb.sphereRadius); rb.sphereRadius = n["SphereRadius"].as<f32>(rb.sphereRadius);
rb.capsuleRadius = n["CapsuleRadius"].as<f32>(rb.capsuleRadius);
rb.capsuleHalfHeight =
n["CapsuleHalfHeight"].as<f32>(rb.capsuleHalfHeight);
rb.cylinderRadius =
n["CylinderRadius"].as<f32>(rb.cylinderRadius);
rb.cylinderHalfHeight =
n["CylinderHalfHeight"].as<f32>(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<int>(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<f32>(c.radius);
c.capsuleHalfHeight =
item["CapsuleHalfHeight"].as<f32>(
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<f32>(rb.mass); rb.mass = n["Mass"].as<f32>(rb.mass);
rb.gravityScale = n["GravityScale"].as<f32>(rb.gravityScale); rb.gravityScale = n["GravityScale"].as<f32>(rb.gravityScale);
rb.friction = n["Friction"].as<f32>(rb.friction); rb.friction = n["Friction"].as<f32>(rb.friction);