diff --git a/engine/src/Physics/PhysX/PhysXWorldBackend.cpp b/engine/src/Physics/PhysX/PhysXWorldBackend.cpp index 2d629ab1..55b917ba 100644 --- a/engine/src/Physics/PhysX/PhysXWorldBackend.cpp +++ b/engine/src/Physics/PhysX/PhysXWorldBackend.cpp @@ -295,20 +295,61 @@ void ApplyDynamicBodyProperties( } } -void SyncActorBindingPose(ActorBinding& binding) { +void SyncShapeBindingState( + const Components::GameObject& actorOwner, + ShapeBinding& shapeBinding) { + if (!shapeBinding.shape || !shapeBinding.collider) { + return; + } + + shapeBinding.shape->setLocalPose( + BuildShapeLocalPose(actorOwner, *shapeBinding.collider)); + shapeBinding.shape->setGeometry(BuildColliderGeometry(*shapeBinding.collider).any()); + shapeBinding.shape->setFlag(physx::PxShapeFlag::eSCENE_QUERY_SHAPE, true); + + const bool isTrigger = shapeBinding.collider->IsTrigger(); + shapeBinding.shape->setFlag(physx::PxShapeFlag::eSIMULATION_SHAPE, !isTrigger); + shapeBinding.shape->setFlag(physx::PxShapeFlag::eTRIGGER_SHAPE, isTrigger); + shapeBinding.shape->userData = shapeBinding.collider; + shapeBinding.sourceGameObject = shapeBinding.collider->GetGameObject(); + + if (shapeBinding.material) { + shapeBinding.material->setStaticFriction( + std::max(0.0f, shapeBinding.collider->GetStaticFriction())); + shapeBinding.material->setDynamicFriction( + std::max(0.0f, shapeBinding.collider->GetDynamicFriction())); + shapeBinding.material->setRestitution( + std::clamp(shapeBinding.collider->GetRestitution(), 0.0f, 1.0f)); + } +} + +bool BindingNeedsActorRebuild(const ActorBinding& binding) { + if (!binding.actor) { + return false; + } + + const bool shouldUseDynamicActor = + binding.rigidbody != nullptr && + binding.rigidbody->GetBodyType() != PhysicsBodyType::Static; + const bool usesDynamicActor = binding.dynamicActor != nullptr; + return shouldUseDynamicActor != usesDynamicActor; +} + +void SyncActorBindingState(ActorBinding& binding) { if (!binding.actor || !binding.owner) { return; } for (ShapeBinding& shapeBinding : binding.shapes) { - if (shapeBinding.shape && shapeBinding.collider) { - shapeBinding.shape->setLocalPose( - BuildShapeLocalPose(*binding.owner, *shapeBinding.collider)); - } + SyncShapeBindingState(*binding.owner, shapeBinding); } const physx::PxTransform targetPose = ToPxTransform(*binding.owner->GetTransform()); if (binding.dynamicActor) { + if (binding.rigidbody) { + ApplyDynamicBodyProperties(*binding.dynamicActor, *binding.rigidbody); + } + if (binding.rigidbody && binding.rigidbody->GetBodyType() == PhysicsBodyType::Kinematic) { binding.dynamicActor->setKinematicTarget(targetPose); } @@ -450,7 +491,7 @@ void PhysXWorldBackend::Step(float fixedDeltaTime) { return; } - SyncActorPosesToScene(); + SyncActorStateToScene(); m_native->scene->simulate(fixedDeltaTime); m_native->scene->fetchResults(true); @@ -515,7 +556,7 @@ bool PhysXWorldBackend::Raycast( return false; } - SyncActorPosesToScene(); + SyncActorStateToScene(); physx::PxRaycastBuffer hitBuffer; const physx::PxHitFlags hitFlags = physx::PxHitFlag::ePOSITION | physx::PxHitFlag::eNORMAL; @@ -570,14 +611,21 @@ size_t PhysXWorldBackend::GetShapeCount() const { #endif } -void PhysXWorldBackend::SyncActorPosesToScene() { +void PhysXWorldBackend::SyncActorStateToScene() { #if XCENGINE_ENABLE_PHYSX if (!m_native) { return; } + for (const auto& [_, binding] : m_native->actorsByOwner) { + if (BindingNeedsActorRebuild(binding)) { + RebuildSceneState(); + break; + } + } + for (auto& [_, binding] : m_native->actorsByOwner) { - SyncActorBindingPose(binding); + SyncActorBindingState(binding); } #endif } diff --git a/engine/src/Physics/PhysX/PhysXWorldBackend.h b/engine/src/Physics/PhysX/PhysXWorldBackend.h index d272effa..d43b56b9 100644 --- a/engine/src/Physics/PhysX/PhysXWorldBackend.h +++ b/engine/src/Physics/PhysX/PhysXWorldBackend.h @@ -43,7 +43,7 @@ public: private: void RebuildSceneState(Components::Component* ignoredComponent = nullptr); - void SyncActorPosesToScene(); + void SyncActorStateToScene(); PhysicsWorldCreateInfo m_createInfo = {}; bool m_initialized = false; diff --git a/tests/Physics/test_physics_world.cpp b/tests/Physics/test_physics_world.cpp index d4784a63..ae11bc75 100644 --- a/tests/Physics/test_physics_world.cpp +++ b/tests/Physics/test_physics_world.cpp @@ -280,4 +280,122 @@ TEST(PhysicsWorld_Test, RaycastCanHitTriggerCollider) { EXPECT_NEAR(hit.normal.z, -1.0f, 0.02f); } +TEST(PhysicsWorld_Test, RuntimeColliderGeometryChangesUpdateRaycast) { + using namespace XCEngine::Components; + + Scene scene("PhysicsScene"); + GameObject* target = scene.CreateGameObject("Target"); + BoxColliderComponent* boxCollider = target->AddComponent(); + boxCollider->SetSize(XCEngine::Math::Vector3(1.0f, 1.0f, 1.0f)); + + XCEngine::Physics::PhysicsWorld world; + XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; + createInfo.scene = &scene; + + const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable(); + ASSERT_EQ(world.Initialize(createInfo), expectedInitialized); + + XCEngine::Physics::RaycastHit hit; + EXPECT_FALSE(world.Raycast( + XCEngine::Math::Vector3(0.0f, 1.5f, -5.0f), + XCEngine::Math::Vector3::Forward(), + 10.0f, + hit)); + + boxCollider->SetSize(XCEngine::Math::Vector3(1.0f, 4.0f, 1.0f)); + + const bool result = world.Raycast( + XCEngine::Math::Vector3(0.0f, 1.5f, -5.0f), + XCEngine::Math::Vector3::Forward(), + 10.0f, + hit); + + if (!expectedInitialized) { + EXPECT_FALSE(result); + EXPECT_EQ(hit.gameObject, nullptr); + return; + } + + ASSERT_TRUE(result); + EXPECT_EQ(hit.gameObject, target); + EXPECT_NEAR(hit.distance, 4.5f, 0.02f); +} + +TEST(PhysicsWorld_Test, RuntimeTriggerFlagChangesUpdateRaycastHit) { + using namespace XCEngine::Components; + + Scene scene("PhysicsScene"); + GameObject* target = scene.CreateGameObject("Target"); + SphereColliderComponent* sphereCollider = target->AddComponent(); + sphereCollider->SetRadius(1.0f); + + XCEngine::Physics::PhysicsWorld world; + XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; + createInfo.scene = &scene; + + const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable(); + ASSERT_EQ(world.Initialize(createInfo), expectedInitialized); + + sphereCollider->SetTrigger(true); + + XCEngine::Physics::RaycastHit hit; + const bool result = world.Raycast( + XCEngine::Math::Vector3(0.0f, 0.0f, -5.0f), + XCEngine::Math::Vector3::Forward(), + 10.0f, + hit); + + if (!expectedInitialized) { + EXPECT_FALSE(result); + EXPECT_EQ(hit.gameObject, nullptr); + return; + } + + ASSERT_TRUE(result); + EXPECT_EQ(hit.gameObject, target); + EXPECT_TRUE(hit.isTrigger); +} + +TEST(PhysicsWorld_Test, RuntimeBodyTypeChangesRebuildActorAndEnableSimulation) { + using namespace XCEngine::Components; + + if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) { + GTEST_SKIP() << "PhysX backend is not available in this build."; + } + + Scene scene("PhysicsScene"); + + GameObject* ground = scene.CreateGameObject("Ground"); + ground->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, -0.5f, 0.0f)); + BoxColliderComponent* groundCollider = ground->AddComponent(); + groundCollider->SetSize(XCEngine::Math::Vector3(20.0f, 1.0f, 20.0f)); + + GameObject* body = scene.CreateGameObject("Body"); + body->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 4.0f, 0.0f)); + RigidbodyComponent* rigidbody = body->AddComponent(); + rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Static); + SphereColliderComponent* sphereCollider = body->AddComponent(); + sphereCollider->SetRadius(0.5f); + + XCEngine::Physics::PhysicsWorld world; + XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; + createInfo.scene = &scene; + ASSERT_TRUE(world.Initialize(createInfo)); + ASSERT_EQ(world.GetNativeActorCount(), 2u); + + const float initialY = body->GetTransform()->GetPosition().y; + world.Step(1.0f / 60.0f); + EXPECT_NEAR(body->GetTransform()->GetPosition().y, initialY, 0.001f); + + rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Dynamic); + world.Step(1.0f / 60.0f); + EXPECT_EQ(world.GetNativeActorCount(), 2u); + + for (int index = 0; index < 30; ++index) { + world.Step(1.0f / 60.0f); + } + + EXPECT_LT(body->GetTransform()->GetPosition().y, initialY - 0.1f); +} + } // namespace