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<T> fallbacks
This commit is contained in:
2026-10-03 16:26:20 +08:00
parent a744b332eb
commit 82910c41e8
5 changed files with 296 additions and 0 deletions
+161
View File
@@ -19,6 +19,9 @@
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
#include <Jolt/Physics/Constraints/FixedConstraint.h>
#include <Jolt/Physics/Constraints/HingeConstraint.h>
#include <Jolt/Physics/Constraints/PointConstraint.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Model.h>
@@ -331,6 +334,7 @@ namespace Engine {
void PhysicsWorld::SyncScene() {
auto &reg = m_Scene.GetRegistry();
auto &bi = m_System->GetBodyInterface();
std::vector<u32> invalidated; // 本帧销毁/重建 body → 约束失效传染
// 1. 孤儿清理:实体销毁 / 组件被移除 → 移除 body
for (auto it = m_Bodies.begin(); it != m_Bodies.end();) {
@@ -339,6 +343,7 @@ namespace Engine {
!reg.all_of<ECS::RigidBodyComponent, ECS::TransformComponent>(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<u32> &invalidated) {
auto &reg = 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<entt::entity>(owner);
const bool ownerGone = !reg.valid(ownerEnt) ||
!reg.all_of<ECS::JointComponent>(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<ECS::JointComponent>();
for (auto [e, jc]: view.each()) {
const u32 bits = static_cast<u32>(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<u32>(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 &reg = 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<ECS::TransformComponent>(
static_cast<entt::entity>(ownerBits));
const auto *tr2 = reg.try_get<ECS::TransformComponent>(
static_cast<entt::entity>(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<JPH::Constraint> 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 形状 ----