Skip to content

Commit

Permalink
Added ClosestHitPerBodyCollisionCollector (#1465)
Browse files Browse the repository at this point in the history
* Added overridable CollisionCollector::OnBodyEnd that is called after all hits for a body have been processed when collecting hits through NarrowPhaseQuery.
* Added ClosestHitPerBodyCollisionCollector which will report the closest / deepest hit per body that the collision query collides with.

Fixes #481
  • Loading branch information
jrouwe authored Jan 19, 2025
1 parent 6ae6658 commit 20a774c
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 75 deletions.
2 changes: 2 additions & 0 deletions Docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
* Added support for CharacterVirtual to override the inner rigid body ID. This can be used to make the simulation deterministic in e.g. client/server setups.
* Added OnContactPersisted, OnContactRemoved, OnCharacterContactPersisted and OnCharacterContactRemoved functions on CharacterContactListener to better match the interface of ContactListener.
* Every CharacterVirtual now has a CharacterID. This ID can be used to identify the character after removal and is used to make the simulation deterministic in case a character collides with multiple other virtual characters.
* Added overridable CollisionCollector::OnBodyEnd that is called after all hits for a body have been processed when collecting hits through NarrowPhaseQuery.
* Added ClosestHitPerBodyCollisionCollector which will report the closest / deepest hit per body that the collision query collides with.

### Bug fixes

Expand Down
3 changes: 3 additions & 0 deletions Jolt/Physics/Collision/CollisionCollector.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ class CollisionCollector
/// before AddHit is called (e.g. the user data pointer or the velocity of the body).
virtual void OnBody([[maybe_unused]] const Body &inBody) { /* Collects nothing by default */ }

/// When running a query through the NarrowPhaseQuery class, this will be called after all AddHit calls have been made for a particular body.
virtual void OnBodyEnd() { /* Does nothing by default */ }

/// Set by the collision detection functions to the current TransformedShape that we're colliding against before calling the AddHit function
void SetContext(const TransformedShape *inContext) { mContext = inContext; }
const TransformedShape *GetContext() const { return mContext; }
Expand Down
85 changes: 85 additions & 0 deletions Jolt/Physics/Collision/CollisionCollectorImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,91 @@ class ClosestHitCollisionCollector : public CollectorType
bool mHadHit = false;
};

/// Implementation that collects the closest / deepest hit for each body and optionally sorts them on distance
template <class CollectorType>
class ClosestHitPerBodyCollisionCollector : public CollectorType
{
public:
/// Redeclare ResultType
using ResultType = typename CollectorType::ResultType;

// See: CollectorType::Reset
virtual void Reset() override
{
CollectorType::Reset();

mHits.clear();
mHadHit = false;
}

// See: CollectorType::OnBody
virtual void OnBody(const Body &inBody) override
{
// Store the early out fraction so we can restore it after we've collected all hits for this body
mPreviousEarlyOutFraction = CollectorType::GetEarlyOutFraction();
}

// See: CollectorType::AddHit
virtual void AddHit(const ResultType &inResult) override
{
float early_out = inResult.GetEarlyOutFraction();
if (!mHadHit || early_out < CollectorType::GetEarlyOutFraction())
{
// Update early out fraction to avoid spending work on collecting further hits for this body
CollectorType::UpdateEarlyOutFraction(early_out);

if (!mHadHit)
{
// First time we have a hit we append it to the array
mHits.push_back(inResult);
mHadHit = true;
}
else
{
// Closer hits will override the previous one
mHits.back() = inResult;
}
}
}

// See: CollectorType::OnBodyEnd
virtual void OnBodyEnd() override
{
if (mHadHit)
{
// Reset the early out fraction to the configured value so that we will continue
// to collect hits at any distance for other bodies
JPH_ASSERT(mPreviousEarlyOutFraction != -FLT_MAX); // Check that we got a call to OnBody
CollectorType::ResetEarlyOutFraction(mPreviousEarlyOutFraction);
mHadHit = false;
}

// For asserting purposes we reset the stored early out fraction so we can detect that OnBody was called
JPH_IF_ENABLE_ASSERTS(mPreviousEarlyOutFraction = -FLT_MAX;)
}

/// Order hits on closest first
void Sort()
{
QuickSort(mHits.begin(), mHits.end(), [](const ResultType &inLHS, const ResultType &inRHS) { return inLHS.GetEarlyOutFraction() < inRHS.GetEarlyOutFraction(); });
}

/// Check if any hits were collected
inline bool HadHit() const
{
return !mHits.empty();
}

Array<ResultType> mHits;

private:
// Store early out fraction that was initially configured for the collector
float mPreviousEarlyOutFraction = -FLT_MAX;

// Flag to indicate if we have a hit for the current body
bool mHadHit = false;
};

/// Simple implementation that collects any hit
template <class CollectorType>
class AnyHitCollisionCollector : public CollectorType
Expand Down
10 changes: 10 additions & 0 deletions Jolt/Physics/Collision/InternalEdgeRemovingCollector.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ JPH_NAMESPACE_BEGIN

/// Removes internal edges from collision results. Can be used to filter out 'ghost collisions'.
/// Based on: Contact generation for meshes - Pierre Terdiman (https://www.codercorner.com/MeshContacts.pdf)
///
/// Note that this class requires that CollideSettingsBase::mActiveEdgeMode == EActiveEdgeMode::CollideWithAll
/// and CollideSettingsBase::mCollectFacesMode == ECollectFacesMode::CollectFaces.
class InternalEdgeRemovingCollector : public CollideShapeCollector
{
static constexpr uint cMaxDelayedResults = 16;
Expand Down Expand Up @@ -221,6 +224,13 @@ class InternalEdgeRemovingCollector : public CollideShapeCollector
mDelayedResults.clear();
}

// See: CollideShapeCollector::OnBodyEnd
virtual void OnBodyEnd() override
{
Flush();
mChainedCollector.OnBodyEnd();
}

/// Version of CollisionDispatch::sCollideShapeVsShape that removes internal edges
static void sCollideShapeVsShape(const Shape *inShape1, const Shape *inShape2, Vec3Arg inScale1, Vec3Arg inScale2, Mat44Arg inCenterOfMassTransform1, Mat44Arg inCenterOfMassTransform2, const SubShapeIDCreator &inSubShapeIDCreator1, const SubShapeIDCreator &inSubShapeIDCreator2, const CollideShapeSettings &inCollideShapeSettings, CollideShapeCollector &ioCollector, const ShapeFilter &inShapeFilter = { })
{
Expand Down
101 changes: 26 additions & 75 deletions Jolt/Physics/Collision/NarrowPhaseQuery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ void NarrowPhaseQuery::CastRay(const RRayCast &inRay, const RayCastSettings &inR
// Do narrow phase collision check
ts.CastRay(mRay, mRayCastSettings, mCollector, mShapeFilter);

// Notify collector of the end of this body
// We do this before updating the early out fraction so that the collector can still modify it
mCollector.OnBodyEnd();

// Update early out fraction based on narrow phase collector
UpdateEarlyOutFraction(mCollector.GetEarlyOutFraction());
}
Expand Down Expand Up @@ -189,6 +193,10 @@ void NarrowPhaseQuery::CollidePoint(RVec3Arg inPoint, CollidePointCollector &ioC
// Do narrow phase collision check
ts.CollidePoint(mPoint, mCollector, mShapeFilter);

// Notify collector of the end of this body
// We do this before updating the early out fraction so that the collector can still modify it
mCollector.OnBodyEnd();

// Update early out fraction based on narrow phase collector
UpdateEarlyOutFraction(mCollector.GetEarlyOutFraction());
}
Expand Down Expand Up @@ -255,6 +263,10 @@ void NarrowPhaseQuery::CollideShape(const Shape *inShape, Vec3Arg inShapeScale,
// Do narrow phase collision check
ts.CollideShape(mShape, mShapeScale, mCenterOfMassTransform, mCollideShapeSettings, mBaseOffset, mCollector, mShapeFilter);

// Notify collector of the end of this body
// We do this before updating the early out fraction so that the collector can still modify it
mCollector.OnBodyEnd();

// Update early out fraction based on narrow phase collector
UpdateEarlyOutFraction(mCollector.GetEarlyOutFraction());
}
Expand Down Expand Up @@ -284,82 +296,13 @@ void NarrowPhaseQuery::CollideShape(const Shape *inShape, Vec3Arg inShapeScale,

void NarrowPhaseQuery::CollideShapeWithInternalEdgeRemoval(const Shape *inShape, Vec3Arg inShapeScale, RMat44Arg inCenterOfMassTransform, const CollideShapeSettings &inCollideShapeSettings, RVec3Arg inBaseOffset, CollideShapeCollector &ioCollector, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter) const
{
JPH_PROFILE_FUNCTION();

class MyCollector : public CollideShapeBodyCollector
{
public:
MyCollector(const Shape *inShape, Vec3Arg inShapeScale, RMat44Arg inCenterOfMassTransform, const CollideShapeSettings &inCollideShapeSettings, RVec3Arg inBaseOffset, CollideShapeCollector &ioCollector, const BodyLockInterface &inBodyLockInterface, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter) :
CollideShapeBodyCollector(ioCollector),
mShape(inShape),
mShapeScale(inShapeScale),
mCenterOfMassTransform(inCenterOfMassTransform),
mBaseOffset(inBaseOffset),
mBodyLockInterface(inBodyLockInterface),
mBodyFilter(inBodyFilter),
mShapeFilter(inShapeFilter),
mCollideShapeSettings(inCollideShapeSettings),
mCollector(ioCollector)
{
// We require these settings for internal edge removal to work
mCollideShapeSettings.mActiveEdgeMode = EActiveEdgeMode::CollideWithAll;
mCollideShapeSettings.mCollectFacesMode = ECollectFacesMode::CollectFaces;
}

virtual void AddHit(const ResultType &inResult) override
{
// Only test shape if it passes the body filter
if (mBodyFilter.ShouldCollide(inResult))
{
// Lock the body
BodyLockRead lock(mBodyLockInterface, inResult);
if (lock.SucceededAndIsInBroadPhase()) // Race condition: body could have been removed since it has been found in the broadphase, ensures body is in the broadphase while we call the callbacks
{
const Body &body = lock.GetBody();

// Check body filter again now that we've locked the body
if (mBodyFilter.ShouldCollideLocked(body))
{
// Collect the transformed shape
TransformedShape ts = body.GetTransformedShape();
// We require these settings for internal edge removal to work
CollideShapeSettings settings = inCollideShapeSettings;
settings.mActiveEdgeMode = EActiveEdgeMode::CollideWithAll;
settings.mCollectFacesMode = ECollectFacesMode::CollectFaces;

// Notify collector of new body
mCollector.OnBody(body);

// Release the lock now, we have all the info we need in the transformed shape
lock.ReleaseLock();

// Do narrow phase collision check
ts.CollideShape(mShape, mShapeScale, mCenterOfMassTransform, mCollideShapeSettings, mBaseOffset, mCollector, mShapeFilter);

// After each body, we need to flush the InternalEdgeRemovingCollector because it uses 'ts' as context and it will go out of scope at the end of this block
mCollector.Flush();

// Update early out fraction based on narrow phase collector
UpdateEarlyOutFraction(mCollector.GetEarlyOutFraction());
}
}
}
}

const Shape * mShape;
Vec3 mShapeScale;
RMat44 mCenterOfMassTransform;
RVec3 mBaseOffset;
const BodyLockInterface & mBodyLockInterface;
const BodyFilter & mBodyFilter;
const ShapeFilter & mShapeFilter;
CollideShapeSettings mCollideShapeSettings;
InternalEdgeRemovingCollector mCollector;
};

// Calculate bounds for shape and expand by max separation distance
AABox bounds = inShape->GetWorldSpaceBounds(inCenterOfMassTransform, inShapeScale);
bounds.ExpandBy(Vec3::sReplicate(inCollideShapeSettings.mMaxSeparationDistance));

// Do broadphase test
MyCollector collector(inShape, inShapeScale, inCenterOfMassTransform, inCollideShapeSettings, inBaseOffset, ioCollector, *mBodyLockInterface, inBodyFilter, inShapeFilter);
mBroadPhaseQuery->CollideAABox(bounds, collector, inBroadPhaseLayerFilter, inObjectLayerFilter);
InternalEdgeRemovingCollector wrapper(ioCollector);
CollideShape(inShape, inShapeScale, inCenterOfMassTransform, settings, inBaseOffset, wrapper, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inShapeFilter);
}

void NarrowPhaseQuery::CastShape(const RShapeCast &inShapeCast, const ShapeCastSettings &inShapeCastSettings, RVec3Arg inBaseOffset, CastShapeCollector &ioCollector, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter) const
Expand Down Expand Up @@ -409,6 +352,10 @@ void NarrowPhaseQuery::CastShape(const RShapeCast &inShapeCast, const ShapeCastS
// Do narrow phase collision check
ts.CastShape(mShapeCast, mShapeCastSettings, mBaseOffset, mCollector, mShapeFilter);

// Notify collector of the end of this body
// We do this before updating the early out fraction so that the collector can still modify it
mCollector.OnBodyEnd();

// Update early out fraction based on narrow phase collector
UpdateEarlyOutFraction(mCollector.GetEarlyOutFraction());
}
Expand Down Expand Up @@ -471,6 +418,10 @@ void NarrowPhaseQuery::CollectTransformedShapes(const AABox &inBox, TransformedS
// Do narrow phase collision check
ts.CollectTransformedShapes(mBox, mCollector, mShapeFilter);

// Notify collector of the end of this body
// We do this before updating the early out fraction so that the collector can still modify it
mCollector.OnBodyEnd();

// Update early out fraction based on narrow phase collector
UpdateEarlyOutFraction(mCollector.GetEarlyOutFraction());
}
Expand Down
28 changes: 28 additions & 0 deletions Samples/SamplesApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ SamplesApp::SamplesApp(const String &inCommandLine) :
mDebugUI->CreateCheckBox(probe_options, "Shrunken Shape + Convex Radius", mUseShrunkenShapeAndConvexRadius, [this](UICheckBox::EState inState) { mUseShrunkenShapeAndConvexRadius = inState == UICheckBox::STATE_CHECKED; });
mDebugUI->CreateCheckBox(probe_options, "Draw Supporting Face", mDrawSupportingFace, [this](UICheckBox::EState inState) { mDrawSupportingFace = inState == UICheckBox::STATE_CHECKED; });
mDebugUI->CreateSlider(probe_options, "Max Hits", float(mMaxHits), 0, 10, 1, [this](float inValue) { mMaxHits = (int)inValue; });
mDebugUI->CreateCheckBox(probe_options, "Closest Hit Per Body", mClosestHitPerBody, [this](UICheckBox::EState inState) { mClosestHitPerBody = inState == UICheckBox::STATE_CHECKED; });
mDebugUI->ShowMenu(probe_options);
});
mDebugUI->CreateTextButton(main_menu, "Shoot Object", [this]() {
Expand Down Expand Up @@ -1166,6 +1167,15 @@ bool SamplesApp::CastProbe(float inProbeLength, float &outFraction, RVec3 &outPo
if (collector.HadHit())
hits.push_back(collector.mHit);
}
else if (mClosestHitPerBody)
{
ClosestHitPerBodyCollisionCollector<CastRayCollector> collector;
mPhysicsSystem->GetNarrowPhaseQuery().CastRay(ray, settings, collector);
collector.Sort();
hits.insert(hits.end(), collector.mHits.begin(), collector.mHits.end());
if ((int)hits.size() > mMaxHits)
hits.resize(mMaxHits);
}
else
{
AllHitCollisionCollector<CastRayCollector> collector;
Expand Down Expand Up @@ -1305,6 +1315,15 @@ bool SamplesApp::CastProbe(float inProbeLength, float &outFraction, RVec3 &outPo
if (collector.HadHit())
hits.push_back(collector.mHit);
}
else if (mClosestHitPerBody)
{
ClosestHitPerBodyCollisionCollector<CollideShapeCollector> collector;
(mPhysicsSystem->GetNarrowPhaseQuery().*collide_shape_function)(shape, Vec3::sOne(), shape_transform, settings, base_offset, collector, { }, { }, { }, { });
collector.Sort();
hits.insert(hits.end(), collector.mHits.begin(), collector.mHits.end());
if ((int)hits.size() > mMaxHits)
hits.resize(mMaxHits);
}
else
{
AllHitCollisionCollector<CollideShapeCollector> collector;
Expand Down Expand Up @@ -1396,6 +1415,15 @@ bool SamplesApp::CastProbe(float inProbeLength, float &outFraction, RVec3 &outPo
if (collector.HadHit())
hits.push_back(collector.mHit);
}
else if (mClosestHitPerBody)
{
ClosestHitPerBodyCollisionCollector<CastShapeCollector> collector;
mPhysicsSystem->GetNarrowPhaseQuery().CastShape(shape_cast, settings, base_offset, collector);
collector.Sort();
hits.insert(hits.end(), collector.mHits.begin(), collector.mHits.end());
if ((int)hits.size() > mMaxHits)
hits.resize(mMaxHits);
}
else
{
AllHitCollisionCollector<CastShapeCollector> collector;
Expand Down
1 change: 1 addition & 0 deletions Samples/SamplesApp.h
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ class SamplesApp : public Application
bool mUseShrunkenShapeAndConvexRadius = false; // Shrink then expand the shape by the convex radius
bool mDrawSupportingFace = false; // Draw the result of GetSupportingFace
int mMaxHits = 10; // The maximum number of hits to request for a collision probe.
bool mClosestHitPerBody = false; // If we are only interested in the closest hit for every body

// Which object to shoot
enum class EShootObjectShape
Expand Down
Loading

0 comments on commit 20a774c

Please sign in to comment.