feat(physics): add physx raycast queries
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Physics/RaycastHit.h>
|
||||
#include <XCEngine/Physics/PhysicsTypes.h>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -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; }
|
||||
|
||||
@@ -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<Components::ColliderComponent*>(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;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Physics/RaycastHit.h>
|
||||
#include <XCEngine/Physics/PhysicsTypes.h>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<BoxColliderComponent>();
|
||||
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<SphereColliderComponent>();
|
||||
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<BoxColliderComponent>();
|
||||
|
||||
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<SphereColliderComponent>();
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user