Browse Source

Created a virtual character class (implemented using collision detection functions only for more fine grained character control) (#122)

- Added base class for Character that provides a generic interface for getting the ground properties
- Created new obstacle course test level with moving platforms / objects
- Renamed AlignedAllocator to STLAlignedAllocator
Jorrit Rouwe 3 years ago
parent
commit
2e1abfb344

+ 2 - 2
Jolt/Core/ByteBuffer.h

@@ -3,12 +3,12 @@
 
 #pragma once
 
-#include <Jolt/Core/AlignedAllocator.h>
+#include <Jolt/Core/STLAlignedAllocator.h>
 
 JPH_NAMESPACE_BEGIN
 
 /// Underlying data type for ByteBuffer
-using ByteBufferVector = vector<uint8, AlignedAllocator<uint8, JPH_CACHE_LINE_SIZE>>;
+using ByteBufferVector = vector<uint8, STLAlignedAllocator<uint8, JPH_CACHE_LINE_SIZE>>;
 
 /// Simple byte buffer, aligned to a cache line
 class ByteBuffer : public ByteBufferVector

+ 10 - 10
Jolt/Core/AlignedAllocator.h → Jolt/Core/STLAlignedAllocator.h

@@ -9,7 +9,7 @@ JPH_NAMESPACE_BEGIN
 
 /// STL allocator that takes care that memory is aligned to N bytes
 template <typename T, size_t N>
-class AlignedAllocator
+class STLAlignedAllocator
 {
 public:
 	using value_type = T;
@@ -27,31 +27,31 @@ public:
 	using difference_type = ptrdiff_t;
 
 	/// Constructor
-	inline					AlignedAllocator() = default;
+	inline					STLAlignedAllocator() = default;
 
 	/// Constructor from other allocator
 	template <typename T2>
-	inline explicit			AlignedAllocator(const AlignedAllocator<T2, N> &) { }
+	inline explicit			STLAlignedAllocator(const STLAlignedAllocator<T2, N> &) { }
 
 	/// Allocate memory
-	inline pointer			allocate(size_type n)
+	inline pointer			allocate(size_type inN)
 	{
-		return (pointer)AlignedAlloc(n * sizeof(value_type), N);
+		return (pointer)AlignedAlloc(inN * sizeof(value_type), N);
 	}
 
 	/// Free memory
-	inline void				deallocate(pointer p, size_type)
+	inline void				deallocate(pointer inPointer, size_type)
 	{
-		AlignedFree(p);
+		AlignedFree(inPointer);
 	}
 
 	/// Allocators are stateless so assumed to be equal
-	inline bool				operator == (const AlignedAllocator<T, N>& other) const
+	inline bool				operator == (const STLAlignedAllocator<T, N> &) const
 	{
 		return true;
 	}
 
-	inline bool				operator != (const AlignedAllocator<T, N>& other) const
+	inline bool				operator != (const STLAlignedAllocator<T, N> &) const
 	{
 		return false;
 	}
@@ -60,7 +60,7 @@ public:
 	template <typename T2>
 	struct rebind
 	{
-		using other = AlignedAllocator<T2, N>;
+		using other = STLAlignedAllocator<T2, N>;
 	};
 };
 

+ 76 - 0
Jolt/Core/STLTempAllocator.h

@@ -0,0 +1,76 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Core/TempAllocator.h>
+
+JPH_NAMESPACE_BEGIN
+
+/// STL allocator that wraps around TempAllocator
+template <typename T>
+class STLTempAllocator
+{
+public:
+	using value_type = T;
+
+	/// Pointer to type
+	using pointer = T *;
+	using const_pointer = const T *;
+
+	/// Reference to type.
+	/// Can be removed in C++20.
+	using reference = T &;
+	using const_reference = const T &;
+
+	using size_type = size_t;
+	using difference_type = ptrdiff_t;
+
+	/// Constructor
+	inline					STLTempAllocator(TempAllocator &inAllocator) : mAllocator(inAllocator) { }
+
+	/// Constructor from other allocator
+	template <typename T2>
+	inline explicit			STLTempAllocator(const STLTempAllocator<T2> &inRHS) : mAllocator(inRHS.GetAllocator()) { }
+
+	/// Allocate memory
+	inline pointer			allocate(size_type inN)
+	{
+		return (pointer)mAllocator.Allocate(uint(inN * sizeof(value_type)));
+	}
+
+	/// Free memory
+	inline void				deallocate(pointer inPointer, size_type inN)
+	{
+		mAllocator.Free(inPointer, uint(inN * sizeof(value_type)));
+	}
+
+	/// Allocators are stateless so assumed to be equal
+	inline bool				operator == (const STLTempAllocator<T> &) const
+	{
+		return true;
+	}
+
+	inline bool				operator != (const STLTempAllocator<T> &) const
+	{
+		return false;
+	}
+
+	/// Converting to allocator for other type
+	template <typename T2>
+	struct rebind
+	{
+		using other = STLTempAllocator<T2>;
+	};
+
+	/// Get our temp allocator
+	TempAllocator &			GetAllocator() const
+	{
+		return mAllocator;
+	}
+
+private:
+	TempAllocator &			mAllocator;
+};
+
+JPH_NAMESPACE_END

+ 6 - 1
Jolt/Jolt.cmake

@@ -8,7 +8,6 @@ set(JOLT_PHYSICS_SRC_FILES
 	${JOLT_PHYSICS_ROOT}/AABBTree/AABBTreeToBuffer.h
 	${JOLT_PHYSICS_ROOT}/AABBTree/NodeCodec/NodeCodecQuadTreeHalfFloat.h
 	${JOLT_PHYSICS_ROOT}/AABBTree/TriangleCodec/TriangleCodecIndexed8BitPackSOA4Flags.h
-	${JOLT_PHYSICS_ROOT}/Core/AlignedAllocator.h
 	${JOLT_PHYSICS_ROOT}/Core/Atomics.h
 	${JOLT_PHYSICS_ROOT}/Core/ByteBuffer.h
 	${JOLT_PHYSICS_ROOT}/Core/Color.cpp
@@ -50,6 +49,8 @@ set(JOLT_PHYSICS_SRC_FILES
 	${JOLT_PHYSICS_ROOT}/Core/StreamWrapper.h
 	${JOLT_PHYSICS_ROOT}/Core/StringTools.cpp
 	${JOLT_PHYSICS_ROOT}/Core/StringTools.h
+	${JOLT_PHYSICS_ROOT}/Core/STLAlignedAllocator.h
+	${JOLT_PHYSICS_ROOT}/Core/STLTempAllocator.h
 	${JOLT_PHYSICS_ROOT}/Core/TempAllocator.h
 	${JOLT_PHYSICS_ROOT}/Core/TickCounter.cpp
 	${JOLT_PHYSICS_ROOT}/Core/TickCounter.h
@@ -164,6 +165,10 @@ set(JOLT_PHYSICS_SRC_FILES
 	${JOLT_PHYSICS_ROOT}/Physics/Body/MotionType.h
 	${JOLT_PHYSICS_ROOT}/Physics/Character/Character.cpp
 	${JOLT_PHYSICS_ROOT}/Physics/Character/Character.h
+	${JOLT_PHYSICS_ROOT}/Physics/Character/CharacterBase.cpp
+	${JOLT_PHYSICS_ROOT}/Physics/Character/CharacterBase.h
+	${JOLT_PHYSICS_ROOT}/Physics/Character/CharacterVirtual.cpp
+	${JOLT_PHYSICS_ROOT}/Physics/Character/CharacterVirtual.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/AABoxCast.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/ActiveEdgeMode.h
 	${JOLT_PHYSICS_ROOT}/Physics/Collision/ActiveEdges.h

+ 34 - 31
Jolt/Physics/Character/Character.cpp

@@ -27,15 +27,10 @@ static inline const NarrowPhaseQuery &sGetNarrowPhaseQuery(PhysicsSystem *inSyst
 	return inLockBodies? inSystem->GetNarrowPhaseQuery() : inSystem->GetNarrowPhaseQueryNoLock();
 }
 
-Character::Character(CharacterSettings *inSettings, Vec3Arg inPosition, QuatArg inRotation, uint64 inUserData, PhysicsSystem *inSystem) :
-	mLayer(inSettings->mLayer),
-	mShape(inSettings->mShape),
-	mSystem(inSystem),
-	mGroundMaterial(PhysicsMaterial::sDefault)
+Character::Character(const CharacterSettings *inSettings, Vec3Arg inPosition, QuatArg inRotation, uint64 inUserData, PhysicsSystem *inSystem) :
+	CharacterBase(inSettings, inSystem),
+	mLayer(inSettings->mLayer)
 {
-	// Initialize max slope angle
-	SetMaxSlopeAngle(inSettings->mMaxSlopeAngle);
-
 	// Construct rigid body
 	BodyCreationSettings settings(mShape, inPosition, inRotation, EMotionType::Dynamic, mLayer);
 	settings.mFriction = inSettings->mFriction;
@@ -151,7 +146,32 @@ void Character::PostSimulation(float inMaxSeparationDistance, bool inLockBodies)
 	mGroundBodyID = collector.mGroundBodyID;
 	mGroundPosition = collector.mGroundPosition;
 	mGroundNormal = collector.mGroundNormal;
-	mGroundMaterial = sGetBodyInterface(mSystem, inLockBodies).GetMaterial(collector.mGroundBodyID, collector.mGroundBodySubShapeID);
+
+	// Get additional data from body
+	BodyLockRead lock(sGetBodyLockInterface(mSystem, inLockBodies), mGroundBodyID);
+	if (lock.Succeeded())
+	{
+		const Body &body = lock.GetBody();
+
+		// Update ground state
+		Vec3 up = -mSystem->GetGravity().Normalized();
+		if (mGroundNormal.Dot(up) > mCosMaxSlopeAngle)
+			mGroundState = EGroundState::OnGround;
+		else
+			mGroundState = EGroundState::Sliding;
+
+		// Copy other body properties
+		mGroundMaterial = body.GetShape()->GetMaterial(mGroundBodySubShapeID);
+		mGroundVelocity = body.GetPointVelocity(mGroundPosition);
+		mGroundUserData = body.GetUserData();
+	}
+	else
+	{
+		mGroundState = EGroundState::InAir;
+		mGroundMaterial = PhysicsMaterial::sDefault;
+		mGroundVelocity = Vec3::sZero();
+		mGroundUserData = 0;
+	}
 }
 
 void Character::SetLinearAndAngularVelocity(Vec3Arg inLinearVelocity, Vec3Arg inAngularVelocity, bool inLockBodies)
@@ -214,6 +234,11 @@ Vec3 Character::GetCenterOfMassPosition(bool inLockBodies) const
 	return sGetBodyInterface(mSystem, inLockBodies).GetCenterOfMassPosition(mBodyID);
 }
 
+Mat44 Character::GetWorldTransform(bool inLockBodies) const
+{
+	return sGetBodyInterface(mSystem, inLockBodies).GetWorldTransform(mBodyID);
+}
+
 void Character::SetLayer(ObjectLayer inLayer, bool inLockBodies)
 {
 	mLayer = inLayer;
@@ -259,26 +284,4 @@ bool Character::SetShape(const Shape *inShape, float inMaxPenetrationDepth, bool
 	return true;
 }
 
-Character::EGroundState Character::GetGroundState() const
-{ 
-	if (mGroundBodyID.IsInvalid())
-		return EGroundState::InAir;
-	
-	Vec3 up = -mSystem->GetGravity().Normalized();
-	if (mGroundNormal.Dot(up) > mCosMaxSlopeAngle)
-		return EGroundState::OnGround;
-	else
-		return EGroundState::Sliding;
-}
-
-uint64 Character::GetGroundUserData(bool inLockBodies) const
-{
-	return sGetBodyInterface(mSystem, inLockBodies).GetUserData(mGroundBodyID);
-}
-
-Vec3 Character::GetGroundVelocity(bool inLockBodies) const
-{
-	return sGetBodyInterface(mSystem, inLockBodies).GetPointVelocity(mGroundBodyID, mGroundPosition);
-}
-
 JPH_NAMESPACE_END

+ 8 - 70
Jolt/Physics/Character/Character.h

@@ -3,16 +3,13 @@
 
 #pragma once
 
-#include <Jolt/Core/Reference.h>
+#include <Jolt/Physics/Character/CharacterBase.h>
 #include <Jolt/Physics/EActivation.h>
 
 JPH_NAMESPACE_BEGIN
 
-class Character;
-class PhysicsSystem;
-
 /// Contains the configuration of a character
-class CharacterSettings : public RefTarget<CharacterSettings>
+class CharacterSettings : public CharacterBaseSettings
 {
 public:
 	/// Layer that this character will be added to
@@ -21,25 +18,18 @@ public:
 	/// Mass of the character
 	float								mMass = 80.0f;
 
-	/// Maximum angle of slope that character can still walk on (radians)
-	float								mMaxSlopeAngle = DegreesToRadians(50.0f);
-
 	/// Friction for the character
 	float								mFriction = 0.2f;
 
 	/// Value to multiply gravity with for this character
 	float								mGravityFactor = 1.0f;
-
-	/// Initial shape that represents the character's volume.
-	/// Usually this is a capsule, make sure the shape is made so that the bottom of the shape is at (0, 0, 0).
-	RefConst<Shape>						mShape;
 };
 
 /// Runtime character object.
 /// This object usually represents the player or a humanoid AI. It uses a single rigid body, 
 /// usually with a capsule shape to simulate movement and collision for the character.
 /// The character is a keyframed object, the application controls it by setting the velocity.
-class Character : public RefTarget<Character>
+class Character : public CharacterBase
 {
 public:
 	/// Constructor
@@ -48,10 +38,10 @@ public:
 	/// @param inRotation Initial rotation for the character (usually only around Y)
 	/// @param inUserData Application specific value
 	/// @param inSystem Physics system that this character will be added to later
-										Character(CharacterSettings *inSettings, Vec3Arg inPosition, QuatArg inRotation, uint64 inUserData, PhysicsSystem *inSystem);
+										Character(const CharacterSettings *inSettings, Vec3Arg inPosition, QuatArg inRotation, uint64 inUserData, PhysicsSystem *inSystem);
 
 	/// Destructor
-										~Character();
+	virtual								~Character() override;
 
 	/// Add bodies and constraints to the system and optionally activate the bodies
 	void								AddToPhysicsSystem(EActivation inActivationMode = EActivation::Activate, bool inLockBodies = true);
@@ -106,52 +96,16 @@ public:
 	/// Position of the center of mass of the underlying rigid body
 	Vec3								GetCenterOfMassPosition(bool inLockBodies = true) const;
 
+	/// Calculate the world transform of the character
+	Mat44								GetWorldTransform(bool inLockBodies = true) const;
+
 	/// Update the layer of the character
 	void								SetLayer(ObjectLayer inLayer, bool inLockBodies = true);
 
-	/// Set the maximum angle of slope that character can still walk on (radians)
-	void								SetMaxSlopeAngle(float inMaxSlopeAngle)					{ mCosMaxSlopeAngle = cos(inMaxSlopeAngle); }
-
 	/// Switch the shape of the character (e.g. for stance). When inMaxPenetrationDepth is not FLT_MAX, it checks
 	/// if the new shape collides before switching shape. Returns true if the switch succeeded.
 	bool								SetShape(const Shape *inShape, float inMaxPenetrationDepth, bool inLockBodies = true);
 
-	/// Get the current shape that the character is using.
-	const Shape *						GetShape() const										{ return mShape; }
-
-	enum class EGroundState
-	{
-		OnGround,						///< Character is on the ground and can move freely
-		Sliding,						///< Character is on a slope that is too steep and should start sliding
-		InAir,							///< Character is in the air
-	};
-
-	///@name Properties of the ground this character is standing on
-
-	/// Current ground state (updated during PostSimlulation())
-	EGroundState						GetGroundState() const;
-
-	/// Get the contact point with the ground
-	Vec3 								GetGroundPosition() const								{ return mGroundPosition; }
-
-	/// Get the contact normal with the ground
-	Vec3	 							GetGroundNormal() const									{ return mGroundNormal; }
-
-	/// Velocity in world space of the point that we're standing on
-	Vec3								GetGroundVelocity(bool inLockBodies = true) const;
-	
-	/// Material that the character is standing on.
-	const PhysicsMaterial *				GetGroundMaterial() const								{ return mGroundMaterial; }
-
-	/// BodyID of the object the character is standing on. Note may have been removed!
-	BodyID								GetGroundBodyID() const									{ return mGroundBodyID; }
-
-	/// Sub part of the body that we're standing on.
-	SubShapeID							GetGroundSubShapeID() const								{ return mGroundBodySubShapeID; }
-
-	/// User data value of the body that we're standing on
-	uint64								GetGroundUserData(bool inLockBodies = true) const;
-
 private:
 	/// Check collisions between inShape and the world
 	void								CheckCollision(const Shape *inShape, float inMaxSeparationDistance, CollideShapeCollector &ioCollector, bool inLockBodies = true) const;
@@ -161,22 +115,6 @@ private:
 
 	/// The layer the body is in
 	ObjectLayer							mLayer;
-
-	/// Cosine of the maximum angle of slope that character can still walk on
-	float								mCosMaxSlopeAngle;
-
-	/// The shape that the body currently has
-	RefConst<Shape>						mShape;
-
-	/// Cached physics system
-	PhysicsSystem *						mSystem;
-
-	// Ground properties
-	BodyID								mGroundBodyID;
-	SubShapeID							mGroundBodySubShapeID;
-	Vec3								mGroundPosition = Vec3::sZero();
-	Vec3								mGroundNormal = Vec3::sZero();
-	RefConst<PhysicsMaterial>			mGroundMaterial;
 };
 
 JPH_NAMESPACE_END

+ 18 - 0
Jolt/Physics/Character/CharacterBase.cpp

@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <Jolt/Jolt.h>
+
+#include <Jolt/Physics/Character/CharacterBase.h>
+
+JPH_NAMESPACE_BEGIN
+
+CharacterBase::CharacterBase(const CharacterBaseSettings *inSettings, PhysicsSystem *inSystem) :
+	mSystem(inSystem),
+	mShape(inSettings->mShape)
+{
+	// Initialize max slope angle
+	SetMaxSlopeAngle(inSettings->mMaxSlopeAngle);
+}
+
+JPH_NAMESPACE_END

+ 99 - 0
Jolt/Physics/Character/CharacterBase.h

@@ -0,0 +1,99 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Core/Reference.h>
+#include <Jolt/Core/NonCopyable.h>
+#include <Jolt/Physics/Body/BodyID.h>
+#include <Jolt/Physics/Collision/Shape/Shape.h>
+#include <Jolt/Physics/Collision/Shape/SubShapeID.h>
+#include <Jolt/Physics/Collision/PhysicsMaterial.h>
+
+JPH_NAMESPACE_BEGIN
+
+class PhysicsSystem;
+
+/// Base class for configuration of a character
+class CharacterBaseSettings : public RefTarget<CharacterBaseSettings>
+{
+public:
+	/// Maximum angle of slope that character can still walk on (radians).
+	float								mMaxSlopeAngle = DegreesToRadians(50.0f);
+
+	/// Initial shape that represents the character's volume.
+	/// Usually this is a capsule, make sure the shape is made so that the bottom of the shape is at (0, 0, 0).
+	RefConst<Shape>						mShape;
+};
+
+/// Base class for character class
+class CharacterBase : public RefTarget<CharacterBase>, public NonCopyable
+{
+public:
+	/// Constructor
+										CharacterBase(const CharacterBaseSettings *inSettings, PhysicsSystem *inSystem);
+
+	/// Destructor
+	virtual								~CharacterBase() = default;
+
+	/// Set the maximum angle of slope that character can still walk on (radians)
+	void								SetMaxSlopeAngle(float inMaxSlopeAngle)					{ mCosMaxSlopeAngle = cos(inMaxSlopeAngle); }
+
+	/// Get the current shape that the character is using.
+	const Shape *						GetShape() const										{ return mShape; }
+
+	enum class EGroundState
+	{
+		OnGround,						///< Character is on the ground and can move freely
+		Sliding,						///< Character is on a slope that is too steep and should start sliding
+		InAir,							///< Character is in the air
+	};
+
+	///@name Properties of the ground this character is standing on
+
+	/// Current ground state
+	EGroundState						GetGroundState() const									{ return mGroundState; }
+
+	/// Get the contact point with the ground
+	Vec3 								GetGroundPosition() const								{ return mGroundPosition; }
+
+	/// Get the contact normal with the ground
+	Vec3	 							GetGroundNormal() const									{ return mGroundNormal; }
+
+	/// Velocity in world space of ground
+	Vec3								GetGroundVelocity() const								{ return mGroundVelocity; }
+	
+	/// Material that the character is standing on
+	const PhysicsMaterial *				GetGroundMaterial() const								{ return mGroundMaterial; }
+
+	/// BodyID of the object the character is standing on. Note may have been removed!
+	BodyID								GetGroundBodyID() const									{ return mGroundBodyID; }
+
+	/// Sub part of the body that we're standing on.
+	SubShapeID							GetGroundSubShapeID() const								{ return mGroundBodySubShapeID; }
+
+	/// User data value of the body that we're standing on
+	uint64								GetGroundUserData() const								{ return mGroundUserData; }
+
+protected:
+	// Cached physics system
+	PhysicsSystem *						mSystem;
+
+	// The shape that the body currently has
+	RefConst<Shape>						mShape;
+
+	// Cosine of the maximum angle of slope that character can still walk on
+	float								mCosMaxSlopeAngle;
+
+	// Ground properties
+	EGroundState						mGroundState = EGroundState::InAir;
+	BodyID								mGroundBodyID;
+	SubShapeID							mGroundBodySubShapeID;
+	Vec3								mGroundPosition = Vec3::sZero();
+	Vec3								mGroundNormal = Vec3::sZero();
+	Vec3								mGroundVelocity = Vec3::sZero();
+	RefConst<PhysicsMaterial>			mGroundMaterial = PhysicsMaterial::sDefault;
+	uint64								mGroundUserData = 0;
+};
+
+JPH_NAMESPACE_END

+ 819 - 0
Jolt/Physics/Character/CharacterVirtual.cpp

@@ -0,0 +1,819 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <Jolt/Jolt.h>
+
+#include <Jolt/Physics/Character/CharacterVirtual.h>
+#include <Jolt/Physics/Body/Body.h>
+#include <Jolt/Physics/PhysicsSystem.h>
+#include <Jolt/Physics/Collision/ShapeCast.h>
+#include <Jolt/Physics/Collision/CollideShape.h>
+
+JPH_NAMESPACE_BEGIN
+
+CharacterVirtual::CharacterVirtual(const CharacterVirtualSettings *inSettings, Vec3Arg inPosition, QuatArg inRotation, PhysicsSystem *inSystem) :
+	CharacterBase(inSettings, inSystem),
+	mUp(inSettings->mUp),
+	mPredictiveContactDistance(inSettings->mPredictiveContactDistance),
+	mMaxCollisionIterations(inSettings->mMaxCollisionIterations),
+	mMaxConstraintIterations(inSettings->mMaxConstraintIterations),
+	mMinTimeRemaining(inSettings->mMinTimeRemaining),
+	mCollisionTolerance(inSettings->mCollisionTolerance),
+	mCharacterPadding(inSettings->mCharacterPadding),
+	mMaxNumHits(inSettings->mMaxNumHits),
+	mPenetrationRecoverySpeed(inSettings->mPenetrationRecoverySpeed),
+	mPosition(inPosition),
+	mRotation(inRotation)
+{
+	// Copy settings
+	SetMaxStrength(inSettings->mMaxStrength);
+	SetMass(inSettings->mMass);
+}
+
+template <class taCollector>
+void CharacterVirtual::sFillContactProperties(Contact &outContact, const Body &inBody, const taCollector &inCollector, const CollideShapeResult &inResult)
+{
+	outContact.mPosition = inResult.mContactPointOn2;	
+	outContact.mLinearVelocity = inBody.GetPointVelocity(inResult.mContactPointOn2);
+	outContact.mNormal = -inResult.mPenetrationAxis.NormalizedOr(Vec3::sZero());
+	outContact.mDistance = -inResult.mPenetrationDepth;
+	outContact.mBodyB = inResult.mBodyID2;
+	outContact.mSubShapeIDB = inResult.mSubShapeID2;
+	outContact.mMotionTypeB = inBody.GetMotionType();
+	outContact.mUserData = inBody.GetUserData();
+	outContact.mMaterial = inCollector.GetContext()->GetMaterial(inResult.mSubShapeID2);
+}
+
+void CharacterVirtual::ContactCollector::AddHit(const CollideShapeResult &inResult)
+{
+	BodyLockRead lock(mSystem->GetBodyLockInterface(), inResult.mBodyID2);
+	if (lock.SucceededAndIsInBroadPhase())
+	{
+		const Body &body = lock.GetBody();
+
+		mContacts.emplace_back();
+		Contact &contact = mContacts.back();
+		sFillContactProperties(contact, body, *this, inResult);
+		contact.mFraction = 0.0f;
+
+		// Protection from excess of contact points
+		if (mContacts.size() == mMaxHits)
+			ForceEarlyOut();
+	}
+}
+
+void CharacterVirtual::ContactCastCollector::AddHit(const ShapeCastResult &inResult)
+{	
+	if (inResult.mFraction > 0.0f // Ignore collisions at fraction = 0
+		&& inResult.mPenetrationAxis.Dot(mDisplacement) > 0.0f) // Ignore penetrations that we're moving away from
+	{
+		// Test if this contact should be ignored
+		for (const IgnoredContact &c : mIgnoredContacts)
+			if (c.mBodyID == inResult.mBodyID2 && c.mSubShapeID == inResult.mSubShapeID2)
+				return;
+
+		BodyLockRead lock(mSystem->GetBodyLockInterface(), inResult.mBodyID2);
+		if (lock.SucceededAndIsInBroadPhase())
+		{
+			const Body &body = lock.GetBody();
+
+			mContacts.emplace_back();
+			Contact &contact = mContacts.back();
+			sFillContactProperties(contact, body, *this, inResult);
+			contact.mFraction = inResult.mFraction;
+
+			// Protection from excess of contact points
+			if (mContacts.size() == mMaxHits)
+				ForceEarlyOut();
+		}
+	}
+}
+
+void CharacterVirtual::GetContactsAtPosition(Vec3Arg inPosition, Vec3Arg inMovementDirection, const Shape *inShape, TempContactList &outContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter) const
+{
+	// Remove previous results
+	outContacts.clear();
+
+	// Query shape transform
+	Mat44 transform = GetCenterOfMassTransform(inPosition);
+
+	// Settings for collide shape
+	CollideShapeSettings settings;
+	settings.mActiveEdgeMode = EActiveEdgeMode::CollideOnlyWithActive;
+	settings.mBackFaceMode = EBackFaceMode::CollideWithBackFaces;
+	settings.mActiveEdgeMovementDirection = inMovementDirection;
+	settings.mMaxSeparationDistance = mCharacterPadding + mPredictiveContactDistance;
+
+	// Collide shape
+	ContactCollector collector(mSystem, mMaxNumHits, outContacts);
+	mSystem->GetNarrowPhaseQuery().CollideShape(inShape, Vec3::sReplicate(1.0f), transform, settings, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter);
+
+	// Reduce distance to contact by padding to ensure we stay away from the object by a little margin
+	// (this will make collision detection cheaper - especially for sweep tests as they won't hit the surface if we're properly sliding)
+	for (Contact &c : outContacts)
+		c.mDistance -= mCharacterPadding;
+}
+
+void CharacterVirtual::RemoveConflictingContacts(TempContactList &ioContacts, IgnoredContactList &outIgnoredContacts) const
+{
+	// Only use this algorithm if we're penetrating further than this (due to numerical precision issues we can always penetrate a little bit and we don't want to discard contacts if they just have a tiny penetration)
+	// We do need to account for padding (see GetContactsAtPosition) that is removed from the contact distances, to compensate we add it to the cMinRequiredPenetration
+	const float cMinRequiredPenetration = 1.25f * mCharacterPadding;
+
+	// Discard conflicting penetrating contacts
+	for (size_t c1 = 0; c1 < ioContacts.size(); c1++)
+	{
+		Contact &contact1 = ioContacts[c1];
+		if (contact1.mDistance <= -cMinRequiredPenetration) // Only for penetrations
+			for (size_t c2 = c1 + 1; c2 < ioContacts.size(); c2++)
+			{
+				Contact &contact2 = ioContacts[c2];
+				if (contact1.mBodyB == contact2.mBodyB // Only same body
+					&& contact2.mDistance <= -cMinRequiredPenetration // Only for penetrations
+					&& contact1.mNormal.Dot(contact2.mNormal) < 0.0f) // Only opposing normals
+				{
+					// Discard contacts with the least amount of penetration
+					if (contact1.mDistance < contact2.mDistance)
+					{
+						// Discard the 2nd contact
+						outIgnoredContacts.emplace_back(contact2.mBodyB, contact2.mSubShapeIDB);
+						ioContacts.erase(ioContacts.begin() + c2);
+						c2--;
+					}
+					else
+					{
+						// Discard the first contact
+						outIgnoredContacts.emplace_back(contact1.mBodyB, contact1.mSubShapeIDB);
+						ioContacts.erase(ioContacts.begin() + c1);
+						c1--;
+						break;
+					}
+				}
+			}
+	}
+}
+
+bool CharacterVirtual::ValidateContact(const Contact &inContact) const
+{
+	if (mListener == nullptr)
+		return true;
+
+	return mListener->OnContactValidate(this, inContact.mBodyB, inContact.mSubShapeIDB);
+}
+
+bool CharacterVirtual::GetFirstContactForSweep(Vec3Arg inPosition, Vec3Arg inDisplacement, Contact &outContact, const IgnoredContactList &inIgnoredContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator) const
+{
+	// Too small distance -> skip checking
+	if (inDisplacement.LengthSq() < 1.0e-8f)
+		return false;
+
+	// Calculate start transform
+	Mat44 start = GetCenterOfMassTransform(inPosition);
+
+	// Settings for the cast
+	ShapeCastSettings settings;
+	settings.mBackFaceModeTriangles = EBackFaceMode::CollideWithBackFaces;
+	settings.mBackFaceModeConvex = EBackFaceMode::IgnoreBackFaces;
+	settings.mActiveEdgeMode = EActiveEdgeMode::CollideOnlyWithActive;
+	settings.mUseShrunkenShapeAndConvexRadius = true;
+	settings.mReturnDeepestPoint = false;
+
+	// Cast shape
+	TempContactList contacts(inAllocator);
+	contacts.reserve(mMaxNumHits);
+	ContactCastCollector collector(mSystem, inDisplacement, mMaxNumHits, inIgnoredContacts, contacts);
+	ShapeCast shape_cast(mShape, Vec3::sReplicate(1.0f), start, inDisplacement);
+	mSystem->GetNarrowPhaseQuery().CastShape(shape_cast, settings, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter);
+	if (contacts.empty())
+		return false;
+
+	// Sort the contacts on fraction
+	sort(contacts.begin(), contacts.end(), [](const Contact &inLHS, const Contact &inRHS) { return inLHS.mFraction < inRHS.mFraction; });
+
+	// Check the first contact that will make us penetrate more than the allowed tolerance
+	bool valid_contact = false;
+	for (const Contact &c : contacts)
+		if (c.mDistance + c.mNormal.Dot(inDisplacement) < -mCollisionTolerance
+			&& ValidateContact(c))
+		{
+			outContact = c;
+			valid_contact = true;
+			break;
+		}
+	if (!valid_contact)
+		return false;
+
+	// Correct fraction for the padding that we want to keep from geometry
+	// We want to maintain distance of cCharacterPadding (p) along plane normal outContact.mNormal (n) to the capsule by moving back along inDisplacement (d) by amount d'
+	// cos(angle between d and -n) = -n dot d / |d| = p / d'
+	// <=> d' = -p |d| / n dot d
+	// The new fraction of collision is then:
+	// f' = f - d' / |d| = f + p / n dot d
+	outContact.mFraction = max(0.0f, outContact.mFraction + mCharacterPadding / outContact.mNormal.Dot(inDisplacement));
+	return true;
+}
+
+void CharacterVirtual::DetermineConstraints(TempContactList &inContacts, ConstraintList &outConstraints) const
+{
+	for (Contact &c : inContacts)
+	{
+		Vec3 contact_velocity = c.mLinearVelocity;
+
+		// Penetrating contact: Add a contact velocity that pushes the character out at the desired speed
+		if (c.mDistance < 0.0f)
+			contact_velocity -= c.mNormal * c.mDistance * mPenetrationRecoverySpeed;
+
+		// Convert to a constraint
+		outConstraints.emplace_back();
+		Constraint &constraint = outConstraints.back();
+		constraint.mContact = &c;
+		constraint.mLinearVelocity = contact_velocity;
+		constraint.mPlane = Plane(c.mNormal, c.mDistance);
+
+		// Next check if the angle is too steep and if it is add an additional constraint that holds the character back
+		if (mCosMaxSlopeAngle < 0.999f) // If cos(slope angle) is close to 1 then there's no limit
+		{
+			float dot = c.mNormal.Dot(mUp);
+			if (dot > 0.0f && dot < mCosMaxSlopeAngle)
+			{
+				// Make horizontal normal
+				Vec3 normal = (c.mNormal - dot * mUp).Normalized();
+
+				// Create a secondary constraint that blocks horizontal movement
+				outConstraints.emplace_back();
+				Constraint &vertical_constraint = outConstraints.back();
+				vertical_constraint.mContact = &c;
+				vertical_constraint.mLinearVelocity = contact_velocity.Dot(normal) * normal; // Project the contact velocity on the new normal so that both planes push at an equal rate
+				vertical_constraint.mPlane = Plane(normal, c.mDistance / normal.Dot(c.mNormal)); // Calculate the distance we have to travel horizontally to hit the contact plane
+			}
+		}
+	}
+}
+
+bool CharacterVirtual::HandleContact(Vec3Arg inVelocity, Constraint &ioConstraint, Vec3Arg inGravity, float inDeltaTime) const
+{
+	Contact &contact = *ioConstraint.mContact;
+
+	// Validate the contact point
+	if (!ValidateContact(contact))
+		return false;
+
+	// Send contact added event
+	CharacterContactSettings settings;
+	if (mListener != nullptr)
+		mListener->OnContactAdded(this, contact.mBodyB, contact.mSubShapeIDB, contact.mPosition, -contact.mNormal, settings);
+	contact.mCanPushCharacter = settings.mCanPushCharacter;
+
+	// If body B cannot receive an impulse, we're done
+	if (!settings.mCanReceiveImpulses || contact.mMotionTypeB != EMotionType::Dynamic)
+		return true;
+
+	// Lock the body we're colliding with
+	BodyLockWrite lock(mSystem->GetBodyLockInterface(), contact.mBodyB);
+	if (!lock.SucceededAndIsInBroadPhase())
+		return false; // Body has been removed, we should not collide with it anymore
+	const Body &body = lock.GetBody();
+
+	// Calculate the velocity that we want to apply at B so that it will start moving at the character's speed at the contact point
+	constexpr float cDamping = 0.9f;
+	constexpr float cPenetrationResolution = 0.4f;
+	Vec3 relative_velocity = inVelocity - contact.mLinearVelocity;
+	float projected_velocity = relative_velocity.Dot(contact.mNormal);
+	float delta_velocity = -projected_velocity * cDamping - min(contact.mDistance, 0.0f) * cPenetrationResolution / inDeltaTime;
+
+	// Don't apply impulses if we're separating
+	if (delta_velocity < 0.0f)
+		return true;
+
+	// Determine mass properties of the body we're colliding with
+	const MotionProperties *motion_properties = body.GetMotionProperties();
+	Vec3 center_of_mass = body.GetCenterOfMassPosition();
+	Mat44 inverse_inertia = body.GetInverseInertia();
+	float inverse_mass = motion_properties->GetInverseMass();
+
+	// Calculate the inverse of the mass of body B as seen at the contact point in the direction of the contact normal
+	Vec3 jacobian = (contact.mPosition - center_of_mass).Cross(contact.mNormal);
+	float inv_effective_mass = inverse_inertia.Multiply3x3(jacobian).Dot(jacobian) + inverse_mass;
+
+	// Impulse P = M dv
+	float impulse = delta_velocity / inv_effective_mass;
+
+	// Clamp the impulse according to the character strength, character strength is a force in newtons, P = F dt
+	float max_impulse = mMaxStrength * inDeltaTime;
+	impulse = min(impulse, max_impulse);
+
+	// Calculate the world space impulse to apply
+	Vec3 world_impulse = -impulse * contact.mNormal;
+
+	// Add the impulse due to gravity working on the player: P = F dt = M g dt
+	float normal_dot_gravity = contact.mNormal.Dot(inGravity);
+	if (normal_dot_gravity < 0.0f)
+		world_impulse -= (mMass * normal_dot_gravity / inGravity.Length() * inDeltaTime) * inGravity;
+
+	// Now apply the impulse (body is already locked so we use the no-lock interface)
+	mSystem->GetBodyInterfaceNoLock().AddImpulse(contact.mBodyB, world_impulse, contact.mPosition);
+	return true;
+}
+
+void CharacterVirtual::SolveConstraints(Vec3Arg inVelocity, Vec3Arg inGravity, float inDeltaTime, float inTimeRemaining, ConstraintList &ioConstraints, IgnoredContactList &ioIgnoredContacts, float &outTimeSimulated, Vec3 &outDisplacement, TempAllocator &inAllocator) const
+{
+	// If there are no constraints we can immediately move to our target
+	if (ioConstraints.empty())
+	{
+		outDisplacement = inVelocity * inTimeRemaining;
+		outTimeSimulated = inTimeRemaining;
+		return;
+	}
+
+	// Create array that holds the constraints in order of time of impact (sort will happen later)
+	vector<Constraint *, STLTempAllocator<Constraint *>> sorted_constraints(inAllocator);
+	sorted_constraints.resize(ioConstraints.size());
+	for (size_t index = 0; index < sorted_constraints.size(); index++)
+		sorted_constraints[index] = &ioConstraints[index];
+
+	// This is the velocity we use for the displacement, if we hit something it will be shortened
+	Vec3 velocity = inVelocity;
+
+	// Start with no displacement
+	outDisplacement = Vec3::sZero();
+	outTimeSimulated = 0.0f;
+
+	// These are the contacts that we hit previously without moving a significant distance
+	vector<Constraint *, STLTempAllocator<Constraint *>> previous_contacts(inAllocator);
+	previous_contacts.resize(mMaxConstraintIterations);
+	int num_previous_contacts = 0;
+
+	// Loop for a max amount of iterations
+	for (uint iteration = 0; iteration < mMaxConstraintIterations; iteration++)
+	{
+		// Calculate time of impact for all constraints
+		for (Constraint &c : ioConstraints)
+		{
+			// Project velocity on plane direction
+			c.mProjectedVelocity = c.mPlane.GetNormal().Dot(c.mLinearVelocity - velocity);
+			if (c.mProjectedVelocity < 1.0e-6f)
+			{
+				c.mTOI = FLT_MAX;
+			}
+			else
+			{
+				// Distance to plane
+				float dist = c.mPlane.SignedDistance(outDisplacement);
+
+				if (dist - c.mProjectedVelocity * inTimeRemaining > -1.0e-4f)
+				{
+					// Too little penetration, accept the movement
+					c.mTOI = FLT_MAX;
+				}
+				else
+				{
+					// Calculate time of impact
+					c.mTOI = max(0.0f, dist / c.mProjectedVelocity);
+				}
+			}
+		}
+				
+		// Sort constraints on proximity
+		sort(sorted_constraints.begin(), sorted_constraints.end(), [](const Constraint *inLHS, const Constraint *inRHS) {
+				// If both constraints hit at t = 0 then order the one that will push the character furthest first
+				// Note that because we add velocity to penetrating contacts, this will also resolve contacts that penetrate the most
+				if (inLHS->mTOI <= 0.0f && inRHS->mTOI <= 0.0f)
+					return inLHS->mProjectedVelocity > inRHS->mProjectedVelocity;
+
+				// Then sort on time of impact
+				if (inLHS->mTOI != inRHS->mTOI)
+					return inLHS->mTOI < inRHS->mTOI;
+
+				// As a tie breaker sort static first so it has the most influence
+				return inLHS->mContact->mMotionTypeB > inRHS->mContact->mMotionTypeB;
+			});
+
+		// Find the first valid constraint
+		Constraint *constraint = nullptr;
+		for (Constraint *c : sorted_constraints)
+		{
+			// Take the first contact and see if we can reach it
+			if (c->mTOI >= inTimeRemaining)
+			{
+				// We can reach our goal!
+				outDisplacement += velocity * inTimeRemaining;
+				outTimeSimulated += inTimeRemaining;
+				return;
+			}
+
+			// Test if this contact was discarded by the contact callback before
+			if (c->mContact->mWasDiscarded)
+				continue;
+
+			// Check if we made contact with this before
+			if (!c->mContact->mHadCollision)
+			{
+				// Handle the contact
+				if (!HandleContact(velocity, *c, inGravity, inDeltaTime))
+				{
+					// Constraint should be ignored, remove it from the list
+					c->mContact->mWasDiscarded = true;
+
+					// Mark it as ignored for GetFirstContactForSweep
+					ioIgnoredContacts.emplace_back(c->mContact->mBodyB, c->mContact->mSubShapeIDB);
+					continue;
+				}
+
+				c->mContact->mHadCollision = true;
+			}
+
+			// Cancel velocity of constraint if it cannot push the character
+			if (!c->mContact->mCanPushCharacter)
+				c->mLinearVelocity = Vec3::sZero();
+
+			// We found the first constraint that we want to collide with
+			constraint = c;
+			break;
+		}
+
+		if (constraint == nullptr)
+		{
+			// All constraints were discarded, we can reach our goal!
+			outDisplacement += velocity * inTimeRemaining;
+			outTimeSimulated += inTimeRemaining;
+			return;
+		}
+
+		// Move to the contact
+		outDisplacement += velocity * constraint->mTOI;
+		inTimeRemaining -= constraint->mTOI;
+		outTimeSimulated += constraint->mTOI;
+
+		// If there's not enough time left to be simulated, bail
+		if (inTimeRemaining < mMinTimeRemaining)
+			return;
+
+		// If we've moved significantly, clear all previous contacts
+		if (constraint->mTOI > 1.0e-4f)
+			num_previous_contacts = 0;
+
+		// Get the normal of the plane we're hitting
+		Vec3 plane_normal = constraint->mPlane.GetNormal();
+
+		// Get the relative velocity between the character and the constraint
+		Vec3 relative_velocity = velocity - constraint->mLinearVelocity;
+
+		// Calculate new velocity if we cancel the relative velocity in the normal direction
+		Vec3 new_velocity = velocity - relative_velocity.Dot(plane_normal) * plane_normal;
+
+		// Find the normal of the previous contact that we will violate the most if we move in this new direction
+		float highest_penetration = 0.0f;
+		Constraint *other_constraint = nullptr;
+		for (Constraint **c = previous_contacts.data(); c < previous_contacts.data() + num_previous_contacts; ++c)
+			if (*c != constraint)
+			{
+				// Calculate how much we will penetrate if we move in this direction
+				Vec3 other_normal = (*c)->mPlane.GetNormal();
+				float penetration = ((*c)->mLinearVelocity - new_velocity).Dot(other_normal);
+				if (penetration > highest_penetration)
+				{
+					// We don't want parallel or anti-parallel normals as that will cause our cross product below to become zero. Slack is approx 10 degrees.
+					float dot = other_normal.Dot(plane_normal);
+					if (dot < 0.984f && dot > -0.984f) 
+					{
+						highest_penetration = penetration;
+						other_constraint = *c;
+					}
+				}
+			}
+
+		// Check if we found a 2nd constraint
+		if (other_constraint != nullptr)
+		{
+			// Calculate the sliding direction and project the new velocity onto that sliding direction
+			Vec3 other_normal = other_constraint->mPlane.GetNormal();
+			Vec3 slide_dir = plane_normal.Cross(other_normal).Normalized();
+			Vec3 velocity_in_slide_dir = new_velocity.Dot(slide_dir) * slide_dir;
+
+			// Cancel the constraint velocity in the other constraint plane's direction so that we won't try to apply it again and keep ping ponging between planes
+			constraint->mLinearVelocity -= min(0.0f, constraint->mLinearVelocity.Dot(other_normal)) * other_normal;
+
+			// Cancel the other constraints velocity in this constraint plane's direction so that we won't try to apply it again and keep ping ponging between planes
+			other_constraint->mLinearVelocity -= min(0.0f, other_constraint->mLinearVelocity.Dot(plane_normal)) * plane_normal;
+
+			// Calculate the velocity of this constraint perpendicular to the slide direction
+			Vec3 perpendicular_velocity = constraint->mLinearVelocity - constraint->mLinearVelocity.Dot(slide_dir) * slide_dir;
+
+			// Calculate the velocity of the other constraint perpendicular to the slide direction
+			Vec3 other_perpendicular_velocity = other_constraint->mLinearVelocity - other_constraint->mLinearVelocity.Dot(slide_dir) * slide_dir;
+
+			// Add all components together
+			velocity = velocity_in_slide_dir + perpendicular_velocity + other_perpendicular_velocity;
+		}			
+		else
+		{
+			// Update the velocity
+			velocity = new_velocity;
+		}
+
+		// Add the contact to the list so that next iteration we can avoid violating it again
+		previous_contacts[num_previous_contacts] = constraint;
+		num_previous_contacts++;
+
+		// If there's not enough velocity left, bail
+		if (velocity.LengthSq() < 1.0e-8f)
+			return;
+	}
+}
+
+void CharacterVirtual::UpdateSupportingContact(TempAllocator &inAllocator)
+{
+	// Flag contacts as having a collision if they're close enough.
+	// Note that if we did MoveShape before we want to preserve any contacts that it marked as colliding
+	for (Contact &c : mActiveContacts)
+		if (!c.mWasDiscarded)
+			c.mHadCollision |= c.mDistance < mCollisionTolerance;
+
+	// Determine if we're supported or not
+	int num_supported = 0;
+	int num_sliding = 0;
+	int num_avg_normal = 0;
+	Vec3 avg_normal = Vec3::sZero();
+	Vec3 avg_velocity = Vec3::sZero();
+	const Contact *supporting_contact = nullptr;
+	float max_cos_angle = -FLT_MAX;
+	for (const Contact &c : mActiveContacts)
+		if (c.mHadCollision)
+		{
+			// Calculate the angle between the plane normal and the up direction
+			float cos_angle = c.mNormal.Dot(mUp);
+
+			// Find the contact with the normal that is pointing most upwards and store it in mSupportingContact
+			if (max_cos_angle < cos_angle)
+			{
+				supporting_contact = &c;
+				max_cos_angle = cos_angle;
+			}
+
+			// Check if this is a sliding or supported contact
+			bool is_supported = cos_angle >= mCosMaxSlopeAngle;
+			if (is_supported)
+				num_supported++;
+			else
+				num_sliding++;
+
+			// If the angle between the two is less than 85 degrees we also use it to calculate the average normal
+			if (cos_angle >= 0.08f)
+			{
+				avg_normal += c.mNormal;
+				num_avg_normal++;
+
+				// For static or dynamic objects or for contacts that don't support us just take the contact velocity
+				if (c.mMotionTypeB != EMotionType::Kinematic || !is_supported)
+					avg_velocity += c.mLinearVelocity;
+				else
+				{
+					// For keyframed objects that support us calculate the velocity at our position rather than at the contact position so that we properly follow the object
+					// Note that we don't just take the point velocity because a point on an object with angular velocity traces an arc, 
+					// so if you just take point velocity * delta time you get an error that accumulates over time
+
+					// Determine center of mass and angular velocity
+					Vec3 angular_velocity, com;
+					{
+						BodyLockRead lock(mSystem->GetBodyLockInterface(), c.mBodyB);
+						if (lock.SucceededAndIsInBroadPhase())
+						{
+							const Body &body = lock.GetBody();
+
+							// Add the linear velocity to the average velocity
+							avg_velocity += body.GetLinearVelocity();
+
+							angular_velocity = body.GetAngularVelocity();
+							com = body.GetCenterOfMassPosition();
+						}
+						else
+						{
+							angular_velocity = Vec3::sZero();
+							com = Vec3::sZero();
+						}
+					}
+
+					// Get angular velocity
+					float angular_velocity_len_sq = angular_velocity.LengthSq();
+					if (angular_velocity_len_sq > 1.0e-12f)
+					{
+						float angular_velocity_len = sqrt(angular_velocity_len_sq);
+
+						// Calculate the rotation that the object will make in the time step
+						Quat rotation = Quat::sRotation(angular_velocity / angular_velocity_len, angular_velocity_len * mLastDeltaTime);
+
+						// Calculate where the new contact position will be
+						Vec3 new_position = com + rotation * (mPosition - com);
+
+						// Calculate the velocity
+						avg_velocity += (new_position - mPosition) / mLastDeltaTime;
+					}
+				}
+			}
+		}
+
+	// Calculate average normal and velocity
+	if (num_avg_normal >= 1)
+	{
+		mGroundNormal = avg_normal.Normalized();
+		mGroundVelocity = avg_velocity / float(num_avg_normal);
+	}
+	else
+	{
+		mGroundNormal = Vec3::sZero();
+		mGroundVelocity = Vec3::sZero();
+	}
+
+	// Copy supporting contact properties
+	if (supporting_contact != nullptr)
+	{
+		mGroundBodyID = supporting_contact->mBodyB;
+		mGroundBodySubShapeID = supporting_contact->mSubShapeIDB;
+		mGroundPosition = supporting_contact->mPosition;
+		mGroundMaterial = supporting_contact->mMaterial;
+		mGroundUserData = supporting_contact->mUserData;
+	}
+	else
+	{
+		mGroundBodyID = BodyID();
+		mGroundBodySubShapeID = SubShapeID();
+		mGroundPosition = Vec3::sZero();
+		mGroundMaterial = PhysicsMaterial::sDefault;
+		mGroundUserData = 0;
+	}
+
+	// Determine ground state
+	if (num_supported > 0)
+	{
+		// We made contact with something that supports us
+		mGroundState = EGroundState::OnGround;
+	}
+	else if (num_sliding > 0)
+	{
+		// If we're sliding we may actually be standing on multiple sliding contacts in such a way that we can't slide off, in this case we're also supported
+
+		// Convert the contacts into constraints
+		TempContactList contacts(mActiveContacts.begin(), mActiveContacts.end(), inAllocator);
+		ConstraintList constraints(inAllocator);
+		constraints.reserve(contacts.size() * 2);
+		DetermineConstraints(contacts, constraints);
+
+		// Solve the displacement using these constraints, this is used to check if we didn't move at all because we are supported
+		Vec3 displacement;
+		float time_simulated;
+		IgnoredContactList ignored_contacts(inAllocator);
+		ignored_contacts.reserve(contacts.size());
+		SolveConstraints(-mUp, mSystem->GetGravity(), 1.0f, 1.0f, constraints, ignored_contacts, time_simulated, displacement, inAllocator);
+
+		// If we're blocked then we're supported, otherwise we're sliding
+		constexpr float cMinRequiredDisplacementSquared = Square(0.01f);
+		if (time_simulated < 0.001f || displacement.LengthSq() < cMinRequiredDisplacementSquared)
+			mGroundState = EGroundState::OnGround;
+		else
+			mGroundState = EGroundState::Sliding;
+	}
+	else
+	{
+		// Not in contact with anything
+		mGroundState = EGroundState::InAir;
+	}
+}
+
+void CharacterVirtual::StoreActiveContacts(const TempContactList &inContacts, TempAllocator &inAllocator)
+{
+	mActiveContacts.assign(inContacts.begin(), inContacts.end());
+
+	UpdateSupportingContact(inAllocator);
+}
+
+void CharacterVirtual::MoveShape(Vec3 &ioPosition, Vec3Arg inVelocity, Vec3Arg inGravity, float inDeltaTime, ContactList *outActiveContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator) const
+{
+	Vec3 movement_direction = inVelocity.NormalizedOr(Vec3::sZero());
+
+	float time_remaining = inDeltaTime;
+	for (uint iteration = 0; iteration < mMaxCollisionIterations && time_remaining >= mMinTimeRemaining; iteration++)
+	{
+		// Determine contacts in the neighborhood
+		TempContactList contacts(inAllocator);
+		contacts.reserve(mMaxNumHits);
+		GetContactsAtPosition(ioPosition, movement_direction, mShape, contacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter);
+
+		// Remove contacts with the same body that have conflicting normals
+		IgnoredContactList ignored_contacts(inAllocator);
+		ignored_contacts.reserve(contacts.size());
+		RemoveConflictingContacts(contacts, ignored_contacts);
+
+		// Convert contacts into constraints
+		ConstraintList constraints(inAllocator);
+		constraints.reserve(contacts.size() * 2);
+		DetermineConstraints(contacts, constraints);
+
+#ifdef JPH_DEBUG_RENDERER
+		if (sDrawConstraints && iteration == 0)
+		{
+			for (const Constraint &c : constraints)
+			{
+				// Draw contact point
+				DebugRenderer::sInstance->DrawMarker(c.mContact->mPosition, Color::sYellow, 0.05f);
+				Vec3 dist_to_plane = -c.mPlane.GetConstant() * c.mPlane.GetNormal();
+
+				// Draw arrow towards surface that we're hitting
+				DebugRenderer::sInstance->DrawArrow(c.mContact->mPosition, c.mContact->mPosition - dist_to_plane, Color::sYellow, 0.05f);
+
+				// Draw plane around the player posiiton indicating the space that we can move
+				DebugRenderer::sInstance->DrawPlane(mPosition + dist_to_plane, c.mPlane.GetNormal(), Color::sCyan, 1.0f);
+			}
+		}
+#endif // JPH_DEBUG_RENDERER
+
+		// Solve the displacement using these constraints
+		Vec3 displacement;
+		float time_simulated;
+		SolveConstraints(inVelocity, inGravity, inDeltaTime, time_remaining, constraints, ignored_contacts, time_simulated, displacement, inAllocator);
+
+		// Store the contacts now that the colliding ones have been marked
+		if (outActiveContacts != nullptr)
+			outActiveContacts->assign(contacts.begin(), contacts.end());
+
+		// Do a sweep to test if the path is really unobstructed
+		Contact cast_contact;
+		if (GetFirstContactForSweep(ioPosition, displacement, cast_contact, ignored_contacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator))
+		{
+			displacement *= cast_contact.mFraction;
+			time_simulated *= cast_contact.mFraction;
+		}
+
+		// Update the position
+		ioPosition += displacement;
+		time_remaining -= time_simulated;
+
+		// If the displacement during this iteration was too small we assume we cannot further progress this update
+		if (displacement.LengthSq() < 1.0e-8f)
+			break;
+	}
+}
+
+void CharacterVirtual::Update(float inDeltaTime, Vec3Arg inGravity, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator)
+{
+	// If there's no delta time, we don't need to do anything
+	if (inDeltaTime <= 0.0f)
+		return;
+
+	// Remember delta time for checking if we're supported by the ground
+	mLastDeltaTime = inDeltaTime;
+
+	// Slide the shape through the world
+	MoveShape(mPosition, mLinearVelocity, inGravity, inDeltaTime, &mActiveContacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inAllocator);
+
+	// Determine the object that we're standing on
+	UpdateSupportingContact(inAllocator);
+}
+
+void CharacterVirtual::RefreshContacts(const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator)
+{
+	// Determine the contacts
+	TempContactList contacts(inAllocator);
+	contacts.reserve(mMaxNumHits);
+	GetContactsAtPosition(mPosition, mLinearVelocity.NormalizedOr(Vec3::sZero()), mShape, contacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter);
+
+	StoreActiveContacts(contacts, inAllocator);
+}
+
+bool CharacterVirtual::SetShape(const Shape *inShape, float inMaxPenetrationDepth, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator)
+{
+	if (mShape == nullptr || mSystem == nullptr)
+	{
+		// It hasn't been initialized yet
+		mShape = inShape;
+		return true;
+	}
+
+	if (inShape != mShape && inShape != nullptr)
+	{
+		// Tentatively set new shape
+		RefConst<Shape> old_shape = mShape;
+		mShape = inShape;
+
+		// Check collision around the new shape
+		TempContactList contacts(inAllocator);
+		contacts.reserve(mMaxNumHits);
+		GetContactsAtPosition(mPosition, mLinearVelocity.NormalizedOr(Vec3::sZero()), inShape, contacts, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter);
+
+		if (inMaxPenetrationDepth < FLT_MAX)
+		{
+			// Test if this results in penetration, if so cancel the transition
+			for (const Contact &c : contacts)
+				if (c.mDistance < -inMaxPenetrationDepth)
+				{
+					mShape = old_shape;
+					return false;
+				}
+		}
+
+		StoreActiveContacts(contacts, inAllocator);
+	}
+
+	return mShape == inShape;
+}
+
+JPH_NAMESPACE_END

+ 289 - 0
Jolt/Physics/Character/CharacterVirtual.h

@@ -0,0 +1,289 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Jolt/Physics/Character/CharacterBase.h>
+#include <Jolt/Physics/Body/MotionType.h>
+#include <Jolt/Physics/Body/BodyFilter.h>
+#include <Jolt/Physics/Collision/BroadPhase/BroadPhaseLayer.h>
+#include <Jolt/Physics/Collision/ObjectLayer.h>
+#include <Jolt/Core/STLTempAllocator.h>
+
+JPH_NAMESPACE_BEGIN
+
+class CharacterVirtual;
+
+/// Contains the configuration of a character
+class CharacterVirtualSettings : public CharacterBaseSettings
+{
+public:
+	/// Vector indicating the up direction of the character
+	Vec3								mUp = Vec3::sAxisY();
+
+	/// Character mass (kg). Used to push down objects with gravity when the character is standing on top.
+	float								mMass = 70.0f;
+
+	/// Maximum force with which the character can push other bodies (N).
+	float								mMaxStrength = 100.0f;
+
+	///@name Movement settings
+	float								mPredictiveContactDistance = 0.1f;						///< How far to scan outside of the shape for predictive contacts
+	uint								mMaxCollisionIterations = 5;							///< Max amount of collision loops
+	uint								mMaxConstraintIterations = 15;							///< How often to try stepping in the constraint solving
+	float								mMinTimeRemaining = 1.0e-4f;							///< Early out condition: If this much time is left to simulate we are done
+	float								mCollisionTolerance = 1.0e-3f;							///< How far we're willing to penetrate geometry
+	float								mCharacterPadding = 0.02f;								///< How far we try to stay away from the geometry, this ensures that the sweep will hit as little as possible lowering the collision cost and reducing the risk of getting stuck
+	uint								mMaxNumHits = 256;										///< Max num hits to collect in order to avoid excess of contact points collection
+	float								mPenetrationRecoverySpeed = 1.0f;						///< This value governs how fast a penetration will be resolved, 0 = nothing is resolved, 1 = everything in one update
+};
+
+/// This class contains settings that allow you to override the behavior of a character's collision response
+class CharacterContactSettings
+{
+public:
+	bool								mCanPushCharacter = true;								///< True when the object can push the virtual character
+	bool								mCanReceiveImpulses = true;								///< True when the virtual character can apply impulses (push) the body
+};
+
+/// This class receives callbacks when a virtual character hits something.
+class CharacterContactListener
+{
+public:
+	/// Destructor
+	virtual								~CharacterContactListener() = default;
+
+	/// Checks if a character can collide with specified body. Return true if the contact is valid.
+	virtual bool						OnContactValidate(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2) { return true; }
+
+	/// Called whenever the character collides with a body. Returns true if the contact can push the character.
+	virtual void						OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, Vec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) { /* Default do nothing */ }
+};
+
+/// Runtime character object.
+/// This object usually represents the player. Contrary to the Character class it doesn't use a rigid body but moves doing collision checks only (hence the name virtual).
+/// The advantage of this is that you can determine when the character moves in the frame (usually this has to happen at a very particular point in the frame)
+/// but the downside is that other objects don't see this virtual character. In order to make this work it is recommended to pair a CharacterVirtual with a Character that
+/// moves along. This Character should be keyframed (or at least have no gravity) and move along with the CharacterVirtual so that other rigid bodies can collide with it.
+class CharacterVirtual : public CharacterBase
+{
+public:
+	/// Constructor
+	/// @param inSettings The settings for the character
+	/// @param inPosition Initial position for the character
+	/// @param inRotation Initial rotation for the character (usually only around the up-axis)
+	/// @param inSystem Physics system that this character will be added to later
+										CharacterVirtual(const CharacterVirtualSettings *inSettings, Vec3Arg inPosition, QuatArg inRotation, PhysicsSystem *inSystem);
+
+	/// Set the contact listener
+	void								SetListener(CharacterContactListener *inListener)		{ mListener = inListener; }
+
+	/// Get the current contact listener
+	CharacterContactListener *			GetListener() const										{ return mListener; }
+
+	/// Get the linear velocity of the character (m / s)
+	Vec3								GetLinearVelocity() const								{ return mLinearVelocity; }
+
+	/// Set the linear velocity of the character (m / s)
+	void								SetLinearVelocity(Vec3Arg inLinearVelocity)				{ mLinearVelocity = inLinearVelocity; }
+
+	/// Get the position of the character
+	Vec3								GetPosition() const										{ return mPosition; }
+
+	/// Set the position of the character
+	void								SetPosition(Vec3Arg inPosition)							{ mPosition = inPosition; }
+
+	/// Get the rotation of the character
+	Quat								GetRotation() const										{ return mRotation; }
+	
+	/// Set the rotation of the character
+	void								SetRotation(QuatArg inRotation)							{ mRotation = inRotation; }
+
+	/// Calculate the world transform of the character
+	Mat44								GetWorldTransform() const								{ return Mat44::sRotationTranslation(mRotation, mPosition); }
+
+	/// Calculates the transform for this character's center of mass
+	Mat44								GetCenterOfMassTransform() const						{ return GetCenterOfMassTransform(mPosition); }
+
+	/// Character mass (kg)
+	void								SetMass(float inMass)									{ mMass = inMass; }
+
+	/// Maximum force with which the character can push other bodies (N)
+	void								SetMaxStrength(float inMaxStrength)						{ mMaxStrength = inMaxStrength; }
+
+	/// Character padding
+	float								GetCharacterPadding() const								{ return mCharacterPadding; }
+
+	/// This is the main update function. It moves the character according to its current velocity. Note it's your own responsibility to apply gravity!
+	/// @param inDeltaTime Time step to simulate.
+	/// @param inGravity Gravity vector (m/s^2)
+	/// @param inBroadPhaseLayerFilter Filter that is used to check if the character collides with something in the broadphase.
+	/// @param inObjectLayerFilter Filter that is used to check if a character collides with a layer.
+	/// @param inBodyFilter Filter that is used to check if a character collides with a body.
+	void								Update(float inDeltaTime, Vec3Arg inGravity, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator);
+
+	/// This function can be used after a character has teleported to determine the new contacts with the world.
+	void								RefreshContacts(const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator);
+
+	/// Switch the shape of the character (e.g. for stance).
+	/// @param inMaxPenetrationDepth When inMaxPenetrationDepth is not FLT_MAX, it checks if the new shape collides before switching shape. This is the max penetration we're willing to accept after the switch.
+	/// @param inBroadPhaseLayerFilter Filter that is used to check if the character collides with something in the broadphase.
+	/// @param inObjectLayerFilter Filter that is used to check if a character collides with a layer.
+	/// @param inBodyFilter Filter that is used to check if a character collides with a body.
+	/// @return Returns true if the switch succeeded.
+	bool								SetShape(const Shape *inShape, float inMaxPenetrationDepth, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator);
+
+#ifdef JPH_DEBUG_RENDERER
+	static inline bool					sDrawConstraints = false;								///< Draw the current state of the constraints for iteration 0 when creating them
+#endif
+
+private:
+	// Encapsulates a collision contact
+	struct Contact
+	{
+		Vec3							mPosition;												///< Position where the character makes contact
+		Vec3							mLinearVelocity;										///< Velocity of the contact point
+		Vec3							mNormal;												///< Contact normal, pointing towards the character
+		float							mDistance;												///< Distance to the contact <= 0 means that it is an actual contact, > 0 means predictive
+		float							mFraction;												///< Fraction along the path where this contact takes place
+		BodyID							mBodyB;													///< ID of body we're colliding with
+		SubShapeID						mSubShapeIDB;											///< Sub shape ID of body we're colliding with
+		EMotionType						mMotionTypeB;											///< Motion type of B, used to determine the priority of the contact
+		uint64							mUserData;												///< User data of B
+		const PhysicsMaterial *			mMaterial;												///< Material of B
+		bool							mHadCollision = false;									///< If the character actually collided with the contact (can be false if a predictive contact never becomes a real one)
+		bool							mWasDiscarded = false;									///< If the contact validate callback chose to discard this contact
+		bool							mCanPushCharacter = true;								///< When true, the velocity of the contact point can push the character
+	};
+
+	using TempContactList = vector<Contact, STLTempAllocator<Contact>>;
+	using ContactList = vector<Contact>;
+
+	// A contact that needs to be ignored
+	struct IgnoredContact
+	{
+										IgnoredContact() = default;
+										IgnoredContact(const BodyID &inBodyID, const SubShapeID &inSubShapeID) : mBodyID(inBodyID), mSubShapeID(inSubShapeID) { }
+
+		BodyID							mBodyID;												///< ID of body we're colliding with
+		SubShapeID						mSubShapeID;											///< Sub shape of body we're colliding with
+	};
+
+	using IgnoredContactList = vector<IgnoredContact, STLTempAllocator<IgnoredContact>>;
+
+	// A constraint that limits the movement of the character
+	struct Constraint
+	{
+		Contact *						mContact;												///< Contact that this constraint was generated from
+		float							mTOI;													///< Calculated time of impact (can be negative if penetrating)
+		float							mProjectedVelocity;										///< Velocity of the contact projected on the contact normal (negative if separating)
+		Vec3							mLinearVelocity;										///< Velocity of the contact (can contain a corrective velocity to resolve penetration)
+		Plane							mPlane;													///< Plane around the origin that describes how far we can displace (from the origin)
+	};
+
+	using ConstraintList = vector<Constraint, STLTempAllocator<Constraint>>;
+
+	// Collision collector that collects hits for CollideShape
+	class ContactCollector : public CollideShapeCollector
+	{
+	public:
+										ContactCollector(PhysicsSystem *inSystem, uint inMaxHits, TempContactList &outContacts) : mSystem(inSystem), mContacts(outContacts), mMaxHits(inMaxHits) { }
+
+		virtual void					AddHit(const CollideShapeResult &inResult) override;
+
+		PhysicsSystem *					mSystem;
+		TempContactList &				mContacts;
+		uint							mMaxHits;
+	};
+
+	// A collision collector that collects hits for CastShape
+	class ContactCastCollector : public CastShapeCollector
+	{
+	public:
+										ContactCastCollector(PhysicsSystem *inSystem, Vec3Arg inDisplacement, uint inMaxHits, const IgnoredContactList &inIgnoredContacts, TempContactList &outContacts) : mSystem(inSystem), mDisplacement(inDisplacement), mIgnoredContacts(inIgnoredContacts), mContacts(outContacts), mMaxHits(inMaxHits) { }
+
+		virtual void					AddHit(const ShapeCastResult &inResult) override;
+
+		PhysicsSystem *					mSystem;
+		Vec3							mDisplacement;
+		const IgnoredContactList &		mIgnoredContacts;
+		TempContactList &				mContacts;
+		uint							mMaxHits;
+	};
+
+	// Helper function to convert a Jolt collision result into a contact
+	template <class taCollector>
+	inline static void					sFillContactProperties(Contact &outContact, const Body &inBody, const taCollector &inCollector, const CollideShapeResult &inResult);
+
+	// Move the shape from ioPosition and try to displace it by inVelocity * inDeltaTime, this will try to slide the shape along the world geometry
+	void								MoveShape(Vec3 &ioPosition, Vec3Arg inVelocity, Vec3Arg inGravity, float inDeltaTime, ContactList *outActiveContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator) const;
+
+	// Ask the callback if inContact is a valid contact point
+	bool								ValidateContact(const Contact &inContact) const;
+
+	// Tests the shape for collision around inPosition
+	void								GetContactsAtPosition(Vec3Arg inPosition, Vec3Arg inMovementDirection, const Shape *inShape, TempContactList &outContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter) const;
+
+	// Remove penetrating contacts with the same body that have conflicting normals, leaving these will make the character mover get stuck
+	void								RemoveConflictingContacts(TempContactList &ioContacts, IgnoredContactList &outIgnoredContacts) const;
+
+	// Convert contacts into constraints. The character is assumed to start at the origin and the constraints are planes around the origin that confine the movement of the character.
+	void								DetermineConstraints(TempContactList &inContacts, ConstraintList &outConstraints) const;
+
+	// Use the constraints to solve the displacement of the character. This will slide the character on the planes around the origin for as far as possible.
+	void								SolveConstraints(Vec3Arg inVelocity, Vec3Arg inGravity, float inDeltaTime, float inTimeRemaining, ConstraintList &ioConstraints, IgnoredContactList &ioIgnoredContacts, float &outTimeSimulated, Vec3 &outDisplacement, TempAllocator &inAllocator) const;
+
+	// Handle contact with physics object that we're colliding against
+	bool								HandleContact(Vec3Arg inVelocity, Constraint &ioConstraint, Vec3Arg inGravity, float inDeltaTime) const;
+
+	// Does a swept test of the shape from inPosition with displacement inDisplacement, returns true if there was a collision
+	bool								GetFirstContactForSweep(Vec3Arg inPosition, Vec3Arg inDisplacement, Contact &outContact, const IgnoredContactList &inIgnoredContacts, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, TempAllocator &inAllocator) const;
+
+	// Store contacts so that CheckSupport and GetStandingPhysicsInstance etc. can return information
+	void								StoreActiveContacts(const TempContactList &inContacts, TempAllocator &inAllocator);
+
+	// This function will determine which contacts are touching the character and will calculate the one that is supporting us
+	void								UpdateSupportingContact(TempAllocator &inAllocator);
+
+	// This function returns the actual center of mass of the shape, not corrected for the character padding
+	inline Mat44						GetCenterOfMassTransform(Vec3Arg inPosition) const		{ return Mat44::sRotationTranslation(mRotation, inPosition).PreTranslated(mShape->GetCenterOfMass()).PostTranslated(mCharacterPadding * mUp); }
+
+	// Our main listener for contacts
+	CharacterContactListener *			mListener = nullptr;
+
+	// The character's world space up axis
+	Vec3								mUp;
+
+	// Movement settings
+	float								mPredictiveContactDistance;								// How far to scan outside of the shape for predictive contacts
+	uint								mMaxCollisionIterations;								// Max amount of collision loops
+	uint								mMaxConstraintIterations;								// How often to try stepping in the constraint solving
+	float								mMinTimeRemaining;										// Early out condition: If this much time is left to simulate we are done
+	float								mCollisionTolerance;									// How far we're willing to penetrate geometry
+	float								mCharacterPadding;										// How far we try to stay away from the geometry, this ensures that the sweep will hit as little as possible lowering the collision cost and reducing the risk of getting stuck
+	uint								mMaxNumHits;											// Max num hits to collect in order to avoid excess of contact points collection
+	float								mPenetrationRecoverySpeed;								// This value governs how fast a penetration will be resolved, 0 = nothing is resolved, 1 = everything in one update
+
+	// Character mass (kg)
+	float								mMass;
+
+	// Maximum force with which the character can push other bodies (N)
+	float								mMaxStrength;
+
+	// Current position (of the base, not the center of mass)
+	Vec3								mPosition = Vec3::sZero();
+
+	// Current rotation (of the base, not of the center of mass)
+	Quat								mRotation = Quat::sIdentity();
+
+	// Current linear velocity
+	Vec3								mLinearVelocity = Vec3::sZero();
+
+	// List of contacts that were active in the last frame
+	ContactList							mActiveContacts;
+
+	// Remembers the delta time of the last update
+	float								mLastDeltaTime = 1.0f / 60.0f;
+};
+
+JPH_NAMESPACE_END

+ 12 - 0
Jolt/Physics/Collision/Shape/SubShapeID.h

@@ -52,6 +52,18 @@ public:
 		return mValue == cEmpty;
 	}
 
+	/// Check equal
+	inline bool			operator == (const SubShapeID &inRHS) const
+	{
+		return mValue == inRHS.mValue;
+	}
+
+	/// Check not-equal
+	inline bool			operator != (const SubShapeID &inRHS) const
+	{
+		return mValue != inRHS.mValue;
+	}
+
 private:
 	friend class SubShapeIDCreator;
 

+ 27 - 0
Jolt/Renderer/DebugRenderer.cpp

@@ -211,6 +211,33 @@ void DebugRenderer::DrawCoordinateSystem(Mat44Arg inTransform, float inSize)
 	DrawArrow(inTransform.GetTranslation(), inTransform * Vec3(0, 0, inSize), Color::sBlue, 0.1f * inSize);
 }
 
+void DebugRenderer::DrawPlane(Vec3Arg inPoint, Vec3Arg inNormal, ColorArg inColor, float inSize)
+{
+	// Create orthogonal basis
+	Vec3 perp1 = inNormal.Cross(Vec3::sAxisY()).NormalizedOr(Vec3::sAxisX());
+	Vec3 perp2 = perp1.Cross(inNormal).Normalized();
+	perp1 = inNormal.Cross(perp2);
+
+	// Calculate corners
+	Vec3 corner1 = inPoint + inSize * (perp1 + perp2);
+	Vec3 corner2 = inPoint + inSize * (perp1 - perp2);
+	Vec3 corner3 = inPoint + inSize * (-perp1 - perp2);
+	Vec3 corner4 = inPoint + inSize * (-perp1 + perp2);
+
+	// Draw cross
+	DrawLine(corner1, corner3, inColor);
+	DrawLine(corner2, corner4, inColor);
+
+	// Draw square
+	DrawLine(corner1, corner2, inColor);
+	DrawLine(corner2, corner3, inColor);
+	DrawLine(corner3, corner4, inColor);
+	DrawLine(corner4, corner1, inColor);
+
+	// Draw normal
+	DrawArrow(inPoint, inPoint + inSize * inNormal, inColor, 0.1f * inSize);
+}
+
 void DebugRenderer::DrawWireTriangle(Vec3Arg inV1, Vec3Arg inV2, Vec3Arg inV3, ColorArg inColor)
 {
 	JPH_PROFILE_FUNCTION();

+ 3 - 0
Jolt/Renderer/DebugRenderer.h

@@ -48,6 +48,9 @@ public:
 	/// Draw coordinate system (3 arrows, x = red, y = green, z = blue)
 	void								DrawCoordinateSystem(Mat44Arg inTransform, float inSize = 1.0f);
 
+	/// Draw a plane through inPoint with normal inNormal
+	void								DrawPlane(Vec3Arg inPoint, Vec3Arg inNormal, ColorArg inColor, float inSize);
+
 	/// Draw wireframe triangle
 	void								DrawWireTriangle(Vec3Arg inV1, Vec3Arg inV2, Vec3Arg inV3, ColorArg inColor);
 

+ 3 - 1
README.md

@@ -69,7 +69,9 @@ For more information see the [Architecture and API documentation](https://jrouwe
 	* Hard keying (kinematic only rigid bodies).
 	* Soft keying (setting velocities on dynamic rigid bodies).
 	* Driving constraint motors to an animated pose.
-* Game character simulation (capsule), although many games may want to implement characters just using collision tests for more control over the simulation.
+* Game character simulation (capsule)
+	* Rigid body character. Moves during the physics simulation. Cheapest option and most accurate collision response between character and dynamic bodies.
+	* Virtual character. Does not have a rigid body in the world but simulates one using collision checks. Updated outside of the physics update for more control. Less accurate interaction with dynamic bodies.
 * Vehicle simulation of wheeled and tracked vehicles.
 * Water buoyancy calculations.
 

+ 4 - 0
Samples/Samples.cmake

@@ -13,8 +13,12 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/BroadPhase/BroadPhaseInsertionTest.h
 	${SAMPLES_ROOT}/Tests/BroadPhase/BroadPhaseTest.cpp
 	${SAMPLES_ROOT}/Tests/BroadPhase/BroadPhaseTest.h
+	${SAMPLES_ROOT}/Tests/Character/CharacterBaseTest.cpp
+	${SAMPLES_ROOT}/Tests/Character/CharacterBaseTest.h
 	${SAMPLES_ROOT}/Tests/Character/CharacterTest.cpp
 	${SAMPLES_ROOT}/Tests/Character/CharacterTest.h
+	${SAMPLES_ROOT}/Tests/Character/CharacterVirtualTest.cpp
+	${SAMPLES_ROOT}/Tests/Character/CharacterVirtualTest.h
 	${SAMPLES_ROOT}/Tests/Constraints/ConeConstraintTest.cpp
 	${SAMPLES_ROOT}/Tests/Constraints/ConeConstraintTest.h
 	${SAMPLES_ROOT}/Tests/Constraints/ConstraintSingularityTest.cpp

+ 5 - 0
Samples/SamplesApp.cpp

@@ -31,6 +31,7 @@
 #include <Jolt/Physics/Collision/Shape/ScaledShape.h>
 #include <Jolt/Physics/Collision/NarrowPhaseStats.h>
 #include <Jolt/Physics/Constraints/DistanceConstraint.h>
+#include <Jolt/Physics/Character/CharacterVirtual.h>
 #include <Utils/Log.h>
 #include <Renderer/DebugRendererImp.h>
 
@@ -223,10 +224,12 @@ static TestNameAndRTTI sRigTests[] =
 };
 
 JPH_DECLARE_RTTI_FOR_FACTORY(CharacterTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(CharacterVirtualTest)
 
 static TestNameAndRTTI sCharacterTests[] =
 {
 	{ "Character",							JPH_RTTI(CharacterTest) },
+	{ "Character Virtual",					JPH_RTTI(CharacterVirtualTest) },
 };
 
 JPH_DECLARE_RTTI_FOR_FACTORY(WaterShapeTest)
@@ -393,6 +396,7 @@ SamplesApp::SamplesApp()
 		mDebugUI->CreateCheckBox(drawing_options, "Draw Mesh Shape Triangle Outlines", MeshShape::sDrawTriangleOutlines, [](UICheckBox::EState inState) { MeshShape::sDrawTriangleOutlines = inState == UICheckBox::STATE_CHECKED; });
 		mDebugUI->CreateCheckBox(drawing_options, "Draw Height Field Shape Triangle Outlines", HeightFieldShape::sDrawTriangleOutlines, [](UICheckBox::EState inState) { HeightFieldShape::sDrawTriangleOutlines = inState == UICheckBox::STATE_CHECKED; });
 		mDebugUI->CreateCheckBox(drawing_options, "Draw Submerged Volumes", Shape::sDrawSubmergedVolumes, [](UICheckBox::EState inState) { Shape::sDrawSubmergedVolumes = inState == UICheckBox::STATE_CHECKED; });
+		mDebugUI->CreateCheckBox(drawing_options, "Draw Character Virtual Constraints", CharacterVirtual::sDrawConstraints, [](UICheckBox::EState inState) { CharacterVirtual::sDrawConstraints = inState == UICheckBox::STATE_CHECKED; });
 		mDebugUI->ShowMenu(drawing_options);
 	});
 #endif // JPH_DEBUG_RENDERER
@@ -529,6 +533,7 @@ void SamplesApp::StartTest(const RTTI *inRTTI)
 	mTest->SetPhysicsSystem(mPhysicsSystem);
 	mTest->SetJobSystem(mJobSystem);
 	mTest->SetDebugRenderer(mDebugRenderer);
+	mTest->SetTempAllocator(mTempAllocator);
 	if (mInstallContactListener)
 	{
 		mContactListener = new ContactListenerImpl;

+ 290 - 0
Samples/Tests/Character/CharacterBaseTest.cpp

@@ -0,0 +1,290 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/Character/CharacterBaseTest.h>
+#include <Jolt/Physics/PhysicsScene.h>
+#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
+#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Core/StringTools.h>
+#include <Application/DebugUI.h>
+#include <Layers.h>
+#include <Utils/Log.h>
+#include <Renderer/DebugRendererImp.h>
+
+JPH_IMPLEMENT_RTTI_ABSTRACT(CharacterBaseTest) 
+{ 
+	JPH_ADD_BASE_CLASS(CharacterBaseTest, Test) 
+}
+
+const char *CharacterBaseTest::sScenes[] =
+{
+	"PerlinMesh",
+	"PerlinHeightField",
+	"ObstacleCourse",
+	"Terrain1",
+	"Terrain2",
+};
+
+const char *CharacterBaseTest::sSceneName = "ObstacleCourse";
+
+// Scene constants
+static const Vec3 cRotatingPosition(-5, 0.25f, 15);
+static const Quat cRotatingOrientation = Quat::sIdentity();
+static const Vec3 cVerticallyMovingPosition(0, 2.0f, 15);
+static const Quat cVerticallyMovingOrientation = Quat::sIdentity();
+static const Vec3 cHorizontallyMovingPosition(5, 1, 15);
+static const Quat cHorizontallyMovingOrientation = Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI);
+static const Vec3 cRampPosition(15, 2.2f, 15);
+static const Quat cRampOrientation = Quat::sRotation(Vec3::sAxisX(), -0.25f * JPH_PI);
+static const Vec3 cRampBlocksStart = cRampPosition + Vec3(-3.0f, 3.0f, 1.5f);
+static const Vec3 cRampBlocksDelta = Vec3(2.0f, 0, 0);
+static const float cRampBlocksTime = 5.0f;
+
+void CharacterBaseTest::Initialize()
+{
+	if (strcmp(sSceneName, "PerlinMesh") == 0)
+	{
+		// Default terrain
+		CreateMeshTerrain();
+	}
+	else if (strcmp(sSceneName, "PerlinHeightField") == 0)
+	{
+		// Default terrain
+		CreateHeightFieldTerrain();
+	}
+	else if (strcmp(sSceneName, "ObstacleCourse") == 0)
+	{
+		// Default terrain
+		CreateFloor();
+
+		{
+			// Create ramps with different inclinations
+			Ref<Shape> ramp = RotatedTranslatedShapeSettings(Vec3(0, 0, -2.5f), Quat::sIdentity(), new BoxShape(Vec3(1.0f, 0.05f, 2.5f))).Create().Get();
+			for (int angle = 0; angle < 18; ++angle)
+				mBodyInterface->CreateAndAddBody(BodyCreationSettings(ramp, Vec3(-15.0f + angle * 2.0f, 0, -10.0f), Quat::sRotation(Vec3::sAxisX(), DegreesToRadians(10.0f * angle)), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+		}
+
+		{
+			// Create wall consisting of vertical pillars
+			// Note: Convex radius 0 because otherwise it will be a bumpy wall
+			Ref<Shape> wall = new BoxShape(Vec3(0.1f, 2.5f, 0.1f), 0.0f); 
+			for (int z = 0; z < 40; ++z)
+				mBodyInterface->CreateAndAddBody(BodyCreationSettings(wall, Vec3(-10.0f, 2.5f, -10.0f + 0.2f * z), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+		}
+
+		{
+			// Kinematic blocks to test interacting with moving objects
+			Ref<Shape> kinematic = new BoxShape(Vec3(1, 0.25f, 3.0f));
+			mRotatingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cRotatingPosition, cRotatingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
+			mVerticallyMovingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cVerticallyMovingPosition, cVerticallyMovingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
+			mHorizontallyMovingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cHorizontallyMovingPosition, cHorizontallyMovingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
+		}
+
+		{
+			// Dynamic blocks to test player pushing blocks
+			Ref<Shape> block = new BoxShape(Vec3::sReplicate(0.5f));
+			for (int y = 0; y < 3; ++y)
+			{
+				BodyCreationSettings bcs(block, Vec3(5.0f, 0.5f + float(y), 0.0f), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
+				bcs.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
+				bcs.mMassPropertiesOverride.mMass = 10.0f;
+				mBodyInterface->CreateAndAddBody(bcs, EActivation::DontActivate);
+			}
+		}
+
+		{
+			// Create ramp
+			BodyCreationSettings ramp(new BoxShape(Vec3(4.0f, 0.1f, 3.0f)), cRampPosition, cRampOrientation, EMotionType::Static, Layers::NON_MOVING);
+			mBodyInterface->CreateAndAddBody(ramp, EActivation::DontActivate);
+
+			// Create blocks on ramp
+			Ref<Shape> block = new BoxShape(Vec3::sReplicate(0.5f));
+			BodyCreationSettings bcs(block, cRampBlocksStart, cRampOrientation, EMotionType::Dynamic, Layers::MOVING);
+			bcs.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
+			bcs.mMassPropertiesOverride.mMass = 10.0f;
+			for (int i = 0; i < 4; ++i)
+			{
+				mRampBlocks.emplace_back(mBodyInterface->CreateAndAddBody(bcs, EActivation::Activate));
+				bcs.mPosition += cRampBlocksDelta;
+			}
+		}
+
+		// Create three funnels with walls that are too steep to climb
+		Ref<Shape> funnel = new BoxShape(Vec3(0.1f, 1.0f, 1.0f));
+		for (int i = 0; i < 2; ++i)
+		{
+			Quat rotation = Quat::sRotation(Vec3::sAxisY(), JPH_PI * i);
+			mBodyInterface->CreateAndAddBody(BodyCreationSettings(funnel, Vec3(5.0f, 0.1f, 5.0f) + rotation * Vec3(0.2f, 0, 0), rotation * Quat::sRotation(Vec3::sAxisZ(), -DegreesToRadians(40.0f)), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+		}
+		for (int i = 0; i < 3; ++i)
+		{
+			Quat rotation = Quat::sRotation(Vec3::sAxisY(), 2.0f / 3.0f * JPH_PI * i);
+			mBodyInterface->CreateAndAddBody(BodyCreationSettings(funnel, Vec3(7.5f, 0.1f, 5.0f) + rotation * Vec3(0.2f, 0, 0), rotation * Quat::sRotation(Vec3::sAxisZ(), -DegreesToRadians(40.0f)), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+		}
+		for (int i = 0; i < 4; ++i)
+		{
+			Quat rotation = Quat::sRotation(Vec3::sAxisY(), 0.5f * JPH_PI * i);
+			mBodyInterface->CreateAndAddBody(BodyCreationSettings(funnel, Vec3(10.0f, 0.1f, 5.0f) + rotation * Vec3(0.2f, 0, 0), rotation * Quat::sRotation(Vec3::sAxisZ(), -DegreesToRadians(40.0f)), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+		}
+	}
+	else
+	{
+		// Load scene
+		Ref<PhysicsScene> scene;
+		if (!ObjectStreamIn::sReadObject((string("Assets/") + sSceneName + ".bof").c_str(), scene))
+			FatalError("Failed to load scene");
+		scene->FixInvalidScales();
+		for (BodyCreationSettings &settings : scene->GetBodies())
+		{
+			settings.mObjectLayer = Layers::NON_MOVING;
+			settings.mFriction = 0.5f;
+		}
+		scene->CreateBodies(mPhysicsSystem);
+	}
+
+	// Create capsule shapes for all stances
+	mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding)).Create().Get();
+	mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightCrouching, cCharacterRadiusCrouching)).Create().Get();
+}
+
+void CharacterBaseTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
+	// Update scene time
+	mTime += inParams.mDeltaTime;
+
+	// Determine controller input
+	Vec3 control_input = Vec3::sZero();
+	if (inParams.mKeyboard->IsKeyPressed(DIK_LEFT))		control_input.SetZ(-1);
+	if (inParams.mKeyboard->IsKeyPressed(DIK_RIGHT))	control_input.SetZ(1);
+	if (inParams.mKeyboard->IsKeyPressed(DIK_UP))		control_input.SetX(1);
+	if (inParams.mKeyboard->IsKeyPressed(DIK_DOWN))		control_input.SetX(-1);
+	if (control_input != Vec3::sZero())
+		control_input = control_input.Normalized();
+
+	// Rotate controls to align with the camera
+	Vec3 cam_fwd = inParams.mCameraState.mForward;
+	cam_fwd.SetY(0.0f);
+	cam_fwd = cam_fwd.NormalizedOr(Vec3::sAxisX());
+	Quat rotation = Quat::sFromTo(Vec3::sAxisX(), cam_fwd);
+	control_input = rotation * control_input;
+
+	// Check actions
+	bool jump = false;
+	bool switch_stance = false;
+	for (int key = inParams.mKeyboard->GetFirstKey(); key != 0; key = inParams.mKeyboard->GetNextKey())
+	{
+		if (key == DIK_RSHIFT)
+			switch_stance = true;
+		else if (key == DIK_RCONTROL)
+			jump = true;
+	}
+
+	HandleInput(control_input, jump, switch_stance, inParams.mDeltaTime);
+
+	// Animate bodies
+	if (!mRotatingBody.IsInvalid())
+		mBodyInterface->MoveKinematic(mRotatingBody, cRotatingPosition, Quat::sRotation(Vec3::sAxisY(), JPH_PI * sin(mTime)), inParams.mDeltaTime);
+	if (!mHorizontallyMovingBody.IsInvalid())
+		mBodyInterface->MoveKinematic(mHorizontallyMovingBody, cHorizontallyMovingPosition + Vec3(3.0f * sin(mTime), 0, 0), cHorizontallyMovingOrientation, inParams.mDeltaTime);
+	if (!mVerticallyMovingBody.IsInvalid())
+		mBodyInterface->MoveKinematic(mVerticallyMovingBody, cVerticallyMovingPosition + Vec3(0, 1.75f * sin(mTime), 0), cVerticallyMovingOrientation, inParams.mDeltaTime);
+
+	// Reset ramp blocks
+	mRampBlocksTimeLeft -= inParams.mDeltaTime;
+	if (mRampBlocksTimeLeft < 0.0f)
+	{
+		for (size_t i = 0; i < mRampBlocks.size(); ++i)
+		{
+			mBodyInterface->SetPositionAndRotation(mRampBlocks[i], cRampBlocksStart + float(i) * cRampBlocksDelta, cRampOrientation, EActivation::Activate);
+			mBodyInterface->SetLinearAndAngularVelocity(mRampBlocks[i], Vec3::sZero(), Vec3::sZero());
+		}
+		mRampBlocksTimeLeft = cRampBlocksTime;
+	}
+}
+
+void CharacterBaseTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
+{
+	inUI->CreateTextButton(inSubMenu, "Select Scene", [this, inUI]() { 
+		UIElement *scene_name = inUI->CreateMenu();
+		for (uint i = 0; i < size(sScenes); ++i)
+			inUI->CreateTextButton(scene_name, sScenes[i], [this, i]() { sSceneName = sScenes[i]; RestartTest(); });
+		inUI->ShowMenu(scene_name);
+	});
+}
+
+void CharacterBaseTest::GetInitialCamera(CameraState& ioState) const
+{
+	// This will become the local space offset, look down the x axis and slightly down
+	ioState.mPos = Vec3::sZero();
+	ioState.mForward = Vec3(10.0f, -2.0f, 0).Normalized();
+}
+
+Mat44 CharacterBaseTest::GetCameraPivot(float inCameraHeading, float inCameraPitch) const 
+{
+	// Pivot is center of character + distance behind based on the heading and pitch of the camera
+	Vec3 fwd = Vec3(cos(inCameraPitch) * cos(inCameraHeading), sin(inCameraPitch), cos(inCameraPitch) * sin(inCameraHeading));
+	return Mat44::sTranslation(GetCharacterPosition() + Vec3(0, cCharacterHeightStanding + cCharacterRadiusStanding, 0) - 5.0f * fwd);
+}
+
+void CharacterBaseTest::SaveState(StateRecorder &inStream) const
+{
+	inStream.Write(mTime);
+	inStream.Write(mRampBlocksTimeLeft);
+}
+
+void CharacterBaseTest::RestoreState(StateRecorder &inStream)
+{
+	inStream.Read(mTime);
+	inStream.Read(mRampBlocksTimeLeft);
+}
+
+void CharacterBaseTest::DrawCharacterState(const CharacterBase *inCharacter, Mat44Arg inCharacterTransform, Vec3Arg inCharacterVelocity)
+{
+	// Draw current location
+	// Drawing prior to update since the physics system state is also that prior to the simulation step (so that all detected collisions etc. make sense)
+	mDebugRenderer->DrawCoordinateSystem(inCharacterTransform);
+
+	// Determine color
+	CharacterBase::EGroundState ground_state = inCharacter->GetGroundState();
+	Color color;
+	switch (ground_state)
+	{
+	case CharacterBase::EGroundState::OnGround:
+		color = Color::sGreen;
+		break;
+	case CharacterBase::EGroundState::Sliding:
+		color = Color::sOrange;
+		break;
+	case CharacterBase::EGroundState::InAir:
+	default:
+		color = Color::sRed;
+		break;
+	}
+
+	// Draw the state of the ground contact
+	if (ground_state != CharacterBase::EGroundState::InAir)
+	{
+		Vec3 ground_position = inCharacter->GetGroundPosition();
+		Vec3 ground_normal = inCharacter->GetGroundNormal();
+		Vec3 ground_velocity = inCharacter->GetGroundVelocity();
+
+		// Draw ground position
+		mDebugRenderer->DrawWireSphere(ground_position, 0.1f, Color::sRed);
+		mDebugRenderer->DrawArrow(ground_position, ground_position + 2.0f * ground_normal, Color::sGreen, 0.1f);
+
+		// Draw ground velocity
+		if (!ground_velocity.IsNearZero())
+			mDebugRenderer->DrawArrow(ground_position, ground_position + ground_velocity, Color::sBlue, 0.1f);
+	}
+
+	// Draw provided character velocity
+	if (!inCharacterVelocity.IsNearZero())
+		mDebugRenderer->DrawArrow(inCharacterTransform.GetTranslation(), inCharacterTransform.GetTranslation() + inCharacterVelocity, Color::sYellow, 0.1f);
+
+	// Draw text info
+	const PhysicsMaterial *ground_material = inCharacter->GetGroundMaterial();
+	mDebugRenderer->DrawText3D(inCharacterTransform.GetTranslation(), StringFormat("Mat: %s\nVel: %.1f m/s", ground_material->GetDebugName(), (double)inCharacterVelocity.Length()), color, 0.25f);
+}

+ 78 - 0
Samples/Tests/Character/CharacterBaseTest.h

@@ -0,0 +1,78 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+#include <Jolt/Physics/Character/CharacterBase.h>
+
+// Base class for the character tests, initializes the test scene.
+class CharacterBaseTest : public Test
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(CharacterBaseTest)
+
+	// Number used to scale the terrain and camera movement to the scene
+	virtual float			GetWorldScale() const override								{ return 0.2f; }
+
+	// Initialize the test
+	virtual void			Initialize() override;
+
+	// Update the test, called before the physics update
+	virtual void			PrePhysicsUpdate(const PreUpdateParams &inParams) override;
+
+	// Override to specify the initial camera state (local to GetCameraPivot)
+	virtual void			GetInitialCamera(CameraState &ioState) const override;
+
+	// Override to specify a camera pivot point and orientation (world space)
+	virtual Mat44			GetCameraPivot(float inCameraHeading, float inCameraPitch) const override;
+
+	// Optional settings menu
+	virtual bool			HasSettingsMenu() const override							{ return true; }
+	virtual void			CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
+
+	// Saving / restoring state for replay
+	virtual void			SaveState(StateRecorder &inStream) const override;
+	virtual void			RestoreState(StateRecorder &inStream) override;
+
+protected:
+	// Get position of the character
+	virtual Vec3			GetCharacterPosition() const = 0;
+
+	// Handle user input to the character
+	virtual void			HandleInput(Vec3Arg inMovementDirection, bool inJump, bool inSwitchStance, float inDeltaTime) = 0;
+
+	// Draw the character state
+	void					DrawCharacterState(const CharacterBase *inCharacter, Mat44Arg inCharacterTransform, Vec3Arg inCharacterVelocity);
+
+	// Character size
+	inline static constexpr float cCharacterHeightStanding = 1.35f;
+	inline static constexpr float cCharacterRadiusStanding = 0.3f;
+	inline static constexpr float cCharacterHeightCrouching = 0.8f;
+	inline static constexpr float cCharacterRadiusCrouching = 0.3f;
+	inline static constexpr float cCharacterSpeed = 6.0f;
+	inline static constexpr float cJumpSpeed = 4.0f;
+
+	// The different stances for the character
+	RefConst<Shape>			mStandingShape;
+	RefConst<Shape>			mCrouchingShape;
+
+	// List of boxes on ramp
+	vector<BodyID>			mRampBlocks;
+	float					mRampBlocksTimeLeft = 0.0f;
+
+private:
+	// List of possible scene names
+	static const char *		sScenes[];
+
+	// Filename of animation to load for this test
+	static const char *		sSceneName;
+
+	// Scene time (for moving bodies)
+	float					mTime = 0.0f;
+
+	// Moving bodies
+	BodyID					mRotatingBody;
+	BodyID					mVerticallyMovingBody;
+	BodyID					mHorizontallyMovingBody;
+};

+ 27 - 137
Samples/Tests/Character/CharacterTest.cpp

@@ -4,35 +4,14 @@
 #include <TestFramework.h>
 
 #include <Tests/Character/CharacterTest.h>
-#include <Jolt/Physics/PhysicsScene.h>
-#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
-#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
-#include <Jolt/Core/StringTools.h>
-#include <Application/DebugUI.h>
 #include <Layers.h>
-#include <Utils/Log.h>
 #include <Renderer/DebugRendererImp.h>
 
 JPH_IMPLEMENT_RTTI_VIRTUAL(CharacterTest) 
 { 
-	JPH_ADD_BASE_CLASS(CharacterTest, Test) 
+	JPH_ADD_BASE_CLASS(CharacterTest, CharacterBaseTest)
 }
 
-const char *CharacterTest::sScenes[] =
-{
-	"PerlinMesh",
-	"PerlinHeightField",
-	"Terrain1",
-	"Terrain2",
-};
-
-const char *CharacterTest::sSceneName = "Terrain2";
-
-static const float cCharacterHeightStanding = 1.35f;
-static const float cCharacterRadiusStanding = 0.3f;
-static const float cCharacterHeightCrouching = 0.8f;
-static const float cCharacterRadiusCrouching = 0.3f;
-static const float cJumpSpeed = 4.0f;
 static const float cCollisionTolerance = 0.05f;
 
 CharacterTest::~CharacterTest()
@@ -42,37 +21,11 @@ CharacterTest::~CharacterTest()
 
 void CharacterTest::Initialize()
 {
-	if (strcmp(sSceneName, "PerlinMesh") == 0)
-	{
-		// Default terrain
-		CreateMeshTerrain();
-	}
-	else if (strcmp(sSceneName, "PerlinHeightField") == 0)
-	{
-		// Default terrain
-		CreateHeightFieldTerrain();
-	}	
-	else
-	{
-		// Load scene
-		Ref<PhysicsScene> scene;
-		if (!ObjectStreamIn::sReadObject((string("Assets/") + sSceneName + ".bof").c_str(), scene))
-			FatalError("Failed to load scene");
-		scene->FixInvalidScales();
-		for (BodyCreationSettings &settings : scene->GetBodies())
-		{
-			settings.mObjectLayer = Layers::NON_MOVING;
-			settings.mFriction = 0.5f;
-		}
-		scene->CreateBodies(mPhysicsSystem);
-	}
-
-	// Create capsule shapes for all stances
-	mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding)).Create().Get();
-	mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightCrouching, cCharacterRadiusCrouching)).Create().Get();
+	CharacterBaseTest::Initialize();
 
 	// Create 'player' character
 	Ref<CharacterSettings> settings = new CharacterSettings();
+	settings->mMaxSlopeAngle = DegreesToRadians(45.0f);
 	settings->mLayer = Layers::MOVING;
 	settings->mShape = mStandingShape;
 	settings->mFriction = 0.5f;
@@ -82,107 +35,44 @@ void CharacterTest::Initialize()
 
 void CharacterTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 {
-	// Get the state of the character
-	Character::EGroundState ground_state = mCharacter->GetGroundState();
+	CharacterBaseTest::PrePhysicsUpdate(inParams);
 
-	// Determine controller input
-	Vec3 control_input = Vec3::sZero();
-	if (inParams.mKeyboard->IsKeyPressed(DIK_LEFT))		control_input.SetX(-1);
-	if (inParams.mKeyboard->IsKeyPressed(DIK_RIGHT))	control_input.SetX(1);
-	if (inParams.mKeyboard->IsKeyPressed(DIK_UP))		control_input.SetZ(-1);
-	if (inParams.mKeyboard->IsKeyPressed(DIK_DOWN))		control_input.SetZ(1);
-	if (control_input != Vec3::sZero())
-		control_input = control_input.Normalized();
+	// Draw state of character
+	DrawCharacterState(mCharacter, mCharacter->GetWorldTransform(), mCharacter->GetLinearVelocity());
+}
+void CharacterTest::PostPhysicsUpdate(float inDeltaTime)
+{
+	// Fetch the new ground properties
+	mCharacter->PostSimulation(cCollisionTolerance);
+}
 
+void CharacterTest::HandleInput(Vec3Arg inMovementDirection, bool inJump, bool inSwitchStance, float inDeltaTime)
+{
 	// Cancel movement in opposite direction of normal when sliding
+	Character::EGroundState ground_state = mCharacter->GetGroundState();
 	if (ground_state == Character::EGroundState::Sliding)
 	{
 		Vec3 normal = mCharacter->GetGroundNormal();
-		normal.SetY(0);
-		if (normal.Dot(control_input) <= 0.0f)
-			control_input = Vec3::sZero();
+		normal.SetY(0.0f);
+		float dot = normal.Dot(inMovementDirection);
+		if (dot < 0.0f)
+			inMovementDirection -= (dot * normal) / normal.LengthSq();
 	}
 
 	// Update velocity
-	const float cMaxSpeed = 6;
 	Vec3 current_velocity = mCharacter->GetLinearVelocity();
-	Vec3 desired_velocity = cMaxSpeed * control_input;
+	Vec3 desired_velocity = cCharacterSpeed * inMovementDirection;
 	desired_velocity.SetY(current_velocity.GetY());
 	Vec3 new_velocity = 0.75f * current_velocity + 0.25f * desired_velocity;
 
-	// Check actions
-	for (int key = inParams.mKeyboard->GetFirstKey(); key != 0; key = inParams.mKeyboard->GetNextKey())
-	{
-		if (key == DIK_RETURN)
-		{
-			// Stance switch
-			mCharacter->SetShape(mCharacter->GetShape() == mStandingShape? mCrouchingShape : mStandingShape, 1.5f * mPhysicsSystem->GetPhysicsSettings().mPenetrationSlop);
-			break;
-		}
-		else if (key == DIK_J)
-		{
-			// Jump
-			if (ground_state == Character::EGroundState::OnGround)
-				new_velocity += Vec3(0, cJumpSpeed, 0);
-		}
-	}
+	// Stance switch
+	if (inSwitchStance)
+		mCharacter->SetShape(mCharacter->GetShape() == mStandingShape? mCrouchingShape : mStandingShape, 1.5f * mPhysicsSystem->GetPhysicsSettings().mPenetrationSlop);
+
+	// Jump
+	if (inJump && ground_state == Character::EGroundState::OnGround)
+		new_velocity += Vec3(0, cJumpSpeed, 0);
 
 	// Update the velocity
 	mCharacter->SetLinearVelocity(new_velocity);
-
-	// Get properties
-	Vec3 position;
-	Quat rotation;
-	mCharacter->GetPositionAndRotation(position, rotation);
-
-	// Draw current location
-	// Drawing prior to update since the physics system state is also that prior to the simulation step (so that all detected collisions etc. make sense)
-	mDebugRenderer->DrawCoordinateSystem(Mat44::sRotationTranslation(rotation, position));
-
-	if (ground_state != Character::EGroundState::InAir)
-	{
-		Vec3 ground_position = mCharacter->GetGroundPosition();
-		Vec3 ground_normal = mCharacter->GetGroundNormal();
-		const PhysicsMaterial *ground_material = mCharacter->GetGroundMaterial();
-
-		// Draw ground position
-		mDebugRenderer->DrawWireSphere(ground_position, 0.1f, Color::sRed);
-		mDebugRenderer->DrawArrow(ground_position, ground_position + 2.0f * ground_normal, Color::sGreen, 0.1f);
-
-		// Draw ground material
-		mDebugRenderer->DrawText3D(ground_position, ground_material->GetDebugName());
-	}
-}
-
-void CharacterTest::PostPhysicsUpdate(float inDeltaTime)
-{
-	// Fetch the new ground properties
-	mCharacter->PostSimulation(cCollisionTolerance);
-}
-
-void CharacterTest::GetInitialCamera(CameraState &ioState) const 
-{
-	// Position camera behind character
-	Vec3 cam_tgt = Vec3(0, cCharacterHeightStanding, 0);
-	ioState.mPos = Vec3(0, 2.5f, 5);
-	ioState.mForward = (cam_tgt - ioState.mPos).Normalized();
-}
-
-Mat44 CharacterTest::GetCameraPivot(float inCameraHeading, float inCameraPitch) const 
-{
-	// Get properties
-	Vec3 position;
-	Quat rotation;
-	mCharacter->GetPositionAndRotation(position, rotation);
-	return Mat44::sRotationTranslation(rotation, position);
-}
-
-void CharacterTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
-{
-	inUI->CreateTextButton(inSubMenu, "Select Scene", [this, inUI]() { 
-		UIElement *scene_name = inUI->CreateMenu();
-		for (uint i = 0; i < size(sScenes); ++i)
-			inUI->CreateTextButton(scene_name, sScenes[i], [this, i]() { sSceneName = sScenes[i]; RestartTest(); });
-		inUI->ShowMenu(scene_name);
-	});
 }

+ 7 - 23
Samples/Tests/Character/CharacterTest.h

@@ -3,11 +3,11 @@
 
 #pragma once
 
-#include <Tests/Test.h>
+#include <Tests/Character/CharacterBaseTest.h>
 #include <Jolt/Physics/Character/Character.h>
 
 // Simple test that test the Character class. Allows the user to move around with the arrow keys and jump with the J button.
-class CharacterTest : public Test
+class CharacterTest : public CharacterBaseTest
 {
 public:
 	JPH_DECLARE_RTTI_VIRTUAL(CharacterTest)
@@ -15,9 +15,6 @@ public:
 	// Destructor
 	virtual					~CharacterTest() override;
 
-	// Number used to scale the terrain and camera movement to the scene
-	virtual float			GetWorldScale() const override								{ return 0.2f; }
-
 	// Initialize the test
 	virtual void			Initialize() override;
 
@@ -27,27 +24,14 @@ public:
 	// Update the test, called after the physics update
 	virtual void			PostPhysicsUpdate(float inDeltaTime) override;
 
-	// Override to specify the initial camera state (local to GetCameraPivot)
-	virtual void			GetInitialCamera(CameraState &ioState) const override;
-
-	// Override to specify a camera pivot point and orientation (world space)
-	virtual Mat44			GetCameraPivot(float inCameraHeading, float inCameraPitch) const override;
+protected:
+	// Get position of the character
+	virtual Vec3			GetCharacterPosition() const override				{ return mCharacter->GetPosition(); }
 
-	// Optional settings menu
-	virtual bool			HasSettingsMenu() const override							{ return true; }
-	virtual void			CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
+	// Handle user input to the character
+	virtual void			HandleInput(Vec3Arg inMovementDirection, bool inJump, bool inSwitchStance, float inDeltaTime) override;
 
 private:
-	// List of possible scene names
-	static const char *		sScenes[];
-
-	// Filename of animation to load for this test
-	static const char *		sSceneName;
-
 	// The 'player' character
 	Ref<Character>			mCharacter;
-
-	// The different stances for the character
-	RefConst<Shape>			mStandingShape;
-	RefConst<Shape>			mCrouchingShape;
 };

+ 144 - 0
Samples/Tests/Character/CharacterVirtualTest.cpp

@@ -0,0 +1,144 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/Character/CharacterVirtualTest.h>
+#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
+#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
+#include <Layers.h>
+#include <Renderer/DebugRendererImp.h>
+#include <Application/DebugUI.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(CharacterVirtualTest) 
+{ 
+	JPH_ADD_BASE_CLASS(CharacterVirtualTest, CharacterBaseTest)
+}
+
+void CharacterVirtualTest::Initialize()
+{
+	CharacterBaseTest::Initialize();
+
+	// Create 'player' character
+	Ref<CharacterVirtualSettings> settings = new CharacterVirtualSettings();
+	settings->mMaxSlopeAngle = sMaxSlopeAngle;
+	settings->mMaxStrength = sMaxStrength;
+	settings->mShape = mStandingShape;
+	settings->mCharacterPadding = sCharacterPadding;
+	settings->mPenetrationRecoverySpeed = sPenetrationRecoverySpeed;
+	settings->mPredictiveContactDistance = sPredictiveContactDistance;
+	mCharacter = new CharacterVirtual(settings, Vec3::sZero(), Quat::sIdentity(), mPhysicsSystem);
+	mCharacter->SetListener(this);
+}
+
+void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
+	CharacterBaseTest::PrePhysicsUpdate(inParams);
+
+	// Draw character pre update (the sim is also drawn pre update)
+	Mat44 com = mCharacter->GetCenterOfMassTransform();
+	if (mCharacter->GetShape() == mStandingShape)
+	{
+		mDebugRenderer->DrawCapsule(com, 0.5f * cCharacterHeightStanding, cCharacterRadiusStanding, Color::sGreen, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
+		mDebugRenderer->DrawCapsule(com, 0.5f * cCharacterHeightStanding, cCharacterRadiusStanding + mCharacter->GetCharacterPadding(), Color::sGrey, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
+	}
+	else
+	{
+		mDebugRenderer->DrawCapsule(com, 0.5f * cCharacterHeightCrouching, cCharacterRadiusCrouching, Color::sGreen, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
+		mDebugRenderer->DrawCapsule(com, 0.5f * cCharacterHeightCrouching, cCharacterRadiusCrouching + mCharacter->GetCharacterPadding(), Color::sGrey, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
+	}
+
+	// Remember old position
+	Vec3 old_position = mCharacter->GetPosition();
+
+	// Update the character position (instant, do not have to wait for physics update)
+	mCharacter->Update(inParams.mDeltaTime, mPhysicsSystem->GetGravity(), mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING), mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING), { }, *mTempAllocator);
+
+	// Calculate effective velocity
+	Vec3 new_position = mCharacter->GetPosition();
+	Vec3 velocity = (new_position - old_position) / inParams.mDeltaTime;
+
+	// Draw state of character
+	DrawCharacterState(mCharacter, mCharacter->GetWorldTransform(), velocity);
+
+	// Draw labels on ramp blocks
+	for (size_t i = 0; i < mRampBlocks.size(); ++i)
+		mDebugRenderer->DrawText3D(mBodyInterface->GetPosition(mRampBlocks[i]), StringFormat("PushesPlayer: %s\nPushable: %s", (i & 1) != 0? "True" : "False", (i & 2) != 0? "True" : "False"), Color::sWhite, 0.25f);
+}
+
+void CharacterVirtualTest::HandleInput(Vec3Arg inMovementDirection, bool inJump, bool inSwitchStance, float inDeltaTime)
+{
+	// Cancel movement in opposite direction of normal when sliding
+	CharacterVirtual::EGroundState ground_state = mCharacter->GetGroundState();
+	if (ground_state == CharacterVirtual::EGroundState::Sliding)
+	{
+		Vec3 normal = mCharacter->GetGroundNormal();
+		normal.SetY(0.0f);
+		float dot = normal.Dot(inMovementDirection);
+		if (dot < 0.0f)
+			inMovementDirection -= (dot * normal) / normal.LengthSq();
+	}
+
+	// Smooth the player input
+	mSmoothMovementDirection = 0.25f * inMovementDirection + 0.75f * mSmoothMovementDirection;
+
+	Vec3 current_vertical_velocity = Vec3(0, mCharacter->GetLinearVelocity().GetY(), 0);
+
+	Vec3 ground_velocity = mCharacter->GetGroundVelocity();
+
+	Vec3 new_velocity;
+	if (ground_state == CharacterVirtual::EGroundState::OnGround // If on ground
+		&& (current_vertical_velocity.GetY() - ground_velocity.GetY()) < 0.1f) // And not moving away from ground
+	{
+		// Assume velocity of ground when on ground
+		new_velocity = ground_velocity;
+		
+		// Jump
+		if (inJump)
+			new_velocity += Vec3(0, cJumpSpeed, 0);
+	}
+	else
+		new_velocity = current_vertical_velocity;
+
+	// Gravity
+	new_velocity += mPhysicsSystem->GetGravity() * inDeltaTime;
+
+	// Player input
+	new_velocity += mSmoothMovementDirection * cCharacterSpeed;
+
+	// Update the velocity
+	mCharacter->SetLinearVelocity(new_velocity);
+
+	// Stance switch
+	if (inSwitchStance)
+		mCharacter->SetShape(mCharacter->GetShape() == mStandingShape? mCrouchingShape : mStandingShape, 1.5f * mPhysicsSystem->GetPhysicsSettings().mPenetrationSlop, mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING), mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING), { }, *mTempAllocator);
+}
+
+void CharacterVirtualTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
+{
+	CharacterBaseTest::CreateSettingsMenu(inUI, inSubMenu);
+
+	inUI->CreateTextButton(inSubMenu, "Configuration Settings", [=]() {
+		UIElement *configuration_settings = inUI->CreateMenu();
+		
+		inUI->CreateSlider(configuration_settings, "Max Slope Angle (degrees)", RadiansToDegrees(sMaxSlopeAngle), 0.0f, 90.0f, 1.0f, [=](float inValue) { sMaxSlopeAngle = DegreesToRadians(inValue); });
+		inUI->CreateSlider(configuration_settings, "Max Strength (N)", sMaxStrength, 0.0f, 500.0f, 1.0f, [=](float inValue) { sMaxStrength = inValue; });
+		inUI->CreateSlider(configuration_settings, "Character Padding", sCharacterPadding, 0.01f, 0.5f, 0.01f, [=](float inValue) { sCharacterPadding = inValue; });
+		inUI->CreateSlider(configuration_settings, "Penetration Recovery Speed", sPenetrationRecoverySpeed, 0.0f, 1.0f, 0.05f, [=](float inValue) { sPenetrationRecoverySpeed = inValue; });
+		inUI->CreateSlider(configuration_settings, "Predictive Contact Distance", sPredictiveContactDistance, 0.01f, 1.0f, 0.01f, [=](float inValue) { sPredictiveContactDistance = inValue; });
+		inUI->CreateTextButton(configuration_settings, "Accept Changes", [=]() { RestartTest(); });
+		inUI->ShowMenu(configuration_settings);
+	});
+}
+
+void CharacterVirtualTest::OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, Vec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings)
+{
+	// Dynamic boxes on the ramp go through all permutations
+	vector<BodyID>::const_iterator i = find(mRampBlocks.begin(), mRampBlocks.end(), inBodyID2);
+	if (i != mRampBlocks.end())
+	{
+		size_t index = i - mRampBlocks.begin();
+		ioSettings.mCanPushCharacter = (index & 1) != 0;
+		ioSettings.mCanReceiveImpulses = (index & 2) != 0;
+	}
+}

+ 47 - 0
Samples/Tests/Character/CharacterVirtualTest.h

@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Character/CharacterBaseTest.h>
+#include <Jolt/Physics/Character/CharacterVirtual.h>
+
+// Simple test that test the CharacterVirtual class. Allows the user to move around with the arrow keys and jump with the J button.
+class CharacterVirtualTest : public CharacterBaseTest, public CharacterContactListener
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(CharacterVirtualTest)
+
+	// Initialize the test
+	virtual void			Initialize() override;
+
+	// Update the test, called before the physics update
+	virtual void			PrePhysicsUpdate(const PreUpdateParams &inParams) override;
+
+	// Optional settings menu
+	virtual void			CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
+
+	// Called whenever the character collides with a body. Returns true if the contact can push the character.
+	virtual void			OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, Vec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override;
+
+protected:
+	// Get position of the character
+	virtual Vec3			GetCharacterPosition() const override				{ return mCharacter->GetPosition(); }
+
+	// Handle user input to the character
+	virtual void			HandleInput(Vec3Arg inMovementDirection, bool inJump, bool inSwitchStance, float inDeltaTime) override;
+
+private:
+	// Test settings
+	static inline float		sMaxSlopeAngle = DegreesToRadians(45.0f);
+	static inline float		sMaxStrength = 100.0f;
+	static inline float		sCharacterPadding = 0.02f;
+	static inline float		sPenetrationRecoverySpeed = 1.0f;
+	static inline float		sPredictiveContactDistance = 0.1f;
+
+	// The 'player' character
+	Ref<CharacterVirtual>	mCharacter;
+
+	// Smoothed value of the player input
+	Vec3					mSmoothMovementDirection = Vec3::sZero();
+};

+ 4 - 0
Samples/Tests/Test.h

@@ -35,6 +35,9 @@ public:
 	// Set the debug renderer
 	void			SetDebugRenderer(DebugRenderer *inDebugRenderer)			{ mDebugRenderer = inDebugRenderer; }
 
+	// Set the temp allocator
+	void			SetTempAllocator(TempAllocator *inTempAllocator)			{ mTempAllocator = inTempAllocator; }
+
 	// Initialize the test
 	virtual void	Initialize()												{ }
 
@@ -97,6 +100,7 @@ protected:
 	PhysicsSystem *	mPhysicsSystem = nullptr;
 	BodyInterface *	mBodyInterface = nullptr;
 	DebugRenderer *	mDebugRenderer = nullptr;
+	TempAllocator *	mTempAllocator = nullptr;
 
 private:
 	bool			mNeedsRestart = false;