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
+57
View File
@@ -116,6 +116,11 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
reg.emplace<Engine::ECS::RigidBodyComponent>(selected);
}
if (!reg.all_of<Engine::ECS::JointComponent>(selected) &&
ImGui::MenuItem("Joint"))
{
reg.emplace<Engine::ECS::JointComponent>(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<Engine::ECS::JointComponent>(selected))
{
if (ImGui::CollapsingHeader("Joint", ImGuiTreeNodeFlags_DefaultOpen))
{
int type = static_cast<int>(jc->type);
if (ImGui::Combo("type", &type, "Fixed\0Hinge\0Ball\0"))
{
jc->type = static_cast<Engine::ECS::JointComponent::Type>(type);
}
// target 实体下拉:实体名列表,选中态按 UUID 匹配(变更写回)
std::string items;
std::vector<Engine::UUID> uuids;
int current = -1;
reg.view<Engine::ECS::IdComponent, Engine::ECS::NameComponent>().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<int>(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", &current, items.c_str()) && current >= 0)
{
jc->target = uuids[static_cast<size_t>(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<Engine::ECS::AnimationComponent>(selected))
{
+23
View File
@@ -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;
+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 形状 ----
@@ -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: