diff --git a/engine/include/XCEngine/Components/RigidbodyComponent.h b/engine/include/XCEngine/Components/RigidbodyComponent.h index ee8a2fac..8ec95440 100644 --- a/engine/include/XCEngine/Components/RigidbodyComponent.h +++ b/engine/include/XCEngine/Components/RigidbodyComponent.h @@ -22,12 +22,28 @@ public: float GetAngularDamping() const { return m_angularDamping; } void SetAngularDamping(float value); + const Math::Vector3& GetLinearVelocity() const { return m_linearVelocity; } + void SetLinearVelocity(const Math::Vector3& value); + + const Math::Vector3& GetAngularVelocity() const { return m_angularVelocity; } + void SetAngularVelocity(const Math::Vector3& value); + bool GetUseGravity() const { return m_useGravity; } void SetUseGravity(bool value) { m_useGravity = value; } bool GetEnableCCD() const { return m_enableCCD; } void SetEnableCCD(bool value) { m_enableCCD = value; } + void AddForce( + const Math::Vector3& force, + Physics::PhysicsForceMode mode = Physics::PhysicsForceMode::Force); + void ClearForces(); + void ConsumePendingForces( + Math::Vector3& outForce, + Math::Vector3& outAcceleration, + Math::Vector3& outImpulse, + Math::Vector3& outVelocityChange); + void Serialize(std::ostream& os) const override; void Deserialize(std::istream& is) override; @@ -36,8 +52,14 @@ private: float m_mass = 1.0f; float m_linearDamping = 0.0f; float m_angularDamping = 0.05f; + Math::Vector3 m_linearVelocity = Math::Vector3::Zero(); + Math::Vector3 m_angularVelocity = Math::Vector3::Zero(); bool m_useGravity = true; bool m_enableCCD = false; + Math::Vector3 m_pendingForce = Math::Vector3::Zero(); + Math::Vector3 m_pendingAcceleration = Math::Vector3::Zero(); + Math::Vector3 m_pendingImpulse = Math::Vector3::Zero(); + Math::Vector3 m_pendingVelocityChange = Math::Vector3::Zero(); }; } // namespace Components diff --git a/engine/include/XCEngine/Physics/PhysicsTypes.h b/engine/include/XCEngine/Physics/PhysicsTypes.h index 991b00fe..a5c3e580 100644 --- a/engine/include/XCEngine/Physics/PhysicsTypes.h +++ b/engine/include/XCEngine/Physics/PhysicsTypes.h @@ -19,6 +19,13 @@ enum class PhysicsBodyType : uint8_t { Kinematic }; +enum class PhysicsForceMode : uint8_t { + Force = 0, + Acceleration, + Impulse, + VelocityChange +}; + struct PhysicsWorldCreateInfo { Components::Scene* scene = nullptr; Math::Vector3 gravity = Math::Vector3(0.0f, -9.81f, 0.0f); diff --git a/engine/src/Components/RigidbodyComponent.cpp b/engine/src/Components/RigidbodyComponent.cpp index 5c280977..23d1be98 100644 --- a/engine/src/Components/RigidbodyComponent.cpp +++ b/engine/src/Components/RigidbodyComponent.cpp @@ -17,6 +17,21 @@ float SanitizeNonNegativeFinite(float value, float fallback) { return std::isfinite(value) && value >= 0.0f ? value : fallback; } +Math::Vector3 SanitizeFiniteVector3(const Math::Vector3& value) { + return Math::Vector3( + std::isfinite(value.x) ? value.x : 0.0f, + std::isfinite(value.y) ? value.y : 0.0f, + std::isfinite(value.z) ? value.z : 0.0f); +} + +bool TryParseVector3(const std::string& value, Math::Vector3& outValue) { + std::string normalized = value; + std::replace(normalized.begin(), normalized.end(), ',', ' '); + std::istringstream stream(normalized); + stream >> outValue.x >> outValue.y >> outValue.z; + return !stream.fail(); +} + } // namespace void RigidbodyComponent::SetMass(float value) { @@ -31,11 +46,62 @@ void RigidbodyComponent::SetAngularDamping(float value) { m_angularDamping = SanitizeNonNegativeFinite(value, 0.05f); } +void RigidbodyComponent::SetLinearVelocity(const Math::Vector3& value) { + m_linearVelocity = SanitizeFiniteVector3(value); +} + +void RigidbodyComponent::SetAngularVelocity(const Math::Vector3& value) { + m_angularVelocity = SanitizeFiniteVector3(value); +} + +void RigidbodyComponent::AddForce( + const Math::Vector3& force, + Physics::PhysicsForceMode mode) { + const Math::Vector3 sanitizedForce = SanitizeFiniteVector3(force); + + switch (mode) { + case Physics::PhysicsForceMode::Acceleration: + m_pendingAcceleration += sanitizedForce; + break; + case Physics::PhysicsForceMode::Impulse: + m_pendingImpulse += sanitizedForce; + break; + case Physics::PhysicsForceMode::VelocityChange: + m_pendingVelocityChange += sanitizedForce; + break; + case Physics::PhysicsForceMode::Force: + default: + m_pendingForce += sanitizedForce; + break; + } +} + +void RigidbodyComponent::ClearForces() { + m_pendingForce = Math::Vector3::Zero(); + m_pendingAcceleration = Math::Vector3::Zero(); + m_pendingImpulse = Math::Vector3::Zero(); + m_pendingVelocityChange = Math::Vector3::Zero(); +} + +void RigidbodyComponent::ConsumePendingForces( + Math::Vector3& outForce, + Math::Vector3& outAcceleration, + Math::Vector3& outImpulse, + Math::Vector3& outVelocityChange) { + outForce = m_pendingForce; + outAcceleration = m_pendingAcceleration; + outImpulse = m_pendingImpulse; + outVelocityChange = m_pendingVelocityChange; + ClearForces(); +} + void RigidbodyComponent::Serialize(std::ostream& os) const { os << "bodyType=" << static_cast(m_bodyType) << ";"; os << "mass=" << m_mass << ";"; os << "linearDamping=" << m_linearDamping << ";"; os << "angularDamping=" << m_angularDamping << ";"; + os << "linearVelocity=" << m_linearVelocity.x << "," << m_linearVelocity.y << "," << m_linearVelocity.z << ";"; + os << "angularVelocity=" << m_angularVelocity.x << "," << m_angularVelocity.y << "," << m_angularVelocity.z << ";"; os << "useGravity=" << (m_useGravity ? 1 : 0) << ";"; os << "enableCCD=" << (m_enableCCD ? 1 : 0) << ";"; } @@ -63,6 +129,16 @@ void RigidbodyComponent::Deserialize(std::istream& is) { SetLinearDamping(std::stof(value)); } else if (key == "angularDamping") { SetAngularDamping(std::stof(value)); + } else if (key == "linearVelocity") { + Math::Vector3 parsedVelocity = Math::Vector3::Zero(); + if (TryParseVector3(value, parsedVelocity)) { + SetLinearVelocity(parsedVelocity); + } + } else if (key == "angularVelocity") { + Math::Vector3 parsedVelocity = Math::Vector3::Zero(); + if (TryParseVector3(value, parsedVelocity)) { + SetAngularVelocity(parsedVelocity); + } } else if (key == "useGravity") { m_useGravity = (std::stoi(value) != 0); } else if (key == "enableCCD") { diff --git a/engine/src/Physics/PhysX/PhysXWorldBackend.cpp b/engine/src/Physics/PhysX/PhysXWorldBackend.cpp index 55b917ba..fc7cf27b 100644 --- a/engine/src/Physics/PhysX/PhysXWorldBackend.cpp +++ b/engine/src/Physics/PhysX/PhysXWorldBackend.cpp @@ -58,6 +58,20 @@ void ResetRaycastHit(RaycastHit& outHit) { outHit = RaycastHit{}; } +physx::PxForceMode::Enum ToPxForceMode(PhysicsForceMode mode) { + switch (mode) { + case PhysicsForceMode::Acceleration: + return physx::PxForceMode::eACCELERATION; + case PhysicsForceMode::Impulse: + return physx::PxForceMode::eIMPULSE; + case PhysicsForceMode::VelocityChange: + return physx::PxForceMode::eVELOCITY_CHANGE; + case PhysicsForceMode::Force: + default: + return physx::PxForceMode::eFORCE; + } +} + physx::PxTransform ToPxTransform(const Components::TransformComponent& transform) { return physx::PxTransform( ToPxVec3(transform.GetPosition()), @@ -295,6 +309,37 @@ void ApplyDynamicBodyProperties( } } +void ClearPendingForces(Components::RigidbodyComponent& rigidbody) { + rigidbody.ClearForces(); +} + +void ApplyPendingForceAccumulator( + physx::PxRigidDynamic& actor, + Math::Vector3& accumulator, + PhysicsForceMode mode) { + if (accumulator.SqrMagnitude() <= Math::EPSILON) { + accumulator = Math::Vector3::Zero(); + return; + } + + actor.addForce(ToPxVec3(accumulator), ToPxForceMode(mode), true); + accumulator = Math::Vector3::Zero(); +} + +void ApplyPendingForces( + physx::PxRigidDynamic& actor, + Components::RigidbodyComponent& rigidbody) { + Math::Vector3 force = Math::Vector3::Zero(); + Math::Vector3 acceleration = Math::Vector3::Zero(); + Math::Vector3 impulse = Math::Vector3::Zero(); + Math::Vector3 velocityChange = Math::Vector3::Zero(); + rigidbody.ConsumePendingForces(force, acceleration, impulse, velocityChange); + ApplyPendingForceAccumulator(actor, force, PhysicsForceMode::Force); + ApplyPendingForceAccumulator(actor, acceleration, PhysicsForceMode::Acceleration); + ApplyPendingForceAccumulator(actor, impulse, PhysicsForceMode::Impulse); + ApplyPendingForceAccumulator(actor, velocityChange, PhysicsForceMode::VelocityChange); +} + void SyncShapeBindingState( const Components::GameObject& actorOwner, ShapeBinding& shapeBinding) { @@ -348,15 +393,29 @@ void SyncActorBindingState(ActorBinding& binding) { if (binding.dynamicActor) { if (binding.rigidbody) { ApplyDynamicBodyProperties(*binding.dynamicActor, *binding.rigidbody); - } - if (binding.rigidbody && binding.rigidbody->GetBodyType() == PhysicsBodyType::Kinematic) { - binding.dynamicActor->setKinematicTarget(targetPose); + if (binding.rigidbody->GetBodyType() == PhysicsBodyType::Dynamic) { + binding.dynamicActor->setLinearVelocity(ToPxVec3(binding.rigidbody->GetLinearVelocity())); + binding.dynamicActor->setAngularVelocity(ToPxVec3(binding.rigidbody->GetAngularVelocity())); + ApplyPendingForces(*binding.dynamicActor, *binding.rigidbody); + } else { + binding.dynamicActor->setLinearVelocity(physx::PxVec3(0.0f), false); + binding.dynamicActor->setAngularVelocity(physx::PxVec3(0.0f), false); + ClearPendingForces(*binding.rigidbody); + } + + if (binding.rigidbody->GetBodyType() == PhysicsBodyType::Kinematic) { + binding.dynamicActor->setKinematicTarget(targetPose); + } } return; } + if (binding.rigidbody) { + ClearPendingForces(*binding.rigidbody); + } + binding.actor->setGlobalPose(targetPose, false); } @@ -373,6 +432,10 @@ void ResetRaycastHit(RaycastHit& outHit) { outHit = RaycastHit{}; } +void ClearPendingForces(Components::RigidbodyComponent& rigidbody) { + rigidbody.ClearForces(); +} + } // namespace struct PhysXWorldBackend::NativeState {}; @@ -508,6 +571,8 @@ void PhysXWorldBackend::Step(float fixedDeltaTime) { const physx::PxTransform pose = binding.dynamicActor->getGlobalPose(); binding.owner->GetTransform()->SetPosition(ToVector3(pose.p)); binding.owner->GetTransform()->SetRotation(ToQuaternion(pose.q)); + binding.rigidbody->SetLinearVelocity(ToVector3(binding.dynamicActor->getLinearVelocity())); + binding.rigidbody->SetAngularVelocity(ToVector3(binding.dynamicActor->getAngularVelocity())); } #else (void)fixedDeltaTime; diff --git a/tests/Components/test_physics_components.cpp b/tests/Components/test_physics_components.cpp index 0a72a61a..73110a58 100644 --- a/tests/Components/test_physics_components.cpp +++ b/tests/Components/test_physics_components.cpp @@ -18,6 +18,8 @@ TEST(PhysicsComponents_Test, Rigidbody_SerializeRoundTripPreservesFields) { source.SetMass(3.5f); source.SetLinearDamping(0.2f); source.SetAngularDamping(0.8f); + source.SetLinearVelocity(XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); + source.SetAngularVelocity(XCEngine::Math::Vector3(-4.0f, 5.0f, -6.0f)); source.SetUseGravity(false); source.SetEnableCCD(true); @@ -31,6 +33,8 @@ TEST(PhysicsComponents_Test, Rigidbody_SerializeRoundTripPreservesFields) { EXPECT_FLOAT_EQ(target.GetMass(), 3.5f); EXPECT_FLOAT_EQ(target.GetLinearDamping(), 0.2f); EXPECT_FLOAT_EQ(target.GetAngularDamping(), 0.8f); + EXPECT_EQ(target.GetLinearVelocity(), XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); + EXPECT_EQ(target.GetAngularVelocity(), XCEngine::Math::Vector3(-4.0f, 5.0f, -6.0f)); EXPECT_FALSE(target.GetUseGravity()); EXPECT_TRUE(target.GetEnableCCD()); } diff --git a/tests/Physics/test_physics_world.cpp b/tests/Physics/test_physics_world.cpp index ae11bc75..90727540 100644 --- a/tests/Physics/test_physics_world.cpp +++ b/tests/Physics/test_physics_world.cpp @@ -398,4 +398,70 @@ TEST(PhysicsWorld_Test, RuntimeBodyTypeChangesRebuildActorAndEnableSimulation) { EXPECT_LT(body->GetTransform()->GetPosition().y, initialY - 0.1f); } +TEST(PhysicsWorld_Test, RuntimeLinearVelocityMovesDynamicBody) { + using namespace XCEngine::Components; + + if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) { + GTEST_SKIP() << "PhysX backend is not available in this build."; + } + + Scene scene("PhysicsScene"); + GameObject* body = scene.CreateGameObject("Body"); + body->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 1.0f, 0.0f)); + RigidbodyComponent* rigidbody = body->AddComponent(); + rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Dynamic); + rigidbody->SetUseGravity(false); + rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(0.0f, 2.0f, 0.0f)); + SphereColliderComponent* sphereCollider = body->AddComponent(); + sphereCollider->SetRadius(0.5f); + + XCEngine::Physics::PhysicsWorld world; + XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; + createInfo.scene = &scene; + ASSERT_TRUE(world.Initialize(createInfo)); + + const float initialY = body->GetTransform()->GetPosition().y; + for (int index = 0; index < 30; ++index) { + world.Step(1.0f / 60.0f); + } + + EXPECT_GT(body->GetTransform()->GetPosition().y, initialY + 0.9f); + EXPECT_NEAR(rigidbody->GetLinearVelocity().y, 2.0f, 0.02f); +} + +TEST(PhysicsWorld_Test, AddForceImpulseMovesDynamicBody) { + using namespace XCEngine::Components; + + if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) { + GTEST_SKIP() << "PhysX backend is not available in this build."; + } + + Scene scene("PhysicsScene"); + GameObject* body = scene.CreateGameObject("Body"); + body->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 1.0f, 0.0f)); + RigidbodyComponent* rigidbody = body->AddComponent(); + rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Dynamic); + rigidbody->SetUseGravity(false); + rigidbody->SetMass(1.0f); + SphereColliderComponent* sphereCollider = body->AddComponent(); + sphereCollider->SetRadius(0.5f); + + XCEngine::Physics::PhysicsWorld world; + XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; + createInfo.scene = &scene; + ASSERT_TRUE(world.Initialize(createInfo)); + + const float initialY = body->GetTransform()->GetPosition().y; + rigidbody->AddForce( + XCEngine::Math::Vector3(0.0f, 3.0f, 0.0f), + XCEngine::Physics::PhysicsForceMode::Impulse); + + for (int index = 0; index < 10; ++index) { + world.Step(1.0f / 60.0f); + } + + EXPECT_GT(body->GetTransform()->GetPosition().y, initialY + 0.35f); + EXPECT_NEAR(rigidbody->GetLinearVelocity().y, 3.0f, 0.02f); +} + } // namespace