Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Modules/PhysX/RNPhysXInternals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ namespace RN

physx::PxQueryHitType::Enum PhysXQueryFilterCallback::preFilter(const physx::PxFilterData &filterData, const physx::PxShape *shape, const physx::PxRigidActor *actor, physx::PxHitFlags &queryFlags)
{
if(ignoreActor && actor == ignoreActor) return physx::PxQueryHitType::eNONE;
const physx::PxFilterData &shapeFilterData = shape->getQueryFilterData();
bool filterMask = (shapeFilterData.word0 & filterData.word1) && (filterData.word0 & shapeFilterData.word1);
bool filterID = (shapeFilterData.word3 == 0 && filterData.word3 == 0) || (shapeFilterData.word2 != filterData.word3 && shapeFilterData.word3 != filterData.word2);
Expand Down
2 changes: 2 additions & 0 deletions Modules/PhysX/RNPhysXInternals.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ namespace RN

class PhysXQueryFilterCallback : public physx::PxQueryFilterCallback
{
public:
physx::PxRigidActor *ignoreActor = nullptr;
physx::PxQueryHitType::Enum preFilter(const physx::PxFilterData &filterData, const physx::PxShape *shape, const physx::PxRigidActor *actor, physx::PxHitFlags &queryFlags) final;
physx::PxQueryHitType::Enum postFilter(const physx::PxFilterData &filterData, const physx::PxQueryHit &hit) final;
};
Expand Down
57 changes: 48 additions & 9 deletions Modules/PhysX/RNPhysXWorld.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -351,28 +351,52 @@ namespace RN

Vector3 diff = to - from;
float distance = diff.GetLength();
if(distance < RN::k::EpsilonFloat) return hit;

diff.Normalize();
physx::PxSweepBuffer callback;

physx::PxFilterData filterData;
filterData.word0 = filterGroup;
filterData.word1 = filterMask;
filterData.word2 = 0;
filterData.word3 = 0;
PhysXQueryFilterCallback queryFilter;

physx::PxTransform pose = physx::PxTransform(physx::PxVec3(from.x, from.y, from.z), physx::PxQuat(rotation.x, rotation.y, rotation.z, rotation.w));
if(shape->GetPhysXShape())
{
if(_scene->sweep(shape->GetPhysXShape()->getGeometry().any(), pose, physx::PxVec3(diff.x, diff.y, diff.z), distance, callback, physx::PxHitFlags(physx::PxHitFlag::eDEFAULT), physx::PxQueryFilterData(filterData, physx::PxQueryFlag::eDYNAMIC | physx::PxQueryFlag::eSTATIC | physx::PxQueryFlag::ePREFILTER), &queryFilter, 0, inflation))
const physx::PxTransform pose(
physx::PxVec3(from.x, from.y, from.z),
physx::PxQuat(rotation.x, rotation.y, rotation.z, rotation.w));

const physx::PxVec3 dir(diff.x, diff.y, diff.z);

auto sweep = [&](PhysXShape *testShape) {
if(!testShape) return;

physx::PxShape *pxShape = testShape->GetPhysXShape();
if(!pxShape) return;

queryFilter.ignoreActor = pxShape->getActor();

physx::PxSweepBuffer callback;
const bool didHit = _scene->sweep(pxShape->getGeometry().any(), pose, dir, distance, callback, physx::PxHitFlags(physx::PxHitFlag::eDEFAULT), physx::PxQueryFilterData(filterData, physx::PxQueryFlag::eDYNAMIC | physx::PxQueryFlag::eSTATIC | physx::PxQueryFlag::ePREFILTER), &queryFilter, 0, inflation);

if(!didHit)
return;

// Keep closest blocking hit across all children
if(hit.distance < 0.0f || callback.block.distance < hit.distance)
{
hit.distance = callback.block.distance;
hit.position.x = callback.block.position.x;
hit.position.y = callback.block.position.y;
hit.position.z = callback.block.position.z;

hit.normal.x = callback.block.normal.x;
hit.normal.y = callback.block.normal.y;
hit.normal.z = callback.block.normal.z;

hit.node = nullptr;
hit.collisionObject = nullptr;

if(callback.block.actor)
{
void *userData = callback.block.actor->userData;
Expand All @@ -384,11 +408,26 @@ namespace RN
}
}

if(callback.block.shape)
{
hit.shapeOther = static_cast<PhysXShape *>(callback.block.shape->userData);
}
hit.shapeSelf = testShape; // the child that hit
hit.shapeOther = callback.block.shape ? static_cast<PhysXShape *>(callback.block.shape->userData) : nullptr;
}
};

if(shape->GetPhysXShape())
{
sweep(shape);
}
else if(shape->IsKindOfClass(PhysXCompoundShape::GetMetaClass()))
{
PhysXCompoundShape *compound = shape->Downcast<PhysXCompoundShape>();

for(size_t i = 0; i < compound->GetNumberOfShapes(); i++)
{
PhysXShape *child = compound->GetShape(i);
sweep(child);
}

Comment on lines +424 to +429
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When sweeping a PhysXCompoundShape, each child is swept using the same pose (derived only from from/rotation). This ignores the per-child transform stored on the compound (GetPosition(i) / GetRotation(i)), so compound children that are offset/rotated will be swept in the wrong place and the returned hit distance/normal can be incorrect. Consider composing a per-child sweep pose (e.g., pose * childLocalTransform) for each child, consistent with how compound transforms are represented elsewhere (and how Jolt compound shapes behave).

Suggested change
for(size_t i = 0; i < compound->GetNumberOfShapes(); i++)
{
PhysXShape *child = compound->GetShape(i);
sweep(child);
}
// For compound shapes, sweep each child using the child's world-space transform.
PhysXContactInfo bestHit;
bestHit.distance = -1.0f;
bestHit.node = nullptr;
bestHit.collisionObject = nullptr;
bestHit.shapeSelf = nullptr;
bestHit.shapeOther = nullptr;
// World-space sweep displacement.
Vector3 delta = to - from;
for(size_t i = 0; i < compound->GetNumberOfShapes(); i++)
{
PhysXShape *child = compound->GetShape(i);
// Compose the child's local transform with the compound's world transform.
const Vector3 childLocalPosition = compound->GetPosition(i);
const Quaternion childLocalRotation = compound->GetRotation(i);
const Vector3 childFrom = from + rotation * childLocalPosition;
const Quaternion childWorldRotation = rotation * childLocalRotation;
const Vector3 childTo = childFrom + delta;
PhysXContactInfo childHit = CastSweep(child, childFrom, childWorldRotation, childTo, inflation, filterGroup, filterMask);
if(childHit.distance >= 0.0f)
{
if(bestHit.distance < 0.0f || childHit.distance < bestHit.distance)
{
bestHit = childHit;
}
}
}
if(bestHit.distance >= 0.0f)
{
// Optionally, we could assign the compound as the "self" shape here.
// For now, keep the child shape that actually hit.
hit = bestHit;
}

Copilot uses AI. Check for mistakes.
// todo(OJ) or return compound shape? if(bestHit.distance >= 0.0f) bestHit.shapeSelf = shape;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid leaving ambiguous inline TODOs in production code. Either resolve the decision about whether shapeSelf should be the compound or the child shape (and document the chosen behavior), or track this with an issue/ticket and remove the inline TODO comment here.

Suggested change
// todo(OJ) or return compound shape? if(bestHit.distance >= 0.0f) bestHit.shapeSelf = shape;
// For compound shapes, hit.shapeSelf is intentionally set to the specific child shape that produced the hit.

Copilot uses AI. Check for mistakes.
}
else
{
Expand Down