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:
@@ -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
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <Jolt/Jolt.h>
|
||||
#include <Jolt/Physics/PhysicsSystem.h>
|
||||
#include <Jolt/Physics/Body/BodyID.h>
|
||||
#include <Jolt/Physics/Constraints/Constraint.h> // JPH::Ref<Constraint>
|
||||
#include <Jolt/Core/JobSystemThreadPool.h>
|
||||
#include <Jolt/Core/TempAllocator.h>
|
||||
|
||||
@@ -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<u32>& 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<JPH::Shape> 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<JPH::Constraint> constraint;
|
||||
ECS::JointComponent snapshot; // 变更检测 → 移除重建
|
||||
u32 targetBits = 0;
|
||||
};
|
||||
std::unordered_map<u32, ConstraintRecord> m_Constraints;
|
||||
|
||||
ECS::Scene& m_Scene;
|
||||
Assets::AssetManager* m_Assets = nullptr; // Mesh 碰撞体解析 modelRef
|
||||
bool m_Enabled = true;
|
||||
|
||||
@@ -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 ® = 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 ® = 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 ® = 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 形状 ----
|
||||
|
||||
@@ -307,6 +307,27 @@ namespace Engine::Serialization {
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
|
||||
if (const auto* jc = reg.try_get<ECS::JointComponent>(e)) {
|
||||
out << YAML::Key << "Joint" << YAML::Value
|
||||
<< YAML::BeginMap;
|
||||
out << YAML::Key << "Type" << YAML::Value
|
||||
<< static_cast<int>(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<bool>(rb.isSensor);
|
||||
// bodyId/bodyValid/velocity 保持默认(对账重建)
|
||||
}
|
||||
|
||||
// Joint:target 只存 UUID,加载后由 PhysicsWorld 惰性反查
|
||||
//(两遍解析不需要——UUID 再生/加载顺序均由逐帧解析消化)
|
||||
if (const YAML::Node n = comps["Joint"]) {
|
||||
auto& jc = handle.AddComponent<ECS::JointComponent>();
|
||||
if (const int type = n["Type"].as<int>(0);
|
||||
type >= 0 && type <= 2) {
|
||||
jc.type =
|
||||
static_cast<ECS::JointComponent::Type>(type);
|
||||
}
|
||||
jc.target = UUID{n["Target"].as<u64>(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<bool>(jc.enableLimit);
|
||||
jc.limitMin = n["LimitMin"].as<f32>(jc.limitMin);
|
||||
jc.limitMax = n["LimitMax"].as<f32>(jc.limitMax);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms:
|
||||
|
||||
Reference in New Issue
Block a user