Browse Source

Support for soft body contact callbacks (#936)

* Note that you will only receive a single contact callback for all contacts with a soft body (for performance reaons).
* Allows overriding the mass and inertia of both bodies so you can e.g. make a cloth that responds to a rigid body but not the other way around
* Ability to turn a rigid body contact into a sensor contact. This will report any collisions but will not trigger any collision response.
Jorrit Rouwe 1 year ago
parent
commit
405f3536d4

+ 12 - 1
Docs/Architecture.md

@@ -243,10 +243,21 @@ Soft bodies use the Body class just like rigid bodies but can be identified by c
 
 
 Soft bodies try to implement as much as possible of the normal Body interface, but this interface provides a simplified version of reality, e.g. Body::GetLinearVelocity will return the average particle speed and Body::GetPosition returns the average particle position. During simulation, a soft body will never update its rotation. Internally it stores particle velocities in local space, so if you rotate a soft body e.g. by calling BodyInterface::SetRotation, the body will rotate but its velocity will as well.
 Soft bodies try to implement as much as possible of the normal Body interface, but this interface provides a simplified version of reality, e.g. Body::GetLinearVelocity will return the average particle speed and Body::GetPosition returns the average particle position. During simulation, a soft body will never update its rotation. Internally it stores particle velocities in local space, so if you rotate a soft body e.g. by calling BodyInterface::SetRotation, the body will rotate but its velocity will as well.
 
 
+### Soft Body Contact Listeners {#soft-body-contact-listener}
+
+Soft Bodies provide contacts with other bodies through the SoftBodyContactListener class. This contact listener works a little bit different from the normal contact listener as you will not receive a contact callback per colliding vertex.
+
+After the broad phase has detected an overlap and the normal layer / collision group filters have had a chance to reject the collision, you will receive a SoftBodyContactListener::OnSoftBodyContactValidate callback. This callback allows you to specify how the vertices of the soft body should interact with the other body. You can override the mass for both bodies and you can turn the contact into a sensor contact.
+
+The simulation will then proceed to do all collision detection and response and after that is finished, you will receive a SoftBodyContactListener::OnSoftBodyContactAdded callback that allows you to inspect all collisions that happened during the simulation step. In order to do this a SoftBodyManifold is provided which allows you to loop over the vertices and ask each vertex what it collided with.
+
+Note that at the time of the callback, multiple threads are operating at the same time. The soft body is stable and can be safely read. The other body that is collided with is not stable however, so you cannot safely read its position/orientation and velocity as it may be modified by another soft body collision at the same time. 
+
+### Soft Body Work In Progress {#soft-body-wip}
+
 Soft bodies are currently in development, please note the following:
 Soft bodies are currently in development, please note the following:
 
 
 * Soft bodies can only collide with rigid bodies, collisions between soft bodies are not implemented yet.
 * Soft bodies can only collide with rigid bodies, collisions between soft bodies are not implemented yet.
-* ContactListener callbacks are not triggered for soft bodies.
 * AddForce/AddTorque/SetLinearVelocity/SetLinearVelocityClamped/SetAngularVelocity/SetAngularVelocityClamped/AddImpulse/AddAngularImpulse have no effect on soft bodies as the velocity is stored per particle rather than per body.
 * AddForce/AddTorque/SetLinearVelocity/SetLinearVelocityClamped/SetAngularVelocity/SetAngularVelocityClamped/AddImpulse/AddAngularImpulse have no effect on soft bodies as the velocity is stored per particle rather than per body.
 * Buoyancy calculations have not been implemented yet.
 * Buoyancy calculations have not been implemented yet.
 * Constraints cannot operate on soft bodies, set the inverse mass of a particle to zero and move it by setting a velocity to constrain a soft body to something else.
 * Constraints cannot operate on soft bodies, set the inverse mass of a particle to zero and move it by setting a velocity to constrain a soft body to something else.

+ 1 - 0
Docs/ReleaseNotes.md

@@ -25,6 +25,7 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Added user data to CharacterVirtual.
 * Added user data to CharacterVirtual.
 * Added fraction hint to PathConstraintPath::GetClosestPoint. This can be used to speed up the search along the curve and to disambiguate fractions in case a path reaches the same point multiple times (i.e. a figure-8).
 * Added fraction hint to PathConstraintPath::GetClosestPoint. This can be used to speed up the search along the curve and to disambiguate fractions in case a path reaches the same point multiple times (i.e. a figure-8).
 * Added ability to update the height field materials after creation.
 * Added ability to update the height field materials after creation.
+* Added SoftBodyContactListener which allows you to get callbacks for collisions between soft bodies and rigid bodies.
 
 
 ### Improvements
 ### Improvements
 * Multithreading the SetupVelocityConstraints job. This was causing a bottleneck in the case that there are a lot of constraints but very few possible collisions.
 * Multithreading the SetupVelocityConstraints job. This was causing a bottleneck in the case that there are a lot of constraints but very few possible collisions.

+ 2 - 0
Jolt/Jolt.cmake

@@ -374,8 +374,10 @@ set(JOLT_PHYSICS_SRC_FILES
 	${JOLT_PHYSICS_ROOT}/Physics/PhysicsUpdateContext.h
 	${JOLT_PHYSICS_ROOT}/Physics/PhysicsUpdateContext.h
 	${JOLT_PHYSICS_ROOT}/Physics/Ragdoll/Ragdoll.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Ragdoll/Ragdoll.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Ragdoll/Ragdoll.h
 	${JOLT_PHYSICS_ROOT}/Physics/Ragdoll/Ragdoll.h
+	${JOLT_PHYSICS_ROOT}/Physics/SoftBody/SoftBodyContactListener.h
 	${JOLT_PHYSICS_ROOT}/Physics/SoftBody/SoftBodyCreationSettings.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/SoftBody/SoftBodyCreationSettings.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/SoftBody/SoftBodyCreationSettings.h
 	${JOLT_PHYSICS_ROOT}/Physics/SoftBody/SoftBodyCreationSettings.h
+	${JOLT_PHYSICS_ROOT}/Physics/SoftBody/SoftBodyManifold.h
 	${JOLT_PHYSICS_ROOT}/Physics/SoftBody/SoftBodyMotionProperties.h
 	${JOLT_PHYSICS_ROOT}/Physics/SoftBody/SoftBodyMotionProperties.h
 	${JOLT_PHYSICS_ROOT}/Physics/SoftBody/SoftBodyMotionProperties.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/SoftBody/SoftBodyMotionProperties.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/SoftBody/SoftBodyShape.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/SoftBody/SoftBodyShape.cpp

+ 1 - 1
Jolt/Physics/Collision/ContactListener.h

@@ -71,7 +71,7 @@ public:
 	virtual					~ContactListener() = default;
 	virtual					~ContactListener() = default;
 
 
 	/// Called after detecting a collision between a body pair, but before calling OnContactAdded and before adding the contact constraint.
 	/// Called after detecting a collision between a body pair, but before calling OnContactAdded and before adding the contact constraint.
-	/// If the function returns false, the contact will not be added and any other contacts between this body pair will not be processed.
+	/// If the function rejects the contact, the contact will not be added and any other contacts between this body pair will not be processed.
 	/// This function will only be called once per PhysicsSystem::Update per body pair and may not be called again the next update
 	/// This function will only be called once per PhysicsSystem::Update per body pair and may not be called again the next update
 	/// if a contact persists and no new contact pairs between sub shapes are found.
 	/// if a contact persists and no new contact pairs between sub shapes are found.
 	/// This is a rather expensive time to reject a contact point since a lot of the collision detection has happened already, make sure you
 	/// This is a rather expensive time to reject a contact point since a lot of the collision detection has happened already, make sure you

+ 2 - 2
Jolt/Physics/PhysicsSystem.cpp

@@ -2529,8 +2529,8 @@ void PhysicsSystem::JobSoftBodyCollide(PhysicsUpdateContext *ioContext) const
 void PhysicsSystem::JobSoftBodySimulate(PhysicsUpdateContext *ioContext, uint inThreadIndex) const
 void PhysicsSystem::JobSoftBodySimulate(PhysicsUpdateContext *ioContext, uint inThreadIndex) const
 {
 {
 #ifdef JPH_ENABLE_ASSERTS
 #ifdef JPH_ENABLE_ASSERTS
-	// Updating velocities of soft bodies
-	BodyAccess::Grant grant(BodyAccess::EAccess::ReadWrite, BodyAccess::EAccess::None);
+	// Updating velocities of soft bodies, allow the contact listener to read the soft body state
+	BodyAccess::Grant grant(BodyAccess::EAccess::ReadWrite, BodyAccess::EAccess::Read);
 #endif
 #endif
 
 
 	// Calculate at which body we start to distribute the workload across the threads
 	// Calculate at which body we start to distribute the workload across the threads

+ 8 - 0
Jolt/Physics/PhysicsSystem.h

@@ -20,6 +20,7 @@ class JobSystem;
 class StateRecorder;
 class StateRecorder;
 class TempAllocator;
 class TempAllocator;
 class PhysicsStepListener;
 class PhysicsStepListener;
+class SoftBodyContactListener;
 
 
 /// The main class for the physics system. It contains all rigid bodies and simulates them.
 /// The main class for the physics system. It contains all rigid bodies and simulates them.
 ///
 ///
@@ -51,6 +52,10 @@ public:
 	void						SetContactListener(ContactListener *inListener)				{ mContactManager.SetContactListener(inListener); }
 	void						SetContactListener(ContactListener *inListener)				{ mContactManager.SetContactListener(inListener); }
 	ContactListener *			GetContactListener() const									{ return mContactManager.GetContactListener(); }
 	ContactListener *			GetContactListener() const									{ return mContactManager.GetContactListener(); }
 
 
+	/// Listener that is notified whenever a contact point between a soft body and another body
+	void						SetSoftBodyContactListener(SoftBodyContactListener *inListener) { mSoftBodyContactListener = inListener; }
+	SoftBodyContactListener *	GetSoftBodyContactListener() const							{ return mSoftBodyContactListener; }
+
 	/// Set the function that combines the friction of two bodies and returns it
 	/// Set the function that combines the friction of two bodies and returns it
 	/// Default method is the geometric mean: sqrt(friction1 * friction2).
 	/// Default method is the geometric mean: sqrt(friction1 * friction2).
 	void						SetCombineFriction(ContactConstraintManager::CombineFunction inCombineFriction) { mContactManager.SetCombineFriction(inCombineFriction); }
 	void						SetCombineFriction(ContactConstraintManager::CombineFunction inCombineFriction) { mContactManager.SetCombineFriction(inCombineFriction); }
@@ -280,6 +285,9 @@ private:
 	/// The broadphase does quick collision detection between body pairs
 	/// The broadphase does quick collision detection between body pairs
 	BroadPhase *				mBroadPhase = nullptr;
 	BroadPhase *				mBroadPhase = nullptr;
 
 
+	/// The soft body contact listener
+	SoftBodyContactListener *	mSoftBodyContactListener = nullptr;
+
     /// Simulation settings
     /// Simulation settings
 	PhysicsSettings				mPhysicsSettings;
 	PhysicsSettings				mPhysicsSettings;
 
 

+ 55 - 0
Jolt/Physics/SoftBody/SoftBodyContactListener.h

@@ -0,0 +1,55 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+JPH_NAMESPACE_BEGIN
+
+class Body;
+class SoftBodyManifold;
+
+/// Return value for the OnSoftBodyContactValidate callback. Determines if the contact will be processed or not.
+enum class SoftBodyValidateResult
+{
+	AcceptContact,														///< Accept this contact
+	RejectContact,														///< Reject this contact
+};
+
+/// Contact settings for a soft body contact.
+/// The values are filled in with their defaults by the system so the callback doesn't need to modify anything, but it can if it wants to.
+class SoftBodyContactSettings
+{
+public:
+	float							mInvMassScale1 = 1.0f;				///< Scale factor for the inverse mass of the soft body (0 = infinite mass, 1 = use original mass, 2 = body has half the mass). For the same contact pair, you should strive to keep the value the same over time.
+	float							mInvMassScale2 = 1.0f;				///< Scale factor for the inverse mass of the other body (0 = infinite mass, 1 = use original mass, 2 = body has half the mass). For the same contact pair, you should strive to keep the value the same over time.
+	float							mInvInertiaScale2 = 1.0f;			///< Scale factor for the inverse inertia of the other body (usually same as mInvMassScale2)
+	bool							mIsSensor;							///< If the contact should be treated as a sensor vs body contact (no collision response)
+};
+
+/// A listener class that receives collision contact events for soft bodies against rigid bodies.
+/// It can be registered with the PhysicsSystem.
+class SoftBodyContactListener
+{
+public:
+	/// Ensure virtual destructor
+	virtual							~SoftBodyContactListener() = default;
+
+	/// Called whenever the soft body's aabox overlaps with another body's aabox (so receiving this callback doesn't tell if any of the vertices will collide).
+	/// This callback can be used to change the behavior of the collision response for all vertices in the soft body or to completely reject the contact.
+	/// Note that this callback is called when all bodies are locked, so don't use any locking functions!
+	/// @param inSoftBody The soft body that collided. It is safe to access this as the soft body is only updated on the current thread.
+	/// @param inOtherBody The other body that collided. Note that accessing the position/orientation/velocity of inOtherBody may result in a race condition as other threads may be modifying the body at the same time.
+	/// @param ioSettings The settings for all contact points that are generated by this collision.
+	/// @return Whether the contact should be processed or not.
+	virtual SoftBodyValidateResult	OnSoftBodyContactValidate([[maybe_unused]] const Body &inSoftBody, [[maybe_unused]] const Body &inOtherBody, [[maybe_unused]] SoftBodyContactSettings &ioSettings) { return SoftBodyValidateResult::AcceptContact; }
+
+	/// Called after all contact points for a soft body have been handled. You only receive one callback per body pair per simulation step and can use inManifold to iterate through all contacts.
+	/// Note that this callback is called when all bodies are locked, so don't use any locking functions!
+	/// You will receive a single callback for a soft body per simulation step for performance reasons, this callback will apply to all vertices in the soft body.
+	/// @param inSoftBody The soft body that collided. It is safe to access this as the soft body is only updated on the current thread.
+	/// @param inManifold The manifold that describes the contact surface between the two bodies. Other bodies may be modified by other threads during this callback.
+	virtual void					OnSoftBodyContactAdded([[maybe_unused]] const Body &inSoftBody, const SoftBodyManifold &inManifold) { /* Do nothing */ }
+};
+
+JPH_NAMESPACE_END

+ 59 - 0
Jolt/Physics/SoftBody/SoftBodyManifold.h

@@ -0,0 +1,59 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Physics/SoftBody/SoftBodyMotionProperties.h>
+
+JPH_NAMESPACE_BEGIN
+
+/// An interface to query which vertices of a soft body are colliding with other bodies
+class SoftBodyManifold
+{
+public:
+	/// Get the vertices of the soft body for iterating
+	const Array<SoftBodyVertex> &	GetVertices() const							{ return mVertices; }
+
+	/// Check if a vertex has collided with something in this update
+	JPH_INLINE bool					HasContact(const SoftBodyVertex &inVertex) const
+	{
+		return inVertex.mHasContact;
+	}
+
+	/// Get the local space contact point (multiply by GetCenterOfMassTransform() of the soft body to get world space)
+	JPH_INLINE Vec3					GetLocalContactPoint(const SoftBodyVertex &inVertex) const
+	{
+		return inVertex.mPosition - inVertex.mCollisionPlane.SignedDistance(inVertex.mPosition) * inVertex.mCollisionPlane.GetNormal();
+	}
+
+	/// Get the contact normal for the vertex (assumes there is a contact).
+	JPH_INLINE Vec3					GetContactNormal(const SoftBodyVertex &inVertex) const
+	{
+		return -inVertex.mCollisionPlane.GetNormal();
+	}
+
+	/// Get the body with which the vertex has collided in this update
+	JPH_INLINE BodyID				GetContactBodyID(const SoftBodyVertex &inVertex) const
+	{
+		return inVertex.mHasContact? mCollidingShapes[inVertex.mCollidingShapeIndex].mBodyID : BodyID();
+	}
+
+private:
+	/// Allow SoftBodyMotionProperties to construct us
+	friend class SoftBodyMotionProperties;
+
+	/// Constructor
+	explicit						SoftBodyManifold(const SoftBodyMotionProperties *inMotionProperties) :
+										mVertices(inMotionProperties->mVertices),
+										mCollidingShapes(inMotionProperties->mCollidingShapes)
+	{
+	}
+
+	using CollidingShape = SoftBodyMotionProperties::CollidingShape;
+
+	const Array<SoftBodyVertex> &	mVertices;
+	const Array<CollidingShape>	&	mCollidingShapes;
+};
+
+JPH_NAMESPACE_END

+ 125 - 96
Jolt/Physics/SoftBody/SoftBodyMotionProperties.cpp

@@ -6,6 +6,8 @@
 
 
 #include <Jolt/Physics/SoftBody/SoftBodyMotionProperties.h>
 #include <Jolt/Physics/SoftBody/SoftBodyMotionProperties.h>
 #include <Jolt/Physics/SoftBody/SoftBodyCreationSettings.h>
 #include <Jolt/Physics/SoftBody/SoftBodyCreationSettings.h>
+#include <Jolt/Physics/SoftBody/SoftBodyContactListener.h>
+#include <Jolt/Physics/SoftBody/SoftBodyManifold.h>
 #include <Jolt/Physics/PhysicsSystem.h>
 #include <Jolt/Physics/PhysicsSystem.h>
 #ifdef JPH_DEBUG_RENDERER
 #ifdef JPH_DEBUG_RENDERER
 	#include <Jolt/Renderer/DebugRenderer.h>
 	#include <Jolt/Renderer/DebugRenderer.h>
@@ -66,6 +68,7 @@ void SoftBodyMotionProperties::Initialize(const SoftBodyCreationSettings &inSett
 		out_vertex.mPreviousPosition = out_vertex.mPosition = rotation * Vec3(in_vertex.mPosition);
 		out_vertex.mPreviousPosition = out_vertex.mPosition = rotation * Vec3(in_vertex.mPosition);
 		out_vertex.mVelocity = rotation.Multiply3x3(Vec3(in_vertex.mVelocity));
 		out_vertex.mVelocity = rotation.Multiply3x3(Vec3(in_vertex.mVelocity));
 		out_vertex.mCollidingShapeIndex = -1;
 		out_vertex.mCollidingShapeIndex = -1;
+		out_vertex.mHasContact = false;
 		out_vertex.mLargestPenetration = -FLT_MAX;
 		out_vertex.mLargestPenetration = -FLT_MAX;
 		out_vertex.mInvMass = in_vertex.mInvMass;
 		out_vertex.mInvMass = in_vertex.mInvMass;
 		mLocalBounds.Encapsulate(out_vertex.mPosition);
 		mLocalBounds.Encapsulate(out_vertex.mPosition);
@@ -96,9 +99,9 @@ void SoftBodyMotionProperties::DetermineCollidingShapes(const SoftBodyUpdateCont
 
 
 	struct Collector : public CollideShapeBodyCollector
 	struct Collector : public CollideShapeBodyCollector
 	{
 	{
-									Collector(Body &inSoftBody, RMat44Arg inTransform, const PhysicsSystem &inSystem, Array<CollidingShape> &ioHits) :
-										mSoftBody(inSoftBody),
-										mInverseTransform(inTransform.InversedRotationTranslation()),
+									Collector(const SoftBodyUpdateContext &inContext, const PhysicsSystem &inSystem, Array<CollidingShape> &ioHits) :
+										mContext(inContext),
+										mInverseTransform(inContext.mCenterOfMassTransform.InversedRotationTranslation()),
 										mBodyLockInterface(inSystem.GetBodyLockInterfaceNoLock()),
 										mBodyLockInterface(inSystem.GetBodyLockInterfaceNoLock()),
 										mCombineFriction(inSystem.GetCombineFriction()),
 										mCombineFriction(inSystem.GetCombineFriction()),
 										mCombineRestitution(inSystem.GetCombineRestitution()),
 										mCombineRestitution(inSystem.GetCombineRestitution()),
@@ -111,34 +114,43 @@ void SoftBodyMotionProperties::DetermineCollidingShapes(const SoftBodyUpdateCont
 			BodyLockRead lock(mBodyLockInterface, inResult);
 			BodyLockRead lock(mBodyLockInterface, inResult);
 			if (lock.Succeeded())
 			if (lock.Succeeded())
 			{
 			{
+				const Body &soft_body = *mContext.mBody;
 				const Body &body = lock.GetBody();
 				const Body &body = lock.GetBody();
 				if (body.IsRigidBody() // TODO: We should support soft body vs soft body
 				if (body.IsRigidBody() // TODO: We should support soft body vs soft body
-					&& !body.IsSensor()
-					&& mSoftBody.GetCollisionGroup().CanCollide(body.GetCollisionGroup()))
+					&& soft_body.GetCollisionGroup().CanCollide(body.GetCollisionGroup()))
 				{
 				{
-					CollidingShape cs;
-					cs.mCenterOfMassTransform = (mInverseTransform * body.GetCenterOfMassTransform()).ToMat44();
-					cs.mShape = body.GetShape();
-					cs.mBodyID = inResult;
-					cs.mMotionType = body.GetMotionType();
-					cs.mUpdateVelocities = false;
-					cs.mFriction = mCombineFriction(mSoftBody, SubShapeID(), body, SubShapeID());
-					cs.mRestitution = mCombineRestitution(mSoftBody, SubShapeID(), body, SubShapeID());
-					if (cs.mMotionType == EMotionType::Dynamic)
+					// Call the contact listener to see if we should accept this contact
+					// If there is no contact listener then we can ignore the contact if the other body is a sensor
+					SoftBodyContactSettings settings;
+					settings.mIsSensor = body.IsSensor();
+					if (mContext.mContactListener == nullptr? !settings.mIsSensor : mContext.mContactListener->OnSoftBodyContactValidate(soft_body, body, settings) == SoftBodyValidateResult::AcceptContact)
 					{
 					{
-						const MotionProperties *mp = body.GetMotionProperties();
-						cs.mInvMass = mp->GetInverseMass();
-						cs.mInvInertia = mp->GetInverseInertiaForRotation(cs.mCenterOfMassTransform.GetRotation());
-						cs.mOriginalLinearVelocity = cs.mLinearVelocity = mInverseTransform.Multiply3x3(mp->GetLinearVelocity());
-						cs.mOriginalAngularVelocity = cs.mAngularVelocity = mInverseTransform.Multiply3x3(mp->GetAngularVelocity());
+						CollidingShape cs;
+						cs.mCenterOfMassTransform = (mInverseTransform * body.GetCenterOfMassTransform()).ToMat44();
+						cs.mShape = body.GetShape();
+						cs.mBodyID = inResult;
+						cs.mMotionType = body.GetMotionType();
+						cs.mIsSensor = settings.mIsSensor;
+						cs.mUpdateVelocities = false;
+						cs.mFriction = mCombineFriction(soft_body, SubShapeID(), body, SubShapeID());
+						cs.mRestitution = mCombineRestitution(soft_body, SubShapeID(), body, SubShapeID());
+						if (cs.mMotionType == EMotionType::Dynamic)
+						{
+							const MotionProperties *mp = body.GetMotionProperties();
+							cs.mInvMass = settings.mInvMassScale2 * mp->GetInverseMass();
+							cs.mInvInertia = settings.mInvInertiaScale2 * mp->GetInverseInertiaForRotation(cs.mCenterOfMassTransform.GetRotation());
+							cs.mSoftBodyInvMassScale = settings.mInvMassScale1;
+							cs.mOriginalLinearVelocity = cs.mLinearVelocity = mInverseTransform.Multiply3x3(mp->GetLinearVelocity());
+							cs.mOriginalAngularVelocity = cs.mAngularVelocity = mInverseTransform.Multiply3x3(mp->GetAngularVelocity());
+						}
+						mHits.push_back(cs);
 					}
 					}
-					mHits.push_back(cs);
 				}
 				}
 			}
 			}
 		}
 		}
 
 
 	private:
 	private:
-		Body &						mSoftBody;
+		const SoftBodyUpdateContext &mContext;
 		RMat44						mInverseTransform;
 		RMat44						mInverseTransform;
 		const BodyLockInterface &	mBodyLockInterface;
 		const BodyLockInterface &	mBodyLockInterface;
 		ContactConstraintManager::CombineFunction mCombineFriction;
 		ContactConstraintManager::CombineFunction mCombineFriction;
@@ -146,7 +158,7 @@ void SoftBodyMotionProperties::DetermineCollidingShapes(const SoftBodyUpdateCont
 		Array<CollidingShape> &		mHits;
 		Array<CollidingShape> &		mHits;
 	};
 	};
 
 
-	Collector collector(*inContext.mBody, inContext.mCenterOfMassTransform, inSystem, mCollidingShapes);
+	Collector collector(inContext, inSystem, mCollidingShapes);
 	AABox bounds = mLocalBounds;
 	AABox bounds = mLocalBounds;
 	bounds.Encapsulate(mLocalPredictedBounds);
 	bounds.Encapsulate(mLocalPredictedBounds);
 	bounds = bounds.Transformed(inContext.mCenterOfMassTransform);
 	bounds = bounds.Transformed(inContext.mCenterOfMassTransform);
@@ -326,88 +338,98 @@ void SoftBodyMotionProperties::ApplyCollisionConstraintsAndUpdateVelocities(cons
 				float projected_distance = -v.mCollisionPlane.SignedDistance(v.mPosition) + vertex_radius;
 				float projected_distance = -v.mCollisionPlane.SignedDistance(v.mPosition) + vertex_radius;
 				if (projected_distance > 0.0f)
 				if (projected_distance > 0.0f)
 				{
 				{
-					// Note that we already calculated the velocity, so this does not affect the velocity (next iteration starts by setting previous position to current position)
-					Vec3 contact_normal = v.mCollisionPlane.GetNormal();
-					v.mPosition += contact_normal * projected_distance;
+					// Remember that there was a collision
+					v.mHasContact = true;
+					mHasContact = true;
 
 
+					// Sensors should not have a collision response
 					CollidingShape &cs = mCollidingShapes[v.mCollidingShapeIndex];
 					CollidingShape &cs = mCollidingShapes[v.mCollidingShapeIndex];
-
-					// Apply friction as described in Detailed Rigid Body Simulation with Extended Position Based Dynamics - Matthias Muller et al.
-					// See section 3.6:
-					// Inverse mass: w1 = 1 / m1, w2 = 1 / m2 + (r2 x n)^T I^-1 (r2 x n) = 0 for a static object
-					// r2 are the contact point relative to the center of mass of body 2
-					// Lagrange multiplier for contact: lambda = -c / (w1 + w2)
-					// Where c is the constraint equation (the distance to the plane, negative because penetrating)
-					// Contact normal force: fn = lambda / dt^2
-					// Delta velocity due to friction dv = -vt / |vt| * min(dt * friction * fn * (w1 + w2), |vt|) = -vt * min(-friction * c / (|vt| * dt), 1)
-					// Note that I think there is an error in the paper, I added a mass term, see: https://github.com/matthias-research/pages/issues/29
-					// Relative velocity: vr = v1 - v2 - omega2 x r2
-					// Normal velocity: vn = vr . contact_normal
-					// Tangential velocity: vt = vr - contact_normal * vn
-					// Impulse: p = dv / (w1 + w2)
-					// Changes in particle velocities:
-					// v1 = v1 + p / m1
-					// v2 = v2 - p / m2 (no change when colliding with a static body)
-					// w2 = w2 - I^-1 (r2 x p) (no change when colliding with a static body)
-					if (cs.mMotionType == EMotionType::Dynamic)
+					if (!cs.mIsSensor)
 					{
 					{
-						// Calculate normal and tangential velocity (equation 30)
-						Vec3 r2 = v.mPosition - cs.mCenterOfMassTransform.GetTranslation();
-						Vec3 v2 = cs.GetPointVelocity(r2);
-						Vec3 relative_velocity = v.mVelocity - v2;
-						Vec3 v_normal = contact_normal * contact_normal.Dot(relative_velocity);
-						Vec3 v_tangential = relative_velocity - v_normal;
-						float v_tangential_length = v_tangential.Length();
-
-						// Calculate inverse effective mass
-						Vec3 r2_cross_n = r2.Cross(contact_normal);
-						float w2 = cs.mInvMass + r2_cross_n.Dot(cs.mInvInertia * r2_cross_n);
-						float w1_plus_w2 = v.mInvMass + w2;
-
-						// Calculate delta relative velocity due to friction (modified equation 31)
-						Vec3 dv;
-						if (v_tangential_length > 0.0f)
-							dv = v_tangential * min(cs.mFriction * projected_distance / (v_tangential_length * dt), 1.0f);
-						else
-							dv = Vec3::sZero();
+						// Note that we already calculated the velocity, so this does not affect the velocity (next iteration starts by setting previous position to current position)
+						Vec3 contact_normal = v.mCollisionPlane.GetNormal();
+						v.mPosition += contact_normal * projected_distance;
+
+						// Apply friction as described in Detailed Rigid Body Simulation with Extended Position Based Dynamics - Matthias Muller et al.
+						// See section 3.6:
+						// Inverse mass: w1 = 1 / m1, w2 = 1 / m2 + (r2 x n)^T I^-1 (r2 x n) = 0 for a static object
+						// r2 are the contact point relative to the center of mass of body 2
+						// Lagrange multiplier for contact: lambda = -c / (w1 + w2)
+						// Where c is the constraint equation (the distance to the plane, negative because penetrating)
+						// Contact normal force: fn = lambda / dt^2
+						// Delta velocity due to friction dv = -vt / |vt| * min(dt * friction * fn * (w1 + w2), |vt|) = -vt * min(-friction * c / (|vt| * dt), 1)
+						// Note that I think there is an error in the paper, I added a mass term, see: https://github.com/matthias-research/pages/issues/29
+						// Relative velocity: vr = v1 - v2 - omega2 x r2
+						// Normal velocity: vn = vr . contact_normal
+						// Tangential velocity: vt = vr - contact_normal * vn
+						// Impulse: p = dv / (w1 + w2)
+						// Changes in particle velocities:
+						// v1 = v1 + p / m1
+						// v2 = v2 - p / m2 (no change when colliding with a static body)
+						// w2 = w2 - I^-1 (r2 x p) (no change when colliding with a static body)
+						if (cs.mMotionType == EMotionType::Dynamic)
+						{
+							// Calculate normal and tangential velocity (equation 30)
+							Vec3 r2 = v.mPosition - cs.mCenterOfMassTransform.GetTranslation();
+							Vec3 v2 = cs.GetPointVelocity(r2);
+							Vec3 relative_velocity = v.mVelocity - v2;
+							Vec3 v_normal = contact_normal * contact_normal.Dot(relative_velocity);
+							Vec3 v_tangential = relative_velocity - v_normal;
+							float v_tangential_length = v_tangential.Length();
+
+							// Calculate resulting inverse mass of vertex
+							float vertex_inv_mass = cs.mSoftBodyInvMassScale * v.mInvMass;
+
+							// Calculate inverse effective mass
+							Vec3 r2_cross_n = r2.Cross(contact_normal);
+							float w2 = cs.mInvMass + r2_cross_n.Dot(cs.mInvInertia * r2_cross_n);
+							float w1_plus_w2 = vertex_inv_mass + w2;
+
+							// Calculate delta relative velocity due to friction (modified equation 31)
+							Vec3 dv;
+							if (v_tangential_length > 0.0f)
+								dv = v_tangential * min(cs.mFriction * projected_distance / (v_tangential_length * dt), 1.0f);
+							else
+								dv = Vec3::sZero();
 
 
-						// Calculate delta relative velocity due to restitution (equation 35)
-						dv += v_normal;
-						float prev_v_normal = (prev_v - v2).Dot(contact_normal);
-						if (prev_v_normal < restitution_treshold)
-							dv += cs.mRestitution * prev_v_normal * contact_normal;
+							// Calculate delta relative velocity due to restitution (equation 35)
+							dv += v_normal;
+							float prev_v_normal = (prev_v - v2).Dot(contact_normal);
+							if (prev_v_normal < restitution_treshold)
+								dv += cs.mRestitution * prev_v_normal * contact_normal;
 
 
-						// Calculate impulse
-						Vec3 p = dv / w1_plus_w2;
+							// Calculate impulse
+							Vec3 p = dv / w1_plus_w2;
 
 
-						// Apply impulse to particle
-						v.mVelocity -= p * v.mInvMass;
+							// Apply impulse to particle
+							v.mVelocity -= p * vertex_inv_mass;
 
 
-						// Apply impulse to rigid body
-						cs.mLinearVelocity += p * cs.mInvMass;
-						cs.mAngularVelocity += cs.mInvInertia * r2.Cross(p);
+							// Apply impulse to rigid body
+							cs.mLinearVelocity += p * cs.mInvMass;
+							cs.mAngularVelocity += cs.mInvInertia * r2.Cross(p);
 
 
-						// Mark that the velocities of the body we hit need to be updated
-						cs.mUpdateVelocities = true;
-					}
-					else
-					{
-						// Body is not moveable, equations are simpler
-
-						// Calculate normal and tangential velocity (equation 30)
-						Vec3 v_normal = contact_normal * contact_normal.Dot(v.mVelocity);
-						Vec3 v_tangential = v.mVelocity - v_normal;
-						float v_tangential_length = v_tangential.Length();
-
-						// Apply friction (modified equation 31)
-						if (v_tangential_length > 0.0f)
-							v.mVelocity -= v_tangential * min(cs.mFriction * projected_distance / (v_tangential_length * dt), 1.0f);
-
-						// Apply restitution (equation 35)
-						v.mVelocity -= v_normal;
-						float prev_v_normal = prev_v.Dot(contact_normal);
-						if (prev_v_normal < restitution_treshold)
-							v.mVelocity -= cs.mRestitution * prev_v_normal * contact_normal;
+							// Mark that the velocities of the body we hit need to be updated
+							cs.mUpdateVelocities = true;
+						}
+						else
+						{
+							// Body is not moveable, equations are simpler
+
+							// Calculate normal and tangential velocity (equation 30)
+							Vec3 v_normal = contact_normal * contact_normal.Dot(v.mVelocity);
+							Vec3 v_tangential = v.mVelocity - v_normal;
+							float v_tangential_length = v_tangential.Length();
+
+							// Apply friction (modified equation 31)
+							if (v_tangential_length > 0.0f)
+								v.mVelocity -= v_tangential * min(cs.mFriction * projected_distance / (v_tangential_length * dt), 1.0f);
+
+							// Apply restitution (equation 35)
+							v.mVelocity -= v_normal;
+							float prev_v_normal = prev_v.Dot(contact_normal);
+							if (prev_v_normal < restitution_treshold)
+								v.mVelocity -= cs.mRestitution * prev_v_normal * contact_normal;
+						}
 					}
 					}
 				}
 				}
 			}
 			}
@@ -418,12 +440,17 @@ void SoftBodyMotionProperties::UpdateSoftBodyState(SoftBodyUpdateContext &ioCont
 {
 {
 	JPH_PROFILE_FUNCTION();
 	JPH_PROFILE_FUNCTION();
 
 
+	// Contact callback
+	if (mHasContact && ioContext.mContactListener != nullptr)
+		ioContext.mContactListener->OnSoftBodyContactAdded(*ioContext.mBody, SoftBodyManifold(this));
+
 	// Loop through vertices once more to update the global state
 	// Loop through vertices once more to update the global state
 	float dt = ioContext.mDeltaTime;
 	float dt = ioContext.mDeltaTime;
 	float max_linear_velocity_sq = Square(GetMaxLinearVelocity());
 	float max_linear_velocity_sq = Square(GetMaxLinearVelocity());
 	float max_v_sq = 0.0f;
 	float max_v_sq = 0.0f;
 	Vec3 linear_velocity = Vec3::sZero(), angular_velocity = Vec3::sZero();
 	Vec3 linear_velocity = Vec3::sZero(), angular_velocity = Vec3::sZero();
 	mLocalPredictedBounds = mLocalBounds = { };
 	mLocalPredictedBounds = mLocalBounds = { };
+	mHasContact = false;
 	for (Vertex &v : mVertices)
 	for (Vertex &v : mVertices)
 	{
 	{
 		// Calculate max square velocity
 		// Calculate max square velocity
@@ -446,6 +473,7 @@ void SoftBodyMotionProperties::UpdateSoftBodyState(SoftBodyUpdateContext &ioCont
 
 
 		// Reset collision data for the next iteration
 		// Reset collision data for the next iteration
 		v.mCollidingShapeIndex = -1;
 		v.mCollidingShapeIndex = -1;
+		v.mHasContact = false;
 		v.mLargestPenetration = -FLT_MAX;
 		v.mLargestPenetration = -FLT_MAX;
 	}
 	}
 
 
@@ -505,6 +533,7 @@ void SoftBodyMotionProperties::InitializeUpdateContext(float inDeltaTime, Body &
 	// Store body
 	// Store body
 	ioContext.mBody = &inSoftBody;
 	ioContext.mBody = &inSoftBody;
 	ioContext.mMotionProperties = this;
 	ioContext.mMotionProperties = this;
+	ioContext.mContactListener = inSystem.GetSoftBodyContactListener();
 
 
 	// Convert gravity to local space
 	// Convert gravity to local space
 	ioContext.mCenterOfMassTransform = inSoftBody.GetCenterOfMassTransform();
 	ioContext.mCenterOfMassTransform = inSoftBody.GetCenterOfMassTransform();

+ 7 - 1
Jolt/Physics/SoftBody/SoftBodyMotionProperties.h

@@ -113,11 +113,14 @@ public:
 	void								UpdateRigidBodyVelocities(const SoftBodyUpdateContext &inContext, PhysicsSystem &inSystem);
 	void								UpdateRigidBodyVelocities(const SoftBodyUpdateContext &inContext, PhysicsSystem &inSystem);
 
 
 private:
 private:
+	// SoftBodyManifold needs to have access to CollidingShape
+	friend class SoftBodyManifold;
+
 	// Collect information about the colliding bodies
 	// Collect information about the colliding bodies
 	struct CollidingShape
 	struct CollidingShape
 	{
 	{
 		/// Get the velocity of a point on this body
 		/// Get the velocity of a point on this body
-		Vec3			GetPointVelocity(Vec3Arg inPointRelativeToCOM) const
+		Vec3							GetPointVelocity(Vec3Arg inPointRelativeToCOM) const
 		{
 		{
 			return mLinearVelocity + mAngularVelocity.Cross(inPointRelativeToCOM);
 			return mLinearVelocity + mAngularVelocity.Cross(inPointRelativeToCOM);
 		}
 		}
@@ -126,9 +129,11 @@ private:
 		RefConst<Shape>					mShape;										///< Shape of the body we hit
 		RefConst<Shape>					mShape;										///< Shape of the body we hit
 		BodyID							mBodyID;									///< Body ID of the body we hit
 		BodyID							mBodyID;									///< Body ID of the body we hit
 		EMotionType						mMotionType;								///< Motion type of the body we hit
 		EMotionType						mMotionType;								///< Motion type of the body we hit
+		bool							mIsSensor;									///< If the contact should be treated as a sensor vs body contact (no collision response)
 		float							mInvMass;									///< Inverse mass of the body we hit
 		float							mInvMass;									///< Inverse mass of the body we hit
 		float							mFriction;									///< Combined friction of the two bodies
 		float							mFriction;									///< Combined friction of the two bodies
 		float							mRestitution;								///< Combined restitution of the two bodies
 		float							mRestitution;								///< Combined restitution of the two bodies
+		float							mSoftBodyInvMassScale;						///< Scale factor for the inverse mass of the soft body vertices
 		bool 							mUpdateVelocities;							///< If the linear/angular velocity changed and the body needs to be updated
 		bool 							mUpdateVelocities;							///< If the linear/angular velocity changed and the body needs to be updated
 		Mat44							mInvInertia;								///< Inverse inertia in local space to the soft body
 		Mat44							mInvInertia;								///< Inverse inertia in local space to the soft body
 		Vec3							mLinearVelocity;							///< Linear velocity of the body in local space to the soft body
 		Vec3							mLinearVelocity;							///< Linear velocity of the body in local space to the soft body
@@ -178,6 +183,7 @@ private:
 	uint32								mNumIterations;								///< Number of solver iterations
 	uint32								mNumIterations;								///< Number of solver iterations
 	float								mPressure;									///< n * R * T, amount of substance * ideal gass constant * absolute temperature, see https://en.wikipedia.org/wiki/Pressure
 	float								mPressure;									///< n * R * T, amount of substance * ideal gass constant * absolute temperature, see https://en.wikipedia.org/wiki/Pressure
 	bool								mUpdatePosition;							///< Update the position of the body while simulating (set to false for something that is attached to the static world)
 	bool								mUpdatePosition;							///< Update the position of the body while simulating (set to false for something that is attached to the static world)
+	bool								mHasContact = false;						///< True if the soft body has collided with anything in the last update
 };
 };
 
 
 JPH_NAMESPACE_END
 JPH_NAMESPACE_END

+ 2 - 0
Jolt/Physics/SoftBody/SoftBodyUpdateContext.h

@@ -11,6 +11,7 @@ JPH_NAMESPACE_BEGIN
 
 
 class Body;
 class Body;
 class SoftBodyMotionProperties;
 class SoftBodyMotionProperties;
+class SoftBodyContactListener;
 
 
 /// Temporary data used by the update of a soft body
 /// Temporary data used by the update of a soft body
 class SoftBodyUpdateContext : public NonCopyable
 class SoftBodyUpdateContext : public NonCopyable
@@ -22,6 +23,7 @@ public:
 	// Input
 	// Input
 	Body *								mBody;										///< Body that is being updated
 	Body *								mBody;										///< Body that is being updated
 	SoftBodyMotionProperties *			mMotionProperties;							///< Motion properties of that body
 	SoftBodyMotionProperties *			mMotionProperties;							///< Motion properties of that body
+	SoftBodyContactListener *			mContactListener;							///< Contact listener to fire callbacks to
 	RMat44								mCenterOfMassTransform;						///< Transform of the body relative to the soft body
 	RMat44								mCenterOfMassTransform;						///< Transform of the body relative to the soft body
 	Vec3								mGravity;									///< Gravity vector in local space of the soft body
 	Vec3								mGravity;									///< Gravity vector in local space of the soft body
 	Vec3								mDisplacementDueToGravity;					///< Displacement of the center of mass due to gravity in the current time step
 	Vec3								mDisplacementDueToGravity;					///< Displacement of the center of mass due to gravity in the current time step

+ 1 - 0
Jolt/Physics/SoftBody/SoftBodyVertex.h

@@ -20,6 +20,7 @@ public:
 	Vec3 			mVelocity;							///< Velocity, relative to the center of mass of the soft body
 	Vec3 			mVelocity;							///< Velocity, relative to the center of mass of the soft body
 	Plane			mCollisionPlane;					///< Nearest collision plane, relative to the center of mass of the soft body
 	Plane			mCollisionPlane;					///< Nearest collision plane, relative to the center of mass of the soft body
 	int				mCollidingShapeIndex;				///< Index in the colliding shapes list of the body we may collide with
 	int				mCollidingShapeIndex;				///< Index in the colliding shapes list of the body we may collide with
+	bool			mHasContact;						///< True if the vertex has collided with anything in the last update
 	float			mLargestPenetration;				///< Used while finding the collision plane, stores the largest penetration found so far
 	float			mLargestPenetration;				///< Used while finding the collision plane, stores the largest penetration found so far
 	float			mInvMass;							///< Inverse mass (1 / mass)
 	float			mInvMass;							///< Inverse mass (1 / mass)
 };
 };

+ 2 - 0
Samples/Samples.cmake

@@ -167,6 +167,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/Rig/RigPileTest.h
 	${SAMPLES_ROOT}/Tests/Rig/RigPileTest.h
 	${SAMPLES_ROOT}/Tests/Rig/SkeletonMapperTest.cpp
 	${SAMPLES_ROOT}/Tests/Rig/SkeletonMapperTest.cpp
 	${SAMPLES_ROOT}/Tests/Rig/SkeletonMapperTest.h
 	${SAMPLES_ROOT}/Tests/Rig/SkeletonMapperTest.h
+	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyContactListenerTest.cpp
+	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyContactListenerTest.h
 	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyFrictionTest.cpp
 	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyFrictionTest.cpp
 	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyFrictionTest.h
 	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyFrictionTest.h
 	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyGravityFactorTest.cpp
 	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyGravityFactorTest.cpp

+ 2 - 0
Samples/SamplesApp.cpp

@@ -320,6 +320,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyUpdatePositionTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyStressTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyStressTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyVsFastMovingTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyVsFastMovingTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyVertexRadiusTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyVertexRadiusTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyContactListenerTest)
 
 
 static TestNameAndRTTI sSoftBodyTests[] =
 static TestNameAndRTTI sSoftBodyTests[] =
 {
 {
@@ -333,6 +334,7 @@ static TestNameAndRTTI sSoftBodyTests[] =
 	{ "Soft Body Update Position",		JPH_RTTI(SoftBodyUpdatePositionTest) },
 	{ "Soft Body Update Position",		JPH_RTTI(SoftBodyUpdatePositionTest) },
 	{ "Soft Body Stress Test",			JPH_RTTI(SoftBodyStressTest) },
 	{ "Soft Body Stress Test",			JPH_RTTI(SoftBodyStressTest) },
 	{ "Soft Body Vertex Radius Test",	JPH_RTTI(SoftBodyVertexRadiusTest) },
 	{ "Soft Body Vertex Radius Test",	JPH_RTTI(SoftBodyVertexRadiusTest) },
+	{ "Soft Body Contact Listener",		JPH_RTTI(SoftBodyContactListenerTest) }
 };
 };
 
 
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, BroadPhaseCastRayTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, BroadPhaseCastRayTest)

+ 128 - 0
Samples/Tests/SoftBody/SoftBodyContactListenerTest.cpp

@@ -0,0 +1,128 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/SoftBody/SoftBodyContactListenerTest.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Jolt/Physics/SoftBody/SoftBodyCreationSettings.h>
+#include <Jolt/Physics/SoftBody/SoftBodyManifold.h>
+#include <Jolt/Physics/Collision/Shape/SphereShape.h>
+#include <Utils/SoftBodyCreator.h>
+#include <Renderer/DebugRendererImp.h>
+#include <Layers.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(SoftBodyContactListenerTest)
+{
+	JPH_ADD_BASE_CLASS(SoftBodyContactListenerTest, Test)
+}
+
+void SoftBodyContactListenerTest::Initialize()
+{
+	// Install contact listener for soft bodies
+	mPhysicsSystem->SetSoftBodyContactListener(this);
+
+	// Floor
+	CreateFloor();
+
+	// Start the 1st cycle
+	StartCycle();
+}
+
+void SoftBodyContactListenerTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
+	mTime += inParams.mDeltaTime;
+	if (mTime > 2.5f)
+	{
+		// Next cycle
+		mCycle = (mCycle + 1) % 7;
+		mTime = 0.0f;
+
+		// Remove the old scene
+		mBodyInterface->RemoveBody(mSoftBodyID);
+		mBodyInterface->DestroyBody(mSoftBodyID);
+		mBodyInterface->RemoveBody(mOtherBodyID);
+		mBodyInterface->DestroyBody(mOtherBodyID);
+
+		// Start the new
+		StartCycle();
+	}
+
+	// Draw current state
+	const char *cycle_names[] = { "Accept contact", "Sphere 10x mass", "Cloth 10x mass", "Sphere infinite mass", "Cloth infinite mass", "Sensor contact", "Reject contact" };
+	mDebugRenderer->DrawText3D(mBodyInterface->GetPosition(mOtherBodyID), cycle_names[mCycle], Color::sWhite, 1.0f);
+}
+
+void SoftBodyContactListenerTest::StartCycle()
+{
+	// Create the cloth
+	Ref<SoftBodySharedSettings> cloth_settings = SoftBodyCreator::CreateCloth(15);
+
+	// Create cloth that's fixated at the corners
+	SoftBodyCreationSettings cloth(cloth_settings, RVec3(0, 5, 0), Quat::sRotation(Vec3::sAxisY(), 0.25f * JPH_PI), Layers::MOVING);
+	cloth.mUpdatePosition = false; // Don't update the position of the cloth as it is fixed to the world
+	cloth.mMakeRotationIdentity = false; // Test explicitly checks if soft bodies with a rotation collide with shapes properly
+	mSoftBodyID = mBodyInterface->CreateAndAddSoftBody(cloth, EActivation::Activate);
+
+	// Create sphere
+	BodyCreationSettings bcs(new SphereShape(1.0f), RVec3(0, 7, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
+	bcs.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
+	bcs.mMassPropertiesOverride.mMass = 100.0f;
+	mOtherBodyID = mBodyInterface->CreateAndAddBody(bcs, EActivation::Activate);
+}
+
+SoftBodyValidateResult SoftBodyContactListenerTest::OnSoftBodyContactValidate(const Body &inSoftBody, const Body &inOtherBody, SoftBodyContactSettings &ioSettings)
+{
+	switch (mCycle)
+	{
+	case 0:
+		// Normal
+		return SoftBodyValidateResult::AcceptContact;
+
+	case 1:
+		// Makes the sphere 10x as heavy
+		ioSettings.mInvMassScale2 = 0.1f;
+		ioSettings.mInvInertiaScale2 = 0.1f;
+		return SoftBodyValidateResult::AcceptContact;
+
+	case 2:
+		// Makes the cloth 10x as heavy
+		ioSettings.mInvMassScale1 = 0.1f;
+		return SoftBodyValidateResult::AcceptContact;
+
+	case 3:
+		// Makes the sphere have infinite mass
+		ioSettings.mInvMassScale2 = 0.0f;
+		ioSettings.mInvInertiaScale2 = 0.0f;
+		return SoftBodyValidateResult::AcceptContact;
+
+	case 4:
+		// Makes the cloth have infinite mass
+		ioSettings.mInvMassScale1 = 0.0f;
+		return SoftBodyValidateResult::AcceptContact;
+
+	case 5:
+		// Sensor contact
+		ioSettings.mIsSensor = true;
+		return SoftBodyValidateResult::AcceptContact;
+
+	default:
+		// No contacts
+		return SoftBodyValidateResult::RejectContact;
+	}
+}
+
+void SoftBodyContactListenerTest::OnSoftBodyContactAdded(const Body &inSoftBody, const SoftBodyManifold &inManifold)
+{
+	// Draw contacts
+	RMat44 com = inSoftBody.GetCenterOfMassTransform();
+	for (const SoftBodyVertex &vertex : inManifold.GetVertices())
+		if (inManifold.HasContact(vertex))
+		{
+			RVec3 position = com * inManifold.GetLocalContactPoint(vertex);
+			Vec3 normal = inManifold.GetContactNormal(vertex);
+			mDebugRenderer->DrawMarker(position, Color::sRed, 0.1f);
+			mDebugRenderer->DrawArrow(position, position + normal, Color::sGreen, 0.1f);
+		}
+}

+ 32 - 0
Samples/Tests/SoftBody/SoftBodyContactListenerTest.h

@@ -0,0 +1,32 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+#include <Jolt/Physics/SoftBody/SoftBodyContactListener.h>
+
+// This test shows how to use contact listeners for soft bodies
+class SoftBodyContactListenerTest : public Test, public SoftBodyContactListener
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, SoftBodyContactListenerTest)
+
+	// See: Test
+	virtual void					Initialize() override;
+	virtual void					PrePhysicsUpdate(const PreUpdateParams &inParams) override;
+	virtual void					GetInitialCamera(CameraState &ioState) const override { ioState.mPos = RVec3(15, 10, 15); }
+
+	// See: SoftBodyContactListener
+	virtual SoftBodyValidateResult	OnSoftBodyContactValidate(const Body &inSoftBody, const Body &inOtherBody, SoftBodyContactSettings &ioSettings) override;
+	virtual void					OnSoftBodyContactAdded(const Body &inSoftBody, const SoftBodyManifold &inManifold) override;
+
+private:
+	void							StartCycle();
+
+	float							mTime = 0.0f;
+	int								mCycle = 0;
+	BodyID							mSoftBodyID;
+	BodyID							mOtherBodyID;
+};