Browse Source

CharacterVirtual can now have an inner rigid body (#1208)

This allows it to interact with sensors through the regular ContactListener and to be found by normal collision checks.
Jorrit Rouwe 1 year ago
parent
commit
d7e8f922d0

+ 8 - 2
Docs/Architecture.md

@@ -521,6 +521,8 @@ The [Character](@ref Character) and [CharacterVirtual](@ref CharacterVirtual) cl
 
 
 The Character class is the simplest controller and is essentially a rigid body that has been configured to only allow translation (and no rotation so it stays upright). It is simulated together with the other rigid bodies so it properly reacts to them. Because it is simulated, it is usually not the best solution for a player as the player usually requires a lot of behavior that is non-physical. This character controller is cheap so it is recommended for e.g. simple AI characters. After every PhysicsSystem::Update call you must call Character::PostSimulation to update the ground contacts.
 The Character class is the simplest controller and is essentially a rigid body that has been configured to only allow translation (and no rotation so it stays upright). It is simulated together with the other rigid bodies so it properly reacts to them. Because it is simulated, it is usually not the best solution for a player as the player usually requires a lot of behavior that is non-physical. This character controller is cheap so it is recommended for e.g. simple AI characters. After every PhysicsSystem::Update call you must call Character::PostSimulation to update the ground contacts.
 
 
+Characters are usually driven in a kinematic way (i.e. by calling Character::SetLinearVelocity or CharacterVirtual::SetLinearVelocity before their update).
+
 The CharacterVirtual class is much more advanced. It is implemented using collision detection functionality only (through NarrowPhaseQuery) and is simulated when CharacterVirtual::Update is called. Since the character is not 'added' to the world, it is not visible to rigid bodies and it only interacts with them during the CharacterVirtual::Update function by applying impulses. This does mean there can be some update order artifacts, like the character slightly hovering above an elevator going down, because the characters moves at a different time than the other rigid bodies. Separating it has the benefit that the update can happen at the appropriate moment in the game code. Multiple CharacterVirtuals can update concurrently, so it is not an issue if the game code is parallelized.
 The CharacterVirtual class is much more advanced. It is implemented using collision detection functionality only (through NarrowPhaseQuery) and is simulated when CharacterVirtual::Update is called. Since the character is not 'added' to the world, it is not visible to rigid bodies and it only interacts with them during the CharacterVirtual::Update function by applying impulses. This does mean there can be some update order artifacts, like the character slightly hovering above an elevator going down, because the characters moves at a different time than the other rigid bodies. Separating it has the benefit that the update can happen at the appropriate moment in the game code. Multiple CharacterVirtuals can update concurrently, so it is not an issue if the game code is parallelized.
 
 
 CharacterVirtual has the following extra functionality:
 CharacterVirtual has the following extra functionality:
@@ -531,9 +533,13 @@ CharacterVirtual has the following extra functionality:
 * Sticking to the ground when walking down a slope through the CharacterVirtual::ExtendedUpdate call
 * Sticking to the ground when walking down a slope through the CharacterVirtual::ExtendedUpdate call
 * Support for specifying a local coordinate system that allows e.g. [walking around in a flying space ship](https://github.com/jrouwe/JoltPhysics/blob/master/Samples/Tests/Character/CharacterSpaceShipTest.cpp) that is equipped with 'inertial dampers' (a sci-fi concept often used in games).
 * Support for specifying a local coordinate system that allows e.g. [walking around in a flying space ship](https://github.com/jrouwe/JoltPhysics/blob/master/Samples/Tests/Character/CharacterSpaceShipTest.cpp) that is equipped with 'inertial dampers' (a sci-fi concept often used in games).
 
 
-If you want CharacterVirtual to have presence in the world, it is recommended to pair it with a slightly smaller [Kinematic](@ref EMotionType) body (or Character). After each update, move this body using BodyInterface::MoveKinematic to the new location. This ensures that standard collision tests like ray casts are able to find the character in the world and that fast moving objects with motion quality [LinearCast](@ref EMotionQuality) will not pass through the character in 1 update. As an alternative to a Kinematic body, you can also use a regular Dynamic body with a [gravity factor](@ref BodyCreationSettings::mGravityFactor) of 0. Ensure that the character only collides with dynamic objects in this case. The advantage of this approach is that the paired body doesn't have infinite mass so is less strong.
+CharacterVirtual should provide everything that Character provides. Since it is not a rigid body, it requires some extra consideration:
+* Collision callbacks are passed through the CharacterContactListener instead of the ContactListener class
+* CharacterVirtual vs sensor contacts are also passed through this listener, you will not receive them through the regular ContactListener
+* CharacterVirtual vs CharacterVirtual collisions can be handled through the CharacterVsCharacterCollision interface
+* Collision checks (e.g. CastRay) do not collide with CharacterVirtual. Use e.g. `NarrowPhaseQuery::CastRay(..., collector)` followed by `CharacterVirtual::GetTransformedShape().CastRay(..., collector)` to include the collision results.
 
 
-Characters are usually driven in a kinematic way (i.e. by calling Character::SetLinearVelocity or CharacterVirtual::SetLinearVelocity before their update).
+You can create a hybrid between these two by setting CharacterVirtualSettings::mInnerBodyShape. This will create an inner rigid body that follows the movement of the CharacterVirtual. This inner rigid body will be detected by sensors and regular collision tests.
 
 
 To get started take a look at the [Character](https://github.com/jrouwe/JoltPhysics/blob/master/Samples/Tests/Character/CharacterTest.cpp) and [CharacterVirtual](https://github.com/jrouwe/JoltPhysics/blob/master/Samples/Tests/Character/CharacterVirtualTest.cpp) examples.
 To get started take a look at the [Character](https://github.com/jrouwe/JoltPhysics/blob/master/Samples/Tests/Character/CharacterTest.cpp) and [CharacterVirtual](https://github.com/jrouwe/JoltPhysics/blob/master/Samples/Tests/Character/CharacterVirtualTest.cpp) examples.
 
 

+ 1 - 0
Docs/ReleaseNotes.md

@@ -26,6 +26,7 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 
 
 * Added CharacterBaseSettings::mEnhancedInternalEdgeRemoval (default false) that allows smoother movement for both the Character and CharacterVirtual class.
 * Added CharacterBaseSettings::mEnhancedInternalEdgeRemoval (default false) that allows smoother movement for both the Character and CharacterVirtual class.
 * Added ability for a CharacterVirtual to collide with another CharacterVirtual by using the new CharacterVsCharacterCollision interface.
 * Added ability for a CharacterVirtual to collide with another CharacterVirtual by using the new CharacterVsCharacterCollision interface.
+* Added the option to add an inner rigid body to a CharacterVirtual. This allows it to interact with sensors through the regular ContactListener and to be found by normal collision checks.
 
 
 #### Vehicles
 #### Vehicles
 
 

+ 28 - 0
Jolt/Physics/Body/BodyFilter.h

@@ -83,6 +83,34 @@ private:
 	Array<BodyID>			mBodyIDs;
 	Array<BodyID>			mBodyIDs;
 };
 };
 
 
+/// Ignores a single body and chains the filter to another filter
+class JPH_EXPORT IgnoreSingleBodyFilterChained : public BodyFilter
+{
+public:
+	/// Constructor
+	explicit				IgnoreSingleBodyFilterChained(const BodyID inBodyID, const BodyFilter &inFilter) :
+		mBodyID(inBodyID),
+		mFilter(inFilter)
+	{
+	}
+
+	/// Filter function. Returns true if we should collide with inBodyID
+	virtual bool			ShouldCollide(const BodyID &inBodyID) const override
+	{
+		return inBodyID != mBodyID && mFilter.ShouldCollide(inBodyID);
+	}
+
+	/// Filter function. Returns true if we should collide with inBody (this is called after the body is locked and makes it possible to filter based on body members)
+	virtual bool			ShouldCollideLocked(const Body &inBody) const override
+	{
+		return mFilter.ShouldCollideLocked(inBody);
+	}
+
+private:
+	BodyID					mBodyID;
+	const BodyFilter &		mFilter;
+};
+
 #ifdef JPH_DEBUG_RENDERER
 #ifdef JPH_DEBUG_RENDERER
 /// Class function to filter out bodies for debug rendering, returns true if body should be rendered
 /// Class function to filter out bodies for debug rendering, returns true if body should be rendered
 class JPH_EXPORT BodyDrawFilter : public NonCopyable
 class JPH_EXPORT BodyDrawFilter : public NonCopyable

+ 5 - 0
Jolt/Physics/Character/Character.cpp

@@ -315,4 +315,9 @@ bool Character::SetShape(const Shape *inShape, float inMaxPenetrationDepth, bool
 	return true;
 	return true;
 }
 }
 
 
+TransformedShape Character::GetTransformedShape(bool inLockBodies) const
+{
+	return sGetBodyInterface(mSystem, inLockBodies).GetTransformedShape(mBodyID);
+}
+
 JPH_NAMESPACE_END
 JPH_NAMESPACE_END

+ 4 - 0
Jolt/Physics/Character/Character.h

@@ -6,6 +6,7 @@
 
 
 #include <Jolt/Physics/Character/CharacterBase.h>
 #include <Jolt/Physics/Character/CharacterBase.h>
 #include <Jolt/Physics/Collision/ObjectLayer.h>
 #include <Jolt/Physics/Collision/ObjectLayer.h>
+#include <Jolt/Physics/Collision/TransformedShape.h>
 #include <Jolt/Physics/EActivation.h>
 #include <Jolt/Physics/EActivation.h>
 
 
 JPH_NAMESPACE_BEGIN
 JPH_NAMESPACE_BEGIN
@@ -115,6 +116,9 @@ public:
 	/// if the new shape collides before switching shape. Returns true if the switch succeeded.
 	/// if the new shape collides before switching shape. Returns true if the switch succeeded.
 	bool								SetShape(const Shape *inShape, float inMaxPenetrationDepth, bool inLockBodies = true);
 	bool								SetShape(const Shape *inShape, float inMaxPenetrationDepth, bool inLockBodies = true);
 
 
+	/// Get the transformed shape that represents the volume of the character, can be used for collision checks.
+	TransformedShape					GetTransformedShape(bool inLockBodies = true) const;
+
 	/// @brief Get all contacts for the character at a particular location
 	/// @brief Get all contacts for the character at a particular location
 	/// @param inPosition Position to test.
 	/// @param inPosition Position to test.
 	/// @param inRotation Rotation at which to test the shape.
 	/// @param inRotation Rotation at which to test the shape.

+ 57 - 4
Jolt/Physics/Character/CharacterVirtual.cpp

@@ -6,6 +6,7 @@
 
 
 #include <Jolt/Physics/Character/CharacterVirtual.h>
 #include <Jolt/Physics/Character/CharacterVirtual.h>
 #include <Jolt/Physics/Body/Body.h>
 #include <Jolt/Physics/Body/Body.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
 #include <Jolt/Physics/PhysicsSystem.h>
 #include <Jolt/Physics/PhysicsSystem.h>
 #include <Jolt/Physics/Collision/ShapeCast.h>
 #include <Jolt/Physics/Collision/ShapeCast.h>
 #include <Jolt/Physics/Collision/CollideShape.h>
 #include <Jolt/Physics/Collision/CollideShape.h>
@@ -103,6 +104,30 @@ CharacterVirtual::CharacterVirtual(const CharacterVirtualSettings *inSettings, R
 	// Copy settings
 	// Copy settings
 	SetMaxStrength(inSettings->mMaxStrength);
 	SetMaxStrength(inSettings->mMaxStrength);
 	SetMass(inSettings->mMass);
 	SetMass(inSettings->mMass);
+
+	// Create an inner rigid body if requested
+	if (inSettings->mInnerBodyShape != nullptr)
+	{
+		BodyCreationSettings settings(inSettings->mInnerBodyShape, GetInnerBodyPosition(), mRotation, EMotionType::Kinematic, inSettings->mInnerBodyLayer);
+		settings.mAllowSleeping = false; // Disable sleeping so that we will receive sensor callbacks
+		settings.mUserData = inUserData;
+		mInnerBodyID = inSystem->GetBodyInterface().CreateAndAddBody(settings, EActivation::Activate);
+	}
+}
+
+CharacterVirtual::~CharacterVirtual()
+{
+	if (!mInnerBodyID.IsInvalid())
+	{
+		mSystem->GetBodyInterface().RemoveBody(mInnerBodyID);
+		mSystem->GetBodyInterface().DestroyBody(mInnerBodyID);
+	}
+}
+
+void CharacterVirtual::UpdateInnerBodyTransform()
+{
+	if (!mInnerBodyID.IsInvalid())
+		mSystem->GetBodyInterface().SetPositionAndRotation(mInnerBodyID, GetInnerBodyPosition(), mRotation, EActivation::DontActivate);
 }
 }
 
 
 void CharacterVirtual::GetAdjustedBodyVelocity(const Body& inBody, Vec3 &outLinearVelocity, Vec3 &outAngularVelocity) const
 void CharacterVirtual::GetAdjustedBodyVelocity(const Body& inBody, Vec3 &outLinearVelocity, Vec3 &outAngularVelocity) const
@@ -321,6 +346,9 @@ void CharacterVirtual::CheckCollision(RVec3Arg inPosition, QuatArg inRotation, V
 	settings.mActiveEdgeMovementDirection = inMovementDirection;
 	settings.mActiveEdgeMovementDirection = inMovementDirection;
 	settings.mMaxSeparationDistance = mCharacterPadding + inMaxSeparationDistance;
 	settings.mMaxSeparationDistance = mCharacterPadding + inMaxSeparationDistance;
 
 
+	// Body filter
+	IgnoreSingleBodyFilterChained body_filter(mInnerBodyID, inBodyFilter);
+
 	// Collide shape
 	// Collide shape
 	if (mEnhancedInternalEdgeRemoval)
 	if (mEnhancedInternalEdgeRemoval)
 	{
 	{
@@ -385,7 +413,7 @@ void CharacterVirtual::CheckCollision(RVec3Arg inPosition, QuatArg inRotation, V
 		bounds.ExpandBy(Vec3::sReplicate(settings.mMaxSeparationDistance));
 		bounds.ExpandBy(Vec3::sReplicate(settings.mMaxSeparationDistance));
 
 
 		// Do broadphase test
 		// Do broadphase test
-		MyCollector collector(inShape, transform, settings, inBaseOffset, ioCollector, mSystem->GetBodyLockInterface(), inBodyFilter, inShapeFilter);
+		MyCollector collector(inShape, transform, settings, inBaseOffset, ioCollector, mSystem->GetBodyLockInterface(), body_filter, inShapeFilter);
 		mSystem->GetBroadPhaseQuery().CollideAABox(bounds, collector, inBroadPhaseLayerFilter, inObjectLayerFilter);
 		mSystem->GetBroadPhaseQuery().CollideAABox(bounds, collector, inBroadPhaseLayerFilter, inObjectLayerFilter);
 	}
 	}
 	else
 	else
@@ -393,7 +421,7 @@ void CharacterVirtual::CheckCollision(RVec3Arg inPosition, QuatArg inRotation, V
 		// Version that uses the cached active edges
 		// Version that uses the cached active edges
 		settings.mActiveEdgeMode = EActiveEdgeMode::CollideOnlyWithActive;
 		settings.mActiveEdgeMode = EActiveEdgeMode::CollideOnlyWithActive;
 
 
-		mSystem->GetNarrowPhaseQuery().CollideShape(inShape, Vec3::sReplicate(1.0f), transform, settings, inBaseOffset, ioCollector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inShapeFilter);
+		mSystem->GetNarrowPhaseQuery().CollideShape(inShape, Vec3::sReplicate(1.0f), transform, settings, inBaseOffset, ioCollector, inBroadPhaseLayerFilter, inObjectLayerFilter, body_filter, inShapeFilter);
 	}
 	}
 
 
 	// Also collide with other characters
 	// Also collide with other characters
@@ -409,9 +437,12 @@ void CharacterVirtual::GetContactsAtPosition(RVec3Arg inPosition, Vec3Arg inMove
 	// Remove previous results
 	// Remove previous results
 	outContacts.clear();
 	outContacts.clear();
 
 
+	// Body filter
+	IgnoreSingleBodyFilterChained body_filter(mInnerBodyID, inBodyFilter);
+
 	// Collide shape
 	// Collide shape
 	ContactCollector collector(mSystem, this, mMaxNumHits, mHitReductionCosMaxAngle, mUp, mPosition, outContacts);
 	ContactCollector collector(mSystem, this, mMaxNumHits, mHitReductionCosMaxAngle, mUp, mPosition, outContacts);
-	CheckCollision(inPosition, mRotation, inMovementDirection, mPredictiveContactDistance, inShape, mPosition, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inShapeFilter);
+	CheckCollision(inPosition, mRotation, inMovementDirection, mPredictiveContactDistance, inShape, mPosition, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, body_filter, inShapeFilter);
 
 
 	// The broadphase bounding boxes will not be deterministic, which means that the order in which the contacts are received by the collector is not deterministic.
 	// The broadphase bounding boxes will not be deterministic, which means that the order in which the contacts are received by the collector is not deterministic.
 	// Therefore we need to sort the contacts to preserve determinism. Note that currently this will fail if we exceed mMaxNumHits hits.
 	// Therefore we need to sort the contacts to preserve determinism. Note that currently this will fail if we exceed mMaxNumHits hits.
@@ -539,6 +570,9 @@ bool CharacterVirtual::GetFirstContactForSweep(RVec3Arg inPosition, Vec3Arg inDi
 	// Calculate how much extra fraction we need to add to the cast to account for the character padding
 	// Calculate how much extra fraction we need to add to the cast to account for the character padding
 	float character_padding_fraction = mCharacterPadding / sqrt(displacement_len_sq);
 	float character_padding_fraction = mCharacterPadding / sqrt(displacement_len_sq);
 
 
+	// Body filter
+	IgnoreSingleBodyFilterChained body_filter(mInnerBodyID, inBodyFilter);
+
 	// Cast shape
 	// Cast shape
 	Contact contact;
 	Contact contact;
 	contact.mFraction = 1.0f + character_padding_fraction;
 	contact.mFraction = 1.0f + character_padding_fraction;
@@ -546,7 +580,7 @@ bool CharacterVirtual::GetFirstContactForSweep(RVec3Arg inPosition, Vec3Arg inDi
 	ContactCastCollector collector(mSystem, this, inDisplacement, mUp, inIgnoredContacts, base_offset, contact);
 	ContactCastCollector collector(mSystem, this, inDisplacement, mUp, inIgnoredContacts, base_offset, contact);
 	collector.ResetEarlyOutFraction(contact.mFraction);
 	collector.ResetEarlyOutFraction(contact.mFraction);
 	RShapeCast shape_cast(mShape, Vec3::sReplicate(1.0f), start, inDisplacement);
 	RShapeCast shape_cast(mShape, Vec3::sReplicate(1.0f), start, inDisplacement);
-	mSystem->GetNarrowPhaseQuery().CastShape(shape_cast, settings, base_offset, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, inBodyFilter, inShapeFilter);
+	mSystem->GetNarrowPhaseQuery().CastShape(shape_cast, settings, base_offset, collector, inBroadPhaseLayerFilter, inObjectLayerFilter, body_filter, inShapeFilter);
 
 
 	// Also collide with other characters
 	// Also collide with other characters
 	if (mCharacterVsCharacterCollision != nullptr)
 	if (mCharacterVsCharacterCollision != nullptr)
@@ -1246,6 +1280,14 @@ void CharacterVirtual::MoveShape(RVec3 &ioPosition, Vec3Arg inVelocity, float in
 	}
 	}
 }
 }
 
 
+void CharacterVirtual::SetUserData(uint64 inUserData)
+{
+	mUserData = inUserData;
+
+	if (!mInnerBodyID.IsInvalid())
+		mSystem->GetBodyInterface().SetUserData(mInnerBodyID, inUserData);
+}
+
 Vec3 CharacterVirtual::CancelVelocityTowardsSteepSlopes(Vec3Arg inDesiredVelocity) const
 Vec3 CharacterVirtual::CancelVelocityTowardsSteepSlopes(Vec3Arg inDesiredVelocity) const
 {
 {
 	// If we're not pushing against a steep slope, return the desired velocity
 	// If we're not pushing against a steep slope, return the desired velocity
@@ -1292,6 +1334,9 @@ void CharacterVirtual::Update(float inDeltaTime, Vec3Arg inGravity, const BroadP
 	// Determine the object that we're standing on
 	// Determine the object that we're standing on
 	UpdateSupportingContact(false, inAllocator);
 	UpdateSupportingContact(false, inAllocator);
 
 
+	// Ensure that the rigid body ends up at the new position
+	UpdateInnerBodyTransform();
+
 	// If we're on the ground
 	// If we're on the ground
 	if (!mGroundBodyID.IsInvalid() && mMass > 0.0f)
 	if (!mGroundBodyID.IsInvalid() && mMass > 0.0f)
 	{
 	{
@@ -1364,6 +1409,9 @@ void CharacterVirtual::MoveToContact(RVec3Arg inPosition, const Contact &inConta
 
 
 	StoreActiveContacts(contacts, inAllocator);
 	StoreActiveContacts(contacts, inAllocator);
 	JPH_ASSERT(mGroundState != EGroundState::InAir);
 	JPH_ASSERT(mGroundState != EGroundState::InAir);
+
+	// Ensure that the rigid body ends up at the new position
+	UpdateInnerBodyTransform();
 }
 }
 
 
 bool CharacterVirtual::SetShape(const Shape *inShape, float inMaxPenetrationDepth, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter, TempAllocator &inAllocator)
 bool CharacterVirtual::SetShape(const Shape *inShape, float inMaxPenetrationDepth, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter, TempAllocator &inAllocator)
@@ -1400,6 +1448,11 @@ bool CharacterVirtual::SetShape(const Shape *inShape, float inMaxPenetrationDept
 	return mShape == inShape;
 	return mShape == inShape;
 }
 }
 
 
+void CharacterVirtual::SetInnerBodyShape(const Shape *inShape)
+{
+	mSystem->GetBodyInterface().SetShape(mInnerBodyID, inShape, false, EActivation::DontActivate);
+}
+
 bool CharacterVirtual::CanWalkStairs(Vec3Arg inLinearVelocity) const
 bool CharacterVirtual::CanWalkStairs(Vec3Arg inLinearVelocity) const
 {
 {
 	// We can only walk stairs if we're supported
 	// We can only walk stairs if we're supported

+ 45 - 8
Jolt/Physics/Character/CharacterVirtual.h

@@ -9,6 +9,7 @@
 #include <Jolt/Physics/Body/BodyFilter.h>
 #include <Jolt/Physics/Body/BodyFilter.h>
 #include <Jolt/Physics/Collision/BroadPhase/BroadPhaseLayer.h>
 #include <Jolt/Physics/Collision/BroadPhase/BroadPhaseLayer.h>
 #include <Jolt/Physics/Collision/ObjectLayer.h>
 #include <Jolt/Physics/Collision/ObjectLayer.h>
+#include <Jolt/Physics/Collision/TransformedShape.h>
 #include <Jolt/Core/STLTempAllocator.h>
 #include <Jolt/Core/STLTempAllocator.h>
 
 
 JPH_NAMESPACE_BEGIN
 JPH_NAMESPACE_BEGIN
@@ -42,6 +43,15 @@ public:
 	uint								mMaxNumHits = 256;										///< Max num hits to collect in order to avoid excess of contact points collection
 	uint								mMaxNumHits = 256;										///< Max num hits to collect in order to avoid excess of contact points collection
 	float								mHitReductionCosMaxAngle = 0.999f;						///< Cos(angle) where angle is the maximum angle between two hits contact normals that are allowed to be merged during hit reduction. Default is around 2.5 degrees. Set to -1 to turn off.
 	float								mHitReductionCosMaxAngle = 0.999f;						///< Cos(angle) where angle is the maximum angle between two hits contact normals that are allowed to be merged during hit reduction. Default is around 2.5 degrees. Set to -1 to turn off.
 	float								mPenetrationRecoverySpeed = 1.0f;						///< This value governs how fast a penetration will be resolved, 0 = nothing is resolved, 1 = everything in one update
 	float								mPenetrationRecoverySpeed = 1.0f;						///< This value governs how fast a penetration will be resolved, 0 = nothing is resolved, 1 = everything in one update
+
+	/// This character can optionally have an inner rigid body. This rigid body can be used to give the character presence in the world. When set it means that:
+	/// - Regular collision checks (e.g. NarrowPhaseQuery::CastRay) will collide with the rigid body (they cannot collide with CharacterVirtual since it is not added to the broad phase)
+	/// - Regular contact callbacks will be called through the ContactListener (next to the ones that will be passed to the CharacterContactListener)
+	/// - Fast moving objects of motion quality LinearCast will not be able to pass through the CharacterVirtual in 1 time step
+	RefConst<Shape>						mInnerBodyShape;
+
+	/// Layer that the inner rigid body will be added to
+	ObjectLayer							mInnerBodyLayer = 0;
 };
 };
 
 
 /// This class contains settings that allow you to override the behavior of a character's collision response
 /// This class contains settings that allow you to override the behavior of a character's collision response
@@ -161,21 +171,24 @@ public:
 	/// @param inPosition Initial position for the character
 	/// @param inPosition Initial position for the character
 	/// @param inRotation Initial rotation for the character (usually only around the up-axis)
 	/// @param inRotation Initial rotation for the character (usually only around the up-axis)
 	/// @param inUserData Application specific value
 	/// @param inUserData Application specific value
-	/// @param inSystem Physics system that this character will be added to later
+	/// @param inSystem Physics system that this character will be added to
 										CharacterVirtual(const CharacterVirtualSettings *inSettings, RVec3Arg inPosition, QuatArg inRotation, uint64 inUserData, PhysicsSystem *inSystem);
 										CharacterVirtual(const CharacterVirtualSettings *inSettings, RVec3Arg inPosition, QuatArg inRotation, uint64 inUserData, PhysicsSystem *inSystem);
 
 
 	/// Constructor without user data
 	/// Constructor without user data
 										CharacterVirtual(const CharacterVirtualSettings *inSettings, RVec3Arg inPosition, QuatArg inRotation, PhysicsSystem *inSystem) : CharacterVirtual(inSettings, inPosition, inRotation, 0, inSystem) { }
 										CharacterVirtual(const CharacterVirtualSettings *inSettings, RVec3Arg inPosition, QuatArg inRotation, PhysicsSystem *inSystem) : CharacterVirtual(inSettings, inPosition, inRotation, 0, inSystem) { }
 
 
+	/// Destructor
+	virtual								~CharacterVirtual() override;
+
 	/// Set the contact listener
 	/// Set the contact listener
 	void								SetListener(CharacterContactListener *inListener)		{ mListener = inListener; }
 	void								SetListener(CharacterContactListener *inListener)		{ mListener = inListener; }
 
 
-	/// Set the character vs character collision interface
-	void								SetCharacterVsCharacterCollision(CharacterVsCharacterCollision *inCharacterVsCharacterCollision) { mCharacterVsCharacterCollision = inCharacterVsCharacterCollision; }
-
 	/// Get the current contact listener
 	/// Get the current contact listener
 	CharacterContactListener *			GetListener() const										{ return mListener; }
 	CharacterContactListener *			GetListener() const										{ return mListener; }
 
 
+	/// Set the character vs character collision interface
+	void								SetCharacterVsCharacterCollision(CharacterVsCharacterCollision *inCharacterVsCharacterCollision) { mCharacterVsCharacterCollision = inCharacterVsCharacterCollision; }
+
 	/// Get the linear velocity of the character (m / s)
 	/// Get the linear velocity of the character (m / s)
 	Vec3								GetLinearVelocity() const								{ return mLinearVelocity; }
 	Vec3								GetLinearVelocity() const								{ return mLinearVelocity; }
 
 
@@ -186,13 +199,16 @@ public:
 	RVec3								GetPosition() const										{ return mPosition; }
 	RVec3								GetPosition() const										{ return mPosition; }
 
 
 	/// Set the position of the character
 	/// Set the position of the character
-	void								SetPosition(RVec3Arg inPosition)						{ mPosition = inPosition; }
+	void								SetPosition(RVec3Arg inPosition)						{ mPosition = inPosition; UpdateInnerBodyTransform(); }
 
 
 	/// Get the rotation of the character
 	/// Get the rotation of the character
 	Quat								GetRotation() const										{ return mRotation; }
 	Quat								GetRotation() const										{ return mRotation; }
 
 
 	/// Set the rotation of the character
 	/// Set the rotation of the character
-	void								SetRotation(QuatArg inRotation)							{ mRotation = inRotation; }
+	void								SetRotation(QuatArg inRotation)							{ mRotation = inRotation; UpdateInnerBodyTransform(); }
+
+	// Get the center of mass position of the shape
+	inline RVec3						GetCenterOfMassPosition() const							{ return mPosition + (mRotation * (mShapeOffset + mShape->GetCenterOfMass()) + mCharacterPadding * mUp); }
 
 
 	/// Calculate the world transform of the character
 	/// Calculate the world transform of the character
 	RMat44								GetWorldTransform() const								{ return RMat44::sRotationTranslation(mRotation, mPosition); }
 	RMat44								GetWorldTransform() const								{ return RMat44::sRotationTranslation(mRotation, mPosition); }
@@ -235,11 +251,14 @@ public:
 
 
 	/// An extra offset applied to the shape in local space. This allows applying an extra offset to the shape in local space. Note that setting it on the fly can cause the shape to teleport into collision.
 	/// An extra offset applied to the shape in local space. This allows applying an extra offset to the shape in local space. Note that setting it on the fly can cause the shape to teleport into collision.
 	Vec3								GetShapeOffset() const									{ return mShapeOffset; }
 	Vec3								GetShapeOffset() const									{ return mShapeOffset; }
-	void								SetShapeOffset(Vec3Arg inShapeOffset)					{ mShapeOffset = inShapeOffset; }
+	void								SetShapeOffset(Vec3Arg inShapeOffset)					{ mShapeOffset = inShapeOffset; UpdateInnerBodyTransform(); }
 
 
 	/// Access to the user data, can be used for anything by the application
 	/// Access to the user data, can be used for anything by the application
 	uint64								GetUserData() const										{ return mUserData; }
 	uint64								GetUserData() const										{ return mUserData; }
-	void								SetUserData(uint64 inUserData)							{ mUserData = inUserData; }
+	void								SetUserData(uint64 inUserData);
+
+	/// Optional inner rigid body that proxies the character in the world. Can be used to update body properties.
+	BodyID								GetInnerBodyID() const									{ return mInnerBodyID; }
 
 
 	/// This function can be called prior to calling Update() to convert a desired velocity into a velocity that won't make the character move further onto steep slopes.
 	/// This function can be called prior to calling Update() to convert a desired velocity into a velocity that won't make the character move further onto steep slopes.
 	/// This velocity can then be set on the character using SetLinearVelocity()
 	/// This velocity can then be set on the character using SetLinearVelocity()
@@ -333,6 +352,12 @@ public:
 	/// @return Returns true if the switch succeeded.
 	/// @return Returns true if the switch succeeded.
 	bool								SetShape(const Shape *inShape, float inMaxPenetrationDepth, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter, TempAllocator &inAllocator);
 	bool								SetShape(const Shape *inShape, float inMaxPenetrationDepth, const BroadPhaseLayerFilter &inBroadPhaseLayerFilter, const ObjectLayerFilter &inObjectLayerFilter, const BodyFilter &inBodyFilter, const ShapeFilter &inShapeFilter, TempAllocator &inAllocator);
 
 
+	/// Updates the shape of the inner rigid body. Should be called after a successful call to SetShape.
+	void								SetInnerBodyShape(const Shape *inShape);
+
+	/// Get the transformed shape that represents the volume of the character, can be used for collision checks.
+	TransformedShape					GetTransformedShape() const								{ return TransformedShape(GetCenterOfMassPosition(), mRotation, mShape, mInnerBodyID); }
+
 	/// @brief Get all contacts for the character at a particular location.
 	/// @brief Get all contacts for the character at a particular location.
 	/// When colliding with another character virtual, this pointer will be provided through CollideShapeCollector::SetUserContext before adding a hit.
 	/// When colliding with another character virtual, this pointer will be provided through CollideShapeCollector::SetUserContext before adding a hit.
 	/// @param inPosition Position to test, note that this position will be corrected for the character padding.
 	/// @param inPosition Position to test, note that this position will be corrected for the character padding.
@@ -552,6 +577,15 @@ private:
 		return RMat44::sRotationTranslation(inRotation, inPosition).PreTranslated(mShapeOffset + inShape->GetCenterOfMass()).PostTranslated(mCharacterPadding * mUp);
 		return RMat44::sRotationTranslation(inRotation, inPosition).PreTranslated(mShapeOffset + inShape->GetCenterOfMass()).PostTranslated(mCharacterPadding * mUp);
 	}
 	}
 
 
+	// This function returns the position of the inner rigid body
+	inline RVec3						GetInnerBodyPosition() const
+	{
+		return mPosition + (mRotation * mShapeOffset + mCharacterPadding * mUp);
+	}
+
+	// Move the inner rigid body to the current position
+	void								UpdateInnerBodyTransform();
+
 	// Our main listener for contacts
 	// Our main listener for contacts
 	CharacterContactListener *			mListener = nullptr;
 	CharacterContactListener *			mListener = nullptr;
 
 
@@ -600,6 +634,9 @@ private:
 
 
 	// User data, can be used for anything by the application
 	// User data, can be used for anything by the application
 	uint64								mUserData = 0;
 	uint64								mUserData = 0;
+
+	// The inner rigid body that proxies the character in the world
+	BodyID								mInnerBodyID;
 };
 };
 
 
 JPH_NAMESPACE_END
 JPH_NAMESPACE_END

+ 34 - 11
Samples/Tests/Character/CharacterBaseTest.cpp

@@ -82,8 +82,9 @@ static const int cMeshWallSegments = 25;
 static const RVec3 cHalfCylinderPosition(5.0f, 0, 8.0f);
 static const RVec3 cHalfCylinderPosition(5.0f, 0, 8.0f);
 static const RVec3 cMeshBoxPosition(30.0f, 1.5f, 5.0f);
 static const RVec3 cMeshBoxPosition(30.0f, 1.5f, 5.0f);
 static const RVec3 cSensorPosition(30, 0.9f, -5);
 static const RVec3 cSensorPosition(30, 0.9f, -5);
-static const RVec3 cCharacterPosition(-4.0f, 0, 3.0f);
-static const RVec3 cCharacterVirtualPosition(-6.0f, 0, 3.0f);
+static const RVec3 cCharacterPosition(-3.5f, 0, 3.0f);
+static const RVec3 cCharacterVirtualPosition(-5.0f, 0, 3.0f);
+static const RVec3 cCharacterVirtualWithInnerBodyPosition(-6.5f, 0, 3.0f);
 static const Vec3 cCharacterVelocity(0, 0, 2);
 static const Vec3 cCharacterVelocity(0, 0, 2);
 
 
 CharacterBaseTest::~CharacterBaseTest()
 CharacterBaseTest::~CharacterBaseTest()
@@ -100,16 +101,22 @@ void CharacterBaseTest::Initialize()
 	case EType::Capsule:
 	case EType::Capsule:
 		mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding)).Create().Get();
 		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();
 		mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightCrouching, cCharacterRadiusCrouching)).Create().Get();
+		mInnerStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cInnerShapeFraction * cCharacterHeightStanding, cInnerShapeFraction * cCharacterRadiusStanding)).Create().Get();
+		mInnerCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cInnerShapeFraction * cCharacterHeightCrouching, cInnerShapeFraction * cCharacterRadiusCrouching)).Create().Get();
 		break;
 		break;
 
 
 	case EType::Cylinder:
 	case EType::Cylinder:
 		mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CylinderShape(0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding)).Create().Get();
 		mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CylinderShape(0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding)).Create().Get();
 		mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CylinderShape(0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching)).Create().Get();
 		mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CylinderShape(0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching)).Create().Get();
+		mInnerStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CylinderShape(cInnerShapeFraction * (0.5f * cCharacterHeightStanding + cCharacterRadiusStanding), cInnerShapeFraction * cCharacterRadiusStanding)).Create().Get();
+		mInnerCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CylinderShape(cInnerShapeFraction * (0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching), cInnerShapeFraction * cCharacterRadiusCrouching)).Create().Get();
 		break;
 		break;
 
 
 	case EType::Box:
 	case EType::Box:
 		mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new BoxShape(Vec3(cCharacterRadiusStanding, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding))).Create().Get();
 		mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new BoxShape(Vec3(cCharacterRadiusStanding, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding))).Create().Get();
 		mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new BoxShape(Vec3(cCharacterRadiusCrouching, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching))).Create().Get();
 		mCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new BoxShape(Vec3(cCharacterRadiusCrouching, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching))).Create().Get();
+		mInnerStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new BoxShape(cInnerShapeFraction * Vec3(cCharacterRadiusStanding, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding))).Create().Get();
+		mInnerCrouchingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new BoxShape(cInnerShapeFraction * Vec3(cCharacterRadiusCrouching, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching))).Create().Get();
 		break;
 		break;
 	}
 	}
 
 
@@ -516,8 +523,7 @@ void CharacterBaseTest::Initialize()
 			mBodyInterface->CreateAndAddBody(box, EActivation::DontActivate);
 			mBodyInterface->CreateAndAddBody(box, EActivation::DontActivate);
 		}
 		}
 
 
-		// Create a sensor.
-		// Note that the CharacterVirtual doesn't interact with sensors, you should pair it with a Character object (see CharacterVirtual class comments)
+		// Create a sensor
 		{
 		{
 			BodyCreationSettings sensor(new BoxShape(Vec3::sReplicate(1.0f)), cSensorPosition, Quat::sIdentity(), EMotionType::Kinematic, Layers::SENSOR);
 			BodyCreationSettings sensor(new BoxShape(Vec3::sReplicate(1.0f)), cSensorPosition, Quat::sIdentity(), EMotionType::Kinematic, Layers::SENSOR);
 			sensor.mIsSensor = true;
 			sensor.mIsSensor = true;
@@ -543,6 +549,17 @@ void CharacterBaseTest::Initialize()
 			mAnimatedCharacterVirtual->SetCharacterVsCharacterCollision(&mCharacterVsCharacterCollision);
 			mAnimatedCharacterVirtual->SetCharacterVsCharacterCollision(&mCharacterVsCharacterCollision);
 			mCharacterVsCharacterCollision.Add(mAnimatedCharacterVirtual);
 			mCharacterVsCharacterCollision.Add(mAnimatedCharacterVirtual);
 		}
 		}
+
+		// Create CharacterVirtual with inner rigid body
+		{
+			CharacterVirtualSettings settings;
+			settings.mShape = mStandingShape;
+			settings.mInnerBodyShape = mInnerStandingShape;
+			settings.mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
+			mAnimatedCharacterVirtualWithInnerBody = new CharacterVirtual(&settings, cCharacterVirtualWithInnerBodyPosition, Quat::sIdentity(), 0, mPhysicsSystem);
+			mAnimatedCharacterVirtualWithInnerBody->SetCharacterVsCharacterCollision(&mCharacterVsCharacterCollision);
+			mCharacterVsCharacterCollision.Add(mAnimatedCharacterVirtualWithInnerBody);
+		}
 	}
 	}
 #ifdef JPH_OBJECT_STREAM
 #ifdef JPH_OBJECT_STREAM
 	else
 	else
@@ -619,26 +636,26 @@ void CharacterBaseTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 		mAnimatedCharacter->SetLinearVelocity(Sin(mTime) * cCharacterVelocity);
 		mAnimatedCharacter->SetLinearVelocity(Sin(mTime) * cCharacterVelocity);
 
 
 	// Animate character virtual
 	// Animate character virtual
-	if (mAnimatedCharacterVirtual != nullptr)
+	for (CharacterVirtual *character : { mAnimatedCharacterVirtual, mAnimatedCharacterVirtualWithInnerBody })
 	{
 	{
 	#ifdef JPH_DEBUG_RENDERER
 	#ifdef JPH_DEBUG_RENDERER
-		mAnimatedCharacterVirtual->GetShape()->Draw(mDebugRenderer, mAnimatedCharacterVirtual->GetCenterOfMassTransform(), Vec3::sReplicate(1.0f), Color::sOrange, false, true);
+		character->GetShape()->Draw(mDebugRenderer, character->GetCenterOfMassTransform(), Vec3::sReplicate(1.0f), Color::sOrange, false, true);
 	#else
 	#else
-		mDebugRenderer->DrawCapsule(mAnimatedCharacterVirtual->GetCenterOfMassTransform(), 0.5f * cCharacterHeightStanding, cCharacterRadiusStanding + mAnimatedCharacterVirtual->GetCharacterPadding(), Color::sOrange, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
+		mDebugRenderer->DrawCapsule(character->GetCenterOfMassTransform(), 0.5f * cCharacterHeightStanding, cCharacterRadiusStanding + character->GetCharacterPadding(), Color::sOrange, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
 	#endif // JPH_DEBUG_RENDERER
 	#endif // JPH_DEBUG_RENDERER
 
 
 		// Update velocity and apply gravity
 		// Update velocity and apply gravity
 		Vec3 velocity;
 		Vec3 velocity;
-		if (mAnimatedCharacterVirtual->GetGroundState() == CharacterVirtual::EGroundState::OnGround)
+		if (character->GetGroundState() == CharacterVirtual::EGroundState::OnGround)
 			velocity = Vec3::sZero();
 			velocity = Vec3::sZero();
 		else
 		else
-			velocity = mAnimatedCharacterVirtual->GetLinearVelocity() * mAnimatedCharacter->GetUp() + mPhysicsSystem->GetGravity() * inParams.mDeltaTime;
+			velocity = character->GetLinearVelocity() * mAnimatedCharacter->GetUp() + mPhysicsSystem->GetGravity() * inParams.mDeltaTime;
 		velocity += Sin(mTime) * cCharacterVelocity;
 		velocity += Sin(mTime) * cCharacterVelocity;
-		mAnimatedCharacterVirtual->SetLinearVelocity(velocity);
+		character->SetLinearVelocity(velocity);
 
 
 		// Move character
 		// Move character
 		CharacterVirtual::ExtendedUpdateSettings update_settings;
 		CharacterVirtual::ExtendedUpdateSettings update_settings;
-		mAnimatedCharacterVirtual->ExtendedUpdate(inParams.mDeltaTime,
+		character->ExtendedUpdate(inParams.mDeltaTime,
 			mPhysicsSystem->GetGravity(),
 			mPhysicsSystem->GetGravity(),
 			update_settings,
 			update_settings,
 			mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING),
 			mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING),
@@ -715,6 +732,9 @@ void CharacterBaseTest::SaveState(StateRecorder &inStream) const
 
 
 	if (mAnimatedCharacterVirtual != nullptr)
 	if (mAnimatedCharacterVirtual != nullptr)
 		mAnimatedCharacterVirtual->SaveState(inStream);
 		mAnimatedCharacterVirtual->SaveState(inStream);
+
+	if (mAnimatedCharacterVirtualWithInnerBody != nullptr)
+		mAnimatedCharacterVirtualWithInnerBody->SaveState(inStream);
 }
 }
 
 
 void CharacterBaseTest::RestoreState(StateRecorder &inStream)
 void CharacterBaseTest::RestoreState(StateRecorder &inStream)
@@ -725,6 +745,9 @@ void CharacterBaseTest::RestoreState(StateRecorder &inStream)
 
 
 	if (mAnimatedCharacterVirtual != nullptr)
 	if (mAnimatedCharacterVirtual != nullptr)
 		mAnimatedCharacterVirtual->RestoreState(inStream);
 		mAnimatedCharacterVirtual->RestoreState(inStream);
+
+	if (mAnimatedCharacterVirtualWithInnerBody != nullptr)
+		mAnimatedCharacterVirtualWithInnerBody->RestoreState(inStream);
 }
 }
 
 
 void CharacterBaseTest::SaveInputState(StateRecorder &inStream) const
 void CharacterBaseTest::SaveInputState(StateRecorder &inStream) const

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

@@ -68,6 +68,7 @@ protected:
 	static constexpr float	cCharacterRadiusStanding = 0.3f;
 	static constexpr float	cCharacterRadiusStanding = 0.3f;
 	static constexpr float	cCharacterHeightCrouching = 0.8f;
 	static constexpr float	cCharacterHeightCrouching = 0.8f;
 	static constexpr float	cCharacterRadiusCrouching = 0.3f;
 	static constexpr float	cCharacterRadiusCrouching = 0.3f;
+	static constexpr float	cInnerShapeFraction = 0.9f;
 
 
 	// Character movement properties
 	// Character movement properties
 	inline static bool		sControlMovementDuringJump = true;							///< If false the character cannot change movement direction in mid air
 	inline static bool		sControlMovementDuringJump = true;							///< If false the character cannot change movement direction in mid air
@@ -77,6 +78,8 @@ protected:
 	// The different stances for the character
 	// The different stances for the character
 	RefConst<Shape>			mStandingShape;
 	RefConst<Shape>			mStandingShape;
 	RefConst<Shape>			mCrouchingShape;
 	RefConst<Shape>			mCrouchingShape;
+	RefConst<Shape>			mInnerCrouchingShape;
+	RefConst<Shape>			mInnerStandingShape;
 
 
 	// List of boxes on ramp
 	// List of boxes on ramp
 	Array<BodyID>			mRampBlocks;
 	Array<BodyID>			mRampBlocks;
@@ -127,6 +130,7 @@ private:
 	// Moving characters
 	// Moving characters
 	Ref<Character>			mAnimatedCharacter;
 	Ref<Character>			mAnimatedCharacter;
 	Ref<CharacterVirtual>	mAnimatedCharacterVirtual;
 	Ref<CharacterVirtual>	mAnimatedCharacterVirtual;
+	Ref<CharacterVirtual>	mAnimatedCharacterVirtualWithInnerBody;
 
 
 	// Player input
 	// Player input
 	Vec3					mControlInput = Vec3::sZero();
 	Vec3					mControlInput = Vec3::sZero();

+ 16 - 2
Samples/Tests/Character/CharacterVirtualTest.cpp

@@ -31,6 +31,8 @@ void CharacterVirtualTest::Initialize()
 	settings->mPredictiveContactDistance = sPredictiveContactDistance;
 	settings->mPredictiveContactDistance = sPredictiveContactDistance;
 	settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
 	settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
 	settings->mEnhancedInternalEdgeRemoval = sEnhancedInternalEdgeRemoval;
 	settings->mEnhancedInternalEdgeRemoval = sEnhancedInternalEdgeRemoval;
+	settings->mInnerBodyShape = sCreateInnerBody? mInnerStandingShape : nullptr;
+	settings->mInnerBodyLayer = Layers::MOVING;
 	mCharacter = new CharacterVirtual(settings, RVec3::sZero(), Quat::sIdentity(), 0, mPhysicsSystem);
 	mCharacter = new CharacterVirtual(settings, RVec3::sZero(), Quat::sIdentity(), 0, mPhysicsSystem);
 	mCharacter->SetCharacterVsCharacterCollision(&mCharacterVsCharacterCollision);
 	mCharacter->SetCharacterVsCharacterCollision(&mCharacterVsCharacterCollision);
 	mCharacterVsCharacterCollision.Add(mCharacter);
 	mCharacterVsCharacterCollision.Add(mCharacter);
@@ -162,7 +164,15 @@ void CharacterVirtualTest::HandleInput(Vec3Arg inMovementDirection, bool inJump,
 
 
 	// Stance switch
 	// Stance switch
 	if (inSwitchStance)
 	if (inSwitchStance)
-		mCharacter->SetShape(mCharacter->GetShape() == mStandingShape? mCrouchingShape : mStandingShape, 1.5f * mPhysicsSystem->GetPhysicsSettings().mPenetrationSlop, mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING), mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING), { }, { }, *mTempAllocator);
+	{
+		bool is_standing = mCharacter->GetShape() == mStandingShape;
+		const Shape *shape = is_standing? mCrouchingShape : mStandingShape;
+		if (mCharacter->SetShape(shape, 1.5f * mPhysicsSystem->GetPhysicsSettings().mPenetrationSlop, mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING), mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING), { }, { }, *mTempAllocator))
+		{
+			const Shape *inner_shape = is_standing? mInnerCrouchingShape : mInnerStandingShape;
+			mCharacter->SetInnerBodyShape(inner_shape);
+		}
+	}
 }
 }
 
 
 void CharacterVirtualTest::AddCharacterMovementSettings(DebugUI* inUI, UIElement* inSubMenu)
 void CharacterVirtualTest::AddCharacterMovementSettings(DebugUI* inUI, UIElement* inSubMenu)
@@ -185,6 +195,7 @@ void CharacterVirtualTest::AddConfigurationSettings(DebugUI *inUI, UIElement *in
 	inUI->CreateCheckBox(inSubMenu, "Enable Walk Stairs", sEnableWalkStairs, [](UICheckBox::EState inState) { sEnableWalkStairs = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Enable Walk Stairs", sEnableWalkStairs, [](UICheckBox::EState inState) { sEnableWalkStairs = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Enable Stick To Floor", sEnableStickToFloor, [](UICheckBox::EState inState) { sEnableStickToFloor = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Enable Stick To Floor", sEnableStickToFloor, [](UICheckBox::EState inState) { sEnableStickToFloor = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Enhanced Internal Edge Removal", sEnhancedInternalEdgeRemoval, [](UICheckBox::EState inState) { sEnhancedInternalEdgeRemoval = inState == UICheckBox::STATE_CHECKED; });
 	inUI->CreateCheckBox(inSubMenu, "Enhanced Internal Edge Removal", sEnhancedInternalEdgeRemoval, [](UICheckBox::EState inState) { sEnhancedInternalEdgeRemoval = inState == UICheckBox::STATE_CHECKED; });
+	inUI->CreateCheckBox(inSubMenu, "Create Inner Body", sCreateInnerBody, [](UICheckBox::EState inState) { sCreateInnerBody = inState == UICheckBox::STATE_CHECKED; });
 }
 }
 
 
 void CharacterVirtualTest::SaveState(StateRecorder &inStream) const
 void CharacterVirtualTest::SaveState(StateRecorder &inStream) const
@@ -208,7 +219,10 @@ void CharacterVirtualTest::RestoreState(StateRecorder &inStream)
 
 
 	bool is_standing = mCharacter->GetShape() == mStandingShape; // Initialize variable for validation mode
 	bool is_standing = mCharacter->GetShape() == mStandingShape; // Initialize variable for validation mode
 	inStream.Read(is_standing);
 	inStream.Read(is_standing);
-	mCharacter->SetShape(is_standing? mStandingShape : mCrouchingShape, FLT_MAX, { }, { }, { }, { }, *mTempAllocator);
+	const Shape *shape = is_standing? mStandingShape : mCrouchingShape;
+	mCharacter->SetShape(shape, FLT_MAX, { }, { }, { }, { }, *mTempAllocator);
+	const Shape *inner_shape = is_standing? mInnerStandingShape : mInnerCrouchingShape;
+	mCharacter->SetInnerBodyShape(inner_shape);
 
 
 	inStream.Read(mAllowSliding);
 	inStream.Read(mAllowSliding);
 	inStream.Read(mDesiredVelocity);
 	inStream.Read(mDesiredVelocity);

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

@@ -63,6 +63,7 @@ private:
 	static inline bool		sEnableWalkStairs = true;
 	static inline bool		sEnableWalkStairs = true;
 	static inline bool		sEnableStickToFloor = true;
 	static inline bool		sEnableStickToFloor = true;
 	static inline bool		sEnhancedInternalEdgeRemoval = false;
 	static inline bool		sEnhancedInternalEdgeRemoval = false;
+	static inline bool		sCreateInnerBody = false;
 	static inline bool		sPlayerCanPushOtherCharacters = true;
 	static inline bool		sPlayerCanPushOtherCharacters = true;
 	static inline bool		sOtherCharactersCanPushPlayer = true;
 	static inline bool		sOtherCharactersCanPushPlayer = true;