Ajout de Jolt Physics + 1ere version des factory entitecomposants - camera, transform, rigidbody, collider, renderer

This commit is contained in:
Tom Ray
2026-03-22 00:28:03 +01:00
parent 6695d46bcd
commit 48348936a8
1147 changed files with 214331 additions and 353 deletions

View File

@@ -0,0 +1,855 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// 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/CylinderShape.h>
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
#include <Jolt/Physics/Constraints/HingeConstraint.h>
#include <Jolt/Core/StringTools.h>
#include <Jolt/ObjectStream/ObjectStreamIn.h>
#include <Application/DebugUI.h>
#include <Layers.h>
#include <Utils/Log.h>
#include <Utils/AssetStream.h>
#include <Renderer/DebugRendererImp.h>
JPH_IMPLEMENT_RTTI_ABSTRACT(CharacterBaseTest)
{
JPH_ADD_BASE_CLASS(CharacterBaseTest, Test)
}
const char *CharacterBaseTest::sScenes[] =
{
"PerlinMesh",
"PerlinHeightField",
"ObstacleCourse",
"InitiallyIntersecting",
#ifdef JPH_OBJECT_STREAM
"Terrain1",
"Terrain2",
#endif // JPH_OBJECT_STREAM
};
const char *CharacterBaseTest::sSceneName = "ObstacleCourse";
// Scene constants
static const RVec3 cRotatingPosition(-5, 0.15f, 15);
static const Quat cRotatingOrientation = Quat::sIdentity();
static const RVec3 cRotatingWallPosition(5, 1.0f, 25.0f);
static const Quat cRotatingWallOrientation = Quat::sIdentity();
static const RVec3 cRotatingAndTranslatingPosition(-10, 0.15f, 27.5f);
static const Quat cRotatingAndTranslatingOrientation = Quat::sIdentity();
static const RVec3 cSmoothVerticallyMovingPosition(0, 2.0f, 15);
static const Quat cSmoothVerticallyMovingOrientation = Quat::sIdentity();
static const RVec3 cReversingVerticallyMovingPosition(0, 0.15f, 25);
static const Quat cReversingVerticallyMovingOrientation = Quat::sIdentity();
static const RVec3 cHorizontallyMovingPosition(5, 1, 15);
static const Quat cHorizontallyMovingOrientation = Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI);
static const RVec3 cConveyorBeltPosition(-10, 0.15f, 15);
static const RVec3 cRampPosition(15, 2.2f, 15);
static const Quat cRampOrientation = Quat::sRotation(Vec3::sAxisX(), -0.25f * JPH_PI);
static const RVec3 cRampBlocksStart = cRampPosition + Vec3(-3.0f, 3.0f, 1.5f);
static const Vec3 cRampBlocksDelta = Vec3(2.0f, 0, 0);
static const float cRampBlocksTime = 5.0f;
static const RVec3 cSmallBumpsPosition(-5.0f, 0, 2.5f);
static const float cSmallBumpHeight = 0.05f;
static const float cSmallBumpWidth = 0.01f;
static const float cSmallBumpDelta = 0.5f;
static const RVec3 cLargeBumpsPosition(-10.0f, 0, 2.5f);
static const float cLargeBumpHeight = 0.3f;
static const float cLargeBumpWidth = 0.1f;
static const float cLargeBumpDelta = 2.0f;
static const RVec3 cStairsPosition(-15.0f, 0, 2.5f);
static const float cStairsStepHeight = 0.3f;
static const RVec3 cMeshStairsPosition(-20.0f, 0, 2.5f);
static const RVec3 cNoStairsPosition(-15.0f, 0, 10.0f);
static const float cNoStairsStepHeight = 0.3f;
static const float cNoStairsStepDelta = 0.05f;
static const RVec3 cMeshNoStairsPosition(-20.0f, 0, 10.0f);
static const RVec3 cMeshWallPosition(-25.0f, 0, -27.0f);
static const float cMeshWallHeight = 3.0f;
static const float cMeshWallWidth = 2.0f;
static const float cMeshWallStepStart = 0.5f;
static const float cMeshWallStepEnd = 4.0f;
static const int cMeshWallSegments = 25;
static const RVec3 cHalfCylinderPosition(5.0f, 0, 8.0f);
static const RVec3 cMeshBoxPosition(30.0f, 1.5f, 5.0f);
static const RVec3 cSensorPosition(30, 0.9f, -5);
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);
CharacterBaseTest::~CharacterBaseTest()
{
if (mAnimatedCharacter != nullptr)
mAnimatedCharacter->RemoveFromPhysicsSystem();
}
void CharacterBaseTest::Initialize()
{
// Create capsule shapes for all stances
switch (sShapeType)
{
case EType::Capsule:
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();
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;
case EType::Cylinder:
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();
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;
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();
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;
case EType::Compound:
{
StaticCompoundShapeSettings standing_compound;
standing_compound.AddShape(Vec3(-0.3f, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding));
standing_compound.AddShape(Vec3(0.3f, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new BoxShape(Vec3(cCharacterRadiusStanding, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding)));
mStandingShape = standing_compound.Create().Get();
StaticCompoundShapeSettings crouching_compound;
crouching_compound.AddShape(Vec3(-0.3f, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightCrouching, cCharacterRadiusCrouching));
crouching_compound.AddShape(Vec3(0.3f, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new BoxShape(Vec3(cCharacterRadiusCrouching, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching)));
mCrouchingShape = crouching_compound.Create().Get();
StaticCompoundShapeSettings inner_standing_compound;
inner_standing_compound.AddShape(Vec3(-0.3f, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cInnerShapeFraction * cCharacterHeightStanding, cInnerShapeFraction * cCharacterRadiusStanding));
inner_standing_compound.AddShape(Vec3(0.3f, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new BoxShape(cInnerShapeFraction * Vec3(cCharacterRadiusStanding, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, cCharacterRadiusStanding)));
mInnerStandingShape = inner_standing_compound.Create().Get();
StaticCompoundShapeSettings inner_crouching_compound;
inner_crouching_compound.AddShape(Vec3(-0.3f, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cInnerShapeFraction * cCharacterHeightCrouching, cInnerShapeFraction * cCharacterRadiusCrouching));
inner_crouching_compound.AddShape(Vec3(0.3f, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, 0), Quat::sIdentity(), new BoxShape(cInnerShapeFraction * Vec3(cCharacterRadiusCrouching, 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching, cCharacterRadiusCrouching)));
mInnerCrouchingShape = inner_crouching_compound.Create().Get();
}
break;
}
if (strcmp(sSceneName, "PerlinMesh") == 0)
{
// Default terrain
CreateMeshTerrain();
}
else if (strcmp(sSceneName, "PerlinHeightField") == 0)
{
// Default terrain
CreateHeightFieldTerrain();
}
else if (strcmp(sSceneName, "InitiallyIntersecting") == 0)
{
CreateFloor();
// Create a grid of boxes that are initially intersecting with the character
RefConst<Shape> box = new BoxShape(Vec3(0.1f, 0.1f, 0.1f));
BodyCreationSettings settings(box, RVec3(0, 0.5f, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
for (int x = 0; x < 4; ++x)
for (int y = 0; y <= 10; ++y)
for (int z = 0; z <= 10; ++z)
{
settings.mPosition = RVec3(-0.5f + 0.1f * x, 0.1f + 0.1f * y, -0.5f + 0.1f * z);
mBodyInterface->CreateAndAddBody(settings, EActivation::DontActivate);
}
}
else if (strcmp(sSceneName, "ObstacleCourse") == 0)
{
// Default terrain
CreateFloor(350.0f);
{
// 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, RVec3(-15.0f + angle * 2.0f, 0, -10.0f), Quat::sRotation(Vec3::sAxisX(), DegreesToRadians(10.0f * angle)), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
}
{
// Create ramps with different inclinations intersecting with a steep slope
Ref<Shape> ramp = RotatedTranslatedShapeSettings(Vec3(0, 0, -2.5f), Quat::sIdentity(), new BoxShape(Vec3(1.0f, 0.05f, 2.5f))).Create().Get();
Ref<Shape> ramp2 = RotatedTranslatedShapeSettings(Vec3(0, 2.0f, 0), Quat::sIdentity(), new BoxShape(Vec3(0.05f, 2.0f, 1.0f))).Create().Get();
for (int angle = 0; angle < 9; ++angle)
{
mBodyInterface->CreateAndAddBody(BodyCreationSettings(ramp, RVec3(-15.0f + angle * 2.0f, 0, -20.0f - angle * 0.1f), Quat::sRotation(Vec3::sAxisX(), DegreesToRadians(10.0f * angle)), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
mBodyInterface->CreateAndAddBody(BodyCreationSettings(ramp2, RVec3(-15.0f + angle * 2.0f, 0, -21.0f), Quat::sRotation(Vec3::sAxisZ(), DegreesToRadians(20.0f)), 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 < 30; ++z)
mBodyInterface->CreateAndAddBody(BodyCreationSettings(wall, RVec3(0.0f, 2.5f, 2.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.15f, 3.0f));
mRotatingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cRotatingPosition, cRotatingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
mRotatingWallBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(3.0f, 1, 0.15f)), cRotatingWallPosition, cRotatingWallOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
mRotatingAndTranslatingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cRotatingAndTranslatingPosition, cRotatingAndTranslatingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
mSmoothVerticallyMovingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cSmoothVerticallyMovingPosition, cSmoothVerticallyMovingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
mReversingVerticallyMovingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cReversingVerticallyMovingPosition, cReversingVerticallyMovingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
mHorizontallyMovingBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(kinematic, cHorizontallyMovingPosition, cHorizontallyMovingOrientation, EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
}
{
// Conveyor belt (only works with virtual character)
mConveyorBeltBody = mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(1, 0.15f, 3.0f)), cConveyorBeltPosition, Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::Activate);
}
{
// A rolling sphere towards the player
BodyCreationSettings bcs(new SphereShape(0.2f), RVec3(0.0f, 0.2f, -1.0f), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
bcs.mLinearVelocity = Vec3(0, 0, 2.0f);
bcs.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
bcs.mMassPropertiesOverride.mMass = 10.0f;
mBodyInterface->CreateAndAddBody(bcs, 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, RVec3(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);
}
}
{
// Dynamic block on a static step (to test pushing block on stairs)
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(0.5f, 0.15f, 0.5f)), RVec3(10.0f, 0.15f, 0.0f), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
BodyCreationSettings bcs(new BoxShape(Vec3::sReplicate(0.5f)), RVec3(10.0f, 0.8f, 0.0f), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
bcs.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
bcs.mMassPropertiesOverride.mMass = 10.0f;
mBodyInterface->CreateAndAddBody(bcs, EActivation::DontActivate);
}
{
// Dynamic spheres to test player pushing stuff you can step on
float h = 0.0f;
for (int y = 0; y < 3; ++y)
{
float r = 0.4f - 0.1f * y;
h += r;
BodyCreationSettings bcs(new SphereShape(r), RVec3(15.0f, h, 0.0f), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
h += r;
bcs.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
bcs.mMassPropertiesOverride.mMass = 10.0f;
mBodyInterface->CreateAndAddBody(bcs, EActivation::DontActivate);
}
}
{
// A seesaw to test character gravity
BodyID b1 = mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(1.0f, 0.2f, 0.05f)), RVec3(20.0f, 0.2f, 0.0f), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
BodyCreationSettings bcs(new BoxShape(Vec3(1.0f, 0.05f, 5.0f)), RVec3(20.0f, 0.45f, 0.0f), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
bcs.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
bcs.mMassPropertiesOverride.mMass = 10.0f;
BodyID b2 = mBodyInterface->CreateAndAddBody(bcs, EActivation::Activate);
// Connect the parts with a hinge
HingeConstraintSettings hinge;
hinge.mPoint1 = hinge.mPoint2 = RVec3(20.0f, 0.4f, 0.0f);
hinge.mHingeAxis1 = hinge.mHingeAxis2 = Vec3::sAxisX();
mPhysicsSystem->AddConstraint(mBodyInterface->CreateConstraint(&hinge, b1, b2));
}
{
// A board above the character to crouch and jump up against
float h = 0.5f * cCharacterHeightCrouching + cCharacterRadiusCrouching + 0.1f;
for (int x = 0; x < 2; ++x)
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(1.0f, h, 0.05f)), RVec3(25.0f, h, x == 0? -0.95f : 0.95f), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
BodyCreationSettings bcs(new BoxShape(Vec3(1.0f, 0.05f, 1.0f)), RVec3(25.0f, 2.0f * h + 0.05f, 0.0f), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
bcs.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
bcs.mMassPropertiesOverride.mMass = 10.0f;
mBodyInterface->CreateAndAddBody(bcs, EActivation::Activate);
}
{
// A floating static block
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3::sReplicate(0.5f)), RVec3(30.0f, 1.5f, 0.0f), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), 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, RVec3(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, RVec3(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, RVec3(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);
}
// Create small bumps
{
BodyCreationSettings step(new BoxShape(Vec3(2.0f, 0.5f * cSmallBumpHeight, 0.5f * cSmallBumpWidth), 0.0f), RVec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
for (int i = 0; i < 10; ++i)
{
step.mPosition = cSmallBumpsPosition + Vec3(0, 0.5f * cSmallBumpHeight, cSmallBumpDelta * i);
mBodyInterface->CreateAndAddBody(step, EActivation::DontActivate);
}
}
// Create large bumps
{
BodyCreationSettings step(new BoxShape(Vec3(2.0f, 0.5f * cLargeBumpHeight, 0.5f * cLargeBumpWidth)), RVec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
for (int i = 0; i < 5; ++i)
{
step.mPosition = cLargeBumpsPosition + Vec3(0, 0.5f * cLargeBumpHeight, cLargeBumpDelta * i);
mBodyInterface->CreateAndAddBody(step, EActivation::DontActivate);
}
}
// Create stairs
{
BodyCreationSettings step(new BoxShape(Vec3(2.0f, 0.5f * cStairsStepHeight, 0.5f * cStairsStepHeight)), RVec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
for (int i = 0; i < 10; ++i)
{
step.mPosition = cStairsPosition + Vec3(0, cStairsStepHeight * (0.5f + i), cStairsStepHeight * i);
mBodyInterface->CreateAndAddBody(step, EActivation::DontActivate);
}
}
// A wall beside the stairs
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(0.5f, 2.0f, 5.0f * cStairsStepHeight)), cStairsPosition + Vec3(-2.5f, 2.0f, 5.0f * cStairsStepHeight), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
// Create stairs from triangles
{
TriangleList triangles;
float rear_z = 10 * cStairsStepHeight;
for (int i = 0; i < 10; ++i)
{
// Start of step
Vec3 base(0, cStairsStepHeight * i, cStairsStepHeight * i);
// Left side
Vec3 b1 = base + Vec3(2.0f, 0, 0);
Vec3 s1 = b1 + Vec3(0, cStairsStepHeight, 0);
Vec3 p1 = s1 + Vec3(0, 0, cStairsStepHeight);
// Right side
Vec3 width(-4.0f, 0, 0);
Vec3 b2 = b1 + width;
Vec3 s2 = s1 + width;
Vec3 p2 = p1 + width;
triangles.push_back(Triangle(s1, b1, s2));
triangles.push_back(Triangle(b1, b2, s2));
triangles.push_back(Triangle(s1, p2, p1));
triangles.push_back(Triangle(s1, s2, p2));
// Side of stairs
Vec3 rb2 = b2; rb2.SetZ(rear_z);
Vec3 rs2 = s2; rs2.SetZ(rear_z);
triangles.push_back(Triangle(s2, b2, rs2));
triangles.push_back(Triangle(rs2, b2, rb2));
}
MeshShapeSettings mesh(triangles);
mesh.SetEmbedded();
BodyCreationSettings mesh_stairs(&mesh, cMeshStairsPosition, Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
mBodyInterface->CreateAndAddBody(mesh_stairs, EActivation::DontActivate);
}
// A wall to the side and behind the stairs
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(0.5f, 2.0f, 0.25f)), cStairsPosition + Vec3(-7.5f, 2.0f, 10.0f * cStairsStepHeight + 0.25f), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
// Create stairs with too little space between the steps
{
BodyCreationSettings step(new BoxShape(Vec3(2.0f, 0.5f * cNoStairsStepHeight, 0.5f * cNoStairsStepHeight)), RVec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
for (int i = 0; i < 10; ++i)
{
step.mPosition = cNoStairsPosition + Vec3(0, cNoStairsStepHeight * (0.5f + i), cNoStairsStepDelta * i);
mBodyInterface->CreateAndAddBody(step, EActivation::DontActivate);
}
}
// Create stairs with too little space between the steps consisting of triangles
{
TriangleList triangles;
for (int i = 0; i < 10; ++i)
{
// Start of step
Vec3 base(0, cStairsStepHeight * i, cNoStairsStepDelta * i);
// Left side
Vec3 b1 = base - Vec3(2.0f, 0, 0);
Vec3 s1 = b1 + Vec3(0, cStairsStepHeight, 0);
Vec3 p1 = s1 + Vec3(0, 0, cNoStairsStepDelta);
// Right side
Vec3 width(4.0f, 0, 0);
Vec3 b2 = b1 + width;
Vec3 s2 = s1 + width;
Vec3 p2 = p1 + width;
triangles.push_back(Triangle(s1, s2, b1));
triangles.push_back(Triangle(b1, s2, b2));
triangles.push_back(Triangle(s1, p1, p2));
triangles.push_back(Triangle(s1, p2, s2));
}
MeshShapeSettings mesh(triangles);
mesh.SetEmbedded();
BodyCreationSettings mesh_stairs(&mesh, cMeshNoStairsPosition, Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
mBodyInterface->CreateAndAddBody(mesh_stairs, EActivation::DontActivate);
}
// Create mesh with walls at varying angles
{
TriangleList triangles;
Vec3 p1(0.5f * cMeshWallWidth, 0, 0);
Vec3 h(0, cMeshWallHeight, 0);
for (int i = 0; i < cMeshWallSegments; ++i)
{
float delta = cMeshWallStepStart + i * (cMeshWallStepEnd - cMeshWallStepStart) / (cMeshWallSegments - 1);
Vec3 p2 = Vec3((i & 1)? 0.5f * cMeshWallWidth : -0.5f * cMeshWallWidth, 0, p1.GetZ() + delta);
triangles.push_back(Triangle(p1, p1 + h, p2 + h));
triangles.push_back(Triangle(p1, p2 + h, p2));
p1 = p2;
}
MeshShapeSettings mesh(triangles);
mesh.SetEmbedded();
BodyCreationSettings wall(&mesh, cMeshWallPosition, Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
mBodyInterface->CreateAndAddBody(wall, EActivation::DontActivate);
}
// Create a half cylinder with caps for testing contact point limit
{
VertexList vertices;
IndexedTriangleList triangles;
// The half cylinder
const int cPosSegments = 2;
const int cAngleSegments = 512;
const float cCylinderLength = 2.0f;
for (int pos = 0; pos < cPosSegments; ++pos)
for (int angle = 0; angle < cAngleSegments; ++angle)
{
uint32 start = (uint32)vertices.size();
float radius = cCharacterRadiusStanding + 0.05f;
float angle_rad = (-0.5f + float(angle) / cAngleSegments) * JPH_PI;
float s = Sin(angle_rad);
float c = Cos(angle_rad);
float x = cCylinderLength * (-0.5f + float(pos) / (cPosSegments - 1));
float y = angle == 0 || angle == cAngleSegments - 1? 0.5f : (1.0f - c) * radius;
float z = s * radius;
vertices.push_back(Float3(x, y, z));
if (pos > 0 && angle > 0)
{
triangles.push_back(IndexedTriangle(start, start - 1, start - cAngleSegments));
triangles.push_back(IndexedTriangle(start - 1, start - cAngleSegments - 1, start - cAngleSegments));
}
}
// Add end caps
uint32 end = cAngleSegments * (cPosSegments - 1);
for (int angle = 0; angle < cAngleSegments - 1; ++angle)
{
triangles.push_back(IndexedTriangle(0, angle + 1, angle));
triangles.push_back(IndexedTriangle(end, end + angle, end + angle + 1));
}
MeshShapeSettings mesh(std::move(vertices), std::move(triangles));
mesh.SetEmbedded();
BodyCreationSettings mesh_cylinder(&mesh, cHalfCylinderPosition, Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
mBodyInterface->CreateAndAddBody(mesh_cylinder, EActivation::DontActivate);
}
// Create a box made out of polygons (character should not get stuck behind back facing side)
{
VertexList vertices = {
Float3(-1, 1, -1),
Float3( 1, 1, -1),
Float3( 1, 1, 1),
Float3(-1, 1, 1),
Float3(-1, -1, -1),
Float3( 1, -1, -1),
Float3( 1, -1, 1),
Float3(-1, -1, 1)
};
IndexedTriangleList triangles = {
IndexedTriangle(0, 3, 2),
IndexedTriangle(0, 2, 1),
IndexedTriangle(4, 5, 6),
IndexedTriangle(4, 6, 7),
IndexedTriangle(0, 4, 3),
IndexedTriangle(3, 4, 7),
IndexedTriangle(2, 6, 5),
IndexedTriangle(2, 5, 1),
IndexedTriangle(3, 7, 6),
IndexedTriangle(3, 6, 2),
IndexedTriangle(0, 1, 5),
IndexedTriangle(0, 5, 4)
};
MeshShapeSettings mesh(std::move(vertices), std::move(triangles));
mesh.SetEmbedded();
BodyCreationSettings box(&mesh, cMeshBoxPosition, Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING);
mBodyInterface->CreateAndAddBody(box, EActivation::DontActivate);
}
// Create a sensor
{
BodyCreationSettings sensor(new BoxShape(Vec3::sOne()), cSensorPosition, Quat::sIdentity(), EMotionType::Kinematic, Layers::SENSOR);
sensor.mIsSensor = true;
mSensorBody = mBodyInterface->CreateAndAddBody(sensor, EActivation::Activate);
}
// Create Character
{
CharacterSettings settings;
settings.mLayer = Layers::MOVING;
settings.mShape = mStandingShape;
settings.mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
mAnimatedCharacter = new Character(&settings, cCharacterPosition, Quat::sIdentity(), 0, mPhysicsSystem);
mAnimatedCharacter->AddToPhysicsSystem();
}
// Create CharacterVirtual
{
CharacterVirtualSettings settings;
settings.mShape = mStandingShape;
settings.mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
mAnimatedCharacterVirtual = new CharacterVirtual(&settings, cCharacterVirtualPosition, Quat::sIdentity(), 0, mPhysicsSystem);
mAnimatedCharacterVirtual->SetCharacterVsCharacterCollision(&mCharacterVsCharacterCollision);
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
else
{
// Load scene
Ref<PhysicsScene> scene;
AssetStream stream(String(sSceneName) + ".bof", std::ios::in | std::ios::binary);
if (!ObjectStreamIn::sReadObject(stream.Get(), 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);
}
#endif // JPH_OBJECT_STREAM
}
void CharacterBaseTest::ProcessInput(const ProcessInputParams &inParams)
{
// Determine controller input
mControlInput = Vec3::sZero();
if (inParams.mKeyboard->IsKeyPressed(EKey::Left)) mControlInput.SetZ(-1);
if (inParams.mKeyboard->IsKeyPressed(EKey::Right)) mControlInput.SetZ(1);
if (inParams.mKeyboard->IsKeyPressed(EKey::Up)) mControlInput.SetX(1);
if (inParams.mKeyboard->IsKeyPressed(EKey::Down)) mControlInput.SetX(-1);
if (mControlInput != Vec3::sZero())
mControlInput = mControlInput.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);
mControlInput = rotation * mControlInput;
// Check actions
mJump = inParams.mKeyboard->IsKeyPressedAndTriggered(EKey::RControl, mWasJump);
mSwitchStance = inParams.mKeyboard->IsKeyPressedAndTriggered(EKey::RShift, mWasSwitchStance);
}
void CharacterBaseTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
// Update scene time
mTime += inParams.mDeltaTime;
// Update camera pivot
mCameraPivot = GetCharacterPosition();
// Animate bodies
if (!mRotatingBody.IsInvalid())
mBodyInterface->MoveKinematic(mRotatingBody, cRotatingPosition, Quat::sRotation(Vec3::sAxisY(), JPH_PI * Sin(mTime)), inParams.mDeltaTime);
if (!mRotatingWallBody.IsInvalid())
mBodyInterface->MoveKinematic(mRotatingWallBody, cRotatingWallPosition, Quat::sRotation(Vec3::sAxisY(), JPH_PI * Sin(mTime)), inParams.mDeltaTime);
if (!mRotatingAndTranslatingBody.IsInvalid())
mBodyInterface->MoveKinematic(mRotatingAndTranslatingBody, cRotatingAndTranslatingPosition + 5.0f * Vec3(Sin(JPH_PI * mTime), 0, Cos(JPH_PI * mTime)), 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 (!mSmoothVerticallyMovingBody.IsInvalid())
mBodyInterface->MoveKinematic(mSmoothVerticallyMovingBody, cSmoothVerticallyMovingPosition + Vec3(0, 1.75f * Sin(mTime), 0), cSmoothVerticallyMovingOrientation, inParams.mDeltaTime);
if (!mReversingVerticallyMovingBody.IsInvalid())
{
RVec3 pos = mBodyInterface->GetPosition(mReversingVerticallyMovingBody);
if (pos.GetY() < cReversingVerticallyMovingPosition.GetY())
mReversingVerticallyMovingVelocity = 1.0f;
else if (pos.GetY() > cReversingVerticallyMovingPosition.GetY() + 5.0f)
mReversingVerticallyMovingVelocity = -1.0f;
mBodyInterface->MoveKinematic(mReversingVerticallyMovingBody, pos + Vec3(0, mReversingVerticallyMovingVelocity * 3.0f * inParams.mDeltaTime, 0), cReversingVerticallyMovingOrientation, inParams.mDeltaTime);
}
// Animate character
if (mAnimatedCharacter != nullptr)
mAnimatedCharacter->SetLinearVelocity(Sin(mTime) * cCharacterVelocity);
// Animate character virtual
for (CharacterVirtual *character : { mAnimatedCharacterVirtual, mAnimatedCharacterVirtualWithInnerBody })
if (character != nullptr)
{
// Draw the character
DrawPaddedCharacter(character->GetShape(), character->GetCharacterPadding(), character->GetCenterOfMassTransform());
// Update velocity and apply gravity
Vec3 velocity;
if (character->GetGroundState() == CharacterVirtual::EGroundState::OnGround)
velocity = Vec3::sZero();
else
velocity = character->GetLinearVelocity() * mAnimatedCharacter->GetUp() + mPhysicsSystem->GetGravity() * inParams.mDeltaTime;
velocity += Sin(mTime) * cCharacterVelocity;
character->SetLinearVelocity(velocity);
// Move character
CharacterVirtual::ExtendedUpdateSettings update_settings;
character->ExtendedUpdate(inParams.mDeltaTime,
mPhysicsSystem->GetGravity(),
update_settings,
mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING),
mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING),
{ },
{ },
*mTempAllocator);
}
// 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;
}
// Call handle input after new velocities have been set to avoid frame delay
HandleInput(mControlInput, mJump, mSwitchStance, inParams.mDeltaTime);
}
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);
});
inUI->CreateTextButton(inSubMenu, "Character Movement Settings", [this, inUI]() {
UIElement *movement_settings = inUI->CreateMenu();
inUI->CreateCheckBox(movement_settings, "Control Movement During Jump", sControlMovementDuringJump, [](UICheckBox::EState inState) { sControlMovementDuringJump = inState == UICheckBox::STATE_CHECKED; });
inUI->CreateSlider(movement_settings, "Character Speed", sCharacterSpeed, 0.1f, 10.0f, 0.1f, [](float inValue) { sCharacterSpeed = inValue; });
inUI->CreateSlider(movement_settings, "Character Jump Speed", sJumpSpeed, 0.1f, 10.0f, 0.1f, [](float inValue) { sJumpSpeed = inValue; });
AddCharacterMovementSettings(inUI, movement_settings);
inUI->ShowMenu(movement_settings);
});
inUI->CreateTextButton(inSubMenu, "Configuration Settings", [this, inUI]() {
UIElement *configuration_settings = inUI->CreateMenu();
inUI->CreateComboBox(configuration_settings, "Shape Type", { "Capsule", "Cylinder", "Box", "Compound" }, (int)sShapeType, [](int inItem) { sShapeType = (EType)inItem; });
AddConfigurationSettings(inUI, configuration_settings);
inUI->CreateTextButton(configuration_settings, "Accept Changes", [this]() { RestartTest(); });
inUI->ShowMenu(configuration_settings);
});
}
void CharacterBaseTest::GetInitialCamera(CameraState& ioState) const
{
// This will become the local space offset, look down the x axis and slightly down
ioState.mPos = RVec3::sZero();
ioState.mForward = Vec3(10.0f, -2.0f, 0).Normalized();
}
RMat44 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 RMat44::sTranslation(mCameraPivot + Vec3(0, cCharacterHeightStanding + cCharacterRadiusStanding, 0) - 5.0f * fwd);
}
void CharacterBaseTest::SaveState(StateRecorder &inStream) const
{
inStream.Write(mTime);
inStream.Write(mRampBlocksTimeLeft);
inStream.Write(mReversingVerticallyMovingVelocity);
if (mAnimatedCharacterVirtual != nullptr)
mAnimatedCharacterVirtual->SaveState(inStream);
if (mAnimatedCharacterVirtualWithInnerBody != nullptr)
mAnimatedCharacterVirtualWithInnerBody->SaveState(inStream);
}
void CharacterBaseTest::RestoreState(StateRecorder &inStream)
{
inStream.Read(mTime);
inStream.Read(mRampBlocksTimeLeft);
inStream.Read(mReversingVerticallyMovingVelocity);
if (mAnimatedCharacterVirtual != nullptr)
mAnimatedCharacterVirtual->RestoreState(inStream);
if (mAnimatedCharacterVirtualWithInnerBody != nullptr)
mAnimatedCharacterVirtualWithInnerBody->RestoreState(inStream);
}
void CharacterBaseTest::SaveInputState(StateRecorder &inStream) const
{
inStream.Write(mControlInput);
inStream.Write(mJump);
inStream.Write(mSwitchStance);
}
void CharacterBaseTest::RestoreInputState(StateRecorder &inStream)
{
inStream.Read(mControlInput);
inStream.Read(mJump);
inStream.Read(mSwitchStance);
}
void CharacterBaseTest::DrawCharacterState(const CharacterBase *inCharacter, RMat44Arg 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, 0.1f);
// Draw the state of the ground contact
CharacterBase::EGroundState ground_state = inCharacter->GetGroundState();
if (ground_state != CharacterBase::EGroundState::InAir)
{
RVec3 ground_position = inCharacter->GetGroundPosition();
Vec3 ground_normal = inCharacter->GetGroundNormal();
Vec3 ground_velocity = inCharacter->GetGroundVelocity();
// Draw ground position
mDebugRenderer->DrawMarker(ground_position, Color::sRed, 0.1f);
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();
Vec3 horizontal_velocity = inCharacterVelocity;
horizontal_velocity.SetY(0);
mDebugRenderer->DrawText3D(inCharacterTransform.GetTranslation(), StringFormat("State: %s\nMat: %s\nHorizontal Vel: %.1f m/s\nVertical Vel: %.1f m/s", CharacterBase::sToString(ground_state), ground_material->GetDebugName(), (double)horizontal_velocity.Length(), (double)inCharacterVelocity.GetY()), Color::sWhite, 0.25f);
}
void CharacterBaseTest::DrawPaddedCharacter(const Shape *inShape, float inPadding, RMat44Arg inCenterOfMass)
{
if (inShape->GetSubType() == EShapeSubType::Capsule)
{
const CapsuleShape *capsule = static_cast<const CapsuleShape *>(inShape);
mDebugRenderer->DrawCapsule(inCenterOfMass, capsule->GetHalfHeightOfCylinder(), capsule->GetRadius() + inPadding, Color::sGrey, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
}
else if (inShape->GetSubType() == EShapeSubType::Cylinder)
{
// Not correct as the edges should be rounded
const CylinderShape *cylinder = static_cast<const CylinderShape *>(inShape);
mDebugRenderer->DrawCylinder(inCenterOfMass, cylinder->GetHalfHeight() + inPadding, cylinder->GetRadius() + inPadding, Color::sGrey, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
}
else if (inShape->GetSubType() == EShapeSubType::Box)
{
// Not correct as the edges should be rounded
const BoxShape *box = static_cast<const BoxShape *>(inShape);
AABox bounds = box->GetLocalBounds();
bounds.ExpandBy(Vec3::sReplicate(inPadding));
mDebugRenderer->DrawWireBox(inCenterOfMass, bounds, Color::sGrey);
}
else if (inShape->GetSubType() == EShapeSubType::RotatedTranslated)
{
const RotatedTranslatedShape *rt = static_cast<const RotatedTranslatedShape *>(inShape);
DrawPaddedCharacter(rt->GetInnerShape(), inPadding, inCenterOfMass);
}
else if (inShape->GetType() == EShapeType::Compound)
{
const CompoundShape *compound = static_cast<const CompoundShape *>(inShape);
for (const CompoundShape::SubShape &sub_shape : compound->GetSubShapes())
DrawPaddedCharacter(sub_shape.mShape, inPadding, inCenterOfMass * sub_shape.GetLocalTransformNoScale(Vec3::sOne()));
}
}

View File

@@ -0,0 +1,145 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Physics/Character/Character.h>
#include <Jolt/Physics/Character/CharacterVirtual.h>
// Base class for the character tests, initializes the test scene.
class CharacterBaseTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, CharacterBaseTest)
// Destructor
virtual ~CharacterBaseTest() 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;
// Process input
virtual void ProcessInput(const ProcessInputParams &inParams) 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 RMat44 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;
// Saving / restoring controller input state for replay
virtual void SaveInputState(StateRecorder &inStream) const override;
virtual void RestoreInputState(StateRecorder &inStream) override;
protected:
// Get position of the character
virtual RVec3 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, RMat44Arg inCharacterTransform, Vec3Arg inCharacterVelocity);
// Add character movement settings
virtual void AddCharacterMovementSettings(DebugUI* inUI, UIElement* inSubMenu) { /* Nothing by default */ }
// Add test configuration settings
virtual void AddConfigurationSettings(DebugUI *inUI, UIElement *inSubMenu) { /* Nothing by default */ }
// Draw the character + padding
void DrawPaddedCharacter(const Shape *inShape, float inPadding, RMat44Arg inCenterOfMass);
// Character size
static constexpr float cCharacterHeightStanding = 1.35f;
static constexpr float cCharacterRadiusStanding = 0.3f;
static constexpr float cCharacterHeightCrouching = 0.8f;
static constexpr float cCharacterRadiusCrouching = 0.3f;
static constexpr float cInnerShapeFraction = 0.9f;
// Character movement properties
inline static bool sControlMovementDuringJump = true; ///< If false the character cannot change movement direction in mid air
inline static float sCharacterSpeed = 6.0f;
inline static float sJumpSpeed = 4.0f;
// The different stances for the character
RefConst<Shape> mStandingShape;
RefConst<Shape> mCrouchingShape;
RefConst<Shape> mInnerCrouchingShape;
RefConst<Shape> mInnerStandingShape;
// List of boxes on ramp
Array<BodyID> mRampBlocks;
float mRampBlocksTimeLeft = 0.0f;
// Conveyor belt body
BodyID mConveyorBeltBody;
// Sensor body
BodyID mSensorBody;
// List of active characters in the scene so they can collide
CharacterVsCharacterCollisionSimple mCharacterVsCharacterCollision;
// Shape types
enum class EType
{
Capsule,
Cylinder,
Box,
Compound
};
// Character shape type
static inline EType sShapeType = EType::Capsule;
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;
// The camera pivot, recorded before the physics update to align with the drawn world
RVec3 mCameraPivot = RVec3::sZero();
// Moving bodies
BodyID mRotatingBody;
BodyID mRotatingWallBody;
BodyID mRotatingAndTranslatingBody;
BodyID mSmoothVerticallyMovingBody;
BodyID mReversingVerticallyMovingBody;
float mReversingVerticallyMovingVelocity = 1.0f;
BodyID mHorizontallyMovingBody;
// Moving characters
Ref<Character> mAnimatedCharacter;
Ref<CharacterVirtual> mAnimatedCharacterVirtual;
Ref<CharacterVirtual> mAnimatedCharacterVirtualWithInnerBody;
// Player input
Vec3 mControlInput = Vec3::sZero();
bool mJump = false;
bool mWasJump = false;
bool mSwitchStance = false;
bool mWasSwitchStance = false;
};

View File

@@ -0,0 +1,204 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Character/CharacterPlanetTest.h>
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Layers.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(CharacterPlanetTest)
{
JPH_ADD_BASE_CLASS(CharacterPlanetTest, Test)
}
void CharacterPlanetTest::Initialize()
{
// Create planet
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new SphereShape(cPlanetRadius), RVec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
// Create spheres
BodyCreationSettings sphere(new SphereShape(0.5f), RVec3::sZero(), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
sphere.mGravityFactor = 0.0f; // We do our own gravity
sphere.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
sphere.mMassPropertiesOverride.mMass = 10.0f;
sphere.mAngularDamping = 0.5f;
default_random_engine random;
for (int i = 0; i < 200; ++i)
{
uniform_real_distribution<float> theta(0, JPH_PI);
uniform_real_distribution<float> phi(0, 2 * JPH_PI);
sphere.mPosition = RVec3(1.1f * cPlanetRadius * Vec3::sUnitSpherical(theta(random), phi(random)));
mBodyInterface->CreateAndAddBody(sphere, EActivation::Activate);
}
// Register to receive OnStep callbacks to apply gravity
mPhysicsSystem->AddStepListener(this);
// Create 'player' character
Ref<CharacterVirtualSettings> settings = new CharacterVirtualSettings();
settings->mShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding)).Create().Get();
settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
mCharacter = new CharacterVirtual(settings, RVec3(0, cPlanetRadius, 0), Quat::sIdentity(), 0, mPhysicsSystem);
mCharacter->SetListener(this);
}
void CharacterPlanetTest::ProcessInput(const ProcessInputParams &inParams)
{
// Determine controller input
Vec3 control_input = Vec3::sZero();
if (inParams.mKeyboard->IsKeyPressed(EKey::Left)) control_input.SetZ(-1);
if (inParams.mKeyboard->IsKeyPressed(EKey::Right)) control_input.SetZ(1);
if (inParams.mKeyboard->IsKeyPressed(EKey::Up)) control_input.SetX(1);
if (inParams.mKeyboard->IsKeyPressed(EKey::Down)) control_input.SetX(-1);
if (control_input != Vec3::sZero())
control_input = control_input.Normalized();
// Smooth the player input
mDesiredVelocity = 0.25f * control_input * cCharacterSpeed + 0.75f * mDesiredVelocity;
// Convert player input to world space
Vec3 up = mCharacter->GetUp();
Vec3 right = inParams.mCameraState.mForward.Cross(up).NormalizedOr(Vec3::sAxisZ());
Vec3 forward = up.Cross(right).NormalizedOr(Vec3::sAxisX());
mDesiredVelocityWS = right * mDesiredVelocity.GetZ() + forward * mDesiredVelocity.GetX();
// Check actions
mJump = inParams.mKeyboard->IsKeyPressedAndTriggered(EKey::RControl, mWasJump);
}
void CharacterPlanetTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
// Calculate up vector based on position on planet surface
Vec3 old_up = mCharacter->GetUp();
Vec3 up = Vec3(mCharacter->GetPosition()).Normalized();
mCharacter->SetUp(up);
// Rotate capsule so it points up relative to the planet surface
mCharacter->SetRotation((Quat::sFromTo(old_up, up) * mCharacter->GetRotation()).Normalized());
// Draw character pre update (the sim is also drawn pre update)
#ifdef JPH_DEBUG_RENDERER
mCharacter->GetShape()->Draw(mDebugRenderer, mCharacter->GetCenterOfMassTransform(), Vec3::sOne(), Color::sGreen, false, true);
#endif // JPH_DEBUG_RENDERER
// Determine new character velocity
Vec3 current_vertical_velocity = mCharacter->GetLinearVelocity().Dot(up) * up;
Vec3 ground_velocity = mCharacter->GetGroundVelocity();
Vec3 new_velocity;
if (mCharacter->GetGroundState() == CharacterVirtual::EGroundState::OnGround // If on ground
&& (current_vertical_velocity - ground_velocity).Dot(up) < 0.1f) // And not moving away from ground
{
// Assume velocity of ground when on ground
new_velocity = ground_velocity;
// Jump
if (mJump)
new_velocity += cJumpSpeed * up;
}
else
new_velocity = current_vertical_velocity;
// Apply gravity
Vec3 gravity = -up * mPhysicsSystem->GetGravity().Length();
new_velocity += gravity * inParams.mDeltaTime;
// Apply player input
new_velocity += mDesiredVelocityWS;
// Update character velocity
mCharacter->SetLinearVelocity(new_velocity);
// Update the character position
CharacterVirtual::ExtendedUpdateSettings update_settings;
mCharacter->ExtendedUpdate(inParams.mDeltaTime,
gravity,
update_settings,
mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING),
mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING),
{ },
{ },
*mTempAllocator);
}
void CharacterPlanetTest::GetInitialCamera(CameraState& ioState) const
{
ioState.mPos = RVec3(0, 0.5f, 0);
ioState.mForward = Vec3(1, -0.3f, 0).Normalized();
}
RMat44 CharacterPlanetTest::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));
RVec3 cam_pos = mCharacter->GetPosition() - 5.0f * (mCharacter->GetRotation() * fwd);
return RMat44::sRotationTranslation(mCharacter->GetRotation(), cam_pos);
}
void CharacterPlanetTest::SaveState(StateRecorder &inStream) const
{
mCharacter->SaveState(inStream);
// Save character up, it's not stored by default but we use it in this case update the rotation of the character
inStream.Write(mCharacter->GetUp());
}
void CharacterPlanetTest::RestoreState(StateRecorder &inStream)
{
mCharacter->RestoreState(inStream);
Vec3 up = mCharacter->GetUp();
inStream.Read(up);
mCharacter->SetUp(up);
}
void CharacterPlanetTest::SaveInputState(StateRecorder &inStream) const
{
inStream.Write(mDesiredVelocity);
inStream.Write(mDesiredVelocityWS);
inStream.Write(mJump);
}
void CharacterPlanetTest::RestoreInputState(StateRecorder &inStream)
{
inStream.Read(mDesiredVelocity);
inStream.Read(mDesiredVelocityWS);
inStream.Read(mJump);
}
void CharacterPlanetTest::OnStep(const PhysicsStepListenerContext &inContext)
{
// Use the length of the global gravity vector
float gravity = inContext.mPhysicsSystem->GetGravity().Length();
// We don't need to lock the bodies since they're already locked in the OnStep callback.
// Note that this means we're responsible for avoiding race conditions with other step listeners while accessing bodies.
// We know that this is safe because in this demo there's only one step listener.
const BodyLockInterface &body_interface = inContext.mPhysicsSystem->GetBodyLockInterfaceNoLock();
// Loop over all active bodies
BodyIDVector body_ids;
inContext.mPhysicsSystem->GetActiveBodies(EBodyType::RigidBody, body_ids);
for (const BodyID &id : body_ids)
{
BodyLockWrite lock(body_interface, id);
if (lock.Succeeded())
{
// Apply gravity towards the center of the planet
Body &body = lock.GetBody();
RVec3 position = body.GetPosition();
float mass = 1.0f / body.GetMotionProperties()->GetInverseMass();
body.AddForce(-gravity * mass * Vec3(position).Normalized());
}
}
}
void CharacterPlanetTest::OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings)
{
// We don't want the spheres to push the player character
ioSettings.mCanPushCharacter = false;
}

View File

@@ -0,0 +1,69 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Physics/Character/CharacterVirtual.h>
#include <Jolt/Physics/PhysicsStepListener.h>
class CharacterPlanetTest : public Test, public PhysicsStepListener, public CharacterContactListener
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, CharacterPlanetTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Demonstrates how to do custom gravity to simulate a character walking on a planet.";
}
// Initialize the test
virtual void Initialize() override;
// Process input
virtual void ProcessInput(const ProcessInputParams &inParams) 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 RMat44 GetCameraPivot(float inCameraHeading, float inCameraPitch) const override;
// Saving / restoring state for replay
virtual void SaveState(StateRecorder &inStream) const override;
virtual void RestoreState(StateRecorder &inStream) override;
// Saving / restoring controller input state for replay
virtual void SaveInputState(StateRecorder &inStream) const override;
virtual void RestoreInputState(StateRecorder &inStream) override;
// See: PhysicsStepListener
virtual void OnStep(const PhysicsStepListenerContext &inContext) override;
// See: CharacterContactListener
virtual void OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override;
private:
// Planet size
static constexpr float cPlanetRadius = 20.0f;
// Character size
static constexpr float cCharacterHeightStanding = 1.35f;
static constexpr float cCharacterRadiusStanding = 0.3f;
static constexpr float cCharacterSpeed = 6.0f;
static constexpr float cJumpSpeed = 4.0f;
// The 'player' character
Ref<CharacterVirtual> mCharacter;
// Player input
Vec3 mDesiredVelocity = Vec3::sZero();
Vec3 mDesiredVelocityWS = Vec3::sZero();
bool mJump = false;
bool mWasJump = false;
};

View File

@@ -0,0 +1,196 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Character/CharacterSpaceShipTest.h>
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Layers.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(CharacterSpaceShipTest)
{
JPH_ADD_BASE_CLASS(CharacterSpaceShipTest, Test)
}
void CharacterSpaceShipTest::Initialize()
{
// Dimensions of our space ship
constexpr float cSpaceShipHeight = 2.0f;
constexpr float cSpaceShipRingHeight = 0.2f;
constexpr float cSpaceShipRadius = 100.0f;
const RVec3 cShipInitialPosition(-25, 15, 0);
// Create floor for reference
CreateFloor();
// Create 'player' character
Ref<CharacterVirtualSettings> settings = new CharacterVirtualSettings();
settings->mShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding)).Create().Get();
settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
mCharacter = new CharacterVirtual(settings, cShipInitialPosition + Vec3(0, cSpaceShipHeight, 0), Quat::sIdentity(), 0, mPhysicsSystem);
mCharacter->SetListener(this);
// Create the space ship
StaticCompoundShapeSettings compound;
compound.SetEmbedded();
for (float h = cSpaceShipRingHeight; h < cSpaceShipHeight; h += cSpaceShipRingHeight)
compound.AddShape(Vec3::sZero(), Quat::sIdentity(), new CylinderShape(h, sqrt(Square(cSpaceShipRadius) - Square(cSpaceShipRadius - cSpaceShipHeight - cSpaceShipRingHeight + h))));
mSpaceShip = mBodyInterface->CreateAndAddBody(BodyCreationSettings(&compound, cShipInitialPosition, Quat::sIdentity(), EMotionType::Kinematic, Layers::MOVING), EActivation::Activate);
mSpaceShipPrevTransform = mBodyInterface->GetCenterOfMassTransform(mSpaceShip);
}
void CharacterSpaceShipTest::ProcessInput(const ProcessInputParams &inParams)
{
// Determine controller input
Vec3 control_input = Vec3::sZero();
if (inParams.mKeyboard->IsKeyPressed(EKey::Left)) control_input.SetZ(-1);
if (inParams.mKeyboard->IsKeyPressed(EKey::Right)) control_input.SetZ(1);
if (inParams.mKeyboard->IsKeyPressed(EKey::Up)) control_input.SetX(1);
if (inParams.mKeyboard->IsKeyPressed(EKey::Down)) control_input.SetX(-1);
if (control_input != Vec3::sZero())
control_input = control_input.Normalized();
// Calculate the desired velocity in local space to the ship based on the camera forward
RMat44 new_space_ship_transform = mBodyInterface->GetCenterOfMassTransform(mSpaceShip);
Vec3 cam_fwd = new_space_ship_transform.GetRotation().Multiply3x3Transposed(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;
// Smooth the player input in local space to the ship
mDesiredVelocity = 0.25f * control_input * cCharacterSpeed + 0.75f * mDesiredVelocity;
// Check actions
mJump = inParams.mKeyboard->IsKeyPressedAndTriggered(EKey::RControl, mWasJump);
}
void CharacterSpaceShipTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
// Update scene time
mTime += inParams.mDeltaTime;
// Update the character so it stays relative to the space ship
RMat44 new_space_ship_transform = mBodyInterface->GetCenterOfMassTransform(mSpaceShip);
mCharacter->SetPosition(new_space_ship_transform * mSpaceShipPrevTransform.Inversed() * mCharacter->GetPosition());
// Update the character rotation and its up vector to match the new up vector of the ship
mCharacter->SetUp(new_space_ship_transform.GetAxisY());
mCharacter->SetRotation(new_space_ship_transform.GetQuaternion());
// Draw character pre update (the sim is also drawn pre update)
// Note that we have first updated the position so that it matches the new position of the ship
#ifdef JPH_DEBUG_RENDERER
mCharacter->GetShape()->Draw(mDebugRenderer, mCharacter->GetCenterOfMassTransform(), Vec3::sOne(), Color::sGreen, false, true);
#endif // JPH_DEBUG_RENDERER
// Determine new character velocity
Vec3 current_vertical_velocity = mCharacter->GetLinearVelocity().Dot(mSpaceShipPrevTransform.GetAxisY()) * mCharacter->GetUp();
Vec3 ground_velocity = mCharacter->GetGroundVelocity();
Vec3 new_velocity;
if (mCharacter->GetGroundState() == 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 (mJump)
new_velocity += cJumpSpeed * mCharacter->GetUp();
}
else
new_velocity = current_vertical_velocity;
// Gravity always acts relative to the ship
Vec3 gravity = new_space_ship_transform.Multiply3x3(mPhysicsSystem->GetGravity());
new_velocity += gravity * inParams.mDeltaTime;
// Transform player input to world space
new_velocity += new_space_ship_transform.Multiply3x3(mDesiredVelocity);
// Update character velocity
mCharacter->SetLinearVelocity(new_velocity);
// Update the character position
CharacterVirtual::ExtendedUpdateSettings update_settings;
mCharacter->ExtendedUpdate(inParams.mDeltaTime,
gravity,
update_settings,
mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING),
mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING),
{ },
{ },
*mTempAllocator);
// Update previous transform
mSpaceShipPrevTransform = new_space_ship_transform;
// Calculate new velocity
UpdateShipVelocity();
}
void CharacterSpaceShipTest::UpdateShipVelocity()
{
// Make it a rocky ride...
mSpaceShipLinearVelocity = Vec3(Sin(mTime), 0, Cos(mTime)) * 50.0f;
mSpaceShipAngularVelocity = Vec3(Sin(2.0f * mTime), 1, Cos(2.0f * mTime)) * 0.5f;
mBodyInterface->SetLinearAndAngularVelocity(mSpaceShip, mSpaceShipLinearVelocity, mSpaceShipAngularVelocity);
}
void CharacterSpaceShipTest::GetInitialCamera(CameraState& ioState) const
{
// This will become the local space offset, look down the x axis and slightly down
ioState.mPos = RVec3::sZero();
ioState.mForward = Vec3(10.0f, -2.0f, 0).Normalized();
}
RMat44 CharacterSpaceShipTest::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 RMat44::sTranslation(mCharacter->GetPosition() + Vec3(0, cCharacterHeightStanding + cCharacterRadiusStanding, 0) - 5.0f * fwd);
}
void CharacterSpaceShipTest::SaveState(StateRecorder &inStream) const
{
mCharacter->SaveState(inStream);
inStream.Write(mTime);
inStream.Write(mSpaceShipPrevTransform);
}
void CharacterSpaceShipTest::RestoreState(StateRecorder &inStream)
{
mCharacter->RestoreState(inStream);
inStream.Read(mTime);
inStream.Read(mSpaceShipPrevTransform);
// Calculate new velocity
UpdateShipVelocity();
}
void CharacterSpaceShipTest::SaveInputState(StateRecorder &inStream) const
{
inStream.Write(mDesiredVelocity);
inStream.Write(mJump);
}
void CharacterSpaceShipTest::RestoreInputState(StateRecorder &inStream)
{
inStream.Read(mDesiredVelocity);
inStream.Read(mJump);
}
void CharacterSpaceShipTest::OnAdjustBodyVelocity(const CharacterVirtual *inCharacter, const Body &inBody2, Vec3 &ioLinearVelocity, Vec3 &ioAngularVelocity)
{
// Cancel out velocity of space ship, we move relative to this which means we don't feel any of the acceleration of the ship (= engage inertial dampeners!)
ioLinearVelocity -= mSpaceShipLinearVelocity;
ioAngularVelocity -= mSpaceShipAngularVelocity;
}

View File

@@ -0,0 +1,79 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
#include <Jolt/Physics/Character/CharacterVirtual.h>
class CharacterSpaceShipTest : public Test, public CharacterContactListener
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, CharacterSpaceShipTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Demonstrates how a character may walk around a fast moving/accelerating sci-fi space ship that is equipped with inertial dampeners.\n"
"Note that this is 'game physics' and not real physics, inertial dampeners only exist in the movies.\n"
"You can walk off the ship and remain attached to the ship. A proper implementation would detect this and detach the character.";
}
// Initialize the test
virtual void Initialize() override;
// Process input
virtual void ProcessInput(const ProcessInputParams &inParams) 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 RMat44 GetCameraPivot(float inCameraHeading, float inCameraPitch) const override;
// Saving / restoring state for replay
virtual void SaveState(StateRecorder &inStream) const override;
virtual void RestoreState(StateRecorder &inStream) override;
// Saving / restoring controller input state for replay
virtual void SaveInputState(StateRecorder &inStream) const override;
virtual void RestoreInputState(StateRecorder &inStream) override;
private:
// Calculate new ship velocity
void UpdateShipVelocity();
/// Callback to adjust the velocity of a body as seen by the character. Can be adjusted to e.g. implement a conveyor belt or an inertial dampener system of a sci-fi space ship.
virtual void OnAdjustBodyVelocity(const CharacterVirtual *inCharacter, const Body &inBody2, Vec3 &ioLinearVelocity, Vec3 &ioAngularVelocity) override;
// Character size
static constexpr float cCharacterHeightStanding = 1.35f;
static constexpr float cCharacterRadiusStanding = 0.3f;
static constexpr float cCharacterSpeed = 6.0f;
static constexpr float cJumpSpeed = 4.0f;
// The 'player' character
Ref<CharacterVirtual> mCharacter;
// The space ship
BodyID mSpaceShip;
// Previous frame space ship transform
RMat44 mSpaceShipPrevTransform;
// Space ship velocity
Vec3 mSpaceShipLinearVelocity;
Vec3 mSpaceShipAngularVelocity;
// Global time
float mTime = 0.0f;
// Player input
Vec3 mDesiredVelocity = Vec3::sZero();
bool mJump = false;
bool mWasJump = false;
};

View File

@@ -0,0 +1,123 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Character/CharacterTest.h>
#include <Layers.h>
#include <Renderer/DebugRendererImp.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(CharacterTest)
{
JPH_ADD_BASE_CLASS(CharacterTest, CharacterBaseTest)
}
static const float cCollisionTolerance = 0.05f;
CharacterTest::~CharacterTest()
{
mCharacter->RemoveFromPhysicsSystem();
}
void CharacterTest::Initialize()
{
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;
settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
mCharacter = new Character(settings, RVec3::sZero(), Quat::sIdentity(), 0, mPhysicsSystem);
mCharacter->AddToPhysicsSystem(EActivation::Activate);
}
void CharacterTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
CharacterBaseTest::PrePhysicsUpdate(inParams);
// 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::SaveState(StateRecorder &inStream) const
{
CharacterBaseTest::SaveState(inStream);
mCharacter->SaveState(inStream);
bool is_standing = mCharacter->GetShape() == mStandingShape;
inStream.Write(is_standing);
}
void CharacterTest::RestoreState(StateRecorder &inStream)
{
CharacterBaseTest::RestoreState(inStream);
mCharacter->RestoreState(inStream);
bool is_standing = mCharacter->GetShape() == mStandingShape; // Initialize variable for validation mode
inStream.Read(is_standing);
mCharacter->SetShape(is_standing? mStandingShape : mCrouchingShape, FLT_MAX);
}
void CharacterTest::HandleInput(Vec3Arg inMovementDirection, bool inJump, bool inSwitchStance, float inDeltaTime)
{
// Cancel movement in opposite direction of normal when touching something we can't walk up
Vec3 movement_direction = inMovementDirection;
Character::EGroundState ground_state = mCharacter->GetGroundState();
if (ground_state == Character::EGroundState::OnSteepGround
|| ground_state == Character::EGroundState::NotSupported)
{
Vec3 normal = mCharacter->GetGroundNormal();
normal.SetY(0.0f);
float dot = normal.Dot(movement_direction);
if (dot < 0.0f)
movement_direction -= (dot * normal) / normal.LengthSq();
}
// Stance switch
if (inSwitchStance)
mCharacter->SetShape(mCharacter->GetShape() == mStandingShape? mCrouchingShape : mStandingShape, 1.5f * mPhysicsSystem->GetPhysicsSettings().mPenetrationSlop);
if (sControlMovementDuringJump || mCharacter->IsSupported())
{
// Update velocity
Vec3 current_velocity = mCharacter->GetLinearVelocity();
Vec3 desired_velocity = sCharacterSpeed * movement_direction;
if (!desired_velocity.IsNearZero() || current_velocity.GetY() < 0.0f || !mCharacter->IsSupported())
desired_velocity.SetY(current_velocity.GetY());
Vec3 new_velocity = 0.75f * current_velocity + 0.25f * desired_velocity;
// Jump
if (inJump && ground_state == Character::EGroundState::OnGround)
new_velocity += Vec3(0, sJumpSpeed, 0);
// Update the velocity
mCharacter->SetLinearVelocity(new_velocity);
}
}
void CharacterTest::OnContactAdded(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings)
{
// Draw a box around the character when it enters the sensor
if (inBody1.GetID() == mSensorBody)
mDebugRenderer->DrawBox(inBody2.GetWorldSpaceBounds(), Color::sGreen, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
else if (inBody2.GetID() == mSensorBody)
mDebugRenderer->DrawBox(inBody1.GetWorldSpaceBounds(), Color::sGreen, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
}
void CharacterTest::OnContactPersisted(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings)
{
// Same behavior as contact added
OnContactAdded(inBody1, inBody2, inManifold, ioSettings);
}

View File

@@ -0,0 +1,54 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Character/CharacterBaseTest.h>
class CharacterTest : public CharacterBaseTest, public ContactListener
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, CharacterTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Shows the Character class. Move around with the arrow keys, Shift for crouch and Ctrl for jump.\n"
"Note that most games should use CharacterVirtual instead of the Character class.";
}
// Destructor
virtual ~CharacterTest() override;
// Initialize the test
virtual void Initialize() override;
// Update the test, called before the physics update
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
// Update the test, called after the physics update
virtual void PostPhysicsUpdate(float inDeltaTime) override;
// Saving / restoring state for replay
virtual void SaveState(StateRecorder &inStream) const override;
virtual void RestoreState(StateRecorder &inStream) override;
// If this test implements a contact listener, it should be returned here
virtual ContactListener *GetContactListener() override { return this; }
// See: ContactListener
virtual void OnContactAdded(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override;
virtual void OnContactPersisted(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override;
protected:
// Get position of the character
virtual RVec3 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:
// The 'player' character
Ref<Character> mCharacter;
};

View File

@@ -0,0 +1,388 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// 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 <Utils/Log.h>
#include <Renderer/DebugRendererImp.h>
#include <Application/DebugUI.h>
//#define CHARACTER_TRACE_CONTACTS
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->mBackFaceMode = sBackFaceMode;
settings->mCharacterPadding = sCharacterPadding;
settings->mPenetrationRecoverySpeed = sPenetrationRecoverySpeed;
settings->mPredictiveContactDistance = sPredictiveContactDistance;
settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
settings->mEnhancedInternalEdgeRemoval = sEnhancedInternalEdgeRemoval;
settings->mInnerBodyShape = sCreateInnerBody? mInnerStandingShape : nullptr;
settings->mInnerBodyLayer = Layers::MOVING;
mCharacter = new CharacterVirtual(settings, RVec3::sZero(), Quat::sIdentity(), 0, mPhysicsSystem);
mCharacter->SetCharacterVsCharacterCollision(&mCharacterVsCharacterCollision);
mCharacterVsCharacterCollision.Add(mCharacter);
// Install contact listener for all characters
for (CharacterVirtual *character : mCharacterVsCharacterCollision.mCharacters)
character->SetListener(this);
// Draw labels on ramp blocks
for (size_t i = 0; i < mRampBlocks.size(); ++i)
SetBodyLabel(mRampBlocks[i], StringFormat("PushesPlayer: %s\nPushable: %s", (i & 1) != 0? "True" : "False", (i & 2) != 0? "True" : "False"));
}
void CharacterVirtualTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
CharacterBaseTest::PrePhysicsUpdate(inParams);
// Draw character pre update (the sim is also drawn pre update)
RMat44 com = mCharacter->GetCenterOfMassTransform();
RMat44 world_transform = mCharacter->GetWorldTransform();
#ifdef JPH_DEBUG_RENDERER
mCharacter->GetShape()->Draw(mDebugRenderer, com, Vec3::sOne(), Color::sGreen, false, true);
#endif // JPH_DEBUG_RENDERER
DrawPaddedCharacter(mCharacter->GetShape(), mCharacter->GetCharacterPadding(), com);
// Remember old position
RVec3 old_position = mCharacter->GetPosition();
// Settings for our update function
CharacterVirtual::ExtendedUpdateSettings update_settings;
if (!sEnableStickToFloor)
update_settings.mStickToFloorStepDown = Vec3::sZero();
else
update_settings.mStickToFloorStepDown = -mCharacter->GetUp() * update_settings.mStickToFloorStepDown.Length();
if (!sEnableWalkStairs)
update_settings.mWalkStairsStepUp = Vec3::sZero();
else
update_settings.mWalkStairsStepUp = mCharacter->GetUp() * update_settings.mWalkStairsStepUp.Length();
// Update the character position
mCharacter->ExtendedUpdate(inParams.mDeltaTime,
-mCharacter->GetUp() * mPhysicsSystem->GetGravity().Length(),
update_settings,
mPhysicsSystem->GetDefaultBroadPhaseLayerFilter(Layers::MOVING),
mPhysicsSystem->GetDefaultLayerFilter(Layers::MOVING),
{ },
{ },
*mTempAllocator);
#ifdef JPH_ENABLE_ASSERTS
// Validate that our contact list is in sync with that of the character
// Note that compound shapes could be non convex so we may detect more contacts than have been reported by the character
// as the character only reports contacts as it is sliding through the world. If 2 sub shapes hit at the same time then
// most likely only one will be reported as it stops the character and prevents the 2nd one from being seen.
uint num_contacts = 0;
for (const CharacterVirtual::Contact &c : mCharacter->GetActiveContacts())
if (c.mHadCollision)
{
JPH_ASSERT(std::find(mActiveContacts.begin(), mActiveContacts.end(), c) != mActiveContacts.end());
num_contacts++;
}
JPH_ASSERT(sShapeType == EType::Compound? num_contacts >= mActiveContacts.size() : num_contacts == mActiveContacts.size());
#endif
// Calculate effective velocity
RVec3 new_position = mCharacter->GetPosition();
Vec3 velocity = Vec3(new_position - old_position) / inParams.mDeltaTime;
// Draw state of character
DrawCharacterState(mCharacter, world_transform, velocity);
}
void CharacterVirtualTest::HandleInput(Vec3Arg inMovementDirection, bool inJump, bool inSwitchStance, float inDeltaTime)
{
bool player_controls_horizontal_velocity = sControlMovementDuringJump || mCharacter->IsSupported();
if (player_controls_horizontal_velocity)
{
// Smooth the player input
mDesiredVelocity = sEnableCharacterInertia? 0.25f * inMovementDirection * sCharacterSpeed + 0.75f * mDesiredVelocity : inMovementDirection * sCharacterSpeed;
// True if the player intended to move
mAllowSliding = !inMovementDirection.IsNearZero();
}
else
{
// While in air we allow sliding
mAllowSliding = true;
}
// Update the character rotation and its up vector to match the up vector set by the user settings
Quat character_up_rotation = Quat::sEulerAngles(Vec3(sUpRotationX, 0, sUpRotationZ));
mCharacter->SetUp(character_up_rotation.RotateAxisY());
mCharacter->SetRotation(character_up_rotation);
// A cheaper way to update the character's ground velocity,
// the platforms that the character is standing on may have changed velocity
mCharacter->UpdateGroundVelocity();
// Determine new basic velocity
Vec3 current_vertical_velocity = mCharacter->GetLinearVelocity().Dot(mCharacter->GetUp()) * mCharacter->GetUp();
Vec3 ground_velocity = mCharacter->GetGroundVelocity();
Vec3 new_velocity;
bool moving_towards_ground = (current_vertical_velocity.GetY() - ground_velocity.GetY()) < 0.1f;
if (mCharacter->GetGroundState() == CharacterVirtual::EGroundState::OnGround // If on ground
&& (sEnableCharacterInertia?
moving_towards_ground // Inertia enabled: And not moving away from ground
: !mCharacter->IsSlopeTooSteep(mCharacter->GetGroundNormal()))) // Inertia disabled: And not on a slope that is too steep
{
// Assume velocity of ground when on ground
new_velocity = ground_velocity;
// Jump
if (inJump && moving_towards_ground)
new_velocity += sJumpSpeed * mCharacter->GetUp();
}
else
new_velocity = current_vertical_velocity;
// Gravity
new_velocity += (character_up_rotation * mPhysicsSystem->GetGravity()) * inDeltaTime;
if (player_controls_horizontal_velocity)
{
// Player input
new_velocity += character_up_rotation * mDesiredVelocity;
}
else
{
// Preserve horizontal velocity
Vec3 current_horizontal_velocity = mCharacter->GetLinearVelocity() - current_vertical_velocity;
new_velocity += current_horizontal_velocity;
}
// Update character velocity
mCharacter->SetLinearVelocity(new_velocity);
// Stance switch
if (inSwitchStance)
{
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)
{
inUI->CreateCheckBox(inSubMenu, "Enable Character Inertia", sEnableCharacterInertia, [](UICheckBox::EState inState) { sEnableCharacterInertia = inState == UICheckBox::STATE_CHECKED; });
inUI->CreateCheckBox(inSubMenu, "Player Can Push Other Virtual Characters", sPlayerCanPushOtherCharacters, [](UICheckBox::EState inState) { sPlayerCanPushOtherCharacters = inState == UICheckBox::STATE_CHECKED; });
inUI->CreateCheckBox(inSubMenu, "Other Virtual Characters Can Push Player", sOtherCharactersCanPushPlayer, [](UICheckBox::EState inState) { sOtherCharactersCanPushPlayer = inState == UICheckBox::STATE_CHECKED; });
}
void CharacterVirtualTest::AddConfigurationSettings(DebugUI *inUI, UIElement *inSubMenu)
{
inUI->CreateComboBox(inSubMenu, "Back Face Mode", { "Ignore", "Collide" }, (int)sBackFaceMode, [=](int inItem) { sBackFaceMode = (EBackFaceMode)inItem; });
inUI->CreateSlider(inSubMenu, "Up Rotation X (degrees)", RadiansToDegrees(sUpRotationX), -90.0f, 90.0f, 1.0f, [](float inValue) { sUpRotationX = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Up Rotation Z (degrees)", RadiansToDegrees(sUpRotationZ), -90.0f, 90.0f, 1.0f, [](float inValue) { sUpRotationZ = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Max Slope Angle (degrees)", RadiansToDegrees(sMaxSlopeAngle), 0.0f, 90.0f, 1.0f, [](float inValue) { sMaxSlopeAngle = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Max Strength (N)", sMaxStrength, 0.0f, 500.0f, 1.0f, [](float inValue) { sMaxStrength = inValue; });
inUI->CreateSlider(inSubMenu, "Character Padding", sCharacterPadding, 0.01f, 0.5f, 0.01f, [](float inValue) { sCharacterPadding = inValue; });
inUI->CreateSlider(inSubMenu, "Penetration Recovery Speed", sPenetrationRecoverySpeed, 0.0f, 1.0f, 0.05f, [](float inValue) { sPenetrationRecoverySpeed = inValue; });
inUI->CreateSlider(inSubMenu, "Predictive Contact Distance", sPredictiveContactDistance, 0.01f, 1.0f, 0.01f, [](float inValue) { sPredictiveContactDistance = inValue; });
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, "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
{
CharacterBaseTest::SaveState(inStream);
mCharacter->SaveState(inStream);
bool is_standing = mCharacter->GetShape() == mStandingShape;
inStream.Write(is_standing);
inStream.Write(mAllowSliding);
inStream.Write(mDesiredVelocity);
inStream.Write(mActiveContacts);
}
void CharacterVirtualTest::RestoreState(StateRecorder &inStream)
{
CharacterBaseTest::RestoreState(inStream);
mCharacter->RestoreState(inStream);
bool is_standing = mCharacter->GetShape() == mStandingShape; // Initialize variable for validation mode
inStream.Read(is_standing);
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(mDesiredVelocity);
inStream.Read(mActiveContacts);
}
void CharacterVirtualTest::OnAdjustBodyVelocity(const CharacterVirtual *inCharacter, const Body &inBody2, Vec3 &ioLinearVelocity, Vec3 &ioAngularVelocity)
{
// Apply artificial velocity to the character when standing on the conveyor belt
if (inBody2.GetID() == mConveyorBeltBody)
ioLinearVelocity += Vec3(0, 0, 2);
}
void CharacterVirtualTest::OnContactCommon(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings)
{
// Draw a box around the character when it enters the sensor
if (inBodyID2 == mSensorBody)
{
AABox box = inCharacter->GetShape()->GetWorldSpaceBounds(inCharacter->GetCenterOfMassTransform(), Vec3::sOne());
mDebugRenderer->DrawBox(box, Color::sGreen, DebugRenderer::ECastShadow::Off, DebugRenderer::EDrawMode::Wireframe);
}
// Dynamic boxes on the ramp go through all permutations
Array<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;
}
// If we encounter an object that can push the player, enable sliding
if (inCharacter == mCharacter
&& ioSettings.mCanPushCharacter
&& mPhysicsSystem->GetBodyInterface().GetMotionType(inBodyID2) != EMotionType::Static)
mAllowSliding = true;
}
void CharacterVirtualTest::OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings)
{
OnContactCommon(inCharacter, inBodyID2, inSubShapeID2, inContactPosition, inContactNormal, ioSettings);
if (inCharacter == mCharacter)
{
#ifdef CHARACTER_TRACE_CONTACTS
Trace("Contact added with body %08x, sub shape %08x", inBodyID2.GetIndexAndSequenceNumber(), inSubShapeID2.GetValue());
#endif
CharacterVirtual::ContactKey c(inBodyID2, inSubShapeID2);
if (std::find(mActiveContacts.begin(), mActiveContacts.end(), c) != mActiveContacts.end())
FatalError("Got an add contact that should have been a persisted contact");
mActiveContacts.push_back(c);
}
}
void CharacterVirtualTest::OnContactPersisted(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings)
{
OnContactCommon(inCharacter, inBodyID2, inSubShapeID2, inContactPosition, inContactNormal, ioSettings);
if (inCharacter == mCharacter)
{
#ifdef CHARACTER_TRACE_CONTACTS
Trace("Contact persisted with body %08x, sub shape %08x", inBodyID2.GetIndexAndSequenceNumber(), inSubShapeID2.GetValue());
#endif
if (std::find(mActiveContacts.begin(), mActiveContacts.end(), CharacterVirtual::ContactKey(inBodyID2, inSubShapeID2)) == mActiveContacts.end())
FatalError("Got a persisted contact that should have been an add contact");
}
}
void CharacterVirtualTest::OnContactRemoved(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2)
{
if (inCharacter == mCharacter)
{
#ifdef CHARACTER_TRACE_CONTACTS
Trace("Contact removed with body %08x, sub shape %08x", inBodyID2.GetIndexAndSequenceNumber(), inSubShapeID2.GetValue());
#endif
ContactSet::iterator it = std::find(mActiveContacts.begin(), mActiveContacts.end(), CharacterVirtual::ContactKey(inBodyID2, inSubShapeID2));
if (it == mActiveContacts.end())
FatalError("Got a remove contact that has not been added");
mActiveContacts.erase(it);
}
}
void CharacterVirtualTest::OnCharacterContactCommon(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings)
{
// Characters can only be pushed in their own update
if (sPlayerCanPushOtherCharacters)
ioSettings.mCanPushCharacter = sOtherCharactersCanPushPlayer || inOtherCharacter == mCharacter;
else if (sOtherCharactersCanPushPlayer)
ioSettings.mCanPushCharacter = inCharacter == mCharacter;
else
ioSettings.mCanPushCharacter = false;
// If the player can be pushed by the other virtual character, we allow sliding
if (inCharacter == mCharacter && ioSettings.mCanPushCharacter)
mAllowSliding = true;
}
void CharacterVirtualTest::OnCharacterContactAdded(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings)
{
OnCharacterContactCommon(inCharacter, inOtherCharacter, inSubShapeID2, inContactPosition, inContactNormal, ioSettings);
if (inCharacter == mCharacter)
{
#ifdef CHARACTER_TRACE_CONTACTS
Trace("Contact added with character %08x, sub shape %08x", inOtherCharacter->GetID().GetValue(), inSubShapeID2.GetValue());
#endif
CharacterVirtual::ContactKey c(inOtherCharacter->GetID(), inSubShapeID2);
if (std::find(mActiveContacts.begin(), mActiveContacts.end(), c) != mActiveContacts.end())
FatalError("Got an add contact that should have been a persisted contact");
mActiveContacts.push_back(c);
}
}
void CharacterVirtualTest::OnCharacterContactPersisted(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings)
{
OnCharacterContactCommon(inCharacter, inOtherCharacter, inSubShapeID2, inContactPosition, inContactNormal, ioSettings);
if (inCharacter == mCharacter)
{
#ifdef CHARACTER_TRACE_CONTACTS
Trace("Contact persisted with character %08x, sub shape %08x", inOtherCharacter->GetID().GetValue(), inSubShapeID2.GetValue());
#endif
if (std::find(mActiveContacts.begin(), mActiveContacts.end(), CharacterVirtual::ContactKey(inOtherCharacter->GetID(), inSubShapeID2)) == mActiveContacts.end())
FatalError("Got a persisted contact that should have been an add contact");
}
}
void CharacterVirtualTest::OnCharacterContactRemoved(const CharacterVirtual *inCharacter, const CharacterID &inOtherCharacterID, const SubShapeID &inSubShapeID2)
{
if (inCharacter == mCharacter)
{
#ifdef CHARACTER_TRACE_CONTACTS
Trace("Contact removed with character %08x, sub shape %08x", inOtherCharacterID.GetValue(), inSubShapeID2.GetValue());
#endif
ContactSet::iterator it = std::find(mActiveContacts.begin(), mActiveContacts.end(), CharacterVirtual::ContactKey(inOtherCharacterID, inSubShapeID2));
if (it == mActiveContacts.end())
FatalError("Got a remove contact that has not been added");
mActiveContacts.erase(it);
}
}
void CharacterVirtualTest::OnContactSolve(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, Vec3Arg inContactVelocity, const PhysicsMaterial *inContactMaterial, Vec3Arg inCharacterVelocity, Vec3 &ioNewCharacterVelocity)
{
// Ignore callbacks for other characters than the player
if (inCharacter != mCharacter)
return;
// Don't allow the player to slide down static not-too-steep surfaces when not actively moving and when not on a moving platform
if (!mAllowSliding && inContactVelocity.IsNearZero() && !inCharacter->IsSlopeTooSteep(inContactNormal))
ioNewCharacterVelocity = Vec3::sZero();
}

View File

@@ -0,0 +1,103 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Character/CharacterBaseTest.h>
class CharacterVirtualTest : public CharacterBaseTest, public CharacterContactListener
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, CharacterVirtualTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Shows the CharacterVirtual class. Move around with the arrow keys, Shift for crouch and Ctrl for jump.";
}
// Initialize the test
virtual void Initialize() override;
// Update the test, called before the physics update
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
// Saving / restoring state for replay
virtual void SaveState(StateRecorder &inStream) const override;
virtual void RestoreState(StateRecorder &inStream) override;
/// Callback to adjust the velocity of a body as seen by the character. Can be adjusted to e.g. implement a conveyor belt or an inertial dampener system of a sci-fi space ship.
virtual void OnAdjustBodyVelocity(const CharacterVirtual *inCharacter, const Body &inBody2, Vec3 &ioLinearVelocity, Vec3 &ioAngularVelocity) override;
// Called whenever the character collides with a body.
virtual void OnContactAdded(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override;
// Called whenever the character persists colliding with a body.
virtual void OnContactPersisted(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override;
// Called whenever the character loses contact with a body.
virtual void OnContactRemoved(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2) override;
// Called whenever the character collides with a virtual character.
virtual void OnCharacterContactAdded(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override;
// Called whenever the character persists colliding with a virtual character.
virtual void OnCharacterContactPersisted(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings) override;
// Called whenever the character loses contact with a virtual character.
virtual void OnCharacterContactRemoved(const CharacterVirtual *inCharacter, const CharacterID &inOtherCharacterID, const SubShapeID &inSubShapeID2) override;
// Called whenever the character movement is solved and a constraint is hit. Allows the listener to override the resulting character velocity (e.g. by preventing sliding along certain surfaces).
virtual void OnContactSolve(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, Vec3Arg inContactVelocity, const PhysicsMaterial *inContactMaterial, Vec3Arg inCharacterVelocity, Vec3 &ioNewCharacterVelocity) override;
protected:
// Common function to be called when contacts are added/persisted
void OnContactCommon(const CharacterVirtual *inCharacter, const BodyID &inBodyID2, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings);
void OnCharacterContactCommon(const CharacterVirtual *inCharacter, const CharacterVirtual *inOtherCharacter, const SubShapeID &inSubShapeID2, RVec3Arg inContactPosition, Vec3Arg inContactNormal, CharacterContactSettings &ioSettings);
// Get position of the character
virtual RVec3 GetCharacterPosition() const override { return mCharacter->GetPosition(); }
// Handle user input to the character
virtual void HandleInput(Vec3Arg inMovementDirection, bool inJump, bool inSwitchStance, float inDeltaTime) override;
// Add character movement settings
virtual void AddCharacterMovementSettings(DebugUI* inUI, UIElement* inSubMenu) override;
// Add test configuration settings
virtual void AddConfigurationSettings(DebugUI *inUI, UIElement *inSubMenu) override;
private:
// Character movement settings
static inline bool sEnableCharacterInertia = true;
// Test configuration settings
static inline EBackFaceMode sBackFaceMode = EBackFaceMode::CollideWithBackFaces;
static inline float sUpRotationX = 0;
static inline float sUpRotationZ = 0;
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;
static inline bool sEnableWalkStairs = true;
static inline bool sEnableStickToFloor = true;
static inline bool sEnhancedInternalEdgeRemoval = false;
static inline bool sCreateInnerBody = false;
static inline bool sPlayerCanPushOtherCharacters = true;
static inline bool sOtherCharactersCanPushPlayer = true;
// The 'player' character
Ref<CharacterVirtual> mCharacter;
// Smoothed value of the player input
Vec3 mDesiredVelocity = Vec3::sZero();
// True when the player is pressing movement controls
bool mAllowSliding = false;
// Track active contacts for debugging purposes
using ContactSet = Array<CharacterVirtual::ContactKey>;
ContactSet mActiveContacts;
};