Browse Source

Added ability to set a surface velocity through a ContactListener (#551)

Added conveyor belt demo

Implements #516
Jorrit Rouwe 2 years ago
parent
commit
8d3008e21b

+ 8 - 0
Docs/Samples.md

@@ -41,6 +41,10 @@ This categories shows vehicles created through the VehicleConstraint. These vehi
 |:-|
 |:-|
 |*Demonstrates a tracked vehicle with a turret constrained to the main body with hinge constraints.*|
 |*Demonstrates a tracked vehicle with a turret constrained to the main body with hinge constraints.*|
 
 
+|[![Motorcycle Demo](https://img.youtube.com/vi/umI8FF0gVxs/hqdefault.jpg)](https://www.youtube.com/watch?v=umI8FF0gVxs)|
+|:-|
+|*Demonstrates a motor cycle.*|
+
 ### Rig (Ragdolls)
 ### Rig (Ragdolls)
 
 
 This category demonstrates how ragdolls can be made and controlled using keyframing or motors.
 This category demonstrates how ragdolls can be made and controlled using keyframing or motors.
@@ -133,6 +137,10 @@ This category contains general simulation tests. It demonstrates things like fri
 |:-|
 |:-|
 |*Shows the difference between compiling Jolt Physics in single precision and double precision (define JPH_DOUBLE_PRECISION).*|
 |*Shows the difference between compiling Jolt Physics in single precision and double precision (define JPH_DOUBLE_PRECISION).*|
 
 
+|[![Conveyor belt](https://img.youtube.com/vi/p_H6egZzbZE/hqdefault.jpg)](https://www.youtube.com/watch?v=p_H6egZzbZE)|
+|:-|
+|*A demo of setting the surface velocity of a body to create a conveyor belt. The boxes have decreasing friction from front to back (last one has zero friction so slowly slides down the ramp).*|
+
 ### Shapes & Scaled Shapes
 ### Shapes & Scaled Shapes
 
 
 These categories show off all of the supported shapes and how they can be scaled at run-time.
 These categories show off all of the supported shapes and how they can be scaled at run-time.

+ 3 - 2
Jolt/Physics/Collision/ContactListener.h

@@ -40,9 +40,10 @@ public:
 class ContactSettings
 class ContactSettings
 {
 {
 public:
 public:
-	float					mCombinedFriction;					///< Combined friction for the body pair (usually calculated by sCombineFriction)
-	float					mCombinedRestitution;				///< Combined restitution for the body pair (usually calculated by sCombineRestitution)
+	float					mCombinedFriction;					///< Combined friction for the body pair (see: PhysicsSystem::SetCombineFriction)
+	float					mCombinedRestitution;				///< Combined restitution for the body pair (see: PhysicsSystem::SetCombineRestitution)
 	bool					mIsSensor;							///< If the contact should be treated as a sensor vs body contact (no collision response)
 	bool					mIsSensor;							///< If the contact should be treated as a sensor vs body contact (no collision response)
+	Vec3					mRelativeSurfaceVelocity = Vec3::sZero(); ///< Relative surface velocity between the bodies (world space surface velocity of body 2 - world space surface velocity of body 1), can be used to create a conveyor belt effect
 };
 };
 
 
 /// Return value for the OnContactValidate callback. Determines if the contact is being processed or not.
 /// Return value for the OnContactValidate callback. Determines if the contact is being processed or not.

+ 34 - 14
Jolt/Physics/Constraints/ContactConstraintManager.cpp

@@ -44,11 +44,12 @@ void ContactConstraintManager::WorldContactPoint::CalculateNonPenetrationConstra
 }
 }
 
 
 template <EMotionType Type1, EMotionType Type2>
 template <EMotionType Type1, EMotionType Type2>
-JPH_INLINE void ContactConstraintManager::WorldContactPoint::CalculateFrictionAndNonPenetrationConstraintProperties(float inDeltaTime, const Body &inBody1, const Body &inBody2, Mat44Arg inInvI1, Mat44Arg inInvI2, RVec3Arg inWorldSpacePosition1, RVec3Arg inWorldSpacePosition2, Vec3Arg inWorldSpaceNormal, Vec3Arg inWorldSpaceTangent1, Vec3Arg inWorldSpaceTangent2, float inCombinedRestitution, float inCombinedFriction, float inMinVelocityForRestitution)
+JPH_INLINE void ContactConstraintManager::WorldContactPoint::CalculateFrictionAndNonPenetrationConstraintProperties(float inDeltaTime, const Body &inBody1, const Body &inBody2, Mat44Arg inInvI1, Mat44Arg inInvI2, RVec3Arg inWorldSpacePosition1, RVec3Arg inWorldSpacePosition2, Vec3Arg inWorldSpaceNormal, Vec3Arg inWorldSpaceTangent1, Vec3Arg inWorldSpaceTangent2, float inCombinedRestitution, float inCombinedFriction, float inMinVelocityForRestitution, float inSurfaceVelocity1, float inSurfaceVelocity2)
 {
 {
 	JPH_DET_LOG("CalculateFrictionAndNonPenetrationConstraintProperties: p1: " << inWorldSpacePosition1 << " p2: " << inWorldSpacePosition2
 	JPH_DET_LOG("CalculateFrictionAndNonPenetrationConstraintProperties: p1: " << inWorldSpacePosition1 << " p2: " << inWorldSpacePosition2
 		<< " normal: " << inWorldSpaceNormal << " tangent1: " << inWorldSpaceTangent1 << " tangent2: " << inWorldSpaceTangent2
 		<< " normal: " << inWorldSpaceNormal << " tangent1: " << inWorldSpaceTangent1 << " tangent2: " << inWorldSpaceTangent2
-		<< " restitution: " << inCombinedRestitution << " friction: " << inCombinedFriction << " minv: " << inMinVelocityForRestitution);
+		<< " restitution: " << inCombinedRestitution << " friction: " << inCombinedFriction << " minv: " << inMinVelocityForRestitution
+		<< " surf1: " << inSurfaceVelocity1 << " surf2: " << inSurfaceVelocity2);
 
 
 	// Calculate collision points relative to body
 	// Calculate collision points relative to body
 	RVec3 p = 0.5_r * (inWorldSpacePosition1 + inWorldSpacePosition2);
 	RVec3 p = 0.5_r * (inWorldSpacePosition1 + inWorldSpacePosition2);
@@ -112,8 +113,8 @@ JPH_INLINE void ContactConstraintManager::WorldContactPoint::CalculateFrictionAn
 	if (inCombinedFriction > 0.0f)
 	if (inCombinedFriction > 0.0f)
 	{
 	{
 		// Implement friction as 2 AxisContraintParts
 		// Implement friction as 2 AxisContraintParts
-		mFrictionConstraint1.TemplatedCalculateConstraintProperties<Type1, Type2>(inDeltaTime, mp1, inInvI1, r1, mp2, inInvI2, r2, inWorldSpaceTangent1);
-		mFrictionConstraint2.TemplatedCalculateConstraintProperties<Type1, Type2>(inDeltaTime, mp1, inInvI1, r1, mp2, inInvI2, r2, inWorldSpaceTangent2);
+		mFrictionConstraint1.TemplatedCalculateConstraintProperties<Type1, Type2>(inDeltaTime, mp1, inInvI1, r1, mp2, inInvI2, r2, inWorldSpaceTangent1, inSurfaceVelocity1);
+		mFrictionConstraint2.TemplatedCalculateConstraintProperties<Type1, Type2>(inDeltaTime, mp1, inInvI1, r1, mp2, inInvI2, r2, inWorldSpaceTangent2, inSurfaceVelocity2);
 	}
 	}
 	else
 	else
 	{
 	{
@@ -156,7 +157,7 @@ void ContactConstraintManager::ContactConstraint::Draw(DebugRenderer *inRenderer
 
 
 	// Draw normal
 	// Draw normal
 	RVec3 wp = transform_body1 * Vec3::sLoadFloat3Unsafe(mContactPoints[0].mContactPoint->mPosition1);
 	RVec3 wp = transform_body1 * Vec3::sLoadFloat3Unsafe(mContactPoints[0].mContactPoint->mPosition1);
-	inRenderer->DrawArrow(wp, wp + mWorldSpaceNormal, Color::sRed, 0.05f);
+	inRenderer->DrawArrow(wp, wp + GetWorldSpaceNormal(), Color::sRed, 0.05f);
 
 
 	// Get tangents
 	// Get tangents
 	Vec3 t1, t2;
 	Vec3 t1, t2;
@@ -629,13 +630,20 @@ JPH_INLINE void ContactConstraintManager::TemplatedCalculateFrictionAndNonPenetr
 	Vec3 t1, t2;
 	Vec3 t1, t2;
 	ioConstraint.GetTangents(t1, t2);
 	ioConstraint.GetTangents(t1, t2);
 
 
+	// Get surface velocity relative to tangents
+	Vec3 relative_surface_velocity = ioConstraint.GetRelativeSurfaceVelocity();
+	float surface_velocity1 = t1.Dot(relative_surface_velocity);
+	float surface_velocity2 = t2.Dot(relative_surface_velocity);
+
+	Vec3 ws_normal = ioConstraint.GetWorldSpaceNormal();
+
 	// Setup velocity constraint properties
 	// Setup velocity constraint properties
 	float min_velocity_for_restitution = mPhysicsSettings.mMinVelocityForRestitution;
 	float min_velocity_for_restitution = mPhysicsSettings.mMinVelocityForRestitution;
 	for (WorldContactPoint &wcp : ioConstraint.mContactPoints)
 	for (WorldContactPoint &wcp : ioConstraint.mContactPoints)
 	{
 	{
 		RVec3 p1 = inTransformBody1 * Vec3::sLoadFloat3Unsafe(wcp.mContactPoint->mPosition1);
 		RVec3 p1 = inTransformBody1 * Vec3::sLoadFloat3Unsafe(wcp.mContactPoint->mPosition1);
 		RVec3 p2 = inTransformBody2 * Vec3::sLoadFloat3Unsafe(wcp.mContactPoint->mPosition2);
 		RVec3 p2 = inTransformBody2 * Vec3::sLoadFloat3Unsafe(wcp.mContactPoint->mPosition2);
-		wcp.CalculateFrictionAndNonPenetrationConstraintProperties<Type1, Type2>(inDeltaTime, inBody1, inBody2, inInvI1, inInvI2, p1, p2, ioConstraint.mWorldSpaceNormal, t1, t2, ioConstraint.mCombinedRestitution, ioConstraint.mCombinedFriction, min_velocity_for_restitution);
+		wcp.CalculateFrictionAndNonPenetrationConstraintProperties<Type1, Type2>(inDeltaTime, inBody1, inBody2, inInvI1, inInvI2, p1, p2, ws_normal, t1, t2, ioConstraint.mCombinedRestitution, ioConstraint.mCombinedFriction, min_velocity_for_restitution, surface_velocity1, surface_velocity2);
 	}
 	}
 }
 }
 
 
@@ -840,8 +848,9 @@ void ContactConstraintManager::GetContactsFromCache(ContactAllocator &ioContactA
 			constraint.mBody1 = body1;
 			constraint.mBody1 = body1;
 			constraint.mBody2 = body2;
 			constraint.mBody2 = body2;
 			constraint.mSortKey = input_hash;
 			constraint.mSortKey = input_hash;
-			constraint.mWorldSpaceNormal = world_space_normal;
+			world_space_normal.StoreFloat3(&constraint.mWorldSpaceNormal);
 			constraint.mCombinedFriction = settings.mCombinedFriction;
 			constraint.mCombinedFriction = settings.mCombinedFriction;
+			settings.mRelativeSurfaceVelocity.StoreFloat3(&constraint.mRelativeSurfaceVelocity);
 			constraint.mCombinedRestitution = settings.mCombinedRestitution;
 			constraint.mCombinedRestitution = settings.mCombinedRestitution;
 			constraint.mContactPoints.resize(output_cm->mNumContactPoints);
 			constraint.mContactPoints.resize(output_cm->mNumContactPoints);
 			for (uint32 i = 0; i < output_cm->mNumContactPoints; ++i)
 			for (uint32 i = 0; i < output_cm->mNumContactPoints; ++i)
@@ -1031,11 +1040,12 @@ bool ContactConstraintManager::TemplatedAddContactConstraint(ContactAllocator &i
 		
 		
 		ContactConstraint &constraint = mConstraints[constraint_idx];
 		ContactConstraint &constraint = mConstraints[constraint_idx];
 		new (&constraint) ContactConstraint();
 		new (&constraint) ContactConstraint();
-		constraint.mWorldSpaceNormal = inManifold.mWorldSpaceNormal;
 		constraint.mBody1 = &inBody1;
 		constraint.mBody1 = &inBody1;
 		constraint.mBody2 = &inBody2;
 		constraint.mBody2 = &inBody2;
 		constraint.mSortKey = key_hash;
 		constraint.mSortKey = key_hash;
+		inManifold.mWorldSpaceNormal.StoreFloat3(&constraint.mWorldSpaceNormal);
 		constraint.mCombinedFriction = settings.mCombinedFriction;
 		constraint.mCombinedFriction = settings.mCombinedFriction;
+		settings.mRelativeSurfaceVelocity.StoreFloat3(&constraint.mRelativeSurfaceVelocity);
 		constraint.mCombinedRestitution = settings.mCombinedRestitution;
 		constraint.mCombinedRestitution = settings.mCombinedRestitution;
 
 
 		JPH_DET_LOG("TemplatedAddContactConstraint: id1: " << constraint.mBody1->GetID() << " id2: " << constraint.mBody2->GetID() << " key: " << constraint.mSortKey);
 		JPH_DET_LOG("TemplatedAddContactConstraint: id1: " << constraint.mBody1->GetID() << " id2: " << constraint.mBody2->GetID() << " key: " << constraint.mSortKey);
@@ -1050,6 +1060,10 @@ bool ContactConstraintManager::TemplatedAddContactConstraint(ContactAllocator &i
 		Vec3 t1, t2;
 		Vec3 t1, t2;
 		constraint.GetTangents(t1, t2);
 		constraint.GetTangents(t1, t2);
 
 
+		// Get surface velocity relative to tangents
+		float surface_velocity1 = t1.Dot(settings.mRelativeSurfaceVelocity);
+		float surface_velocity2 = t2.Dot(settings.mRelativeSurfaceVelocity);
+
 		constraint.mContactPoints.resize(num_contact_points);
 		constraint.mContactPoints.resize(num_contact_points);
 		for (int i = 0; i < num_contact_points; ++i)
 		for (int i = 0; i < num_contact_points; ++i)
 		{
 		{
@@ -1089,7 +1103,7 @@ bool ContactConstraintManager::TemplatedAddContactConstraint(ContactAllocator &i
 			wcp.mContactPoint = &cp;
 			wcp.mContactPoint = &cp;
 
 
 			// Setup velocity constraint
 			// Setup velocity constraint
-			wcp.CalculateFrictionAndNonPenetrationConstraintProperties<Type1, Type2>(delta_time, inBody1, inBody2, inInvI1, inInvI2, p1_ws, p2_ws, inManifold.mWorldSpaceNormal, t1, t2, settings.mCombinedRestitution, settings.mCombinedFriction, mPhysicsSettings.mMinVelocityForRestitution);
+			wcp.CalculateFrictionAndNonPenetrationConstraintProperties<Type1, Type2>(delta_time, inBody1, inBody2, inInvI1, inInvI2, p1_ws, p2_ws, inManifold.mWorldSpaceNormal, t1, t2, settings.mCombinedRestitution, settings.mCombinedFriction, mPhysicsSettings.mMinVelocityForRestitution, surface_velocity1, surface_velocity2);
 		}
 		}
 
 
 	#ifdef JPH_DEBUG_RENDERER
 	#ifdef JPH_DEBUG_RENDERER
@@ -1376,6 +1390,8 @@ JPH_INLINE void ContactConstraintManager::sWarmStartConstraint(ContactConstraint
 	// Calculate tangents
 	// Calculate tangents
 	Vec3 t1, t2;
 	Vec3 t1, t2;
 	ioConstraint.GetTangents(t1, t2);
 	ioConstraint.GetTangents(t1, t2);
+
+	Vec3 ws_normal = ioConstraint.GetWorldSpaceNormal();
 		
 		
 	for (WorldContactPoint &wcp : ioConstraint.mContactPoints)
 	for (WorldContactPoint &wcp : ioConstraint.mContactPoints)
 	{
 	{
@@ -1386,7 +1402,7 @@ JPH_INLINE void ContactConstraintManager::sWarmStartConstraint(ContactConstraint
 			wcp.mFrictionConstraint1.TemplatedWarmStart<Type1, Type2>(ioMotionProperties1, ioMotionProperties2, t1, inWarmStartImpulseRatio);
 			wcp.mFrictionConstraint1.TemplatedWarmStart<Type1, Type2>(ioMotionProperties1, ioMotionProperties2, t1, inWarmStartImpulseRatio);
 			wcp.mFrictionConstraint2.TemplatedWarmStart<Type1, Type2>(ioMotionProperties1, ioMotionProperties2, t2, inWarmStartImpulseRatio);
 			wcp.mFrictionConstraint2.TemplatedWarmStart<Type1, Type2>(ioMotionProperties1, ioMotionProperties2, t2, inWarmStartImpulseRatio);
 		}
 		}
-		wcp.mNonPenetrationConstraint.TemplatedWarmStart<Type1, Type2>(ioMotionProperties1, ioMotionProperties2, ioConstraint.mWorldSpaceNormal, inWarmStartImpulseRatio);
+		wcp.mNonPenetrationConstraint.TemplatedWarmStart<Type1, Type2>(ioMotionProperties1, ioMotionProperties2, ws_normal, inWarmStartImpulseRatio);
 	}
 	}
 }
 }
 
 
@@ -1455,11 +1471,13 @@ JPH_INLINE bool ContactConstraintManager::sSolveVelocityConstraint(ContactConstr
 		}
 		}
 	}
 	}
 
 
+	Vec3 ws_normal = ioConstraint.GetWorldSpaceNormal();
+
 	// Then apply all non-penetration constraints
 	// Then apply all non-penetration constraints
 	for (WorldContactPoint &wcp : ioConstraint.mContactPoints)
 	for (WorldContactPoint &wcp : ioConstraint.mContactPoints)
 	{
 	{
 		// Solve non penetration velocities
 		// Solve non penetration velocities
-		if (wcp.mNonPenetrationConstraint.TemplatedSolveVelocityConstraint<Type1, Type2>(ioMotionProperties1, ioMotionProperties2, ioConstraint.mWorldSpaceNormal, 0.0f, FLT_MAX))
+		if (wcp.mNonPenetrationConstraint.TemplatedSolveVelocityConstraint<Type1, Type2>(ioMotionProperties1, ioMotionProperties2, ws_normal, 0.0f, FLT_MAX))
 			any_impulse_applied = true;
 			any_impulse_applied = true;
 	}
 	}
 
 
@@ -1564,6 +1582,8 @@ bool ContactConstraintManager::SolvePositionConstraints(const uint32 *inConstrai
 		RMat44 transform1 = body1.GetCenterOfMassTransform();
 		RMat44 transform1 = body1.GetCenterOfMassTransform();
 		RMat44 transform2 = body2.GetCenterOfMassTransform();
 		RMat44 transform2 = body2.GetCenterOfMassTransform();
 
 
+		Vec3 ws_normal = constraint.GetWorldSpaceNormal();
+
 		for (WorldContactPoint &wcp : constraint.mContactPoints)
 		for (WorldContactPoint &wcp : constraint.mContactPoints)
 		{
 		{
 			// Calculate new contact point positions in world space (the bodies may have moved)
 			// Calculate new contact point positions in world space (the bodies may have moved)
@@ -1573,16 +1593,16 @@ bool ContactConstraintManager::SolvePositionConstraints(const uint32 *inConstrai
 			// Calculate separation along the normal (negative if interpenetrating)
 			// Calculate separation along the normal (negative if interpenetrating)
 			// Allow a little penetration by default (PhysicsSettings::mPenetrationSlop) to avoid jittering between contact/no-contact which wipes out the contact cache and warm start impulses
 			// Allow a little penetration by default (PhysicsSettings::mPenetrationSlop) to avoid jittering between contact/no-contact which wipes out the contact cache and warm start impulses
 			// Clamp penetration to a max PhysicsSettings::mMaxPenetrationDistance so that we don't apply a huge impulse if we're penetrating a lot
 			// Clamp penetration to a max PhysicsSettings::mMaxPenetrationDistance so that we don't apply a huge impulse if we're penetrating a lot
-			float separation = max(Vec3(p2 - p1).Dot(constraint.mWorldSpaceNormal) + mPhysicsSettings.mPenetrationSlop, -mPhysicsSettings.mMaxPenetrationDistance);
+			float separation = max(Vec3(p2 - p1).Dot(ws_normal) + mPhysicsSettings.mPenetrationSlop, -mPhysicsSettings.mMaxPenetrationDistance);
 
 
 			// Only enforce constraint when separation < 0 (otherwise we're apart)
 			// Only enforce constraint when separation < 0 (otherwise we're apart)
 			if (separation < 0.0f)
 			if (separation < 0.0f)
 			{
 			{
 				// Update constraint properties (bodies may have moved)
 				// Update constraint properties (bodies may have moved)
-				wcp.CalculateNonPenetrationConstraintProperties(delta_time, body1, body2, p1, p2, constraint.mWorldSpaceNormal);
+				wcp.CalculateNonPenetrationConstraintProperties(delta_time, body1, body2, p1, p2, ws_normal);
 
 
 				// Solve position errors
 				// Solve position errors
-				if (wcp.mNonPenetrationConstraint.SolvePositionConstraint(body1, body2, constraint.mWorldSpaceNormal, separation, mPhysicsSettings.mBaumgarte))
+				if (wcp.mNonPenetrationConstraint.SolvePositionConstraint(body1, body2, ws_normal, separation, mPhysicsSettings.mBaumgarte))
 					any_impulse_applied = true;
 					any_impulse_applied = true;
 			}
 			}
 		}
 		}

+ 18 - 4
Jolt/Physics/Constraints/ContactConstraintManager.h

@@ -423,7 +423,7 @@ private:
 		/// Calculate constraint properties below
 		/// Calculate constraint properties below
 		void					CalculateNonPenetrationConstraintProperties(float inDeltaTime, const Body &inBody1, const Body &inBody2, RVec3Arg inWorldSpacePosition1, RVec3Arg inWorldSpacePosition2, Vec3Arg inWorldSpaceNormal);
 		void					CalculateNonPenetrationConstraintProperties(float inDeltaTime, const Body &inBody1, const Body &inBody2, RVec3Arg inWorldSpacePosition1, RVec3Arg inWorldSpacePosition2, Vec3Arg inWorldSpaceNormal);
 		template <EMotionType Type1, EMotionType Type2>
 		template <EMotionType Type1, EMotionType Type2>
-		JPH_INLINE void			CalculateFrictionAndNonPenetrationConstraintProperties(float inDeltaTime, const Body &inBody1, const Body &inBody2, Mat44Arg inInvI1, Mat44Arg inInvI2, RVec3Arg inWorldSpacePosition1, RVec3Arg inWorldSpacePosition2, Vec3Arg inWorldSpaceNormal, Vec3Arg inWorldSpaceTangent1, Vec3Arg inWorldSpaceTangent2, float inCombinedRestitution, float inCombinedFriction, float inMinVelocityForRestitution);
+		JPH_INLINE void			CalculateFrictionAndNonPenetrationConstraintProperties(float inDeltaTime, const Body &inBody1, const Body &inBody2, Mat44Arg inInvI1, Mat44Arg inInvI2, RVec3Arg inWorldSpacePosition1, RVec3Arg inWorldSpacePosition2, Vec3Arg inWorldSpaceNormal, Vec3Arg inWorldSpaceTangent1, Vec3Arg inWorldSpaceTangent2, float inCombinedRestitution, float inCombinedFriction, float inMinVelocityForRestitution, float inSurfaceVelocity1, float inSurfaceVelocity2);
 
 
 		/// The constraint parts
 		/// The constraint parts
 		AxisConstraintPart		mNonPenetrationConstraint;
 		AxisConstraintPart		mNonPenetrationConstraint;
@@ -445,18 +445,32 @@ private:
 		void					Draw(DebugRenderer *inRenderer, ColorArg inManifoldColor) const;
 		void					Draw(DebugRenderer *inRenderer, ColorArg inManifoldColor) const;
 	#endif // JPH_DEBUG_RENDERER
 	#endif // JPH_DEBUG_RENDERER
 
 
+		/// Convert the world space normal to a Vec3
+		JPH_INLINE Vec3			GetWorldSpaceNormal() const
+		{
+			return Vec3::sLoadFloat3Unsafe(mWorldSpaceNormal);
+		}
+
+		/// Convert the relative surface velocity to a Vec3
+		JPH_INLINE Vec3			GetRelativeSurfaceVelocity() const
+		{
+			return Vec3::sLoadFloat3Unsafe(mRelativeSurfaceVelocity);
+		}
+
 		/// Get the tangents for this contact constraint
 		/// Get the tangents for this contact constraint
 		JPH_INLINE void			GetTangents(Vec3 &outTangent1, Vec3 &outTangent2) const
 		JPH_INLINE void			GetTangents(Vec3 &outTangent1, Vec3 &outTangent2) const
 		{
 		{
-			outTangent1 = mWorldSpaceNormal.GetNormalizedPerpendicular();
-			outTangent2 = mWorldSpaceNormal.Cross(outTangent1);
+			Vec3 ws_normal = GetWorldSpaceNormal();
+			outTangent1 = ws_normal.GetNormalizedPerpendicular();
+			outTangent2 = ws_normal.Cross(outTangent1);
 		}
 		}
 
 
-		Vec3					mWorldSpaceNormal;
 		Body *					mBody1;
 		Body *					mBody1;
 		Body *					mBody2;
 		Body *					mBody2;
 		uint64					mSortKey;
 		uint64					mSortKey;
+		Float3					mWorldSpaceNormal;
 		float					mCombinedFriction;
 		float					mCombinedFriction;
+		Float3					mRelativeSurfaceVelocity;
 		float					mCombinedRestitution;
 		float					mCombinedRestitution;
 		WorldContactPoints		mContactPoints;
 		WorldContactPoints		mContactPoints;
 	};
 	};

+ 2 - 0
Samples/Samples.cmake

@@ -93,6 +93,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/General/ContactListenerTest.h
 	${SAMPLES_ROOT}/Tests/General/ContactListenerTest.h
 	${SAMPLES_ROOT}/Tests/General/ContactManifoldTest.cpp
 	${SAMPLES_ROOT}/Tests/General/ContactManifoldTest.cpp
 	${SAMPLES_ROOT}/Tests/General/ContactManifoldTest.h
 	${SAMPLES_ROOT}/Tests/General/ContactManifoldTest.h
+	${SAMPLES_ROOT}/Tests/General/ConveyorBeltTest.cpp
+	${SAMPLES_ROOT}/Tests/General/ConveyorBeltTest.h
 	${SAMPLES_ROOT}/Tests/General/DampingTest.cpp
 	${SAMPLES_ROOT}/Tests/General/DampingTest.cpp
 	${SAMPLES_ROOT}/Tests/General/DampingTest.h
 	${SAMPLES_ROOT}/Tests/General/DampingTest.h
 	${SAMPLES_ROOT}/Tests/General/DynamicMeshTest.cpp
 	${SAMPLES_ROOT}/Tests/General/DynamicMeshTest.cpp

+ 2 - 0
Samples/SamplesApp.cpp

@@ -70,6 +70,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, IslandTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, FunnelTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, FunnelTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, FrictionTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, FrictionTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, FrictionPerTriangleTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, FrictionPerTriangleTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ConveyorBeltTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, GravityFactorTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, GravityFactorTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, RestitutionTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, RestitutionTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, DampingTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, DampingTest)
@@ -106,6 +107,7 @@ static TestNameAndRTTI sGeneralTests[] =
 	{ "2D Funnel",							JPH_RTTI(TwoDFunnelTest) },
 	{ "2D Funnel",							JPH_RTTI(TwoDFunnelTest) },
 	{ "Friction",							JPH_RTTI(FrictionTest) },
 	{ "Friction",							JPH_RTTI(FrictionTest) },
 	{ "Friction (Per Triangle)",			JPH_RTTI(FrictionPerTriangleTest) },
 	{ "Friction (Per Triangle)",			JPH_RTTI(FrictionPerTriangleTest) },
+	{ "Conveyor Belt",						JPH_RTTI(ConveyorBeltTest) },
 	{ "Gravity Factor",						JPH_RTTI(GravityFactorTest) },
 	{ "Gravity Factor",						JPH_RTTI(GravityFactorTest) },
 	{ "Restitution",						JPH_RTTI(RestitutionTest) },
 	{ "Restitution",						JPH_RTTI(RestitutionTest) },
 	{ "Damping",							JPH_RTTI(DampingTest) },
 	{ "Damping",							JPH_RTTI(DampingTest) },

+ 77 - 0
Samples/Tests/General/ConveyorBeltTest.cpp

@@ -0,0 +1,77 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/General/ConveyorBeltTest.h>
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Layers.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(ConveyorBeltTest) 
+{ 
+	JPH_ADD_BASE_CLASS(ConveyorBeltTest, Test) 
+}
+
+void ConveyorBeltTest::Initialize()
+{
+	// Floor
+	CreateFloor();
+
+	// Create conveyor belts
+	const float cBeltWidth = 10.0f;
+	const float cBeltLength = 50.0f;
+	BodyCreationSettings belt_settings(new BoxShape(Vec3(cBeltWidth, 0.1f, cBeltLength)), RVec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
+	for (int i = 0; i < 4; ++i)
+	{
+		belt_settings.mFriction = 0.25f * (i + 1);
+		belt_settings.mRotation = Quat::sRotation(Vec3::sAxisY(), 0.5f * JPH_PI * i) * Quat::sRotation(Vec3::sAxisX(), DegreesToRadians(1.0f));
+		belt_settings.mPosition = RVec3(belt_settings.mRotation * Vec3(cBeltLength, 6.0f, cBeltWidth));
+		mBelts.push_back(mBodyInterface->CreateAndAddBody(belt_settings, EActivation::DontActivate));
+	}
+
+	// Bodies with decreasing friction
+	BodyCreationSettings cargo_settings(new BoxShape(Vec3::sReplicate(2.0f)), RVec3::sZero(), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
+	for (int i = 0; i <= 10; ++i)
+	{
+		cargo_settings.mPosition = RVec3(-cBeltLength + i * 10.0f, 10.0f, -cBeltLength);
+		cargo_settings.mFriction = 1.0f - 0.1f * i;
+		mBodyInterface->CreateAndAddBody(cargo_settings, EActivation::Activate);
+	}
+
+	// Create 2 cylinders
+	BodyCreationSettings cylinder_settings(new CylinderShape(6.0f, 1.0f), RVec3(0, 1.0f, -20.0f), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING);
+	mBodyInterface->CreateAndAddBody(cylinder_settings, EActivation::Activate);
+	cylinder_settings.mPosition.SetZ(20.0f);
+	mBodyInterface->CreateAndAddBody(cylinder_settings, EActivation::Activate);
+
+	// Let a dynamic belt rest on it
+	BodyCreationSettings dynamic_belt(new BoxShape(Vec3(5.0f, 0.1f, 25.0f), 0.0f), RVec3(0, 3.0f, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
+	mBelts.push_back(mBodyInterface->CreateAndAddBody(dynamic_belt, EActivation::Activate));
+
+	// Create cargo on the dynamic belt
+	cargo_settings.mPosition = RVec3(0, 6.0f, 15.0f);
+	cargo_settings.mFriction = 1.0f;
+	mBodyInterface->CreateAndAddBody(cargo_settings, EActivation::Activate);
+}
+
+void ConveyorBeltTest::OnContactAdded(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings)
+{
+	// Determine the world space surface velocity of both bodies
+	const Vec3 cLocalSpaceVelocity(0, 0, -10.0f);
+	bool body1_belt = std::find(mBelts.begin(), mBelts.end(), inBody1.GetID()) != mBelts.end();
+	Vec3 body1_surface_velocity = body1_belt? inBody1.GetRotation() * cLocalSpaceVelocity : Vec3::sZero();
+	bool body2_belt = std::find(mBelts.begin(), mBelts.end(), inBody2.GetID()) != mBelts.end();
+	Vec3 body2_surface_velocity = body2_belt? inBody2.GetRotation() * cLocalSpaceVelocity : Vec3::sZero();
+
+	// Calculate the relative surface velocity
+	ioSettings.mRelativeSurfaceVelocity = body2_surface_velocity - body1_surface_velocity;
+}
+
+void ConveyorBeltTest::OnContactPersisted(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings)
+{
+	// Same behavior as contact added
+	OnContactAdded(inBody1, inBody2, inManifold, ioSettings);
+}

+ 27 - 0
Samples/Tests/General/ConveyorBeltTest.h

@@ -0,0 +1,27 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+
+// This test shows how to create a conveyor belt
+class ConveyorBeltTest : public Test, public ContactListener
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, ConveyorBeltTest)
+
+	// See: Test
+	virtual void			Initialize() override;
+
+	// If this test implements a contact listener, it should be returned here
+	virtual ContactListener *GetContactListener() override		{ return this; }
+
+	// See: ContactListener
+	virtual void			OnContactAdded(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override;
+	virtual void			OnContactPersisted(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override;
+
+private:
+	BodyIDVector			mBelts;
+};

+ 48 - 0
UnitTests/Physics/ContactListenerTests.cpp

@@ -8,6 +8,7 @@
 #include "LoggingContactListener.h"
 #include "LoggingContactListener.h"
 #include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
 #include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
 #include <Jolt/Physics/Collision/Shape/SphereShape.h>
 #include <Jolt/Physics/Collision/Shape/SphereShape.h>
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
 
 
 TEST_SUITE("ContactListenerTests")
 TEST_SUITE("ContactListenerTests")
 {
 {
@@ -364,4 +365,51 @@ TEST_SUITE("ContactListenerTests")
 			CHECK((listener.mRemoved == 1 && !listener.mWasInContact));
 			CHECK((listener.mRemoved == 1 && !listener.mWasInContact));
 		}
 		}
 	}
 	}
+
+	TEST_CASE("TestSurfaceVelocity")
+	{
+		PhysicsTestContext c(1.0f / 60.0f, 1, 1);
+
+		Body &floor = c.CreateBox(RVec3(0, -1, 0), Quat::sRotation(Vec3::sAxisY(), DegreesToRadians(10.0f)), EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING, Vec3(100.0f, 1.0f, 100.0f));
+		floor.SetFriction(1.0f);
+
+		Body &box = c.CreateBox(RVec3(0, 0.999f, 0), Quat::sRotation(Vec3::sAxisY(), DegreesToRadians(30.0f)), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3::sReplicate(1.0f));
+		box.SetFriction(1.0f);
+
+		// Contact listener sets a constant surface velocity
+		class ContactListenerImpl : public ContactListener
+		{
+		public:
+							ContactListenerImpl(Body &inFloor, Body &inBox) : mFloor(inFloor), mBox(inBox) { }
+
+			virtual void	OnContactAdded(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override
+			{
+				// Ensure that the body order is as expected
+				JPH_ASSERT(inBody1.GetID() == mFloor.GetID() || inBody2.GetID() == mBox.GetID());
+
+				// Calculate the relative surface velocity
+				ioSettings.mRelativeSurfaceVelocity = -(inBody1.GetRotation() * mLocalSpaceVelocity);
+			}
+
+			virtual void	OnContactPersisted(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override
+			{
+				OnContactAdded(inBody1, inBody2, inManifold, ioSettings);
+			}
+
+			Body &			mFloor;
+			Body &			mBox;
+			Vec3			mLocalSpaceVelocity = Vec3(0, 0, -2.0f);
+		};
+
+		// Set listener
+		ContactListenerImpl listener(floor, box);
+		c.GetSystem()->SetContactListener(&listener);
+
+		// Simulate
+		c.Simulate(5.0f);
+
+		// Check that the box is moving
+		CHECK_APPROX_EQUAL(box.GetLinearVelocity(), floor.GetRotation() * listener.mLocalSpaceVelocity, 0.005f);
+		CHECK_APPROX_EQUAL(box.GetAngularVelocity(), Vec3::sZero(), 1.0e-4f);
+	}
 }
 }