diff --git a/engine/include/XCEngine/Physics/PhysicsWorld.h b/engine/include/XCEngine/Physics/PhysicsWorld.h index 25e9040a..72dcdfb6 100644 --- a/engine/include/XCEngine/Physics/PhysicsWorld.h +++ b/engine/include/XCEngine/Physics/PhysicsWorld.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -29,6 +30,11 @@ public: bool Initialize(const PhysicsWorldCreateInfo& createInfo); void Shutdown(); void Step(float fixedDeltaTime); + bool Raycast( + const Math::Vector3& origin, + const Math::Vector3& direction, + float maxDistance, + RaycastHit& outHit); bool IsInitialized() const { return m_initialized; } const PhysicsWorldCreateInfo& GetCreateInfo() const { return m_createInfo; } diff --git a/engine/src/Physics/PhysX/PhysXWorldBackend.cpp b/engine/src/Physics/PhysX/PhysXWorldBackend.cpp index 9822ca3c..2d629ab1 100644 --- a/engine/src/Physics/PhysX/PhysXWorldBackend.cpp +++ b/engine/src/Physics/PhysX/PhysXWorldBackend.cpp @@ -54,6 +54,10 @@ float Max3(float a, float b, float c) { return std::max(a, std::max(b, c)); } +void ResetRaycastHit(RaycastHit& outHit) { + outHit = RaycastHit{}; +} + physx::PxTransform ToPxTransform(const Components::TransformComponent& transform) { return physx::PxTransform( ToPxVec3(transform.GetPosition()), @@ -291,6 +295,30 @@ void ApplyDynamicBodyProperties( } } +void SyncActorBindingPose(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)); + } + } + + const physx::PxTransform targetPose = ToPxTransform(*binding.owner->GetTransform()); + if (binding.dynamicActor) { + if (binding.rigidbody && binding.rigidbody->GetBodyType() == PhysicsBodyType::Kinematic) { + binding.dynamicActor->setKinematicTarget(targetPose); + } + + return; + } + + binding.actor->setGlobalPose(targetPose, false); +} + } // namespace #else namespace { @@ -300,6 +328,10 @@ bool IsRelevantPhysicsComponent(Components::Component* component) { return false; } +void ResetRaycastHit(RaycastHit& outHit) { + outHit = RaycastHit{}; +} + } // namespace struct PhysXWorldBackend::NativeState {}; @@ -418,27 +450,7 @@ void PhysXWorldBackend::Step(float fixedDeltaTime) { return; } - for (auto& [_, binding] : m_native->actorsByOwner) { - if (!binding.actor || !binding.owner) { - continue; - } - - for (ShapeBinding& shapeBinding : binding.shapes) { - if (shapeBinding.shape && shapeBinding.collider) { - shapeBinding.shape->setLocalPose( - BuildShapeLocalPose(*binding.owner, *shapeBinding.collider)); - } - } - - const physx::PxTransform targetPose = ToPxTransform(*binding.owner->GetTransform()); - if (binding.dynamicActor) { - if (binding.rigidbody && binding.rigidbody->GetBodyType() == PhysicsBodyType::Kinematic) { - binding.dynamicActor->setKinematicTarget(targetPose); - } - } else { - binding.actor->setGlobalPose(targetPose, false); - } - } + SyncActorPosesToScene(); m_native->scene->simulate(fixedDeltaTime); m_native->scene->fetchResults(true); @@ -486,6 +498,53 @@ void PhysXWorldBackend::RemoveGameObject(Components::GameObject* gameObject) { RebuildSceneState(); } +bool PhysXWorldBackend::Raycast( + const Math::Vector3& origin, + const Math::Vector3& direction, + float maxDistance, + RaycastHit& outHit) { + ResetRaycastHit(outHit); + +#if XCENGINE_ENABLE_PHYSX + if (!m_initialized || !m_native || !m_native->scene || maxDistance <= 0.0f) { + return false; + } + + const Math::Vector3 normalizedDirection = direction.Normalized(); + if (normalizedDirection.SqrMagnitude() <= Math::EPSILON) { + return false; + } + + SyncActorPosesToScene(); + + physx::PxRaycastBuffer hitBuffer; + const physx::PxHitFlags hitFlags = physx::PxHitFlag::ePOSITION | physx::PxHitFlag::eNORMAL; + const bool hasHit = m_native->scene->raycast( + ToPxVec3(origin), + ToPxVec3(normalizedDirection), + maxDistance, + hitBuffer, + hitFlags); + if (!hasHit || !hitBuffer.hasBlock || !hitBuffer.block.shape) { + return false; + } + + const physx::PxRaycastHit& hit = hitBuffer.block; + auto* collider = static_cast(hit.shape->userData); + outHit.gameObject = collider ? collider->GetGameObject() : nullptr; + outHit.point = ToVector3(hit.position); + outHit.normal = ToVector3(hit.normal); + outHit.distance = hit.distance; + outHit.isTrigger = hit.shape->getFlags().isSet(physx::PxShapeFlag::eTRIGGER_SHAPE); + return true; +#else + (void)origin; + (void)direction; + (void)maxDistance; + return false; +#endif +} + size_t PhysXWorldBackend::GetActorCount() const { #if XCENGINE_ENABLE_PHYSX return m_native ? m_native->actorsByOwner.size() : 0u; @@ -511,6 +570,18 @@ size_t PhysXWorldBackend::GetShapeCount() const { #endif } +void PhysXWorldBackend::SyncActorPosesToScene() { +#if XCENGINE_ENABLE_PHYSX + if (!m_native) { + return; + } + + for (auto& [_, binding] : m_native->actorsByOwner) { + SyncActorBindingPose(binding); + } +#endif +} + void PhysXWorldBackend::RebuildSceneState(Components::Component* ignoredComponent) { #if XCENGINE_ENABLE_PHYSX if (!m_native || !m_native->scene || !m_native->physics || !m_initialized) { @@ -593,10 +664,12 @@ void PhysXWorldBackend::RebuildSceneState(Components::Component* ignoredComponen shape->setLocalPose(BuildShapeLocalPose(*gameObject, *collider)); shape->setSimulationFilterData(physx::PxFilterData()); + shape->setFlag(physx::PxShapeFlag::eSCENE_QUERY_SHAPE, true); if (collider->IsTrigger()) { shape->setFlag(physx::PxShapeFlag::eSIMULATION_SHAPE, false); shape->setFlag(physx::PxShapeFlag::eTRIGGER_SHAPE, true); } + shape->userData = collider; ShapeBinding shapeBinding = {}; shapeBinding.collider = collider; diff --git a/engine/src/Physics/PhysX/PhysXWorldBackend.h b/engine/src/Physics/PhysX/PhysXWorldBackend.h index 04bf10bb..d272effa 100644 --- a/engine/src/Physics/PhysX/PhysXWorldBackend.h +++ b/engine/src/Physics/PhysX/PhysXWorldBackend.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -31,12 +32,18 @@ public: void AddComponent(Components::Component* component); void RemoveComponent(Components::Component* component); void RemoveGameObject(Components::GameObject* gameObject); + bool Raycast( + const Math::Vector3& origin, + const Math::Vector3& direction, + float maxDistance, + RaycastHit& outHit); size_t GetActorCount() const; size_t GetShapeCount() const; private: void RebuildSceneState(Components::Component* ignoredComponent = nullptr); + void SyncActorPosesToScene(); PhysicsWorldCreateInfo m_createInfo = {}; bool m_initialized = false; diff --git a/engine/src/Physics/PhysicsWorld.cpp b/engine/src/Physics/PhysicsWorld.cpp index 44a08b37..c9b131a9 100644 --- a/engine/src/Physics/PhysicsWorld.cpp +++ b/engine/src/Physics/PhysicsWorld.cpp @@ -89,6 +89,19 @@ void PhysicsWorld::Step(float fixedDeltaTime) { m_backend->Step(fixedDeltaTime); } +bool PhysicsWorld::Raycast( + const Math::Vector3& origin, + const Math::Vector3& direction, + float maxDistance, + RaycastHit& outHit) { + if (!m_backend || !m_initialized) { + outHit = RaycastHit{}; + return false; + } + + return m_backend->Raycast(origin, direction, maxDistance, outHit); +} + size_t PhysicsWorld::GetNativeActorCount() const { return m_backend ? m_backend->GetActorCount() : 0u; } diff --git a/tests/Physics/test_physics_world.cpp b/tests/Physics/test_physics_world.cpp index 59df9ae8..d4784a63 100644 --- a/tests/Physics/test_physics_world.cpp +++ b/tests/Physics/test_physics_world.cpp @@ -160,4 +160,124 @@ TEST(PhysicsWorld_Test, DynamicSimulationWritesBackTransform) { EXPECT_LT(ball->GetTransform()->GetPosition().y, initialY - 0.1f); } +TEST(PhysicsWorld_Test, RaycastHitsClosestCollider) { + using namespace XCEngine::Components; + + 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* target = scene.CreateGameObject("Target"); + target->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 2.0f, 0.0f)); + SphereColliderComponent* sphereCollider = target->AddComponent(); + sphereCollider->SetRadius(0.5f); + + 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; + const bool result = world.Raycast( + XCEngine::Math::Vector3(0.0f, 5.0f, 0.0f), + XCEngine::Math::Vector3::Down(), + 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, 2.5f, 0.02f); + EXPECT_NEAR(hit.point.x, 0.0f, 0.001f); + EXPECT_NEAR(hit.point.y, 2.5f, 0.02f); + EXPECT_NEAR(hit.point.z, 0.0f, 0.001f); + EXPECT_NEAR(hit.normal.x, 0.0f, 0.001f); + EXPECT_NEAR(hit.normal.y, 1.0f, 0.02f); + EXPECT_NEAR(hit.normal.z, 0.0f, 0.001f); + EXPECT_FALSE(hit.isTrigger); +} + +TEST(PhysicsWorld_Test, RaycastMissClearsHitOutput) { + using namespace XCEngine::Components; + + Scene scene("PhysicsScene"); + GameObject* target = scene.CreateGameObject("Target"); + target->AddComponent(); + + 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; + hit.gameObject = target; + hit.point = XCEngine::Math::Vector3::One(); + hit.normal = XCEngine::Math::Vector3::Up(); + hit.distance = 123.0f; + hit.isTrigger = true; + + const bool result = world.Raycast( + XCEngine::Math::Vector3(0.0f, 5.0f, 0.0f), + XCEngine::Math::Vector3::Up(), + 10.0f, + hit); + + EXPECT_FALSE(result); + EXPECT_EQ(hit.gameObject, nullptr); + EXPECT_EQ(hit.point, XCEngine::Math::Vector3::Zero()); + EXPECT_EQ(hit.normal, XCEngine::Math::Vector3::Zero()); + EXPECT_FLOAT_EQ(hit.distance, 0.0f); + EXPECT_FALSE(hit.isTrigger); +} + +TEST(PhysicsWorld_Test, RaycastCanHitTriggerCollider) { + using namespace XCEngine::Components; + + Scene scene("PhysicsScene"); + GameObject* trigger = scene.CreateGameObject("Trigger"); + SphereColliderComponent* sphereCollider = trigger->AddComponent(); + sphereCollider->SetRadius(1.0f); + sphereCollider->SetTrigger(true); + + 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; + 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, trigger); + EXPECT_NEAR(hit.distance, 4.0f, 0.02f); + EXPECT_TRUE(hit.isTrigger); + EXPECT_NEAR(hit.normal.x, 0.0f, 0.001f); + EXPECT_NEAR(hit.normal.y, 0.0f, 0.001f); + EXPECT_NEAR(hit.normal.z, -1.0f, 0.02f); +} + } // namespace