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,276 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Vehicle/MotorcycleTest.h>
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/Collision/Shape/OffsetCenterOfMassShape.h>
#include <Jolt/Physics/Collision/ShapeCast.h>
#include <Jolt/Physics/Collision/CollisionCollectorImpl.h>
#include <Jolt/Physics/Vehicle/MotorcycleController.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Application/DebugUI.h>
#include <Layers.h>
#include <Renderer/DebugRendererImp.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(MotorcycleTest)
{
JPH_ADD_BASE_CLASS(MotorcycleTest, VehicleTest)
}
MotorcycleTest::~MotorcycleTest()
{
mPhysicsSystem->RemoveStepListener(mVehicleConstraint);
}
void MotorcycleTest::Initialize()
{
VehicleTest::Initialize();
// Loosely based on: https://www.whitedogbikes.com/whitedogblog/yamaha-xj-900-specs/
const float back_wheel_radius = 0.31f;
const float back_wheel_width = 0.05f;
const float back_wheel_pos_z = -0.75f;
const float back_suspension_min_length = 0.3f;
const float back_suspension_max_length = 0.5f;
const float back_suspension_freq = 2.0f;
const float back_brake_torque = 250.0f;
const float front_wheel_radius = 0.31f;
const float front_wheel_width = 0.05f;
const float front_wheel_pos_z = 0.75f;
const float front_suspension_min_length = 0.3f;
const float front_suspension_max_length = 0.5f;
const float front_suspension_freq = 1.5f;
const float front_brake_torque = 500.0f;
const float half_vehicle_length = 0.4f;
const float half_vehicle_width = 0.2f;
const float half_vehicle_height = 0.3f;
const float max_steering_angle = DegreesToRadians(30);
// Angle of the front suspension
const float caster_angle = DegreesToRadians(30);
// Create vehicle body
RVec3 position(0, 2, 0);
RefConst<Shape> motorcycle_shape = OffsetCenterOfMassShapeSettings(Vec3(0, -half_vehicle_height, 0), new BoxShape(Vec3(half_vehicle_width, half_vehicle_height, half_vehicle_length))).Create().Get();
BodyCreationSettings motorcycle_body_settings(motorcycle_shape, position, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
motorcycle_body_settings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
motorcycle_body_settings.mMassPropertiesOverride.mMass = 240.0f;
mMotorcycleBody = mBodyInterface->CreateBody(motorcycle_body_settings);
mBodyInterface->AddBody(mMotorcycleBody->GetID(), EActivation::Activate);
// Create vehicle constraint
VehicleConstraintSettings vehicle;
vehicle.mDrawConstraintSize = 0.1f;
vehicle.mMaxPitchRollAngle = DegreesToRadians(60.0f);
// Wheels
WheelSettingsWV *front = new WheelSettingsWV;
front->mPosition = Vec3(0.0f, -0.9f * half_vehicle_height, front_wheel_pos_z);
front->mMaxSteerAngle = max_steering_angle;
front->mSuspensionDirection = Vec3(0, -1, Tan(caster_angle)).Normalized();
front->mSteeringAxis = -front->mSuspensionDirection;
front->mRadius = front_wheel_radius;
front->mWidth = front_wheel_width;
front->mSuspensionMinLength = front_suspension_min_length;
front->mSuspensionMaxLength = front_suspension_max_length;
front->mSuspensionSpring.mFrequency = front_suspension_freq;
front->mMaxBrakeTorque = front_brake_torque;
WheelSettingsWV *back = new WheelSettingsWV;
back->mPosition = Vec3(0.0f, -0.9f * half_vehicle_height, back_wheel_pos_z);
back->mMaxSteerAngle = 0.0f;
back->mRadius = back_wheel_radius;
back->mWidth = back_wheel_width;
back->mSuspensionMinLength = back_suspension_min_length;
back->mSuspensionMaxLength = back_suspension_max_length;
back->mSuspensionSpring.mFrequency = back_suspension_freq;
back->mMaxBrakeTorque = back_brake_torque;
if (sOverrideFrontSuspensionForcePoint)
{
front->mEnableSuspensionForcePoint = true;
front->mSuspensionForcePoint = front->mPosition + front->mSuspensionDirection * front->mSuspensionMinLength;
}
if (sOverrideRearSuspensionForcePoint)
{
back->mEnableSuspensionForcePoint = true;
back->mSuspensionForcePoint = back->mPosition + back->mSuspensionDirection * back->mSuspensionMinLength;
}
vehicle.mWheels = { front, back };
MotorcycleControllerSettings *controller = new MotorcycleControllerSettings;
controller->mEngine.mMaxTorque = 150.0f;
controller->mEngine.mMinRPM = 1000.0f;
controller->mEngine.mMaxRPM = 10000.0f;
controller->mTransmission.mShiftDownRPM = 2000.0f;
controller->mTransmission.mShiftUpRPM = 8000.0f;
controller->mTransmission.mGearRatios = { 2.27f, 1.63f, 1.3f, 1.09f, 0.96f, 0.88f }; // From: https://www.blocklayer.com/rpm-gear-bikes
controller->mTransmission.mReverseGearRatios = { -4.0f };
controller->mTransmission.mClutchStrength = 2.0f;
vehicle.mController = controller;
// Differential (not really applicable to a motorcycle but we need one anyway to drive it)
controller->mDifferentials.resize(1);
controller->mDifferentials[0].mLeftWheel = -1;
controller->mDifferentials[0].mRightWheel = 1;
controller->mDifferentials[0].mDifferentialRatio = 1.93f * 40.0f / 16.0f; // Combining primary and final drive (back divided by front sprockets) from: https://www.blocklayer.com/rpm-gear-bikes
mVehicleConstraint = new VehicleConstraint(*mMotorcycleBody, vehicle);
mVehicleConstraint->SetVehicleCollisionTester(new VehicleCollisionTesterCastCylinder(Layers::MOVING, 1.0f)); // Use half wheel width as convex radius so we get a rounded cylinder
mPhysicsSystem->AddConstraint(mVehicleConstraint);
mPhysicsSystem->AddStepListener(mVehicleConstraint);
UpdateCameraPivot();
}
void MotorcycleTest::ProcessInput(const ProcessInputParams &inParams)
{
// Determine acceleration and brake
mForward = 0.0f;
mBrake = 0.0f;
if (inParams.mKeyboard->IsKeyPressed(EKey::Z))
mBrake = 1.0f;
else if (inParams.mKeyboard->IsKeyPressed(EKey::Up))
mForward = 1.0f;
else if (inParams.mKeyboard->IsKeyPressed(EKey::Down))
mForward = -1.0f;
// Check if we're reversing direction
if (mPreviousForward * mForward < 0.0f)
{
// Get vehicle velocity in local space to the body of the vehicle
float velocity = (mMotorcycleBody->GetRotation().Conjugated() * mMotorcycleBody->GetLinearVelocity()).GetZ();
if ((mForward > 0.0f && velocity < -0.1f) || (mForward < 0.0f && velocity > 0.1f))
{
// Brake while we've not stopped yet
mForward = 0.0f;
mBrake = 1.0f;
}
else
{
// When we've come to a stop, accept the new direction
mPreviousForward = mForward;
}
}
// Steering
float right = 0.0f;
if (inParams.mKeyboard->IsKeyPressed(EKey::Left))
right = -1.0f;
else if (inParams.mKeyboard->IsKeyPressed(EKey::Right))
right = 1.0f;
const float steer_speed = 4.0f;
if (right > mRight)
mRight = min(mRight + steer_speed * inParams.mDeltaTime, right);
else if (right < mRight)
mRight = max(mRight - steer_speed * inParams.mDeltaTime, right);
// When leaned, we don't want to use the brakes fully as we'll spin out
if (mBrake > 0.0f)
{
Vec3 world_up = -mPhysicsSystem->GetGravity().Normalized();
Vec3 up = mMotorcycleBody->GetRotation() * mVehicleConstraint->GetLocalUp();
Vec3 fwd = mMotorcycleBody->GetRotation() * mVehicleConstraint->GetLocalForward();
float sin_lean_angle = abs(world_up.Cross(up).Dot(fwd));
float brake_multiplier = Square(1.0f - sin_lean_angle);
mBrake *= brake_multiplier;
}
}
void MotorcycleTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
VehicleTest::PrePhysicsUpdate(inParams);
UpdateCameraPivot();
// On user input, assure that the motorcycle is active
if (mRight != 0.0f || mForward != 0.0f || mBrake != 0.0f)
mBodyInterface->ActivateBody(mMotorcycleBody->GetID());
// Pass the input on to the constraint
MotorcycleController *controller = static_cast<MotorcycleController *>(mVehicleConstraint->GetController());
controller->SetDriverInput(mForward, mRight, mBrake, false);
controller->EnableLeanController(sEnableLeanController);
if (sOverrideGravity)
{
// When overriding gravity is requested, we cast a sphere downwards (opposite to the previous up position) and use the contact normal as the new gravity direction
SphereShape sphere(0.5f);
sphere.SetEmbedded();
RShapeCast shape_cast(&sphere, Vec3::sOne(), RMat44::sTranslation(mMotorcycleBody->GetPosition()), -3.0f * mVehicleConstraint->GetWorldUp());
ShapeCastSettings settings;
ClosestHitCollisionCollector<CastShapeCollector> collector;
mPhysicsSystem->GetNarrowPhaseQuery().CastShape(shape_cast, settings, mMotorcycleBody->GetPosition(), collector, SpecifiedBroadPhaseLayerFilter(BroadPhaseLayers::NON_MOVING), SpecifiedObjectLayerFilter(Layers::NON_MOVING));
if (collector.HadHit())
mVehicleConstraint->OverrideGravity(9.81f * collector.mHit.mPenetrationAxis.Normalized());
else
mVehicleConstraint->ResetGravityOverride();
}
// Draw our wheels (this needs to be done in the pre update since we draw the bodies too in the state before the step)
for (uint w = 0; w < 2; ++w)
{
const WheelSettings *settings = mVehicleConstraint->GetWheels()[w]->GetSettings();
RMat44 wheel_transform = mVehicleConstraint->GetWheelWorldTransform(w, Vec3::sAxisY(), Vec3::sAxisX()); // The cylinder we draw is aligned with Y so we specify that as rotational axis
mDebugRenderer->DrawCylinder(wheel_transform, 0.5f * settings->mWidth, settings->mRadius, Color::sGreen);
}
}
void MotorcycleTest::SaveInputState(StateRecorder &inStream) const
{
inStream.Write(mForward);
inStream.Write(mPreviousForward);
inStream.Write(mRight);
inStream.Write(mBrake);
}
void MotorcycleTest::RestoreInputState(StateRecorder &inStream)
{
inStream.Read(mForward);
inStream.Read(mPreviousForward);
inStream.Read(mRight);
inStream.Read(mBrake);
}
void MotorcycleTest::GetInitialCamera(CameraState &ioState) const
{
// Position camera behind motorcycle
RVec3 cam_tgt = RVec3(0, 0, 5);
ioState.mPos = RVec3(0, 2.5f, -5);
ioState.mForward = Vec3(cam_tgt - ioState.mPos).Normalized();
}
void MotorcycleTest::UpdateCameraPivot()
{
// Pivot is center of motorcycle and rotates with motorcycle around Y axis only
Vec3 fwd = mMotorcycleBody->GetRotation().RotateAxisZ();
fwd.SetY(0.0f);
float len = fwd.Length();
if (len != 0.0f)
fwd /= len;
else
fwd = Vec3::sAxisZ();
Vec3 up = Vec3::sAxisY();
Vec3 right = up.Cross(fwd);
mCameraPivot = RMat44(Vec4(right, 0), Vec4(up, 0), Vec4(fwd, 0), mMotorcycleBody->GetPosition());
}
void MotorcycleTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
{
VehicleTest::CreateSettingsMenu(inUI, inSubMenu);
inUI->CreateCheckBox(inSubMenu, "Override Front Suspension Force Point", sOverrideFrontSuspensionForcePoint, [](UICheckBox::EState inState) { sOverrideFrontSuspensionForcePoint = inState == UICheckBox::STATE_CHECKED; });
inUI->CreateCheckBox(inSubMenu, "Override Rear Suspension Force Point", sOverrideRearSuspensionForcePoint, [](UICheckBox::EState inState) { sOverrideRearSuspensionForcePoint = inState == UICheckBox::STATE_CHECKED; });
inUI->CreateCheckBox(inSubMenu, "Enable Lean Controller", sEnableLeanController, [](UICheckBox::EState inState) { sEnableLeanController = inState == UICheckBox::STATE_CHECKED; });
inUI->CreateCheckBox(inSubMenu, "Override Gravity", sOverrideGravity, [](UICheckBox::EState inState) { sOverrideGravity = inState == UICheckBox::STATE_CHECKED; });
inUI->CreateTextButton(inSubMenu, "Accept", [this]() { RestartTest(); });
}

View File

@@ -0,0 +1,54 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Vehicle/VehicleTest.h>
#include <Jolt/Physics/Vehicle/VehicleConstraint.h>
class MotorcycleTest : public VehicleTest
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, MotorcycleTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "This test shows how a motorcycle could be made with the vehicle constraint.\n"
"Use the arrow keys to drive. Z for hand brake.";
}
// Destructor
virtual ~MotorcycleTest() override;
// See: Test
virtual void Initialize() override;
virtual void ProcessInput(const ProcessInputParams &inParams) override;
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
virtual void SaveInputState(StateRecorder &inStream) const override;
virtual void RestoreInputState(StateRecorder &inStream) override;
virtual void GetInitialCamera(CameraState &ioState) const override;
virtual RMat44 GetCameraPivot(float inCameraHeading, float inCameraPitch) const override { return mCameraPivot; }
virtual void CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
private:
void UpdateCameraPivot();
static inline bool sOverrideFrontSuspensionForcePoint = false; ///< If true, the front suspension force point is overridden
static inline bool sOverrideRearSuspensionForcePoint = false; ///< If true, the rear suspension force point is overridden
static inline bool sEnableLeanController = true; ///< If true, the lean controller is enabled
static inline bool sOverrideGravity = false; ///< If true, gravity is overridden to always oppose the ground normal
Body * mMotorcycleBody; ///< The vehicle
Ref<VehicleConstraint> mVehicleConstraint; ///< The vehicle constraint
RMat44 mCameraPivot = RMat44::sIdentity(); ///< The camera pivot, recorded before the physics update to align with the drawn world
// Player input
float mForward = 0.0f;
float mPreviousForward = 1.0f; ///< Keeps track of last motorcycle direction so we know when to brake and when to accelerate
float mRight = 0.0f; ///< Keeps track of the current steering angle
float mBrake = 0.0f;
};

View File

@@ -0,0 +1,357 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Vehicle/TankTest.h>
#include <Jolt/Physics/Collision/CollisionCollectorImpl.h>
#include <Jolt/Physics/Collision/RayCast.h>
#include <Jolt/Physics/Collision/CastResult.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
#include <Jolt/Physics/Collision/Shape/OffsetCenterOfMassShape.h>
#include <Jolt/Physics/Vehicle/TrackedVehicleController.h>
#include <Jolt/Physics/Collision/GroupFilterTable.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Application/DebugUI.h>
#include <Layers.h>
#include <Renderer/DebugRendererImp.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(TankTest)
{
JPH_ADD_BASE_CLASS(TankTest, VehicleTest)
}
TankTest::~TankTest()
{
mPhysicsSystem->RemoveStepListener(mVehicleConstraint);
}
void TankTest::Initialize()
{
VehicleTest::Initialize();
const float wheel_radius = 0.3f;
const float wheel_width = 0.1f;
const float half_vehicle_length = 3.2f;
const float half_vehicle_width = 1.7f;
const float half_vehicle_height = 0.5f;
const float suspension_min_length = 0.3f;
const float suspension_max_length = 0.5f;
const float suspension_frequency = 1.0f;
const float half_turret_width = 1.4f;
const float half_turret_length = 2.0f;
const float half_turret_height = 0.4f;
const float half_barrel_length = 1.5f;
const float barrel_radius = 0.1f;
const float barrel_rotation_offset = 0.2f;
static Vec3 wheel_pos[] = {
Vec3(0.0f, -0.0f, 2.95f),
Vec3(0.0f, -0.3f, 2.1f),
Vec3(0.0f, -0.3f, 1.4f),
Vec3(0.0f, -0.3f, 0.7f),
Vec3(0.0f, -0.3f, 0.0f),
Vec3(0.0f, -0.3f, -0.7f),
Vec3(0.0f, -0.3f, -1.4f),
Vec3(0.0f, -0.3f, -2.1f),
Vec3(0.0f, -0.0f, -2.75f),
};
// Create filter to prevent body, turret and barrel from colliding
GroupFilter *filter = new GroupFilterTable;
// Create tank body
RVec3 body_position(0, 2, 0);
RefConst<Shape> tank_body_shape = OffsetCenterOfMassShapeSettings(Vec3(0, -half_vehicle_height, 0), new BoxShape(Vec3(half_vehicle_width, half_vehicle_height, half_vehicle_length))).Create().Get();
BodyCreationSettings tank_body_settings(tank_body_shape, body_position, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
tank_body_settings.mCollisionGroup.SetGroupFilter(filter);
tank_body_settings.mCollisionGroup.SetGroupID(0);
tank_body_settings.mCollisionGroup.SetSubGroupID(0);
tank_body_settings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
tank_body_settings.mMassPropertiesOverride.mMass = 4000.0f;
mTankBody = mBodyInterface->CreateBody(tank_body_settings);
mBodyInterface->AddBody(mTankBody->GetID(), EActivation::Activate);
// Create vehicle constraint
VehicleConstraintSettings vehicle;
vehicle.mDrawConstraintSize = 0.1f;
vehicle.mMaxPitchRollAngle = DegreesToRadians(60.0f);
TrackedVehicleControllerSettings *controller = new TrackedVehicleControllerSettings;
vehicle.mController = controller;
for (int t = 0; t < 2; ++t)
{
VehicleTrackSettings &track = controller->mTracks[t];
// Last wheel is driven wheel
track.mDrivenWheel = (uint)(vehicle.mWheels.size() + size(wheel_pos) - 1);
for (uint wheel = 0; wheel < size(wheel_pos); ++wheel)
{
WheelSettingsTV *w = new WheelSettingsTV;
w->mPosition = wheel_pos[wheel];
w->mPosition.SetX(t == 0? half_vehicle_width : -half_vehicle_width);
w->mRadius = wheel_radius;
w->mWidth = wheel_width;
w->mSuspensionMinLength = suspension_min_length;
w->mSuspensionMaxLength = wheel == 0 || wheel == size(wheel_pos) - 1? suspension_min_length : suspension_max_length;
w->mSuspensionSpring.mFrequency = suspension_frequency;
// Add the wheel to the vehicle
track.mWheels.push_back((uint)vehicle.mWheels.size());
vehicle.mWheels.push_back(w);
}
}
mVehicleConstraint = new VehicleConstraint(*mTankBody, vehicle);
mVehicleConstraint->SetVehicleCollisionTester(new VehicleCollisionTesterRay(Layers::MOVING));
#ifdef JPH_DEBUG_RENDERER
static_cast<TrackedVehicleController *>(mVehicleConstraint->GetController())->SetRPMMeter(Vec3(0, 2, 0), 0.5f);
#endif // JPH_DEBUG_RENDERER
mPhysicsSystem->AddConstraint(mVehicleConstraint);
mPhysicsSystem->AddStepListener(mVehicleConstraint);
// Create turret
RVec3 turret_position = body_position + Vec3(0, half_vehicle_height + half_turret_height, 0);
BodyCreationSettings turret_body_setings(new BoxShape(Vec3(half_turret_width, half_turret_height, half_turret_length)), turret_position, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
turret_body_setings.mCollisionGroup.SetGroupFilter(filter);
turret_body_setings.mCollisionGroup.SetGroupID(0);
turret_body_setings.mCollisionGroup.SetSubGroupID(0);
turret_body_setings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
turret_body_setings.mMassPropertiesOverride.mMass = 2000.0f;
mTurretBody = mBodyInterface->CreateBody(turret_body_setings);
mBodyInterface->AddBody(mTurretBody->GetID(), EActivation::Activate);
// Attach turret to body
HingeConstraintSettings turret_hinge;
turret_hinge.mPoint1 = turret_hinge.mPoint2 = body_position + Vec3(0, half_vehicle_height, 0);
turret_hinge.mHingeAxis1 = turret_hinge.mHingeAxis2 = Vec3::sAxisY();
turret_hinge.mNormalAxis1 = turret_hinge.mNormalAxis2 = Vec3::sAxisZ();
turret_hinge.mMotorSettings = MotorSettings(0.5f, 1.0f);
mTurretHinge = static_cast<HingeConstraint *>(turret_hinge.Create(*mTankBody, *mTurretBody));
mTurretHinge->SetMotorState(EMotorState::Position);
mPhysicsSystem->AddConstraint(mTurretHinge);
// Create barrel
RVec3 barrel_position = turret_position + Vec3(0, 0, half_turret_length + half_barrel_length - barrel_rotation_offset);
BodyCreationSettings barrel_body_setings(new CylinderShape(half_barrel_length, barrel_radius), barrel_position, Quat::sRotation(Vec3::sAxisX(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING);
barrel_body_setings.mCollisionGroup.SetGroupFilter(filter);
barrel_body_setings.mCollisionGroup.SetGroupID(0);
barrel_body_setings.mCollisionGroup.SetSubGroupID(0);
barrel_body_setings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
barrel_body_setings.mMassPropertiesOverride.mMass = 200.0f;
mBarrelBody = mBodyInterface->CreateBody(barrel_body_setings);
mBodyInterface->AddBody(mBarrelBody->GetID(), EActivation::Activate);
// Attach barrel to turret
HingeConstraintSettings barrel_hinge;
barrel_hinge.mPoint1 = barrel_hinge.mPoint2 = barrel_position - Vec3(0, 0, half_barrel_length);
barrel_hinge.mHingeAxis1 = barrel_hinge.mHingeAxis2 = -Vec3::sAxisX();
barrel_hinge.mNormalAxis1 = barrel_hinge.mNormalAxis2 = Vec3::sAxisZ();
barrel_hinge.mLimitsMin = DegreesToRadians(-10.0f);
barrel_hinge.mLimitsMax = DegreesToRadians(40.0f);
barrel_hinge.mMotorSettings = MotorSettings(10.0f, 1.0f);
mBarrelHinge = static_cast<HingeConstraint *>(barrel_hinge.Create(*mTurretBody, *mBarrelBody));
mBarrelHinge->SetMotorState(EMotorState::Position);
mPhysicsSystem->AddConstraint(mBarrelHinge);
// Update camera pivot
mCameraPivot = mTankBody->GetPosition();
}
void TankTest::ProcessInput(const ProcessInputParams &inParams)
{
const float min_velocity_pivot_turn = 1.0f;
// Determine acceleration and brake
mForward = 0.0f;
mBrake = 0.0f;
if (inParams.mKeyboard->IsKeyPressed(EKey::RShift))
mBrake = 1.0f;
else if (inParams.mKeyboard->IsKeyPressed(EKey::Up))
mForward = 1.0f;
else if (inParams.mKeyboard->IsKeyPressed(EKey::Down))
mForward = -1.0f;
// Steering
mLeftRatio = 1.0f;
mRightRatio = 1.0f;
float velocity = (mTankBody->GetRotation().Conjugated() * mTankBody->GetLinearVelocity()).GetZ();
if (inParams.mKeyboard->IsKeyPressed(EKey::Left))
{
if (mBrake == 0.0f && mForward == 0.0f && abs(velocity) < min_velocity_pivot_turn)
{
// Pivot turn
mLeftRatio = -1.0f;
mForward = 1.0f;
}
else
mLeftRatio = 0.6f;
}
else if (inParams.mKeyboard->IsKeyPressed(EKey::Right))
{
if (mBrake == 0.0f && mForward == 0.0f && abs(velocity) < min_velocity_pivot_turn)
{
// Pivot turn
mRightRatio = -1.0f;
mForward = 1.0f;
}
else
mRightRatio = 0.6f;
}
// Check if we're reversing direction
if (mPreviousForward * mForward < 0.0f)
{
// Get vehicle velocity in local space to the body of the vehicle
if ((mForward > 0.0f && velocity < -0.1f) || (mForward < 0.0f && velocity > 0.1f))
{
// Brake while we've not stopped yet
mForward = 0.0f;
mBrake = 1.0f;
}
else
{
// When we've come to a stop, accept the new direction
mPreviousForward = mForward;
}
}
// Cast ray to find target
RRayCast ray { inParams.mCameraState.mPos, 1000.0f * inParams.mCameraState.mForward };
RayCastSettings ray_settings;
ClosestHitCollisionCollector<CastRayCollector> collector;
IgnoreMultipleBodiesFilter body_filter;
body_filter.Reserve(3);
body_filter.IgnoreBody(mTankBody->GetID());
body_filter.IgnoreBody(mTurretBody->GetID());
body_filter.IgnoreBody(mBarrelBody->GetID());
mPhysicsSystem->GetNarrowPhaseQuery().CastRay(ray, ray_settings, collector, {}, {}, body_filter);
RVec3 hit_pos = collector.HadHit()? ray.GetPointOnRay(collector.mHit.mFraction) : ray.mOrigin + ray.mDirection;
mDebugRenderer->DrawMarker(hit_pos, Color::sGreen, 1.0f);
// Orient the turret towards the hit position
RMat44 turret_to_world = mTankBody->GetCenterOfMassTransform() * mTurretHinge->GetConstraintToBody1Matrix();
Vec3 hit_pos_in_turret = Vec3(turret_to_world.InversedRotationTranslation() * hit_pos);
mTurretHeading = ATan2(hit_pos_in_turret.GetZ(), hit_pos_in_turret.GetY());
// Orient barrel towards the hit position
RMat44 barrel_to_world = mTurretBody->GetCenterOfMassTransform() * mBarrelHinge->GetConstraintToBody1Matrix();
Vec3 hit_pos_in_barrel = Vec3(barrel_to_world.InversedRotationTranslation() * hit_pos);
mBarrelPitch = ATan2(hit_pos_in_barrel.GetZ(), hit_pos_in_barrel.GetY());
// If user wants to fire
mFire = inParams.mKeyboard->IsKeyPressed(EKey::Return);
}
void TankTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
VehicleTest::PrePhysicsUpdate(inParams);
const float bullet_radius = 0.061f; // 120 mm
const Vec3 bullet_pos = Vec3(0, 1.6f, 0);
const Vec3 bullet_velocity = Vec3(0, 400.0f, 0); // Normal exit velocities are around 1100-1700 m/s, use a lower variable as we have a limit to max velocity (See: https://tanks-encyclopedia.com/coldwar-usa-120mm-gun-tank-m1e1-abrams/)
const float bullet_mass = 40.0f; // Normal projectile weight is around 7 kg, use an increased value so the momentum is more realistic (with the lower exit velocity)
const float bullet_reload_time = 2.0f;
// Update camera pivot
mCameraPivot = mTankBody->GetPosition();
// Assure the tank stays active as we're controlling the turret with the mouse
mBodyInterface->ActivateBody(mTankBody->GetID());
// Pass the input on to the constraint
static_cast<TrackedVehicleController *>(mVehicleConstraint->GetController())->SetDriverInput(mForward, mLeftRatio, mRightRatio, mBrake);
mTurretHinge->SetTargetAngle(mTurretHeading);
mBarrelHinge->SetTargetAngle(mBarrelPitch);
// Update reload time
mReloadTime = max(0.0f, mReloadTime - inParams.mDeltaTime);
// Shoot bullet
if (mReloadTime == 0.0f && mFire)
{
// Create bullet
BodyCreationSettings bullet_creation_settings(new SphereShape(bullet_radius), mBarrelBody->GetCenterOfMassTransform() * bullet_pos, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
bullet_creation_settings.mMotionQuality = EMotionQuality::LinearCast;
bullet_creation_settings.mFriction = 1.0f;
bullet_creation_settings.mRestitution = 0.0f;
bullet_creation_settings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
bullet_creation_settings.mMassPropertiesOverride.mMass = bullet_mass;
Body *bullet = mBodyInterface->CreateBody(bullet_creation_settings);
bullet->SetLinearVelocity(mBarrelBody->GetRotation() * bullet_velocity);
mBodyInterface->AddBody(bullet->GetID(), EActivation::Activate);
// Start reloading
mReloadTime = bullet_reload_time;
// Apply opposite impulse to turret body
mBodyInterface->AddImpulse(mTurretBody->GetID(), -bullet->GetLinearVelocity() * bullet_mass);
}
// Draw our wheels (this needs to be done in the pre update since we draw the bodies too in the state before the step)
for (uint w = 0; w < mVehicleConstraint->GetWheels().size(); ++w)
{
const WheelSettings *settings = mVehicleConstraint->GetWheels()[w]->GetSettings();
RMat44 wheel_transform = mVehicleConstraint->GetWheelWorldTransform(w, Vec3::sAxisY(), Vec3::sAxisX()); // The cylinder we draw is aligned with Y so we specify that as rotational axis
mDebugRenderer->DrawCylinder(wheel_transform, 0.5f * settings->mWidth, settings->mRadius, Color::sGreen);
}
}
void TankTest::SaveState(StateRecorder &inStream) const
{
VehicleTest::SaveState(inStream);
inStream.Write(mReloadTime);
}
void TankTest::RestoreState(StateRecorder &inStream)
{
VehicleTest::RestoreState(inStream);
inStream.Read(mReloadTime);
}
void TankTest::SaveInputState(StateRecorder &inStream) const
{
inStream.Write(mForward);
inStream.Write(mPreviousForward);
inStream.Write(mLeftRatio);
inStream.Write(mRightRatio);
inStream.Write(mBrake);
inStream.Write(mTurretHeading);
inStream.Write(mBarrelPitch);
inStream.Write(mFire);
}
void TankTest::RestoreInputState(StateRecorder &inStream)
{
inStream.Read(mForward);
inStream.Read(mPreviousForward);
inStream.Read(mLeftRatio);
inStream.Read(mRightRatio);
inStream.Read(mBrake);
inStream.Read(mTurretHeading);
inStream.Read(mBarrelPitch);
inStream.Read(mFire);
}
void TankTest::GetInitialCamera(CameraState &ioState) const
{
// Position camera behind tank
ioState.mPos = RVec3(0, 4.0f, 0);
ioState.mForward = Vec3(0, -2.0f, 10.0f).Normalized();
}
RMat44 TankTest::GetCameraPivot(float inCameraHeading, float inCameraPitch) const
{
// Pivot is center of tank + a distance away from the tank 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 - 10.0f * fwd);
}

View File

@@ -0,0 +1,57 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Vehicle/VehicleTest.h>
#include <Jolt/Physics/Vehicle/VehicleConstraint.h>
#include <Jolt/Physics/Constraints/HingeConstraint.h>
class TankTest : public VehicleTest
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, TankTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Shows how a tank could be made with a vehicle constraint.\n"
"Use the arrow keys to drive. Shift to brake. Enter to fire.";
}
// Destructor
virtual ~TankTest() override;
// See: Test
virtual void Initialize() override;
virtual void ProcessInput(const ProcessInputParams &inParams) override;
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
virtual void SaveState(StateRecorder &inStream) const override;
virtual void RestoreState(StateRecorder &inStream) override;
virtual void SaveInputState(StateRecorder &inStream) const override;
virtual void RestoreInputState(StateRecorder &inStream) override;
virtual void GetInitialCamera(CameraState &ioState) const override;
virtual RMat44 GetCameraPivot(float inCameraHeading, float inCameraPitch) const override;
private:
Body * mTankBody; ///< The body of the tank
Body * mTurretBody; ///< The body of the turret of the tank
Body * mBarrelBody; ///< The body of the barrel of the tank
Ref<VehicleConstraint> mVehicleConstraint; ///< The vehicle constraint
Ref<HingeConstraint> mTurretHinge; ///< Hinge connecting tank body and turret
Ref<HingeConstraint> mBarrelHinge; ///< Hinge connecting tank turret and barrel
float mReloadTime = 0.0f; ///< How long it still takes to reload the main gun
RVec3 mCameraPivot = RVec3::sZero(); ///< The camera pivot, recorded before the physics update to align with the drawn world
// Player input
float mForward = 0.0f;
float mPreviousForward = 1.0f; ///< Keeps track of last car direction so we know when to brake and when to accelerate
float mLeftRatio = 0.0f;
float mRightRatio = 0.0f;
float mBrake = 0.0f;
float mTurretHeading = 0.0f;
float mBarrelPitch = 0.0f;
bool mFire = false;
};

View File

@@ -0,0 +1,347 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Vehicle/VehicleConstraintTest.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
#include <Jolt/Physics/Collision/Shape/OffsetCenterOfMassShape.h>
#include <Jolt/Physics/Collision/ShapeCast.h>
#include <Jolt/Physics/Collision/CollisionCollectorImpl.h>
#include <Jolt/Physics/Vehicle/WheeledVehicleController.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Application/DebugUI.h>
#include <Layers.h>
#include <Renderer/DebugRendererImp.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(VehicleConstraintTest)
{
JPH_ADD_BASE_CLASS(VehicleConstraintTest, VehicleTest)
}
VehicleConstraintTest::~VehicleConstraintTest()
{
mPhysicsSystem->RemoveStepListener(mVehicleConstraint);
}
void VehicleConstraintTest::Initialize()
{
VehicleTest::Initialize();
const float wheel_radius = 0.3f;
const float wheel_width = 0.1f;
const float half_vehicle_length = 2.0f;
const float half_vehicle_width = 0.9f;
const float half_vehicle_height = 0.2f;
// Create collision testers
mTesters[0] = new VehicleCollisionTesterRay(Layers::MOVING);
mTesters[1] = new VehicleCollisionTesterCastSphere(Layers::MOVING, 0.5f * wheel_width);
mTesters[2] = new VehicleCollisionTesterCastCylinder(Layers::MOVING);
// Create vehicle body
RVec3 position(0, 2, 0);
RefConst<Shape> car_shape = OffsetCenterOfMassShapeSettings(Vec3(0, -half_vehicle_height, 0), new BoxShape(Vec3(half_vehicle_width, half_vehicle_height, half_vehicle_length))).Create().Get();
BodyCreationSettings car_body_settings(car_shape, position, Quat::sRotation(Vec3::sAxisZ(), sInitialRollAngle), EMotionType::Dynamic, Layers::MOVING);
car_body_settings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
car_body_settings.mMassPropertiesOverride.mMass = 1500.0f;
mCarBody = mBodyInterface->CreateBody(car_body_settings);
mBodyInterface->AddBody(mCarBody->GetID(), EActivation::Activate);
// Create vehicle constraint
VehicleConstraintSettings vehicle;
vehicle.mDrawConstraintSize = 0.1f;
vehicle.mMaxPitchRollAngle = sMaxRollAngle;
// Suspension direction
Vec3 front_suspension_dir = Vec3(Tan(sFrontSuspensionSidewaysAngle), -1, Tan(sFrontSuspensionForwardAngle)).Normalized();
Vec3 front_steering_axis = Vec3(-Tan(sFrontKingPinAngle), 1, -Tan(sFrontCasterAngle)).Normalized();
Vec3 front_wheel_up = Vec3(Sin(sFrontCamber), Cos(sFrontCamber), 0);
Vec3 front_wheel_forward = Vec3(-Sin(sFrontToe), 0, Cos(sFrontToe));
Vec3 rear_suspension_dir = Vec3(Tan(sRearSuspensionSidewaysAngle), -1, Tan(sRearSuspensionForwardAngle)).Normalized();
Vec3 rear_steering_axis = Vec3(-Tan(sRearKingPinAngle), 1, -Tan(sRearCasterAngle)).Normalized();
Vec3 rear_wheel_up = Vec3(Sin(sRearCamber), Cos(sRearCamber), 0);
Vec3 rear_wheel_forward = Vec3(-Sin(sRearToe), 0, Cos(sRearToe));
Vec3 flip_x(-1, 1, 1);
// Wheels, left front
WheelSettingsWV *w1 = new WheelSettingsWV;
w1->mPosition = Vec3(half_vehicle_width, -0.9f * half_vehicle_height, half_vehicle_length - 2.0f * wheel_radius);
w1->mSuspensionDirection = front_suspension_dir;
w1->mSteeringAxis = front_steering_axis;
w1->mWheelUp = front_wheel_up;
w1->mWheelForward = front_wheel_forward;
w1->mSuspensionMinLength = sFrontSuspensionMinLength;
w1->mSuspensionMaxLength = sFrontSuspensionMaxLength;
w1->mSuspensionSpring.mFrequency = sFrontSuspensionFrequency;
w1->mSuspensionSpring.mDamping = sFrontSuspensionDamping;
w1->mMaxSteerAngle = sMaxSteeringAngle;
w1->mMaxHandBrakeTorque = 0.0f; // Front wheel doesn't have hand brake
// Right front
WheelSettingsWV *w2 = new WheelSettingsWV;
w2->mPosition = Vec3(-half_vehicle_width, -0.9f * half_vehicle_height, half_vehicle_length - 2.0f * wheel_radius);
w2->mSuspensionDirection = flip_x * front_suspension_dir;
w2->mSteeringAxis = flip_x * front_steering_axis;
w2->mWheelUp = flip_x * front_wheel_up;
w2->mWheelForward = flip_x * front_wheel_forward;
w2->mSuspensionMinLength = sFrontSuspensionMinLength;
w2->mSuspensionMaxLength = sFrontSuspensionMaxLength;
w2->mSuspensionSpring.mFrequency = sFrontSuspensionFrequency;
w2->mSuspensionSpring.mDamping = sFrontSuspensionDamping;
w2->mMaxSteerAngle = sMaxSteeringAngle;
w2->mMaxHandBrakeTorque = 0.0f; // Front wheel doesn't have hand brake
// Left rear
WheelSettingsWV *w3 = new WheelSettingsWV;
w3->mPosition = Vec3(half_vehicle_width, -0.9f * half_vehicle_height, -half_vehicle_length + 2.0f * wheel_radius);
w3->mSuspensionDirection = rear_suspension_dir;
w3->mSteeringAxis = rear_steering_axis;
w3->mWheelUp = rear_wheel_up;
w3->mWheelForward = rear_wheel_forward;
w3->mSuspensionMinLength = sRearSuspensionMinLength;
w3->mSuspensionMaxLength = sRearSuspensionMaxLength;
w3->mSuspensionSpring.mFrequency = sRearSuspensionFrequency;
w3->mSuspensionSpring.mDamping = sRearSuspensionDamping;
w3->mMaxSteerAngle = 0.0f;
// Right rear
WheelSettingsWV *w4 = new WheelSettingsWV;
w4->mPosition = Vec3(-half_vehicle_width, -0.9f * half_vehicle_height, -half_vehicle_length + 2.0f * wheel_radius);
w4->mSuspensionDirection = flip_x * rear_suspension_dir;
w4->mSteeringAxis = flip_x * rear_steering_axis;
w4->mWheelUp = flip_x * rear_wheel_up;
w4->mWheelForward = flip_x * rear_wheel_forward;
w4->mSuspensionMinLength = sRearSuspensionMinLength;
w4->mSuspensionMaxLength = sRearSuspensionMaxLength;
w4->mSuspensionSpring.mFrequency = sRearSuspensionFrequency;
w4->mSuspensionSpring.mDamping = sRearSuspensionDamping;
w4->mMaxSteerAngle = 0.0f;
vehicle.mWheels = { w1, w2, w3, w4 };
for (WheelSettings *w : vehicle.mWheels)
{
w->mRadius = wheel_radius;
w->mWidth = wheel_width;
}
WheeledVehicleControllerSettings *controller = new WheeledVehicleControllerSettings;
vehicle.mController = controller;
// Differential
controller->mDifferentials.resize(sFourWheelDrive? 2 : 1);
controller->mDifferentials[0].mLeftWheel = 0;
controller->mDifferentials[0].mRightWheel = 1;
if (sFourWheelDrive)
{
controller->mDifferentials[1].mLeftWheel = 2;
controller->mDifferentials[1].mRightWheel = 3;
// Split engine torque
controller->mDifferentials[0].mEngineTorqueRatio = controller->mDifferentials[1].mEngineTorqueRatio = 0.5f;
}
// Anti rollbars
if (sAntiRollbar)
{
vehicle.mAntiRollBars.resize(2);
vehicle.mAntiRollBars[0].mLeftWheel = 0;
vehicle.mAntiRollBars[0].mRightWheel = 1;
vehicle.mAntiRollBars[1].mLeftWheel = 2;
vehicle.mAntiRollBars[1].mRightWheel = 3;
}
mVehicleConstraint = new VehicleConstraint(*mCarBody, vehicle);
// The vehicle settings were tweaked with a buggy implementation of the longitudinal tire impulses, this meant that PhysicsSettings::mNumVelocitySteps times more impulse
// could be applied than intended. To keep the behavior of the vehicle the same we increase the max longitudinal impulse by the same factor. In a future version the vehicle
// will be retweaked.
static_cast<WheeledVehicleController *>(mVehicleConstraint->GetController())->SetTireMaxImpulseCallback(
[](uint, float &outLongitudinalImpulse, float &outLateralImpulse, float inSuspensionImpulse, float inLongitudinalFriction, float inLateralFriction, float, float, float)
{
outLongitudinalImpulse = 10.0f * inLongitudinalFriction * inSuspensionImpulse;
outLateralImpulse = inLateralFriction * inSuspensionImpulse;
});
mPhysicsSystem->AddConstraint(mVehicleConstraint);
mPhysicsSystem->AddStepListener(mVehicleConstraint);
UpdateCameraPivot();
}
void VehicleConstraintTest::ProcessInput(const ProcessInputParams &inParams)
{
// Determine acceleration and brake
mForward = 0.0f;
if (inParams.mKeyboard->IsKeyPressed(EKey::Up))
mForward = 1.0f;
else if (inParams.mKeyboard->IsKeyPressed(EKey::Down))
mForward = -1.0f;
// Check if we're reversing direction
mBrake = 0.0f;
if (mPreviousForward * mForward < 0.0f)
{
// Get vehicle velocity in local space to the body of the vehicle
float velocity = (mCarBody->GetRotation().Conjugated() * mCarBody->GetLinearVelocity()).GetZ();
if ((mForward > 0.0f && velocity < -0.1f) || (mForward < 0.0f && velocity > 0.1f))
{
// Brake while we've not stopped yet
mForward = 0.0f;
mBrake = 1.0f;
}
else
{
// When we've come to a stop, accept the new direction
mPreviousForward = mForward;
}
}
// Hand brake will cancel gas pedal
mHandBrake = 0.0f;
if (inParams.mKeyboard->IsKeyPressed(EKey::Z))
{
mForward = 0.0f;
mHandBrake = 1.0f;
}
// Steering
mRight = 0.0f;
if (inParams.mKeyboard->IsKeyPressed(EKey::Left))
mRight = -1.0f;
else if (inParams.mKeyboard->IsKeyPressed(EKey::Right))
mRight = 1.0f;
}
void VehicleConstraintTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
VehicleTest::PrePhysicsUpdate(inParams);
UpdateCameraPivot();
// On user input, assure that the car is active
if (mRight != 0.0f || mForward != 0.0f || mBrake != 0.0f || mHandBrake != 0.0f)
mBodyInterface->ActivateBody(mCarBody->GetID());
WheeledVehicleController *controller = static_cast<WheeledVehicleController *>(mVehicleConstraint->GetController());
// Update vehicle statistics
controller->GetEngine().mMaxTorque = sMaxEngineTorque;
controller->GetTransmission().mClutchStrength = sClutchStrength;
// Set slip ratios to the same for everything
float limited_slip_ratio = sLimitedSlipDifferentials? 1.4f : FLT_MAX;
controller->SetDifferentialLimitedSlipRatio(limited_slip_ratio);
for (VehicleDifferentialSettings &d : controller->GetDifferentials())
d.mLimitedSlipRatio = limited_slip_ratio;
// Pass the input on to the constraint
controller->SetDriverInput(mForward, mRight, mBrake, mHandBrake);
// Set the collision tester
mVehicleConstraint->SetVehicleCollisionTester(mTesters[sCollisionMode]);
if (sOverrideGravity)
{
// When overriding gravity is requested, we cast a sphere downwards (opposite to the previous up position) and use the contact normal as the new gravity direction
SphereShape sphere(0.5f);
sphere.SetEmbedded();
RShapeCast shape_cast(&sphere, Vec3::sOne(), RMat44::sTranslation(mCarBody->GetPosition()), -3.0f * mVehicleConstraint->GetWorldUp());
ShapeCastSettings settings;
ClosestHitCollisionCollector<CastShapeCollector> collector;
mPhysicsSystem->GetNarrowPhaseQuery().CastShape(shape_cast, settings, mCarBody->GetPosition(), collector, SpecifiedBroadPhaseLayerFilter(BroadPhaseLayers::NON_MOVING), SpecifiedObjectLayerFilter(Layers::NON_MOVING));
if (collector.HadHit())
mVehicleConstraint->OverrideGravity(9.81f * collector.mHit.mPenetrationAxis.Normalized());
else
mVehicleConstraint->ResetGravityOverride();
}
// Draw our wheels (this needs to be done in the pre update since we draw the bodies too in the state before the step)
for (uint w = 0; w < 4; ++w)
{
const WheelSettings *settings = mVehicleConstraint->GetWheels()[w]->GetSettings();
RMat44 wheel_transform = mVehicleConstraint->GetWheelWorldTransform(w, Vec3::sAxisY(), Vec3::sAxisX()); // The cylinder we draw is aligned with Y so we specify that as rotational axis
mDebugRenderer->DrawCylinder(wheel_transform, 0.5f * settings->mWidth, settings->mRadius, Color::sGreen);
}
}
void VehicleConstraintTest::SaveInputState(StateRecorder &inStream) const
{
inStream.Write(mForward);
inStream.Write(mPreviousForward);
inStream.Write(mRight);
inStream.Write(mBrake);
inStream.Write(mHandBrake);
}
void VehicleConstraintTest::RestoreInputState(StateRecorder &inStream)
{
inStream.Read(mForward);
inStream.Read(mPreviousForward);
inStream.Read(mRight);
inStream.Read(mBrake);
inStream.Read(mHandBrake);
}
void VehicleConstraintTest::GetInitialCamera(CameraState &ioState) const
{
// Position camera behind car
RVec3 cam_tgt = RVec3(0, 0, 5);
ioState.mPos = RVec3(0, 2.5f, -5);
ioState.mForward = Vec3(cam_tgt - ioState.mPos).Normalized();
}
void VehicleConstraintTest::UpdateCameraPivot()
{
// Pivot is center of car and rotates with car around Y axis only
Vec3 fwd = mCarBody->GetRotation().RotateAxisZ();
fwd.SetY(0.0f);
float len = fwd.Length();
if (len != 0.0f)
fwd /= len;
else
fwd = Vec3::sAxisZ();
Vec3 up = Vec3::sAxisY();
Vec3 right = up.Cross(fwd);
mCameraPivot = RMat44(Vec4(right, 0), Vec4(up, 0), Vec4(fwd, 0), mCarBody->GetPosition());
}
void VehicleConstraintTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
{
VehicleTest::CreateSettingsMenu(inUI, inSubMenu);
inUI->CreateSlider(inSubMenu, "Initial Roll Angle", RadiansToDegrees(sInitialRollAngle), 0.0f, 90.0f, 1.0f, [](float inValue) { sInitialRollAngle = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Max Roll Angle", RadiansToDegrees(sMaxRollAngle), 0.0f, 90.0f, 1.0f, [](float inValue) { sMaxRollAngle = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Max Steering Angle", RadiansToDegrees(sMaxSteeringAngle), 0.0f, 90.0f, 1.0f, [](float inValue) { sMaxSteeringAngle = DegreesToRadians(inValue); });
inUI->CreateComboBox(inSubMenu, "Collision Mode", { "Ray", "Cast Sphere", "Cast Cylinder" }, sCollisionMode, [](int inItem) { sCollisionMode = inItem; });
inUI->CreateCheckBox(inSubMenu, "4 Wheel Drive", sFourWheelDrive, [](UICheckBox::EState inState) { sFourWheelDrive = inState == UICheckBox::STATE_CHECKED; });
inUI->CreateCheckBox(inSubMenu, "Anti Rollbars", sAntiRollbar, [](UICheckBox::EState inState) { sAntiRollbar = inState == UICheckBox::STATE_CHECKED; });
inUI->CreateCheckBox(inSubMenu, "Limited Slip Differentials", sLimitedSlipDifferentials, [](UICheckBox::EState inState) { sLimitedSlipDifferentials = inState == UICheckBox::STATE_CHECKED; });
inUI->CreateCheckBox(inSubMenu, "Override Gravity", sOverrideGravity, [](UICheckBox::EState inState) { sOverrideGravity = inState == UICheckBox::STATE_CHECKED; });
inUI->CreateSlider(inSubMenu, "Max Engine Torque", float(sMaxEngineTorque), 100.0f, 2000.0f, 10.0f, [](float inValue) { sMaxEngineTorque = inValue; });
inUI->CreateSlider(inSubMenu, "Clutch Strength", float(sClutchStrength), 1.0f, 40.0f, 1.0f, [](float inValue) { sClutchStrength = inValue; });
inUI->CreateSlider(inSubMenu, "Front Caster Angle", RadiansToDegrees(sFrontCasterAngle), -89.0f, 89.0f, 1.0f, [](float inValue) { sFrontCasterAngle = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Front King Pin Angle", RadiansToDegrees(sFrontKingPinAngle), -89.0f, 89.0f, 1.0f, [](float inValue) { sFrontKingPinAngle = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Front Camber", RadiansToDegrees(sFrontCamber), -89.0f, 89.0f, 1.0f, [](float inValue) { sFrontCamber = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Front Toe", RadiansToDegrees(sFrontToe), -89.0f, 89.0f, 1.0f, [](float inValue) { sFrontToe = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Front Suspension Forward Angle", RadiansToDegrees(sFrontSuspensionForwardAngle), -89.0f, 89.0f, 1.0f, [](float inValue) { sFrontSuspensionForwardAngle = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Front Suspension Sideways Angle", RadiansToDegrees(sFrontSuspensionSidewaysAngle), -89.0f, 89.0f, 1.0f, [](float inValue) { sFrontSuspensionSidewaysAngle = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Front Suspension Min Length", sFrontSuspensionMinLength, 0.0f, 3.0f, 0.01f, [](float inValue) { sFrontSuspensionMinLength = inValue; });
inUI->CreateSlider(inSubMenu, "Front Suspension Max Length", sFrontSuspensionMaxLength, 0.0f, 3.0f, 0.01f, [](float inValue) { sFrontSuspensionMaxLength = inValue; });
inUI->CreateSlider(inSubMenu, "Front Suspension Frequency", sFrontSuspensionFrequency, 0.1f, 5.0f, 0.01f, [](float inValue) { sFrontSuspensionFrequency = inValue; });
inUI->CreateSlider(inSubMenu, "Front Suspension Damping", sFrontSuspensionDamping, 0.0f, 2.0f, 0.01f, [](float inValue) { sFrontSuspensionDamping = inValue; });
inUI->CreateSlider(inSubMenu, "Rear Caster Angle", RadiansToDegrees(sRearCasterAngle), -89.0f, 89.0f, 1.0f, [](float inValue) { sRearCasterAngle = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Rear King Pin Angle", RadiansToDegrees(sRearKingPinAngle), -89.0f, 89.0f, 1.0f, [](float inValue) { sRearKingPinAngle = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Rear Camber", RadiansToDegrees(sRearCamber), -89.0f, 89.0f, 1.0f, [](float inValue) { sRearCamber = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Rear Toe", RadiansToDegrees(sRearToe), -89.0f, 89.0f, 1.0f, [](float inValue) { sRearToe = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Rear Suspension Forward Angle", RadiansToDegrees(sRearSuspensionForwardAngle), -89.0f, 89.0f, 1.0f, [](float inValue) { sRearSuspensionForwardAngle = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Rear Suspension Sideways Angle", RadiansToDegrees(sRearSuspensionSidewaysAngle), -89.0f, 89.0f, 1.0f, [](float inValue) { sRearSuspensionSidewaysAngle = DegreesToRadians(inValue); });
inUI->CreateSlider(inSubMenu, "Rear Suspension Min Length", sRearSuspensionMinLength, 0.0f, 3.0f, 0.01f, [](float inValue) { sRearSuspensionMinLength = inValue; });
inUI->CreateSlider(inSubMenu, "Rear Suspension Max Length", sRearSuspensionMaxLength, 0.0f, 3.0f, 0.01f, [](float inValue) { sRearSuspensionMaxLength = inValue; });
inUI->CreateSlider(inSubMenu, "Rear Suspension Frequency", sRearSuspensionFrequency, 0.1f, 5.0f, 0.01f, [](float inValue) { sRearSuspensionFrequency = inValue; });
inUI->CreateSlider(inSubMenu, "Rear Suspension Damping", sRearSuspensionDamping, 0.0f, 2.0f, 0.01f, [](float inValue) { sRearSuspensionDamping = inValue; });
inUI->CreateTextButton(inSubMenu, "Accept", [this]() { RestartTest(); });
}

View File

@@ -0,0 +1,82 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Vehicle/VehicleTest.h>
#include <Jolt/Physics/Vehicle/VehicleConstraint.h>
class VehicleConstraintTest : public VehicleTest
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, VehicleConstraintTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Shows how a car could be made with a vehicle constraint.\n"
"Use the arrow keys to drive. Z for hand brake.";
}
// Destructor
virtual ~VehicleConstraintTest() override;
// See: Test
virtual void Initialize() override;
virtual void ProcessInput(const ProcessInputParams &inParams) override;
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
virtual void SaveInputState(StateRecorder &inStream) const override;
virtual void RestoreInputState(StateRecorder &inStream) override;
virtual void GetInitialCamera(CameraState &ioState) const override;
virtual RMat44 GetCameraPivot(float inCameraHeading, float inCameraPitch) const override { return mCameraPivot; }
virtual void CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
private:
void UpdateCameraPivot();
static inline float sInitialRollAngle = 0;
static inline float sMaxRollAngle = DegreesToRadians(60.0f);
static inline float sMaxSteeringAngle = DegreesToRadians(30.0f);
static inline int sCollisionMode = 2;
static inline bool sFourWheelDrive = false;
static inline bool sAntiRollbar = true;
static inline bool sLimitedSlipDifferentials = true;
static inline bool sOverrideGravity = false; ///< If true, gravity is overridden to always oppose the ground normal
static inline float sMaxEngineTorque = 500.0f;
static inline float sClutchStrength = 10.0f;
static inline float sFrontCasterAngle = 0.0f;
static inline float sFrontKingPinAngle = 0.0f;
static inline float sFrontCamber = 0.0f;
static inline float sFrontToe = 0.0f;
static inline float sFrontSuspensionForwardAngle = 0.0f;
static inline float sFrontSuspensionSidewaysAngle = 0.0f;
static inline float sFrontSuspensionMinLength = 0.3f;
static inline float sFrontSuspensionMaxLength = 0.5f;
static inline float sFrontSuspensionFrequency = 1.5f;
static inline float sFrontSuspensionDamping = 0.5f;
static inline float sRearSuspensionForwardAngle = 0.0f;
static inline float sRearSuspensionSidewaysAngle = 0.0f;
static inline float sRearCasterAngle = 0.0f;
static inline float sRearKingPinAngle = 0.0f;
static inline float sRearCamber = 0.0f;
static inline float sRearToe = 0.0f;
static inline float sRearSuspensionMinLength = 0.3f;
static inline float sRearSuspensionMaxLength = 0.5f;
static inline float sRearSuspensionFrequency = 1.5f;
static inline float sRearSuspensionDamping = 0.5f;
Body * mCarBody; ///< The vehicle
Ref<VehicleConstraint> mVehicleConstraint; ///< The vehicle constraint
Ref<VehicleCollisionTester> mTesters[3]; ///< Collision testers for the wheel
RMat44 mCameraPivot = RMat44::sIdentity(); ///< The camera pivot, recorded before the physics update to align with the drawn world
// Player input
float mForward = 0.0f;
float mPreviousForward = 1.0f; ///< Keeps track of last car direction so we know when to brake and when to accelerate
float mRight = 0.0f;
float mBrake = 0.0f;
float mHandBrake = 0.0f;
};

View File

@@ -0,0 +1,237 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Vehicle/VehicleSixDOFTest.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
#include <Jolt/Physics/Collision/GroupFilterTable.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Application/DebugUI.h>
#include <Layers.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(VehicleSixDOFTest)
{
JPH_ADD_BASE_CLASS(VehicleSixDOFTest, VehicleTest)
}
void VehicleSixDOFTest::Initialize()
{
VehicleTest::Initialize();
const float half_vehicle_length = 2.0f;
const float half_vehicle_width = 0.9f;
const float half_vehicle_height = 0.2f;
const float half_wheel_height = 0.3f;
const float half_wheel_width = 0.05f;
const float half_wheel_travel = 0.5f;
Vec3 wheel_position[] =
{
Vec3(-half_vehicle_width, -half_vehicle_height, half_vehicle_length - 2.0f * half_wheel_height),
Vec3(half_vehicle_width, -half_vehicle_height, half_vehicle_length - 2.0f * half_wheel_height),
Vec3(-half_vehicle_width, -half_vehicle_height, -half_vehicle_length + 2.0f * half_wheel_height),
Vec3(half_vehicle_width, -half_vehicle_height, -half_vehicle_length + 2.0f * half_wheel_height),
};
RVec3 position(0, 2, 0);
RefConst<Shape> body_shape = new BoxShape(Vec3(half_vehicle_width, half_vehicle_height, half_vehicle_length));
Ref<CylinderShape> wheel_shape = new CylinderShape(half_wheel_width, half_wheel_height);
wheel_shape->SetDensity(1.0e4f);
// Create group filter
Ref<GroupFilterTable> group_filter = new GroupFilterTable;
// Create vehicle body
mCarBody = mBodyInterface->CreateBody(BodyCreationSettings(body_shape, position, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
mCarBody->SetCollisionGroup(CollisionGroup(group_filter, 0, 0));
mBodyInterface->AddBody(mCarBody->GetID(), EActivation::Activate);
// Create wheels
for (int i = 0; i < (int)EWheel::Num; ++i)
{
bool is_front = sIsFrontWheel((EWheel)i);
bool is_left = sIsLeftWheel((EWheel)i);
RVec3 wheel_pos1 = position + wheel_position[i];
RVec3 wheel_pos2 = wheel_pos1 - Vec3(0, half_wheel_travel, 0);
// Create body
Body &wheel = *mBodyInterface->CreateBody(BodyCreationSettings(wheel_shape, wheel_pos2, Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), EMotionType::Dynamic, Layers::MOVING));
wheel.SetFriction(1.0f);
wheel.SetCollisionGroup(CollisionGroup(group_filter, 0, 0));
mBodyInterface->AddBody(wheel.GetID(), EActivation::Activate);
// Create constraint
SixDOFConstraintSettings settings;
settings.mPosition1 = wheel_pos1;
settings.mPosition2 = wheel_pos2;
settings.mAxisX1 = settings.mAxisX2 = is_left? -Vec3::sAxisX() : Vec3::sAxisX();
settings.mAxisY1 = settings.mAxisY2 = Vec3::sAxisY();
// The suspension works in the Y translation axis only
settings.MakeFixedAxis(EAxis::TranslationX);
settings.SetLimitedAxis(EAxis::TranslationY, -half_wheel_travel, half_wheel_travel);
settings.MakeFixedAxis(EAxis::TranslationZ);
settings.mMotorSettings[EAxis::TranslationY] = MotorSettings(2.0f, 1.0f, 1.0e5f, 0.0f);
// Front wheel can rotate around the Y axis
if (is_front)
settings.SetLimitedAxis(EAxis::RotationY, -cMaxSteeringAngle, cMaxSteeringAngle);
else
settings.MakeFixedAxis(EAxis::RotationY);
// The Z axis is static
settings.MakeFixedAxis(EAxis::RotationZ);
// The main engine drives the X axis
settings.MakeFreeAxis(EAxis::RotationX);
settings.mMotorSettings[EAxis::RotationX] = MotorSettings(2.0f, 1.0f, 0.0f, 0.5e4f);
// The front wheel needs to be able to steer around the Y axis
// However the motors work in the constraint space of the wheel, and since this rotates around the
// X axis we need to drive both the Y and Z to steer
if (is_front)
settings.mMotorSettings[EAxis::RotationY] = settings.mMotorSettings[EAxis::RotationZ] = MotorSettings(10.0f, 1.0f, 0.0f, 1.0e6f);
SixDOFConstraint *wheel_constraint = static_cast<SixDOFConstraint *>(settings.Create(*mCarBody, wheel));
mPhysicsSystem->AddConstraint(wheel_constraint);
mWheels[i] = wheel_constraint;
// Drive the suspension
wheel_constraint->SetTargetPositionCS(Vec3(0, -half_wheel_travel, 0));
wheel_constraint->SetMotorState(EAxis::TranslationY, EMotorState::Position);
// The front wheels steer around the Y axis, but in constraint space of the wheel this means we need to drive
// both Y and Z (see comment above)
if (is_front)
{
wheel_constraint->SetTargetOrientationCS(Quat::sIdentity());
wheel_constraint->SetMotorState(EAxis::RotationY, EMotorState::Position);
wheel_constraint->SetMotorState(EAxis::RotationZ, EMotorState::Position);
}
}
UpdateCameraPivot();
}
void VehicleSixDOFTest::ProcessInput(const ProcessInputParams &inParams)
{
const float max_rotation_speed = 10.0f * JPH_PI;
// Determine steering and speed
mSteeringAngle = 0.0f;
mSpeed = 0.0f;
if (inParams.mKeyboard->IsKeyPressed(EKey::Left)) mSteeringAngle = cMaxSteeringAngle;
if (inParams.mKeyboard->IsKeyPressed(EKey::Right)) mSteeringAngle = -cMaxSteeringAngle;
if (inParams.mKeyboard->IsKeyPressed(EKey::Up)) mSpeed = max_rotation_speed;
if (inParams.mKeyboard->IsKeyPressed(EKey::Down)) mSpeed = -max_rotation_speed;
}
void VehicleSixDOFTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
VehicleTest::PrePhysicsUpdate(inParams);
UpdateCameraPivot();
// On user input, assure that the car is active
if (mSteeringAngle != 0.0f || mSpeed != 0.0f)
mBodyInterface->ActivateBody(mCarBody->GetID());
// Brake if current velocity is in the opposite direction of the desired velocity
float car_speed = mCarBody->GetLinearVelocity().Dot(mCarBody->GetRotation().RotateAxisZ());
bool brake = mSpeed != 0.0f && car_speed != 0.0f && Sign(mSpeed) != Sign(car_speed);
// Front wheels
const EWheel front_wheels[] = { EWheel::LeftFront, EWheel::RightFront };
for (EWheel w : front_wheels)
{
SixDOFConstraint *wheel_constraint = mWheels[(int)w];
if (wheel_constraint == nullptr)
continue;
// Steer front wheels
Quat steering_rotation = Quat::sRotation(Vec3::sAxisY(), mSteeringAngle);
wheel_constraint->SetTargetOrientationCS(steering_rotation);
if (brake)
{
// Brake on front wheels
wheel_constraint->SetTargetAngularVelocityCS(Vec3::sZero());
wheel_constraint->SetMotorState(EAxis::RotationX, EMotorState::Velocity);
}
else if (mSpeed != 0.0f)
{
// Front wheel drive, since the motors are applied in the constraint space of the wheel
// it is always applied on the X axis
wheel_constraint->SetTargetAngularVelocityCS(Vec3(sIsLeftWheel(w)? -mSpeed : mSpeed, 0, 0));
wheel_constraint->SetMotorState(EAxis::RotationX, EMotorState::Velocity);
}
else
{
// Free spin
wheel_constraint->SetMotorState(EAxis::RotationX, EMotorState::Off);
}
}
// Rear wheels
const EWheel rear_wheels[] = { EWheel::LeftRear, EWheel::RightRear };
for (EWheel w : rear_wheels)
{
SixDOFConstraint *wheel_constraint = mWheels[(int)w];
if (wheel_constraint == nullptr)
continue;
if (brake)
{
// Brake on rear wheels
wheel_constraint->SetTargetAngularVelocityCS(Vec3::sZero());
wheel_constraint->SetMotorState(EAxis::RotationX, EMotorState::Velocity);
}
else
{
// Free spin
wheel_constraint->SetMotorState(EAxis::RotationX, EMotorState::Off);
}
}
}
void VehicleSixDOFTest::GetInitialCamera(CameraState &ioState) const
{
// Position camera behind car
RVec3 cam_tgt = RVec3(0, 0, 5);
ioState.mPos = RVec3(0, 2.5_r, -5);
ioState.mForward = Vec3(cam_tgt - ioState.mPos).Normalized();
}
void VehicleSixDOFTest::UpdateCameraPivot()
{
// Pivot is center of car and rotates with car around Y axis only
Vec3 fwd = mCarBody->GetRotation().RotateAxisZ();
fwd.SetY(0.0f);
float len = fwd.Length();
if (len != 0.0f)
fwd /= len;
else
fwd = Vec3::sAxisZ();
Vec3 up = Vec3::sAxisY();
Vec3 right = up.Cross(fwd);
mCameraPivot = RMat44(Vec4(right, 0), Vec4(up, 0), Vec4(fwd, 0), mCarBody->GetPosition());
}
void VehicleSixDOFTest::SaveInputState(StateRecorder &inStream) const
{
inStream.Write(mSteeringAngle);
inStream.Write(mSpeed);
}
void VehicleSixDOFTest::RestoreInputState(StateRecorder &inStream)
{
inStream.Read(mSteeringAngle);
inStream.Read(mSpeed);
}

View File

@@ -0,0 +1,58 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Vehicle/VehicleTest.h>
#include <Jolt/Physics/Constraints/SixDOFConstraint.h>
class VehicleSixDOFTest : public VehicleTest
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, VehicleSixDOFTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "Shows how a car could be made with a SixDOFConstraint.\n"
"Use the arrow keys to drive.";
}
// See: Test
virtual void Initialize() override;
virtual void ProcessInput(const ProcessInputParams &inParams) override;
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
virtual void SaveInputState(StateRecorder &inStream) const override;
virtual void RestoreInputState(StateRecorder &inStream) override;
virtual void GetInitialCamera(CameraState &ioState) const override;
virtual RMat44 GetCameraPivot(float inCameraHeading, float inCameraPitch) const override { return mCameraPivot; }
private:
static constexpr float cMaxSteeringAngle = DegreesToRadians(30);
using EAxis = SixDOFConstraintSettings::EAxis;
void UpdateCameraPivot();
enum class EWheel : int
{
LeftFront,
RightFront,
LeftRear,
RightRear,
Num,
};
static inline bool sIsFrontWheel(EWheel inWheel) { return inWheel == EWheel::LeftFront || inWheel == EWheel::RightFront; }
static inline bool sIsLeftWheel(EWheel inWheel) { return inWheel == EWheel::LeftFront || inWheel == EWheel::LeftRear; }
Body * mCarBody;
Ref<SixDOFConstraint> mWheels[int(EWheel::Num)];
RMat44 mCameraPivot = RMat44::sIdentity(); ///< The camera pivot, recorded before the physics update to align with the drawn world
// Player input
float mSteeringAngle = 0.0f;
float mSpeed = 0.0f;
};

View File

@@ -0,0 +1,174 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Vehicle/VehicleStressTest.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/Vehicle/WheeledVehicleController.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Layers.h>
#include <Renderer/DebugRendererImp.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(VehicleStressTest)
{
JPH_ADD_BASE_CLASS(VehicleStressTest, VehicleTest)
}
VehicleStressTest::~VehicleStressTest()
{
for (Ref<VehicleConstraint> &c : mVehicles)
mPhysicsSystem->RemoveStepListener(c);
}
void VehicleStressTest::Initialize()
{
CreateMeshTerrain();
// Create walls so the vehicles don't fall off
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(50.0f, 5.0f, 0.5f)), RVec3(0, 0, -50), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(50.0f, 5.0f, 0.5f)), RVec3(0, 0, 50), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(0.5f, 5.0f, 50.0f)), RVec3(-50, 0, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new BoxShape(Vec3(0.5f, 5.0f, 50.0f)), RVec3(50, 0, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
const float wheel_radius = 0.3f;
const float wheel_width = 0.1f;
const float half_vehicle_length = 2.0f;
const float half_vehicle_width = 0.9f;
const float half_vehicle_height = 0.2f;
const float max_steering_angle = DegreesToRadians(30.0f);
// Create vehicle body
RefConst<Shape> car_shape = new BoxShape(Vec3(half_vehicle_width, half_vehicle_height, half_vehicle_length));
BodyCreationSettings car_body_settings(car_shape, RVec3::sZero(), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
car_body_settings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
car_body_settings.mMassPropertiesOverride.mMass = 1500.0f;
// Create vehicle constraint
VehicleConstraintSettings vehicle;
// Wheels, left front
WheelSettingsWV *w1 = new WheelSettingsWV;
w1->mPosition = Vec3(half_vehicle_width, -0.9f * half_vehicle_height, half_vehicle_length - 2.0f * wheel_radius);
w1->mMaxSteerAngle = max_steering_angle;
w1->mMaxHandBrakeTorque = 0.0f; // Front wheel doesn't have hand brake
// Right front
WheelSettingsWV *w2 = new WheelSettingsWV;
w2->mPosition = Vec3(-half_vehicle_width, -0.9f * half_vehicle_height, half_vehicle_length - 2.0f * wheel_radius);
w2->mMaxSteerAngle = max_steering_angle;
w2->mMaxHandBrakeTorque = 0.0f; // Front wheel doesn't have hand brake
// Left rear
WheelSettingsWV *w3 = new WheelSettingsWV;
w3->mPosition = Vec3(half_vehicle_width, -0.9f * half_vehicle_height, -half_vehicle_length + 2.0f * wheel_radius);
w3->mMaxSteerAngle = 0.0f;
// Right rear
WheelSettingsWV *w4 = new WheelSettingsWV;
w4->mPosition = Vec3(-half_vehicle_width, -0.9f * half_vehicle_height, -half_vehicle_length + 2.0f * wheel_radius);
w4->mMaxSteerAngle = 0.0f;
vehicle.mWheels = { w1, w2, w3, w4 };
for (WheelSettings *w : vehicle.mWheels)
{
w->mRadius = wheel_radius;
w->mWidth = wheel_width;
}
// Controller
WheeledVehicleControllerSettings *controller = new WheeledVehicleControllerSettings;
vehicle.mController = controller;
vehicle.mMaxPitchRollAngle = DegreesToRadians(60.0f);
// Differential
controller->mDifferentials.resize(1);
controller->mDifferentials[0].mLeftWheel = 0;
controller->mDifferentials[0].mRightWheel = 1;
for (int x = 0; x < 15; ++x)
for (int y = 0; y < 15; ++y)
{
// Create body
car_body_settings.mPosition = RVec3(-28.0f + x * 4.0f, 2.0f, -35.0f + y * 5.0f);
Body *car_body = mBodyInterface->CreateBody(car_body_settings);
mBodyInterface->AddBody(car_body->GetID(), EActivation::Activate);
// Create constraint
VehicleConstraint *c = new VehicleConstraint(*car_body, vehicle);
c->SetNumStepsBetweenCollisionTestActive(2); // Only test collision every other step to speed up simulation
c->SetNumStepsBetweenCollisionTestInactive(0); // Disable collision testing when inactive
// Set the collision tester
VehicleCollisionTester *tester = new VehicleCollisionTesterRay(Layers::MOVING);
c->SetVehicleCollisionTester(tester);
// Add the vehicle
mPhysicsSystem->AddConstraint(c);
mPhysicsSystem->AddStepListener(c);
mVehicles.push_back(c);
}
}
void VehicleStressTest::ProcessInput(const ProcessInputParams &inParams)
{
// Determine acceleration and brake
mForward = 0.0f;
if (inParams.mKeyboard->IsKeyPressed(EKey::Up))
mForward = 1.0f;
else if (inParams.mKeyboard->IsKeyPressed(EKey::Down))
mForward = -1.0f;
// Steering
mRight = 0.0f;
if (inParams.mKeyboard->IsKeyPressed(EKey::Left))
mRight = -1.0f;
else if (inParams.mKeyboard->IsKeyPressed(EKey::Right))
mRight = 1.0f;
// Hand brake will cancel gas pedal
mHandBrake = 0.0f;
if (inParams.mKeyboard->IsKeyPressed(EKey::Z))
{
mForward = 0.0f;
mHandBrake = 1.0f;
}
}
void VehicleStressTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
for (VehicleConstraint *c : mVehicles)
{
// On user input, assure that the car is active
if (mRight != 0.0f || mForward != 0.0f)
mBodyInterface->ActivateBody(c->GetVehicleBody()->GetID());
// Pass the input on to the constraint
WheeledVehicleController *controller = static_cast<WheeledVehicleController *>(c->GetController());
controller->SetDriverInput(mForward, mRight, 0.0f, mHandBrake);
// Draw our wheels (this needs to be done in the pre update since we draw the bodies too in the state before the step)
for (uint w = 0; w < 4; ++w)
{
const WheelSettings *settings = c->GetWheels()[w]->GetSettings();
RMat44 wheel_transform = c->GetWheelWorldTransform(w, Vec3::sAxisY(), Vec3::sAxisX()); // The cylinder we draw is aligned with Y so we specify that as rotational axis
mDebugRenderer->DrawCylinder(wheel_transform, 0.5f * settings->mWidth, settings->mRadius, Color::sGreen);
}
}
}
void VehicleStressTest::SaveInputState(StateRecorder &inStream) const
{
inStream.Write(mForward);
inStream.Write(mRight);
inStream.Write(mHandBrake);
}
void VehicleStressTest::RestoreInputState(StateRecorder &inStream)
{
inStream.Read(mForward);
inStream.Read(mRight);
inStream.Read(mHandBrake);
}

View File

@@ -0,0 +1,38 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Vehicle/VehicleTest.h>
#include <Jolt/Physics/Vehicle/VehicleConstraint.h>
class VehicleStressTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, VehicleStressTest)
// Description of the test
virtual const char * GetDescription() const override
{
return "This test simulates a large number of vehicles to test performance.";
}
// Destructor
virtual ~VehicleStressTest() override;
// See: Test
virtual void Initialize() override;
virtual void ProcessInput(const ProcessInputParams &inParams) override;
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
virtual void SaveInputState(StateRecorder &inStream) const override;
virtual void RestoreInputState(StateRecorder &inStream) override;
private:
Array<Ref<VehicleConstraint>> mVehicles; ///< The vehicle constraints
// Player input
float mForward = 0.0f;
float mRight = 0.0f;
float mHandBrake = 0.0f;
};

View File

@@ -0,0 +1,338 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <TestFramework.h>
#include <Tests/Vehicle/VehicleTest.h>
#include <Jolt/Physics/Constraints/DistanceConstraint.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
#include <Jolt/Physics/Collision/GroupFilterTable.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Jolt/Physics/PhysicsScene.h>
#include <Jolt/ObjectStream/ObjectStreamIn.h>
#include <Layers.h>
#include <Application/DebugUI.h>
#include <Utils/Log.h>
#include <Utils/AssetStream.h>
#include <Renderer/DebugRendererImp.h>
JPH_IMPLEMENT_RTTI_VIRTUAL(VehicleTest)
{
JPH_ADD_BASE_CLASS(VehicleTest, Test)
}
const char *VehicleTest::sScenes[] =
{
"Flat",
"Flat With Slope",
"Steep Slope",
"Step",
"Dynamic Step",
"Playground",
"Loop",
#ifdef JPH_OBJECT_STREAM
"Terrain1",
#endif // JPH_OBJECT_STREAM
};
const char *VehicleTest::sSceneName = "Playground";
void VehicleTest::Initialize()
{
if (strcmp(sSceneName, "Flat") == 0)
{
// Flat test floor
Body &floor = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(Vec3(1000.0f, 1.0f, 1000.0f), 0.0f), RVec3(0.0f, -1.0f, 0.0f), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
floor.SetFriction(1.0f);
mBodyInterface->AddBody(floor.GetID(), EActivation::DontActivate);
// Load a race track to have something to assess speed and steering behavior
LoadRaceTrack("Racetracks/Zandvoort.csv");
}
else if (strcmp(sSceneName, "Flat With Slope") == 0)
{
const float cSlopeStartDistance = 100.0f;
const float cSlopeLength = 100.0f;
const float cSlopeAngle = DegreesToRadians(30.0f);
// Flat test floor
Body &floor = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(Vec3(1000.0f, 1.0f, 1000.0f), 0.0f), RVec3(0.0f, -1.0f, 0.0f), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
floor.SetFriction(1.0f);
mBodyInterface->AddBody(floor.GetID(), EActivation::DontActivate);
Body &slope_up = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(Vec3(25.0f, 1.0f, cSlopeLength), 0.0f), RVec3(0.0f, cSlopeLength * Sin(cSlopeAngle) - 1.0f, cSlopeStartDistance + cSlopeLength * Cos(cSlopeAngle)), Quat::sRotation(Vec3::sAxisX(), -cSlopeAngle), EMotionType::Static, Layers::NON_MOVING));
slope_up.SetFriction(1.0f);
mBodyInterface->AddBody(slope_up.GetID(), EActivation::DontActivate);
Body &slope_down = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(Vec3(25.0f, 1.0f, cSlopeLength), 0.0f), RVec3(0.0f, cSlopeLength * Sin(cSlopeAngle) - 1.0f, cSlopeStartDistance + 3.0f * cSlopeLength * Cos(cSlopeAngle)), Quat::sRotation(Vec3::sAxisX(), cSlopeAngle), EMotionType::Static, Layers::NON_MOVING));
slope_down.SetFriction(1.0f);
mBodyInterface->AddBody(slope_down.GetID(), EActivation::DontActivate);
}
else if (strcmp(sSceneName, "Steep Slope") == 0)
{
// Steep slope test floor (20 degrees = 36% grade)
Body &floor = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(Vec3(1000.0f, 1.0f, 1000.0f), 0.0f), RVec3(0.0f, -1.0f, 0.0f), Quat::sRotation(Vec3::sAxisX(), DegreesToRadians(-20.0f)), EMotionType::Static, Layers::NON_MOVING));
floor.SetFriction(1.0f);
mBodyInterface->AddBody(floor.GetID(), EActivation::DontActivate);
}
else if (strcmp(sSceneName, "Step") == 0)
{
// Flat test floor
Body &floor = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(Vec3(1000.0f, 1.0f, 1000.0f), 0.0f), RVec3(0.0f, -1.0f, 0.0f), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
floor.SetFriction(1.0f);
mBodyInterface->AddBody(floor.GetID(), EActivation::DontActivate);
// A 5cm step rotated under an angle
constexpr float cStepHeight = 0.05f;
Body &step = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(Vec3(5.0f, 0.5f * cStepHeight, 5.0f), 0.0f), RVec3(-2.0f, 0.5f * cStepHeight, 60.0f), Quat::sRotation(Vec3::sAxisY(), -0.3f * JPH_PI), EMotionType::Static, Layers::NON_MOVING));
step.SetFriction(1.0f);
mBodyInterface->AddBody(step.GetID(), EActivation::DontActivate);
}
else if (strcmp(sSceneName, "Dynamic Step") == 0)
{
// Flat test floor
Body &floor = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(Vec3(1000.0f, 1.0f, 1000.0f), 0.0f), RVec3(0.0f, -1.0f, 0.0f), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
floor.SetFriction(1.0f);
mBodyInterface->AddBody(floor.GetID(), EActivation::DontActivate);
// A dynamic body that acts as a step to test sleeping behavior
constexpr float cStepHeight = 0.05f;
Body &step = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(Vec3(15.0f, 0.5f * cStepHeight, 15.0f), 0.0f), RVec3(-2.0f, 0.5f * cStepHeight, 30.0f), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
step.SetFriction(1.0f);
mBodyInterface->AddBody(step.GetID(), EActivation::Activate);
}
else if (strcmp(sSceneName, "Playground") == 0)
{
// Scene with hilly terrain and some objects to drive into
Body &floor = CreateMeshTerrain();
floor.SetFriction(1.0f);
CreateBridge();
CreateWall();
CreateRubble();
}
else if (strcmp(sSceneName, "Loop") == 0)
{
CreateFloor();
TriangleList triangles;
const int cNumSegments = 100;
const float cLoopWidth = 20.0f;
const float cLoopRadius = 20.0f;
const float cLoopThickness = 0.5f;
Vec3 prev_center = Vec3::sZero();
Vec3 prev_center_bottom = Vec3::sZero();
for (int i = 0; i < cNumSegments; ++i)
{
float angle = i * 2.0f * JPH_PI / (cNumSegments - 1);
Vec3 radial(0, -Cos(angle), Sin(angle));
Vec3 center = Vec3(-i * cLoopWidth / (cNumSegments - 1), cLoopRadius, cLoopRadius) + cLoopRadius * radial;
Vec3 half_width(0.5f * cLoopWidth, 0, 0);
Vec3 center_bottom = center + cLoopThickness * radial;
if (i > 0)
{
// Top surface
triangles.push_back(Triangle(prev_center + half_width, prev_center - half_width, center - half_width));
triangles.push_back(Triangle(prev_center + half_width, center - half_width, center + half_width));
// Bottom surface
triangles.push_back(Triangle(prev_center_bottom + half_width, center_bottom - half_width, prev_center_bottom - half_width));
triangles.push_back(Triangle(prev_center_bottom + half_width, center_bottom + half_width, center_bottom - half_width));
// Sides
triangles.push_back(Triangle(prev_center + half_width, center + half_width, prev_center_bottom + half_width));
triangles.push_back(Triangle(prev_center_bottom + half_width, center + half_width, center_bottom + half_width));
triangles.push_back(Triangle(prev_center - half_width, prev_center_bottom - half_width, center - half_width));
triangles.push_back(Triangle(prev_center_bottom - half_width, center_bottom - half_width, center - half_width));
}
prev_center = center;
prev_center_bottom = center_bottom;
}
MeshShapeSettings mesh(triangles);
mesh.SetEmbedded();
Body &loop = *mBodyInterface->CreateBody(BodyCreationSettings(&mesh, RVec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
loop.SetFriction(1.0f);
mBodyInterface->AddBody(loop.GetID(), EActivation::Activate);
}
#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");
for (BodyCreationSettings &body : scene->GetBodies())
body.mObjectLayer = Layers::NON_MOVING;
scene->FixInvalidScales();
scene->CreateBodies(mPhysicsSystem);
}
#endif // JPH_OBJECT_STREAM
}
void VehicleTest::CreateBridge()
{
const int cChainLength = 20;
// Build a collision group filter that disables collision between adjacent bodies
Ref<GroupFilterTable> group_filter = new GroupFilterTable(cChainLength);
for (CollisionGroup::SubGroupID i = 0; i < cChainLength - 1; ++i)
group_filter->DisableCollision(i, i + 1);
Vec3 part_half_size = Vec3(2.5f, 0.25f, 1.0f);
RefConst<Shape> part_shape = new BoxShape(part_half_size);
Vec3 large_part_half_size = Vec3(2.5f, 0.25f, 22.5f);
RefConst<Shape> large_part_shape = new BoxShape(large_part_half_size);
Quat first_part_rot = Quat::sRotation(Vec3::sAxisX(), DegreesToRadians(-10.0f));
RVec3 prev_pos(-25, 7, 0);
Body *prev_part = nullptr;
for (int i = 0; i < cChainLength; ++i)
{
RVec3 pos = prev_pos + Vec3(0, 0, 2.0f * part_half_size.GetZ());
Body &part = i == 0? *mBodyInterface->CreateBody(BodyCreationSettings(large_part_shape, pos - first_part_rot * Vec3(0, large_part_half_size.GetY() - part_half_size.GetY(), large_part_half_size.GetZ() - part_half_size.GetZ()), first_part_rot, EMotionType::Static, Layers::NON_MOVING))
: *mBodyInterface->CreateBody(BodyCreationSettings(part_shape, pos, Quat::sIdentity(), i == 19? EMotionType::Static : EMotionType::Dynamic, i == 19? Layers::NON_MOVING : Layers::MOVING));
part.SetCollisionGroup(CollisionGroup(group_filter, 1, CollisionGroup::SubGroupID(i)));
part.SetFriction(1.0f);
mBodyInterface->AddBody(part.GetID(), EActivation::Activate);
if (prev_part != nullptr)
{
DistanceConstraintSettings dc;
dc.mPoint1 = prev_pos + Vec3(-part_half_size.GetX(), 0, part_half_size.GetZ());
dc.mPoint2 = pos + Vec3(-part_half_size.GetX(), 0, -part_half_size.GetZ());
mPhysicsSystem->AddConstraint(dc.Create(*prev_part, part));
dc.mPoint1 = prev_pos + Vec3(part_half_size.GetX(), 0, part_half_size.GetZ());
dc.mPoint2 = pos + Vec3(part_half_size.GetX(), 0, -part_half_size.GetZ());
mPhysicsSystem->AddConstraint(dc.Create(*prev_part, part));
}
prev_part = &part;
prev_pos = pos;
}
}
void VehicleTest::CreateWall()
{
RefConst<Shape> box_shape = new BoxShape(Vec3(0.5f, 0.5f, 0.5f));
for (int i = 0; i < 3; ++i)
for (int j = i / 2; j < 5 - (i + 1) / 2; ++j)
{
RVec3 position(2.0f + j * 1.0f + (i & 1? 0.5f : 0.0f), 2.0f + i * 1.0f, 10.0f);
mBodyInterface->CreateAndAddBody(BodyCreationSettings(box_shape, position, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
}
}
void VehicleTest::CreateRubble()
{
// Flat and light objects
RefConst<Shape> box_shape = new BoxShape(Vec3(0.5f, 0.1f, 0.5f));
for (int i = 0; i < 5; ++i)
for (int j = 0; j < 5; ++j)
{
RVec3 position(-5.0f + j, 2.0f + i * 0.2f, 10.0f + 0.5f * i);
mBodyInterface->CreateAndAddBody(BodyCreationSettings(box_shape, position, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
}
// Light convex shapes
default_random_engine random;
uniform_real_distribution<float> hull_size(0.2f, 0.4f);
for (int i = 0; i < 10; ++i)
for (int j = 0; j < 10; ++j)
{
// Create random points
Array<Vec3> points;
for (int k = 0; k < 20; ++k)
points.push_back(hull_size(random) * Vec3::sRandom(random));
mBodyInterface->CreateAndAddBody(BodyCreationSettings(new ConvexHullShapeSettings(points), RVec3(-5.0f + 0.5f * j, 2.0f, 15.0f + 0.5f * i), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING), EActivation::Activate);
}
}
void VehicleTest::LoadRaceTrack(const char *inFileName)
{
// Open the track file
AssetStream asset_stream(inFileName, std::ios::in);
std::istream &stream = asset_stream.Get();
// Ignore header line
String line;
std::getline(stream, line);
// Read coordinates
struct Segment
{
RVec3 mCenter;
float mWidthLeft;
float mWidthRight;
};
Array<Segment> segments;
Real x, y;
float wl, wr;
char c;
RVec3 track_center = RVec3::sZero();
while (stream >> x >> c >> y >> c >> wl >> c >> wr)
{
RVec3 center(x, 0, -y);
segments.push_back({ center, wl, wr });
track_center += center;
}
if (!segments.empty())
track_center /= (float)segments.size();
// Convert to line segments
RVec3 prev_tleft = RVec3::sZero(), prev_tright = RVec3::sZero();
for (size_t i = 0; i < segments.size(); ++i)
{
const Segment &segment = segments[i];
const Segment &next_segment = segments[(i + 1) % segments.size()];
// Calculate left and right point of the track
Vec3 fwd = Vec3(next_segment.mCenter - segment.mCenter);
Vec3 right = fwd.Cross(Vec3::sAxisY()).Normalized();
RVec3 tcenter = segment.mCenter - track_center + Vec3(0, 0.1f, 0); // Put a bit above the floor to avoid z fighting
RVec3 tleft = tcenter - right * segment.mWidthLeft;
RVec3 tright = tcenter + right * segment.mWidthRight;
mTrackData.push_back({ tleft, tright });
// Connect left and right point with the previous left and right point
if (i > 0)
{
mTrackData.push_back({ prev_tleft, tleft });
mTrackData.push_back({ prev_tright, tright });
}
prev_tleft = tleft;
prev_tright = tright;
}
}
void VehicleTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
// Render the track
for (const Line &l : mTrackData)
mDebugRenderer->DrawLine(l.mStart, l.mEnd, Color::sBlack);
}
void VehicleTest::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);
});
}

View File

@@ -0,0 +1,42 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Tests/Test.h>
// This is the base class for vehicle tests, it will create some sample geometry
class VehicleTest : public Test
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, VehicleTest)
// See: Test
virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override;
virtual void Initialize() override;
// Optional settings menu
virtual bool HasSettingsMenu() const override { return true; }
virtual void CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
private:
// List of possible scene names
static const char * sScenes[];
// Filename of animation to load for this test
static const char * sSceneName;
void CreateBridge();
void CreateWall();
void CreateRubble();
void LoadRaceTrack(const char *inFileName);
// A set of line segments to render a race track
struct Line
{
RVec3 mStart;
RVec3 mEnd;
};
Array<Line> mTrackData;
};