4134 lines
141 KiB
C++
4134 lines
141 KiB
C++
/*
|
|
*
|
|
* Singe 3
|
|
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301, USA.
|
|
*
|
|
*/
|
|
|
|
// Jolt Physics behind the C interface in physics.h: the only C++ in Singe. Nothing of Jolt's
|
|
// crosses the header; the engine sees plain C functions and node handles.
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <Jolt/Jolt.h>
|
|
#include <Jolt/RegisterTypes.h>
|
|
#include <Jolt/Core/Factory.h>
|
|
#include <Jolt/Core/TempAllocator.h>
|
|
#include <Jolt/Core/JobSystemThreadPool.h>
|
|
#include <Jolt/Physics/PhysicsSettings.h>
|
|
#include <Jolt/Physics/PhysicsSystem.h>
|
|
#include <Jolt/Physics/Body/BodyCreationSettings.h>
|
|
#include <Jolt/Physics/Body/BodyLock.h>
|
|
#include <Jolt/Physics/Body/BodyLockMulti.h>
|
|
#include <Jolt/Physics/Collision/BroadPhase/BroadPhaseLayer.h>
|
|
#include <Jolt/Physics/Collision/ObjectLayer.h>
|
|
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
|
|
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
|
|
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
|
|
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
|
|
#include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
|
|
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
|
#include <Jolt/Physics/Collision/Shape/HeightFieldShape.h>
|
|
#include <Jolt/Physics/Constraints/HingeConstraint.h>
|
|
#include <Jolt/Physics/Constraints/PointConstraint.h>
|
|
#include <Jolt/Physics/Constraints/SliderConstraint.h>
|
|
#include <Jolt/Physics/Collision/CastResult.h>
|
|
#include <Jolt/Physics/Collision/RayCast.h>
|
|
#include <Jolt/Physics/Collision/ContactListener.h>
|
|
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
|
|
#include <Jolt/Physics/Character/CharacterVirtual.h>
|
|
#include <Jolt/Physics/Vehicle/VehicleConstraint.h>
|
|
#include <Jolt/Physics/Vehicle/WheeledVehicleController.h>
|
|
#include <Jolt/Physics/Vehicle/TrackedVehicleController.h>
|
|
#include <Jolt/Physics/Vehicle/MotorcycleController.h>
|
|
#include <Jolt/Physics/Vehicle/VehicleCollisionTester.h>
|
|
#include <Jolt/Physics/Collision/BroadPhase/BroadPhaseQuery.h>
|
|
#include <Jolt/Physics/Collision/CollisionCollectorImpl.h>
|
|
#include <Jolt/Physics/Collision/TransformedShape.h>
|
|
#include <Jolt/Physics/Collision/GroupFilterTable.h>
|
|
#include <Jolt/Physics/Constraints/SwingTwistConstraint.h>
|
|
#include <Jolt/Physics/SoftBody/SoftBodySharedSettings.h>
|
|
#include <Jolt/Physics/SoftBody/SoftBodyCreationSettings.h>
|
|
#include <Jolt/Physics/SoftBody/SoftBodyMotionProperties.h>
|
|
#ifdef JPH_DEBUG_RENDERER
|
|
#include <Jolt/Renderer/DebugRendererSimple.h>
|
|
#endif
|
|
#include <mutex>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
extern "C" {
|
|
#include "util.h"
|
|
#include "scene.h"
|
|
#include "model.h"
|
|
}
|
|
#include "physics.h"
|
|
|
|
|
|
#define MAX_BODIES 4096
|
|
#define MAX_PLAYERS 16
|
|
#define MAX_VEHICLES 16
|
|
#define MAX_WATERS 8
|
|
#define MAX_PLAYER_TRIGGERS 16
|
|
#define MAX_RAGDOLLS 8
|
|
#define MAX_SOFT 16
|
|
#define MAX_SOFT_PINS 32
|
|
#define ROPE_SIDES 6
|
|
#define ROPE_INDICES_PER_QUAD 6 // Two triangles between neighbouring rings
|
|
#define SOFT_WELD 1.0e-4f // Mesh vertices this close are one particle
|
|
#define SOFT_VERTEX_RADIUS 0.03f // Cloth and pressure bodies; a rope uses its own radius
|
|
#define STRETCH_COMPLIANCE 1.0e-3f // At stiffness 0; stiffness 1 is rigid
|
|
#define BEND_COMPLIANCE 1.0e-2f
|
|
#define DEFAULT_SOFT_STRETCH 0.9f
|
|
#define DEFAULT_SOFT_BEND 0.2f
|
|
#define DEFAULT_SOFT_MASS 1.0f
|
|
#define DEFAULT_SOFT_DAMPING 0.1f
|
|
#define SOFT_ITERATIONS 8
|
|
#define MAX_RAGDOLL_PARTS 48
|
|
#define RAGDOLL_RADIUS_RATIO 0.22f // Capsule radius as a share of the bone length ...
|
|
#define RAGDOLL_RADIUS_MIN 0.02f // ... and never thinner than this
|
|
#define RAGDOLL_SWING_DEGREES 45.0f
|
|
#define RAGDOLL_TWIST_DEGREES 30.0f
|
|
#define RAGDOLL_MOTOR_HZ 4.0f
|
|
#define RAGDOLL_MOTOR_DAMPING 1.0f
|
|
#define RAGDOLL_LINEAR_DAMPING 0.2f
|
|
#define RAGDOLL_ANGULAR_DAMPING 0.5f
|
|
#define LEAF_BONE_RADII 4.0f // A bone with no child joint is this many minimum radii long ...
|
|
#define MIN_BONE_RADII 2.0f // ... and no bone is shorter than this many
|
|
#define MAX_CONE_DEGREES 179.0f // Widest swing or twist a ragdoll joint allows
|
|
#define DEFAULT_BUOYANCY 1.2f
|
|
#define DEFAULT_SINK_SPEED 0.3f
|
|
#define DEFAULT_SWIM_DRAG 2.0f
|
|
#define MAX_WHEELS 16
|
|
#define DEFAULT_ENGINE_TORQUE 500.0f
|
|
#define DEFAULT_ENGINE_MAX_RPM 6000.0f
|
|
#define MIN_ENGINE_TORQUE 1.0f
|
|
#define MIN_ENGINE_RPM 1.0f // The lowest idle a script may ask for ...
|
|
#define MIN_ENGINE_MAX_RPM 100.0f // ... and the lowest redline
|
|
#define DEFAULT_STEER_DEGREES 35.0f
|
|
#define MAX_STEER_DEGREES 89.0f
|
|
#define DEFAULT_BRAKE_TORQUE 1500.0f
|
|
#define DEBUG_CONTACT_SIZE 0.1f // Half the cross drawn at a contact
|
|
#define DEFAULT_HANDBRAKE_TORQUE 4000.0f
|
|
#define DEFAULT_SUSPENSION_HZ 1.5f
|
|
#define DEFAULT_SUSPENSION_DAMPING 0.5f
|
|
#define MIN_SUSPENSION_HZ 0.1f
|
|
#define VEHICLE_MAX_TILT_DEGREES 60.0f
|
|
#define FRONT_EPSILON 1.0e-4f // A wheel this far ahead of the mean is a front wheel
|
|
#define WHEEL_CAST_RADIUS 0.05f // Convex radius of the cylinder a wheel feels the ground with
|
|
#define MIN_INVERSE_INERTIA 1.0e-9f // Below this the chassis' roll inertia is taken as LEAN_INERTIA
|
|
#define TANK_PIVOT_THROTTLE 0.35f // Track drive when a tank turns on the spot
|
|
#define TANK_TURN_RATIO 0.9f // How much the inner track slows at full steer ...
|
|
#define TANK_TURN_MIN 0.1f // ... but never below this
|
|
#define DEFAULT_THRUST 2000.0f // Boats: propeller force ...
|
|
#define DEFAULT_THRUST_Y -0.2f // ... applied here in the hull's frame ...
|
|
#define DEFAULT_THRUST_Z 1.0f
|
|
#define DEFAULT_RUDDER 800.0f // ... and turning torque ...
|
|
#define RUDDER_FULL_SPEED 3.0f // ... which bites fully from this speed ...
|
|
#define RUDDER_MIN_BITE 0.2f // ... and this much when still
|
|
#define BOAT_BRAKE_RATIO 0.5f // A boat's braking force as a share of its thrust
|
|
#define LEAN_SPRING 5000.0f // Jolt's motorcycle lean spring and damping ...
|
|
#define LEAN_DAMPING 1000.0f
|
|
#define LEAN_INERTIA 40.0f // ... for a chassis of this roll inertia (kg m^2); scaled from there
|
|
#define DEFAULT_STEP_HEIGHT 0.3f // For a DEFAULT_EXTENT player; scales with the shape
|
|
#define DEFAULT_EXTENT 0.3f // The half-extent Jolt's character defaults were tuned for
|
|
#define PADDING_PER_EXTENT (0.02f / 0.3f)
|
|
#define PREDICTIVE_PER_EXTENT (0.1f / 0.3f)
|
|
#define TOLERANCE_PER_EXTENT (0.001f / 0.3f)
|
|
#define STICK_PER_EXTENT (0.5f / 0.3f)
|
|
#define STEP_TEST_PER_EXTENT (0.15f / 0.3f)
|
|
#define DEFAULT_SLOPE_DEGREES 45.0f
|
|
#define MAX_SLOPE_DEGREES 89.0f
|
|
#define DEFAULT_PUSH_STRENGTH 300.0f
|
|
#define STANDING_VERTICAL_SPEED 0.1f // Rising faster than this off the ground counts as airborne
|
|
#define ZERO_GRAVITY_SQ 1.0e-8f // Gravity shorter than this leaves up as +Y
|
|
#define MAX_BODY_PAIRS 65536 // Broad phase pairs a step may find: many more than touch
|
|
#define MAX_CONTACTS 8192
|
|
#define TEMP_ALLOCATOR_BYTES (16 * 1024 * 1024)
|
|
#define MIN_JOB_THREADS 1
|
|
#define STEP_SECONDS (1.0 / 60.0)
|
|
#define MAX_STEPS_PER_FRAME 4
|
|
#define MIN_DIMENSION 0.001f
|
|
#define MIN_MASS 0.001f
|
|
#define MIN_SCALE 1.0e-6f // A node axis scaled below this is not divided by
|
|
#define ZERO_LENGTH_SQ 1.0e-10f
|
|
#define DEFAULT_FRICTION 0.5f
|
|
#define DEFAULT_BOUNCE 0.1f
|
|
#define NO_HANDLE -1
|
|
#define DEFAULT_RAY_DISTANCE 1000.0f
|
|
#define WORLD_NODE -1 // A joint's other side fixed to the world
|
|
|
|
|
|
// Two object layers: what never moves and what may. Static bodies never collide with each other.
|
|
namespace {
|
|
|
|
const JPH::ObjectLayer LAYER_NON_MOVING = 0;
|
|
const JPH::ObjectLayer LAYER_MOVING = 1;
|
|
const JPH::BroadPhaseLayer BROAD_NON_MOVING(0);
|
|
const JPH::BroadPhaseLayer BROAD_MOVING(1);
|
|
const JPH::uint BROAD_COUNT = 2;
|
|
|
|
|
|
class ObjectPairFilterT final : public JPH::ObjectLayerPairFilter {
|
|
public:
|
|
bool ShouldCollide(JPH::ObjectLayer a, JPH::ObjectLayer b) const override {
|
|
return (a == LAYER_MOVING) || (b == LAYER_MOVING);
|
|
}
|
|
};
|
|
|
|
|
|
class BroadPhaseLayersT final : public JPH::BroadPhaseLayerInterface {
|
|
public:
|
|
JPH::uint GetNumBroadPhaseLayers() const override {
|
|
return BROAD_COUNT;
|
|
}
|
|
|
|
JPH::BroadPhaseLayer GetBroadPhaseLayer(JPH::ObjectLayer layer) const override {
|
|
return (layer == LAYER_NON_MOVING) ? BROAD_NON_MOVING : BROAD_MOVING;
|
|
}
|
|
|
|
#if defined(JPH_EXTERNAL_PROFILE) || defined(JPH_PROFILE_ENABLED)
|
|
const char *GetBroadPhaseLayerName(JPH::BroadPhaseLayer layer) const override {
|
|
return (layer == BROAD_NON_MOVING) ? "NON_MOVING" : "MOVING";
|
|
}
|
|
#endif
|
|
};
|
|
|
|
|
|
class ObjectVsBroadPhaseFilterT final : public JPH::ObjectVsBroadPhaseLayerFilter {
|
|
public:
|
|
bool ShouldCollide(JPH::ObjectLayer layer, JPH::BroadPhaseLayer broad) const override {
|
|
return (layer == LAYER_MOVING) || (broad == BROAD_MOVING);
|
|
}
|
|
};
|
|
|
|
|
|
// A body on a scene node.
|
|
struct BodyRecordT {
|
|
int32_t node;
|
|
uint32_t generation; // The node's, so a reused handle is not mistaken for this body
|
|
JPH::BodyID id;
|
|
BodyTypeE type;
|
|
bool trigger; // A sensor: reports overlaps, pushes nothing
|
|
bool enabled; // In the world (bodySetEnabled)
|
|
bool used;
|
|
bool water; // A trigger full of water (bodySetWater)
|
|
float waterDensity;
|
|
float waterLinearDrag;
|
|
float waterAngularDrag;
|
|
Vec3T current; // Flow inside the water
|
|
float buoyancy; // How this body floats in water (bodySetBuoyancy)
|
|
};
|
|
|
|
|
|
// A water surface found this step, for players and boats to test against.
|
|
struct WaterSurfaceT {
|
|
JPH::AABox box;
|
|
JPH::RVec3 position;
|
|
JPH::Vec3 normal;
|
|
JPH::Vec3 current;
|
|
};
|
|
|
|
|
|
// A character controller on a node: Jolt's CharacterVirtual, moved by intent rather than force.
|
|
struct PlayerRecordT {
|
|
int32_t node;
|
|
uint32_t generation;
|
|
JPH::Ref<JPH::CharacterVirtual> character;
|
|
float extent; // Smallest half-size of the shape: the unit the tolerances scale by
|
|
float height; // Of the shape, for the swimming test
|
|
float sinkSpeed; // Swimming: how fast it sinks with no input
|
|
float swimDrag;
|
|
bool swimming;
|
|
JPH::Vec3 swimCurrent;
|
|
int32_t inside[MAX_PLAYER_TRIGGERS]; // Trigger nodes the player is in
|
|
int32_t insideCount;
|
|
Vec3T intent; // Wanted horizontal velocity, consumed by the next steps
|
|
float jumpSpeed; // Pending jump, consumed by the next step
|
|
float gravityScale;
|
|
float stepHeight;
|
|
bool enabled;
|
|
bool used;
|
|
};
|
|
|
|
|
|
// One bone of a ragdoll: a joint node with at least one child joint, as a capsule body.
|
|
struct RagdollPartT {
|
|
int32_t joint;
|
|
int32_t parent; // Part index of the nearest ancestor joint with a part, or -1
|
|
float radius; // 0: from the bone length
|
|
float swing; // Degrees
|
|
float twist;
|
|
JPH::BodyID body;
|
|
JPH::Ref<JPH::Constraint> constraint;
|
|
JPH::Vec3 offsetPosition; // The joint's origin in the body's frame
|
|
JPH::Quat offsetRotation; // The joint's rotation in the body's frame
|
|
JPH::Quat rest; // This body's rotation relative to the parent body when activated
|
|
JPH::Vec3 scale; // The joint's world scale, kept for its children
|
|
};
|
|
|
|
|
|
// A skinned model's skeleton as bodies and joints, switched on and off.
|
|
struct RagdollRecordT {
|
|
int32_t node; // The model instance's root
|
|
uint32_t generation;
|
|
int32_t skinned; // The node carrying the skin
|
|
RagdollPartT parts[MAX_RAGDOLL_PARTS];
|
|
int32_t partCount;
|
|
JPH::Ref<JPH::GroupFilterTable> filter;
|
|
float strength;
|
|
bool strengthChanged;
|
|
bool active;
|
|
bool used;
|
|
};
|
|
|
|
|
|
// A mesh vertex's position quantised to SOFT_WELD cells, for welding vertices into particles.
|
|
struct WeldKeyT {
|
|
int32_t x;
|
|
int32_t y;
|
|
int32_t z;
|
|
|
|
bool operator==(const WeldKeyT &other) const {
|
|
return (x == other.x) && (y == other.y) && (z == other.z);
|
|
}
|
|
};
|
|
|
|
|
|
struct WeldHashT {
|
|
size_t operator()(const WeldKeyT &key) const {
|
|
return ((size_t)(uint32_t)key.x * 73856093u) ^ ((size_t)(uint32_t)key.y * 19349663u) ^ ((size_t)(uint32_t)key.z * 83492791u);
|
|
}
|
|
};
|
|
|
|
|
|
// A vertex of a soft body held in place, or held to a node.
|
|
struct SoftPinT {
|
|
int32_t vertex;
|
|
int32_t follow; // -1: held where it was pinned
|
|
};
|
|
|
|
|
|
// A soft body driving a mesh: cloth or a pressure body from the node's own mesh (its vertices
|
|
// welded by position into particles), or a rope whose tube mesh the engine makes and moves.
|
|
struct SoftRecordT {
|
|
int32_t node;
|
|
uint32_t generation;
|
|
SoftKindE kind;
|
|
int32_t mesh;
|
|
JPH::BodyID body;
|
|
JPH::Ref<JPH::SoftBodySharedSettings> shared;
|
|
int32_t *meshToSoft; // Mesh vertex to particle
|
|
float *positions; // Particles, world space
|
|
int32_t count;
|
|
float *meshPositions; // Scratch for the mesh rewrite
|
|
int32_t meshVertexCount;
|
|
SoftPinT pins[MAX_SOFT_PINS];
|
|
int32_t pinCount;
|
|
float stretch;
|
|
float bend;
|
|
float pressure;
|
|
float mass;
|
|
float damping;
|
|
float ropeRadius;
|
|
bool used;
|
|
};
|
|
|
|
|
|
// A wheel of a vehicle: the node the engine poses, and its geometry relative to the chassis.
|
|
struct WheelRecordT {
|
|
int32_t node;
|
|
uint32_t generation;
|
|
Vec3T rest; // Where the wheel sits in the chassis' frame, taken when it was added
|
|
float radius;
|
|
float width;
|
|
float suspension;
|
|
bool steered;
|
|
bool driven;
|
|
bool steeredSet; // vehicleSetWheel called; otherwise front wheels steer and all drive
|
|
};
|
|
|
|
|
|
// A vehicle on a chassis body: the recipe, and the Jolt constraint built from it on demand.
|
|
struct VehicleRecordT {
|
|
int32_t node;
|
|
uint32_t generation;
|
|
VehicleKindE kind;
|
|
JPH::Ref<JPH::VehicleConstraint> constraint;
|
|
JPH::Ref<JPH::VehicleCollisionTester> tester;
|
|
WheelRecordT wheels[MAX_WHEELS];
|
|
int32_t wheelCount;
|
|
float maxTorque;
|
|
float maxRpm;
|
|
float minRpm;
|
|
float gears[VEHICLE_MAX_GEARS];
|
|
int32_t gearCount;
|
|
float reverseGear;
|
|
bool automatic;
|
|
float suspensionHz;
|
|
float suspensionDamping;
|
|
float maxSteer; // Degrees
|
|
float brakeTorque;
|
|
float handBrakeTorque;
|
|
float antiRoll;
|
|
float inputForward;
|
|
float inputRight;
|
|
float inputBrake;
|
|
float inputHandBrake;
|
|
float thrust; // Boats: propeller force ...
|
|
Vec3T thrustPoint; // ... applied here in the hull's frame
|
|
float rudder; // ... and turning torque
|
|
bool dirty; // Settings changed: rebuild before the next step
|
|
bool used;
|
|
};
|
|
|
|
|
|
// Collects contacts from Jolt's job threads; the engine drains it after the step. Trigger
|
|
// overlaps are counted per (trigger, body) pair, since Jolt reports every sub-shape pair on its
|
|
// own (a mesh trigger: one per triangle) and the engine wants one enter and one leave.
|
|
class ContactListenerT final : public JPH::ContactListener {
|
|
public:
|
|
std::mutex lock;
|
|
std::vector<PhysicsEventT> events;
|
|
std::unordered_map<uint64_t, int32_t> overlaps; // (trigger, body) to sub-shape pairs touching
|
|
|
|
void OnContactAdded(const JPH::Body &a, const JPH::Body &b, const JPH::ContactManifold &manifold, JPH::ContactSettings &settings) override;
|
|
void OnContactRemoved(const JPH::SubShapeIDPair &pair) override;
|
|
void forget(int32_t node); // Drops the overlap counts a node is party to
|
|
void overlap(int32_t trigger, int32_t node, bool entered); // One sub-shape pair started or stopped touching
|
|
void push(const PhysicsEventT &event); // Queues an event, taking the lock
|
|
void pushLocked(const PhysicsEventT &event); // Queues an event under a lock already held
|
|
};
|
|
|
|
|
|
// A constraint between two bodies (or one body and the world).
|
|
struct JointRecordT {
|
|
JPH::Ref<JPH::Constraint> constraint;
|
|
JointTypeE type;
|
|
int32_t nodeA;
|
|
int32_t nodeB;
|
|
bool used;
|
|
};
|
|
|
|
|
|
// Contacts between a player and bodies, reported like body contacts: the player's node first,
|
|
// or the trigger first when the other is a sensor. These arrive from the character's own update
|
|
// on the engine thread, but share the body listener's queue and lock.
|
|
class PlayerListenerT final : public JPH::CharacterContactListener {
|
|
public:
|
|
bool OnContactValidate(const JPH::CharacterVirtual *character, const JPH::CharacterContact &contact) override;
|
|
void OnContactAdded(const JPH::CharacterVirtual *character, const JPH::CharacterContact &contact, JPH::CharacterContactSettings &settings) override;
|
|
};
|
|
|
|
|
|
// The tables are fixed-size (bodyCount is the highest slot ever used, so loops stay short);
|
|
// each is indexed by node handle through a vector grown on demand, since handles are small
|
|
// dense integers.
|
|
struct WorldT {
|
|
JPH::TempAllocatorImpl *tempAllocator;
|
|
JPH::JobSystemThreadPool *jobs;
|
|
BroadPhaseLayersT broadPhaseLayers;
|
|
ObjectVsBroadPhaseFilterT objectVsBroadPhase;
|
|
ObjectPairFilterT objectPairs;
|
|
JPH::PhysicsSystem *system;
|
|
ContactListenerT *contacts;
|
|
BodyRecordT *bodies;
|
|
int32_t bodyCount;
|
|
std::vector<JointRecordT> joints;
|
|
PlayerRecordT *players;
|
|
int32_t playerCount;
|
|
PlayerListenerT *playerListener;
|
|
VehicleRecordT *vehicles;
|
|
int32_t vehicleCount;
|
|
RagdollRecordT *ragdolls;
|
|
int32_t ragdollCount;
|
|
SoftRecordT *softs;
|
|
int32_t softCount;
|
|
std::vector<int32_t> bodyOfNode; // Node handle to slot in each table, or NO_HANDLE
|
|
std::vector<int32_t> playerOfNode;
|
|
std::vector<int32_t> vehicleOfNode;
|
|
std::vector<int32_t> ragdollOfNode;
|
|
std::vector<int32_t> softOfNode;
|
|
JPH::uint32 ragdollGroups; // Next collision group id
|
|
WaterSurfaceT waters[MAX_WATERS];
|
|
int32_t waterCount;
|
|
double accumulator; // Seconds owed to the fixed step
|
|
uint64_t lastTick;
|
|
bool enabled;
|
|
bool planar; // New bodies keep to the XY plane (2D games)
|
|
};
|
|
|
|
|
|
WorldT *_world = nullptr;
|
|
|
|
|
|
void _applyWater(float dt);
|
|
JPH::RefConst<JPH::Shape> _buildHeightField(int32_t mesh, Vec3T scale);
|
|
JPH::RefConst<JPH::Shape> _buildMeshShape(int32_t node, ShapeTypeE shape, Vec3T position, QuatT rotation);
|
|
JPH::RefConst<JPH::Shape> _buildShape(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, float c, Vec3T position, QuatT rotation, Vec3T scale);
|
|
bool _buildSoft(SoftRecordT *record);
|
|
bool _buildVehicle(VehicleRecordT *record);
|
|
void _collectGeometry(int32_t node, const Mat4T *toBody, ShapeTypeE shape, JPH::Array<JPH::Vec3> &points, JPH::IndexedTriangleList &triangles, JPH::VertexList &vertices);
|
|
void _destroyBody(JPH::BodyID &id);
|
|
void _drawDebug(void);
|
|
void _driveVehicles(void);
|
|
BodyRecordT *_find(int32_t node);
|
|
PlayerRecordT *_findPlayer(int32_t node);
|
|
RagdollRecordT *_findRagdoll(int32_t node);
|
|
int32_t _findSkinned(int32_t node);
|
|
SoftRecordT *_findSoft(int32_t node);
|
|
VehicleRecordT *_findVehicle(int32_t node);
|
|
JPH::Quat _fromQuat(QuatT q);
|
|
JPH::Vec3 _fromVec3(Vec3T v);
|
|
int32_t _indexGet(const std::vector<int32_t> &index, int32_t node);
|
|
void _indexSet(std::vector<int32_t> &index, int32_t node, int32_t slot);
|
|
int32_t _nearestSoftVertex(const SoftRecordT *record, Vec3T point);
|
|
void _pinSoft(void);
|
|
void _playerInside(PlayerRecordT *record, const int32_t *now, int32_t count);
|
|
void _playerTriggers(PlayerRecordT *record);
|
|
JPH::Vec3 _playerUp(void);
|
|
void _poseWheels(void);
|
|
int32_t _ragdollPartOf(RagdollRecordT *record, int32_t joint);
|
|
void _release(BodyRecordT *record);
|
|
void _releasePlayer(PlayerRecordT *record);
|
|
void _releaseRagdoll(RagdollRecordT *record);
|
|
void _releaseRagdollBodies(RagdollRecordT *record);
|
|
void _releaseSoft(SoftRecordT *record);
|
|
void _releaseSoftBody(SoftRecordT *record);
|
|
void _releaseVehicle(VehicleRecordT *record);
|
|
void _resetRagdoll(RagdollRecordT *record);
|
|
void _resetSoft(SoftRecordT *record);
|
|
void _resetVehicle(VehicleRecordT *record);
|
|
void _ropeMesh(const SoftRecordT *record, Vec3T *out);
|
|
void _setDrivetrain(const VehicleRecordT *record, JPH::VehicleEngineSettings &engine, JPH::VehicleTransmissionSettings &transmission);
|
|
float _softInvMass(const SoftRecordT *record);
|
|
void _softSetMasses(SoftRecordT *record);
|
|
void _steerRagdolls(void);
|
|
void _step(void);
|
|
void _stepPlayers(float dt);
|
|
QuatT _toQuat(JPH::Quat q);
|
|
Vec3T _toVec3(JPH::Vec3 v);
|
|
void _trace(const char *fmt, ...);
|
|
PhysicsEventT _triggerEvent(PhysicsEventTypeE type, int32_t trigger, int32_t node);
|
|
int32_t _triggerNode(JPH::BodyID id);
|
|
bool _underWater(JPH::RVec3Arg point, JPH::Vec3 *current);
|
|
void _writePlayers(void);
|
|
void _writeRagdolls(void);
|
|
void _writeSoft(void);
|
|
|
|
|
|
uint32_t _debugMask = DEBUG_NONE;
|
|
|
|
|
|
void ContactListenerT::OnContactAdded(const JPH::Body &a, const JPH::Body &b, const JPH::ContactManifold &manifold, JPH::ContactSettings &settings) {
|
|
PhysicsEventT event;
|
|
JPH::Vec3 relative;
|
|
|
|
(void)settings;
|
|
if (a.IsSensor() || b.IsSensor()) {
|
|
const JPH::Body &trigger = a.IsSensor() ? a : b;
|
|
const JPH::Body &other = a.IsSensor() ? b : a;
|
|
int32_t node = (int32_t)(uint32_t)other.GetUserData();
|
|
|
|
// A player's inner body: the engine reports players entering triggers itself.
|
|
if (_findPlayer(node) == nullptr) {
|
|
overlap((int32_t)(uint32_t)trigger.GetUserData(), node, true);
|
|
}
|
|
return;
|
|
}
|
|
event.type = PHYSICS_EVENT_COLLISION;
|
|
event.nodeA = (int32_t)(uint32_t)a.GetUserData();
|
|
event.nodeB = (int32_t)(uint32_t)b.GetUserData();
|
|
event.point = _toVec3(JPH::Vec3(manifold.GetWorldSpaceContactPointOn1(0)));
|
|
relative = a.GetLinearVelocity() - b.GetLinearVelocity();
|
|
event.speed = fabsf(relative.Dot(manifold.mWorldSpaceNormal));
|
|
push(event);
|
|
}
|
|
|
|
|
|
// A contact ending only matters for triggers. This runs inside Jolt's step on a job thread,
|
|
// where the locking interface deadlocks, so the bodies are read through the lock-free one (a
|
|
// body destroyed since the contact was made simply fails to lock).
|
|
void ContactListenerT::OnContactRemoved(const JPH::SubShapeIDPair &pair) {
|
|
JPH::BodyLockRead lockA(_world->system->GetBodyLockInterfaceNoLock(), pair.GetBody1ID());
|
|
JPH::BodyLockRead lockB(_world->system->GetBodyLockInterfaceNoLock(), pair.GetBody2ID());
|
|
const JPH::Body *trigger;
|
|
const JPH::Body *other;
|
|
int32_t node;
|
|
|
|
if (!lockA.Succeeded() || !lockB.Succeeded()) {
|
|
return;
|
|
}
|
|
if (lockA.GetBody().IsSensor()) {
|
|
trigger = &lockA.GetBody();
|
|
other = &lockB.GetBody();
|
|
} else if (lockB.GetBody().IsSensor()) {
|
|
trigger = &lockB.GetBody();
|
|
other = &lockA.GetBody();
|
|
} else {
|
|
return;
|
|
}
|
|
node = (int32_t)(uint32_t)other->GetUserData();
|
|
if (_findPlayer(node) == nullptr) {
|
|
overlap((int32_t)(uint32_t)trigger->GetUserData(), node, false);
|
|
}
|
|
}
|
|
|
|
|
|
void ContactListenerT::forget(int32_t node) {
|
|
std::lock_guard<std::mutex> guard(lock);
|
|
std::unordered_map<uint64_t, int32_t>::iterator it = overlaps.begin();
|
|
|
|
while (it != overlaps.end()) {
|
|
if (((int32_t)(uint32_t)(it->first >> 32) == node) || ((int32_t)(uint32_t)it->first == node)) {
|
|
it = overlaps.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void ContactListenerT::overlap(int32_t trigger, int32_t node, bool entered) {
|
|
uint64_t key = ((uint64_t)(uint32_t)trigger << 32) | (uint32_t)node;
|
|
std::lock_guard<std::mutex> guard(lock);
|
|
|
|
if (entered) {
|
|
if (++overlaps[key] != 1) {
|
|
return;
|
|
}
|
|
} else {
|
|
std::unordered_map<uint64_t, int32_t>::iterator it = overlaps.find(key);
|
|
|
|
if ((it == overlaps.end()) || (--it->second > 0)) {
|
|
return;
|
|
}
|
|
overlaps.erase(it);
|
|
}
|
|
pushLocked(_triggerEvent(entered ? PHYSICS_EVENT_ENTER : PHYSICS_EVENT_LEAVE, trigger, node));
|
|
}
|
|
|
|
|
|
void ContactListenerT::push(const PhysicsEventT &event) {
|
|
std::lock_guard<std::mutex> guard(lock);
|
|
|
|
pushLocked(event);
|
|
}
|
|
|
|
|
|
void ContactListenerT::pushLocked(const PhysicsEventT &event) {
|
|
if (events.size() < PHYSICS_MAX_EVENTS) {
|
|
events.push_back(event);
|
|
}
|
|
}
|
|
|
|
|
|
#ifdef JPH_DEBUG_RENDERER
|
|
// Jolt's debug drawing lands in the scene's line list: triangles as their edges, text dropped.
|
|
class LineRendererT final : public JPH::DebugRendererSimple {
|
|
public:
|
|
void DrawLine(JPH::RVec3Arg from, JPH::RVec3Arg to, JPH::ColorArg colour) override {
|
|
sceneDrawLine(_toVec3(from), _toVec3(to), colour.r, colour.g, colour.b);
|
|
}
|
|
void DrawTriangle(JPH::RVec3Arg a, JPH::RVec3Arg b, JPH::RVec3Arg c, JPH::ColorArg colour, ECastShadow shadow) override {
|
|
(void)shadow;
|
|
DrawLine(a, b, colour);
|
|
DrawLine(b, c, colour);
|
|
DrawLine(c, a, colour);
|
|
}
|
|
void DrawText3D(JPH::RVec3Arg position, const JPH::string_view &text, JPH::ColorArg colour, float height) override {
|
|
(void)position;
|
|
(void)text;
|
|
(void)colour;
|
|
(void)height;
|
|
}
|
|
};
|
|
|
|
|
|
// Static bodies only draw when asked: a level mesh is a lot of lines.
|
|
class MovingFilterT final : public JPH::BodyDrawFilter {
|
|
public:
|
|
bool ShouldDraw(const JPH::Body &body) const override {
|
|
return !body.IsStatic();
|
|
}
|
|
};
|
|
|
|
|
|
LineRendererT *_renderer = nullptr;
|
|
#endif
|
|
|
|
|
|
// What physicsSetDebug asked for, as lines in the scene for this frame.
|
|
void _drawDebug(void) {
|
|
#ifdef JPH_DEBUG_RENDERER
|
|
JPH::BodyManager::DrawSettings settings;
|
|
MovingFilterT moving;
|
|
int32_t x;
|
|
|
|
if (_debugMask == DEBUG_NONE) {
|
|
return;
|
|
}
|
|
if (_renderer == nullptr) {
|
|
_renderer = new LineRendererT();
|
|
}
|
|
settings.mDrawShape = (_debugMask & DEBUG_SHAPES) != 0;
|
|
settings.mDrawShapeWireframe = true;
|
|
settings.mDrawSoftBodyEdgeConstraints = settings.mDrawShape;
|
|
settings.mDrawVelocity = (_debugMask & DEBUG_VELOCITIES) != 0;
|
|
if (settings.mDrawShape || settings.mDrawVelocity) {
|
|
_world->system->DrawBodies(settings, _renderer, (_debugMask & DEBUG_STATIC) ? nullptr : &moving);
|
|
}
|
|
if (_debugMask & DEBUG_CONSTRAINTS) {
|
|
_world->system->DrawConstraints(_renderer);
|
|
_world->system->DrawConstraintLimits(_renderer);
|
|
}
|
|
if (_debugMask & DEBUG_SHAPES) {
|
|
for (x = 0; x < _world->playerCount; x++) {
|
|
PlayerRecordT *record = &_world->players[x];
|
|
|
|
if (record->used && record->enabled) {
|
|
record->character->GetShape()->Draw(_renderer, record->character->GetCenterOfMassTransform(), JPH::Vec3::sOne(), JPH::Color::sYellow, false, true);
|
|
}
|
|
}
|
|
}
|
|
if (_debugMask & DEBUG_CONTACTS) {
|
|
std::lock_guard<std::mutex> guard(_world->contacts->lock);
|
|
|
|
for (const PhysicsEventT &event : _world->contacts->events) {
|
|
if (event.type == PHYSICS_EVENT_COLLISION) {
|
|
JPH::Vec3 at = _fromVec3(event.point);
|
|
|
|
_renderer->DrawLine(at - JPH::Vec3::sAxisX() * DEBUG_CONTACT_SIZE, at + JPH::Vec3::sAxisX() * DEBUG_CONTACT_SIZE, JPH::Color::sRed);
|
|
_renderer->DrawLine(at - JPH::Vec3::sAxisY() * DEBUG_CONTACT_SIZE, at + JPH::Vec3::sAxisY() * DEBUG_CONTACT_SIZE, JPH::Color::sRed);
|
|
_renderer->DrawLine(at - JPH::Vec3::sAxisZ() * DEBUG_CONTACT_SIZE, at + JPH::Vec3::sAxisZ() * DEBUG_CONTACT_SIZE, JPH::Color::sRed);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
// Runs the fixed steps the accumulator owes: kinematic bodies go where their nodes went first,
|
|
// dynamic bodies drive their nodes afterwards.
|
|
void _step(void) {
|
|
int32_t steps = 0;
|
|
int32_t x;
|
|
Vec3T position;
|
|
QuatT rotation;
|
|
Vec3T scale;
|
|
|
|
sceneUpdateTransforms();
|
|
{
|
|
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
|
|
float dt = (float)(STEP_SECONDS * (int32_t)(_world->accumulator / STEP_SECONDS));
|
|
|
|
// Kinematic bodies go where their nodes went, with the velocity that implies.
|
|
for (x = 0; x < _world->bodyCount; x++) {
|
|
BodyRecordT *record = &_world->bodies[x];
|
|
|
|
if (!record->used || (record->type != BODY_KINEMATIC) || !record->enabled) {
|
|
continue;
|
|
}
|
|
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) {
|
|
_release(record);
|
|
continue;
|
|
}
|
|
nodeGetWorldTransform(record->node, &position, &rotation, &scale);
|
|
bodies.MoveKinematic(record->id, JPH::RVec3(position.x, position.y, position.z), _fromQuat(rotation), dt);
|
|
}
|
|
while ((_world->accumulator >= STEP_SECONDS) && (steps < MAX_STEPS_PER_FRAME)) {
|
|
_applyWater((float)STEP_SECONDS);
|
|
_driveVehicles();
|
|
_steerRagdolls();
|
|
_pinSoft();
|
|
_stepPlayers((float)STEP_SECONDS);
|
|
_world->system->Update((float)STEP_SECONDS, 1, _world->tempAllocator, _world->jobs);
|
|
_world->accumulator -= STEP_SECONDS;
|
|
steps++;
|
|
}
|
|
_writePlayers();
|
|
_poseWheels();
|
|
_writeRagdolls();
|
|
_writeSoft();
|
|
// Dynamic bodies drive their nodes.
|
|
for (x = 0; x < _world->bodyCount; x++) {
|
|
BodyRecordT *record = &_world->bodies[x];
|
|
JPH::RVec3 where;
|
|
JPH::Quat how;
|
|
|
|
if (!record->used || (record->type != BODY_DYNAMIC) || !record->enabled) {
|
|
continue;
|
|
}
|
|
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) {
|
|
_release(record);
|
|
continue;
|
|
}
|
|
bodies.GetPositionAndRotation(record->id, where, how);
|
|
nodeSetWorldTransform(record->node, vec3((float)where.GetX(), (float)where.GetY(), (float)where.GetZ()), _toQuat(how));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// A height field from a heightmap mesh's samples, resampled to the square even count Jolt wants,
|
|
// with the node's scale baked into the sizes; null for any other mesh.
|
|
JPH::RefConst<JPH::Shape> _buildHeightField(int32_t mesh, Vec3T scale) {
|
|
const float *heights;
|
|
int32_t columns;
|
|
int32_t rows;
|
|
float sizeX;
|
|
float sizeY;
|
|
float sizeZ;
|
|
int32_t n;
|
|
int32_t x;
|
|
int32_t y;
|
|
float *samples;
|
|
|
|
if (!meshGetHeights(mesh, &heights, &columns, &rows, &sizeX, &sizeY, &sizeZ)) {
|
|
return nullptr;
|
|
}
|
|
n = SDL_max(columns + 1, rows + 1);
|
|
if (n & 1) {
|
|
n++;
|
|
}
|
|
n = SDL_max(n, 4);
|
|
samples = (float *)SDL_malloc(sizeof(float) * (size_t)n * (size_t)n);
|
|
if (samples == nullptr) {
|
|
utilDie("Out of memory building a height field.");
|
|
}
|
|
for (y = 0; y < n; y++) {
|
|
for (x = 0; x < n; x++) {
|
|
// Bilinear from the mesh's grid; Jolt's row index runs along +Z like the mesh's.
|
|
float u = (float)x / (float)(n - 1) * (float)columns;
|
|
float v = (float)y / (float)(n - 1) * (float)rows;
|
|
int32_t x0 = SDL_min((int32_t)u, columns - 1);
|
|
int32_t y0 = SDL_min((int32_t)v, rows - 1);
|
|
float fx = u - (float)x0;
|
|
float fy = v - (float)y0;
|
|
float h = (heights[y0 * (columns + 1) + x0] * (1.0f - fx) + heights[y0 * (columns + 1) + x0 + 1] * fx) * (1.0f - fy) + (heights[(y0 + 1) * (columns + 1) + x0] * (1.0f - fx) + heights[(y0 + 1) * (columns + 1) + x0 + 1] * fx) * fy;
|
|
|
|
samples[y * n + x] = h;
|
|
}
|
|
}
|
|
JPH::HeightFieldShapeSettings settings(samples, JPH::Vec3(-sizeX * scale.x / 2.0f, 0.0f, -sizeZ * scale.z / 2.0f), JPH::Vec3(sizeX * scale.x / (float)(n - 1), sizeY * scale.y, sizeZ * scale.z / (float)(n - 1)), (JPH::uint32)n);
|
|
JPH::ShapeSettings::ShapeResult result = settings.Create();
|
|
|
|
SDL_free(samples);
|
|
if (result.HasError()) {
|
|
utilTrace("Physics: height field: %s", result.GetError().c_str());
|
|
return nullptr;
|
|
}
|
|
return result.Get();
|
|
}
|
|
|
|
|
|
// A convex hull or a triangle mesh from the geometry under a node (its own mesh and every
|
|
// descendant's, a model instance included), in the body's frame: the node's world scale is
|
|
// baked in, its position and rotation are the body's.
|
|
JPH::RefConst<JPH::Shape> _buildMeshShape(int32_t node, ShapeTypeE shape, Vec3T position, QuatT rotation) {
|
|
JPH::Array<JPH::Vec3> points;
|
|
JPH::VertexList vertices;
|
|
JPH::IndexedTriangleList triangles;
|
|
Mat4T bodyWorld = mat4Compose(position, rotation, vec3(1.0f, 1.0f, 1.0f));
|
|
Mat4T toBody;
|
|
|
|
if (!mat4Invert(bodyWorld, &toBody)) {
|
|
return nullptr;
|
|
}
|
|
_collectGeometry(node, &toBody, shape, points, triangles, vertices);
|
|
if (shape == SHAPE_HULL) {
|
|
JPH::ConvexHullShapeSettings settings(points);
|
|
JPH::Shape::ShapeResult result;
|
|
|
|
if (points.size() < 4) {
|
|
utilTrace("Physics: node %d has too little geometry for a hull.", node);
|
|
return nullptr;
|
|
}
|
|
result = settings.Create();
|
|
if (result.HasError()) {
|
|
utilTrace("Physics: hull: %s", result.GetError().c_str());
|
|
return nullptr;
|
|
}
|
|
return result.Get();
|
|
}
|
|
{
|
|
JPH::MeshShapeSettings settings(vertices, triangles);
|
|
JPH::Shape::ShapeResult result;
|
|
|
|
if (triangles.empty()) {
|
|
utilTrace("Physics: node %d has no triangles for a mesh shape.", node);
|
|
return nullptr;
|
|
}
|
|
result = settings.Create();
|
|
if (result.HasError()) {
|
|
utilTrace("Physics: mesh: %s", result.GetError().c_str());
|
|
return nullptr;
|
|
}
|
|
return result.Get();
|
|
}
|
|
}
|
|
|
|
|
|
// Gathers the node's mesh and its descendants' into the body's frame: points for a hull,
|
|
// vertices and triangles for a mesh.
|
|
void _collectGeometry(int32_t node, const Mat4T *toBody, ShapeTypeE shape, JPH::Array<JPH::Vec3> &points, JPH::IndexedTriangleList &triangles, JPH::VertexList &vertices) {
|
|
const float *positions;
|
|
const uint32_t *indices;
|
|
int32_t vertexCount;
|
|
int32_t indexCount;
|
|
int32_t mesh = nodeGetMesh(node);
|
|
int32_t x;
|
|
|
|
if ((mesh != NO_HANDLE) && meshGetGeometry(mesh, &positions, &vertexCount, &indices, &indexCount)) {
|
|
Vec3T position;
|
|
QuatT rotation;
|
|
Vec3T scale;
|
|
Mat4T local;
|
|
uint32_t base = (uint32_t)vertices.size();
|
|
|
|
nodeGetWorldTransform(node, &position, &rotation, &scale);
|
|
local = mat4Multiply(*toBody, mat4Compose(position, rotation, scale));
|
|
for (x = 0; x < vertexCount; x++) {
|
|
Vec3T v = mat4TransformPoint(local, vec3(positions[x * 3], positions[x * 3 + 1], positions[x * 3 + 2]));
|
|
|
|
if (shape == SHAPE_HULL) {
|
|
points.push_back(JPH::Vec3(v.x, v.y, v.z));
|
|
} else {
|
|
vertices.push_back(JPH::Float3(v.x, v.y, v.z));
|
|
}
|
|
}
|
|
if (shape != SHAPE_HULL) {
|
|
for (x = 0; x + 2 < indexCount; x += 3) {
|
|
triangles.push_back(JPH::IndexedTriangle(base + indices[x], base + indices[x + 1], base + indices[x + 2]));
|
|
}
|
|
}
|
|
}
|
|
for (x = 0; x < nodeGetChildCount(node); x++) {
|
|
_collectGeometry(nodeGetChild(node, x), toBody, shape, points, triangles, vertices);
|
|
}
|
|
}
|
|
|
|
|
|
// Takes a body out of the world (a no-op when it is not in it) and destroys it.
|
|
void _destroyBody(JPH::BodyID &id) {
|
|
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
|
|
|
|
bodies.RemoveBody(id);
|
|
bodies.DestroyBody(id);
|
|
id = JPH::BodyID();
|
|
}
|
|
|
|
|
|
// The record for a node's body, or NULL. A record whose node was deleted (or reused) is
|
|
// released on the way.
|
|
BodyRecordT *_find(int32_t node) {
|
|
BodyRecordT *record;
|
|
int32_t slot;
|
|
|
|
if (_world == nullptr) {
|
|
return nullptr;
|
|
}
|
|
slot = _indexGet(_world->bodyOfNode, node);
|
|
if (slot == NO_HANDLE) {
|
|
return nullptr;
|
|
}
|
|
record = &_world->bodies[slot];
|
|
if (!nodeValid(node) || (nodeGetGeneration(node) != record->generation)) {
|
|
_release(record);
|
|
return nullptr;
|
|
}
|
|
return record;
|
|
}
|
|
|
|
|
|
JPH::Quat _fromQuat(QuatT q) {
|
|
return JPH::Quat(q.x, q.y, q.z, q.w).Normalized();
|
|
}
|
|
|
|
|
|
JPH::Vec3 _fromVec3(Vec3T v) {
|
|
return JPH::Vec3(v.x, v.y, v.z);
|
|
}
|
|
|
|
|
|
// The slot a node maps to in one of the tables, or NO_HANDLE.
|
|
int32_t _indexGet(const std::vector<int32_t> &index, int32_t node) {
|
|
if ((node < 0) || ((size_t)node >= index.size())) {
|
|
return NO_HANDLE;
|
|
}
|
|
return index[(size_t)node];
|
|
}
|
|
|
|
|
|
// Maps a node to a slot (NO_HANDLE unmaps it), growing the index to reach it.
|
|
void _indexSet(std::vector<int32_t> &index, int32_t node, int32_t slot) {
|
|
if (node < 0) {
|
|
return;
|
|
}
|
|
if ((size_t)node >= index.size()) {
|
|
if (slot == NO_HANDLE) {
|
|
return;
|
|
}
|
|
index.resize((size_t)node + 1, NO_HANDLE);
|
|
}
|
|
index[(size_t)node] = slot;
|
|
}
|
|
|
|
|
|
// Takes the body out of the world (its vehicle and joints first) and frees its slot.
|
|
void _release(BodyRecordT *record) {
|
|
VehicleRecordT *vehicle = _findVehicle(record->node);
|
|
int32_t x;
|
|
|
|
if (vehicle != nullptr) {
|
|
_releaseVehicle(vehicle);
|
|
}
|
|
for (x = 0; x < (int32_t)_world->joints.size(); x++) {
|
|
if (_world->joints[(size_t)x].used && ((_world->joints[(size_t)x].nodeA == record->node) || (_world->joints[(size_t)x].nodeB == record->node))) {
|
|
jointDelete(x);
|
|
}
|
|
}
|
|
_world->contacts->forget(record->node);
|
|
_indexSet(_world->bodyOfNode, record->node, NO_HANDLE);
|
|
_destroyBody(record->id);
|
|
memset(record, 0, sizeof(*record));
|
|
}
|
|
|
|
|
|
// Triggers are not solid for a player: the engine tests players against them itself.
|
|
bool PlayerListenerT::OnContactValidate(const JPH::CharacterVirtual *character, const JPH::CharacterContact &contact) {
|
|
(void)character;
|
|
return !contact.mIsSensorB;
|
|
}
|
|
|
|
|
|
void PlayerListenerT::OnContactAdded(const JPH::CharacterVirtual *character, const JPH::CharacterContact &contact, JPH::CharacterContactSettings &settings) {
|
|
PhysicsEventT event;
|
|
|
|
(void)settings;
|
|
event.type = PHYSICS_EVENT_COLLISION;
|
|
event.nodeA = (int32_t)(uint32_t)character->GetUserData();
|
|
event.nodeB = (int32_t)(uint32_t)contact.mUserData;
|
|
event.point = _toVec3(JPH::Vec3(contact.mPosition));
|
|
event.speed = fabsf((character->GetLinearVelocity() - contact.mLinearVelocity).Dot(contact.mContactNormal));
|
|
_world->contacts->push(event);
|
|
}
|
|
|
|
|
|
// The collision shape for a body or a player, scaled by the node.
|
|
JPH::RefConst<JPH::Shape> _buildShape(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, float c, Vec3T position, QuatT rotation, Vec3T scale) {
|
|
float radius;
|
|
float height;
|
|
|
|
switch (shape) {
|
|
case SHAPE_BOX:
|
|
return new JPH::BoxShape(JPH::Vec3(SDL_max(a * scale.x, MIN_DIMENSION) / 2.0f, SDL_max(b * scale.y, MIN_DIMENSION) / 2.0f, SDL_max(c * scale.z, MIN_DIMENSION) / 2.0f), 0.0f);
|
|
case SHAPE_SPHERE:
|
|
return new JPH::SphereShape(SDL_max(a * SDL_max(scale.x, SDL_max(scale.y, scale.z)), MIN_DIMENSION));
|
|
case SHAPE_CAPSULE:
|
|
radius = SDL_max(a * SDL_max(scale.x, scale.z), MIN_DIMENSION);
|
|
height = SDL_max(b * scale.y, MIN_DIMENSION);
|
|
return new JPH::CapsuleShape(SDL_max(height / 2.0f - radius, MIN_DIMENSION), radius);
|
|
case SHAPE_CYLINDER:
|
|
radius = SDL_max(a * SDL_max(scale.x, scale.z), MIN_DIMENSION);
|
|
height = SDL_max(b * scale.y, MIN_DIMENSION);
|
|
return new JPH::CylinderShape(height / 2.0f, radius);
|
|
case SHAPE_HULL:
|
|
case SHAPE_MESH:
|
|
if ((shape == SHAPE_MESH) && (type == BODY_DYNAMIC)) {
|
|
utilTrace("Physics: a mesh shape can only be static or kinematic; use a hull for node %d.", node);
|
|
return nullptr;
|
|
}
|
|
if ((shape == SHAPE_MESH) && (nodeGetMesh(node) != NO_HANDLE)) {
|
|
// A heightmap mesh gets Jolt's height field, far cheaper than its triangles.
|
|
JPH::RefConst<JPH::Shape> field = _buildHeightField(nodeGetMesh(node), scale);
|
|
|
|
if (field != nullptr) {
|
|
return field;
|
|
}
|
|
}
|
|
return _buildMeshShape(node, shape, position, rotation);
|
|
default:
|
|
utilTrace("Physics: unknown shape %d.", (int32_t)shape);
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
|
|
PlayerRecordT *_findPlayer(int32_t node) {
|
|
int32_t slot = (_world != nullptr) ? _indexGet(_world->playerOfNode, node) : NO_HANDLE;
|
|
|
|
return (slot == NO_HANDLE) ? nullptr : &_world->players[slot];
|
|
}
|
|
|
|
|
|
// Which way is up for players: against gravity (a 2D game's gravity points down the screen, +Y).
|
|
JPH::Vec3 _playerUp(void) {
|
|
JPH::Vec3 gravity = _world->system->GetGravity();
|
|
|
|
if (gravity.LengthSq() < ZERO_GRAVITY_SQ) {
|
|
return JPH::Vec3::sAxisY();
|
|
}
|
|
return -gravity.Normalized();
|
|
}
|
|
|
|
|
|
void _releasePlayer(PlayerRecordT *record) {
|
|
if (record->used) {
|
|
_indexSet(_world->playerOfNode, record->node, NO_HANDLE);
|
|
}
|
|
record->character = nullptr;
|
|
record->node = 0;
|
|
record->generation = 0;
|
|
record->intent = vec3(0.0f, 0.0f, 0.0f);
|
|
record->jumpSpeed = 0.0f;
|
|
record->gravityScale = 1.0f;
|
|
record->stepHeight = DEFAULT_STEP_HEIGHT;
|
|
record->height = 0.0f;
|
|
record->sinkSpeed = DEFAULT_SINK_SPEED;
|
|
record->swimDrag = DEFAULT_SWIM_DRAG;
|
|
record->swimming = false;
|
|
record->swimCurrent = JPH::Vec3::sZero();
|
|
record->insideCount = 0;
|
|
record->enabled = false;
|
|
record->used = false;
|
|
}
|
|
|
|
|
|
// Moves every player by its intent, gravity and pending jump for one fixed step, before the
|
|
// world steps so platforms carry it and the bodies it shoves feel it this step.
|
|
void _stepPlayers(float dt) {
|
|
JPH::Vec3 gravity = _world->system->GetGravity();
|
|
JPH::Vec3 up = _playerUp();
|
|
int32_t x;
|
|
|
|
for (x = 0; x < _world->playerCount; x++) {
|
|
PlayerRecordT *record = &_world->players[x];
|
|
JPH::Vec3 velocity;
|
|
JPH::Vec3 scaledGravity;
|
|
float vertical;
|
|
bool onGround;
|
|
JPH::CharacterVirtual::ExtendedUpdateSettings settings;
|
|
|
|
if (!record->used || !record->enabled) {
|
|
continue;
|
|
}
|
|
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) {
|
|
_releasePlayer(record);
|
|
continue;
|
|
}
|
|
scaledGravity = gravity * record->gravityScale;
|
|
record->character->SetUp(up);
|
|
velocity = record->character->GetLinearVelocity();
|
|
vertical = (velocity - record->character->GetGroundVelocity()).Dot(up);
|
|
onGround = record->character->GetGroundState() == JPH::CharacterBase::EGroundState::OnGround;
|
|
record->swimming = _underWater(record->character->GetPosition() + up * (record->height * 0.5f), &record->swimCurrent);
|
|
if (record->swimming) {
|
|
// Afloat: the script steers in three axes and the water drags it toward that; with no
|
|
// vertical intent it sinks slowly, and the current carries it.
|
|
JPH::Vec3 intent = JPH::Vec3(record->intent.x, record->intent.y, _world->planar ? 0.0f : record->intent.z);
|
|
JPH::Vec3 target = intent - up * intent.Dot(up) + up * ((intent.Dot(up) != 0.0f) ? intent.Dot(up) : -record->sinkSpeed) + record->swimCurrent;
|
|
|
|
velocity = velocity + (target - velocity) * SDL_min(1.0f, record->swimDrag * dt);
|
|
} else {
|
|
if (onGround && (vertical < STANDING_VERTICAL_SPEED)) {
|
|
// Standing: ride the ground.
|
|
velocity = record->character->GetGroundVelocity();
|
|
} else {
|
|
// Airborne (or on a slope too steep): keep only the vertical part, and keep falling.
|
|
velocity = up * velocity.Dot(up) + scaledGravity * dt;
|
|
}
|
|
// A jump granted with ground underfoot is taken even as a platform lifts the player.
|
|
if (onGround && (record->jumpSpeed > 0.0f)) {
|
|
velocity += up * record->jumpSpeed;
|
|
}
|
|
}
|
|
if (!record->swimming) {
|
|
velocity += JPH::Vec3(record->intent.x, 0.0f, _world->planar ? 0.0f : record->intent.z);
|
|
}
|
|
if (_world->planar) {
|
|
velocity.SetZ(0.0f);
|
|
}
|
|
record->jumpSpeed = 0.0f;
|
|
record->character->SetLinearVelocity(velocity);
|
|
settings.mWalkStairsStepUp = up * record->stepHeight;
|
|
settings.mStickToFloorStepDown = -up * SDL_max(record->stepHeight, record->extent * STICK_PER_EXTENT);
|
|
settings.mWalkStairsMinStepForward = record->extent * PADDING_PER_EXTENT;
|
|
settings.mWalkStairsStepForwardTest = record->extent * STEP_TEST_PER_EXTENT;
|
|
record->character->ExtendedUpdate(dt, scaledGravity, settings, _world->system->GetDefaultBroadPhaseLayerFilter(LAYER_MOVING), _world->system->GetDefaultLayerFilter(LAYER_MOVING), {}, {}, *_world->tempAllocator);
|
|
if (_world->planar) {
|
|
JPH::RVec3 where = record->character->GetPosition();
|
|
|
|
where.SetZ(0.0);
|
|
record->character->SetPosition(where);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
SoftRecordT *_findSoft(int32_t node) {
|
|
int32_t slot = (_world != nullptr) ? _indexGet(_world->softOfNode, node) : NO_HANDLE;
|
|
|
|
return (slot == NO_HANDLE) ? nullptr : &_world->softs[slot];
|
|
}
|
|
|
|
|
|
void _releaseSoftBody(SoftRecordT *record) {
|
|
if (!record->body.IsInvalid()) {
|
|
_destroyBody(record->body);
|
|
}
|
|
}
|
|
|
|
|
|
// A rope's tube mesh is the engine's own and goes with it; cloth and pressure bodies use the
|
|
// node's mesh, which stays.
|
|
void _releaseSoft(SoftRecordT *record) {
|
|
_releaseSoftBody(record);
|
|
if (record->kind == SOFT_ROPE) {
|
|
meshDelete(record->mesh);
|
|
}
|
|
SDL_free(record->meshToSoft);
|
|
SDL_free(record->positions);
|
|
SDL_free(record->meshPositions);
|
|
_resetSoft(record);
|
|
}
|
|
|
|
|
|
void _resetSoft(SoftRecordT *record) {
|
|
if (record->used) {
|
|
_indexSet(_world->softOfNode, record->node, NO_HANDLE);
|
|
}
|
|
record->shared = nullptr;
|
|
record->body = JPH::BodyID();
|
|
record->node = 0;
|
|
record->generation = 0;
|
|
record->kind = SOFT_CLOTH;
|
|
record->mesh = -1;
|
|
record->meshToSoft = nullptr;
|
|
record->positions = nullptr;
|
|
record->count = 0;
|
|
record->meshPositions = nullptr;
|
|
record->meshVertexCount = 0;
|
|
record->pinCount = 0;
|
|
record->stretch = DEFAULT_SOFT_STRETCH;
|
|
record->bend = DEFAULT_SOFT_BEND;
|
|
record->pressure = 0.0f;
|
|
record->mass = DEFAULT_SOFT_MASS;
|
|
record->damping = DEFAULT_SOFT_DAMPING;
|
|
record->ropeRadius = 0.0f;
|
|
record->used = false;
|
|
}
|
|
|
|
|
|
// The particle nearest a world point.
|
|
int32_t _nearestSoftVertex(const SoftRecordT *record, Vec3T point) {
|
|
int32_t best = -1;
|
|
float bestDist = 0.0f;
|
|
int32_t x;
|
|
|
|
for (x = 0; x < record->count; x++) {
|
|
float dx = record->positions[x * 3] - point.x;
|
|
float dy = record->positions[x * 3 + 1] - point.y;
|
|
float dz = record->positions[x * 3 + 2] - point.z;
|
|
float d = dx * dx + dy * dy + dz * dz;
|
|
|
|
if ((best < 0) || (d < bestDist)) {
|
|
best = x;
|
|
bestDist = d;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
|
|
// (Re)makes the Jolt body from the record's particles, constraints and pins: particles in
|
|
// world space with the body itself pinned at the origin, so what Jolt gives back needs no
|
|
// transform. Cloth and pressure bodies get Jolt's edge, shear and bend constraints from their
|
|
// faces; ropes get an edge per segment.
|
|
bool _buildSoft(SoftRecordT *record) {
|
|
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
|
|
int32_t x;
|
|
float invMass = _softInvMass(record);
|
|
float stretch = STRETCH_COMPLIANCE * (1.0f - SDL_clamp(record->stretch, 0.0f, 1.0f));
|
|
float bend = BEND_COMPLIANCE * (1.0f - SDL_clamp(record->bend, 0.0f, 1.0f));
|
|
|
|
_releaseSoftBody(record);
|
|
record->shared = new JPH::SoftBodySharedSettings();
|
|
for (x = 0; x < record->count; x++) {
|
|
record->shared->mVertices.push_back(JPH::SoftBodySharedSettings::Vertex(JPH::Float3(record->positions[x * 3], record->positions[x * 3 + 1], record->positions[x * 3 + 2]), JPH::Float3(0.0f, 0.0f, 0.0f), invMass));
|
|
}
|
|
for (x = 0; x < record->pinCount; x++) {
|
|
record->shared->mVertices[(size_t)record->pins[x].vertex].mInvMass = 0.0f;
|
|
}
|
|
if (record->kind == SOFT_ROPE) {
|
|
for (x = 0; x + 1 < record->count; x++) {
|
|
record->shared->mEdgeConstraints.push_back(JPH::SoftBodySharedSettings::Edge((JPH::uint32)x, (JPH::uint32)(x + 1), stretch));
|
|
}
|
|
record->shared->CalculateEdgeLengths();
|
|
} else {
|
|
const uint32_t *indices;
|
|
int32_t indexCount;
|
|
int32_t vertexCount;
|
|
const float *unused;
|
|
JPH::Array<JPH::SoftBodySharedSettings::VertexAttributes> attributes;
|
|
|
|
if (!meshGetGeometry(record->mesh, &unused, &vertexCount, &indices, &indexCount) || (vertexCount != record->meshVertexCount)) {
|
|
utilTrace("Physics: the mesh under soft body node %d is gone.", record->node);
|
|
return false;
|
|
}
|
|
for (x = 0; x + 2 < indexCount; x += 3) {
|
|
JPH::SoftBodySharedSettings::Face face;
|
|
|
|
face.mVertex[0] = (JPH::uint32)record->meshToSoft[indices[x]];
|
|
face.mVertex[1] = (JPH::uint32)record->meshToSoft[indices[x + 1]];
|
|
face.mVertex[2] = (JPH::uint32)record->meshToSoft[indices[x + 2]];
|
|
if (!face.IsDegenerate()) {
|
|
record->shared->AddFace(face);
|
|
}
|
|
}
|
|
attributes.resize((size_t)record->count, JPH::SoftBodySharedSettings::VertexAttributes(stretch, stretch, bend));
|
|
record->shared->CreateConstraints(attributes.data(), (JPH::uint)attributes.size(), JPH::SoftBodySharedSettings::EBendType::Distance);
|
|
}
|
|
record->shared->Optimize();
|
|
{
|
|
JPH::SoftBodyCreationSettings settings(record->shared, JPH::RVec3::sZero(), JPH::Quat::sIdentity(), LAYER_MOVING);
|
|
JPH::Body *body;
|
|
|
|
settings.mUserData = (JPH::uint64)(uint32_t)record->node;
|
|
settings.mUpdatePosition = false;
|
|
settings.mPressure = record->pressure;
|
|
settings.mLinearDamping = record->damping;
|
|
settings.mNumIterations = SOFT_ITERATIONS;
|
|
settings.mFacesDoubleSided = (record->kind == SOFT_CLOTH);
|
|
settings.mVertexRadius = (record->kind == SOFT_ROPE) ? record->ropeRadius : SOFT_VERTEX_RADIUS;
|
|
settings.mFriction = DEFAULT_FRICTION;
|
|
body = bodies.CreateSoftBody(settings);
|
|
if (body == nullptr) {
|
|
utilTrace("Physics: unable to create a soft body (too many?).");
|
|
return false;
|
|
}
|
|
record->body = body->GetID();
|
|
bodies.AddBody(record->body, JPH::EActivation::Activate);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// Pins that follow nodes are dragged to them before every step.
|
|
void _pinSoft(void) {
|
|
int32_t x;
|
|
int32_t p;
|
|
|
|
for (x = 0; x < _world->softCount; x++) {
|
|
SoftRecordT *record = &_world->softs[x];
|
|
bool any = false;
|
|
|
|
if (!record->used || record->body.IsInvalid()) {
|
|
continue;
|
|
}
|
|
for (p = 0; p < record->pinCount; p++) {
|
|
any = any || (record->pins[p].follow >= 0);
|
|
}
|
|
if (!any) {
|
|
continue;
|
|
}
|
|
{
|
|
JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), record->body);
|
|
|
|
if (!lock.Succeeded()) {
|
|
continue;
|
|
}
|
|
JPH::SoftBodyMotionProperties *motion = static_cast<JPH::SoftBodyMotionProperties *>(lock.GetBody().GetMotionProperties());
|
|
|
|
for (p = 0; p < record->pinCount; p++) {
|
|
if ((record->pins[p].follow >= 0) && nodeValid(record->pins[p].follow)) {
|
|
Vec3T where = nodeGetWorldPosition(record->pins[p].follow);
|
|
JPH::SoftBodyVertex &vertex = motion->GetVertex((JPH::uint)record->pins[p].vertex);
|
|
|
|
vertex.mPosition = JPH::Vec3(where.x, where.y, where.z);
|
|
vertex.mVelocity = JPH::Vec3::sZero();
|
|
vertex.mInvMass = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
// Outside the lock: activating takes locks of its own.
|
|
_world->system->GetBodyInterface().ActivateBody(record->body);
|
|
}
|
|
}
|
|
|
|
|
|
// A tube round a rope's particles: ROPE_SIDES vertices per particle, framed by the segment.
|
|
void _ropeMesh(const SoftRecordT *record, Vec3T *out) {
|
|
int32_t x;
|
|
int32_t side;
|
|
|
|
for (x = 0; x < record->count; x++) {
|
|
Vec3T here = vec3(record->positions[x * 3], record->positions[x * 3 + 1], record->positions[x * 3 + 2]);
|
|
Vec3T next = (x + 1 < record->count) ? vec3(record->positions[(x + 1) * 3], record->positions[(x + 1) * 3 + 1], record->positions[(x + 1) * 3 + 2]) : here;
|
|
Vec3T before = (x > 0) ? vec3(record->positions[(x - 1) * 3], record->positions[(x - 1) * 3 + 1], record->positions[(x - 1) * 3 + 2]) : here;
|
|
Vec3T tangent = vec3Subtract(next, before);
|
|
Vec3T helper;
|
|
Vec3T u;
|
|
Vec3T v;
|
|
|
|
tangent = (vec3Length(tangent) > 1e-6f) ? vec3Normalize(tangent) : vec3(0.0f, 1.0f, 0.0f);
|
|
helper = (fabsf(tangent.y) < 0.9f) ? vec3(0.0f, 1.0f, 0.0f) : vec3(1.0f, 0.0f, 0.0f);
|
|
u = vec3Normalize(vec3Cross(tangent, helper));
|
|
v = vec3Cross(tangent, u);
|
|
for (side = 0; side < ROPE_SIDES; side++) {
|
|
float angle = (float)side / (float)ROPE_SIDES * 2.0f * SDL_PI_F;
|
|
|
|
out[x * ROPE_SIDES + side] = vec3Add(here, vec3Add(vec3Scale(u, SDL_cosf(angle) * record->ropeRadius), vec3Scale(v, SDL_sinf(angle) * record->ropeRadius)));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Every particle's inverse mass, from the body's mass shared out evenly.
|
|
float _softInvMass(const SoftRecordT *record) {
|
|
return (float)record->count / SDL_max(record->mass, MIN_MASS);
|
|
}
|
|
|
|
|
|
// Writes the particles' inverse masses into the live body (pins stay held) and wakes it.
|
|
void _softSetMasses(SoftRecordT *record) {
|
|
float invMass = _softInvMass(record);
|
|
int32_t v;
|
|
|
|
if (record->body.IsInvalid()) {
|
|
return;
|
|
}
|
|
{
|
|
JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), record->body);
|
|
|
|
if (lock.Succeeded()) {
|
|
JPH::SoftBodyMotionProperties *motion = static_cast<JPH::SoftBodyMotionProperties *>(lock.GetBody().GetMotionProperties());
|
|
|
|
for (v = 0; v < record->count; v++) {
|
|
motion->GetVertex((JPH::uint)v).mInvMass = invMass;
|
|
}
|
|
for (v = 0; v < record->pinCount; v++) {
|
|
motion->GetVertex((JPH::uint)record->pins[v].vertex).mInvMass = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
// Outside the lock: activating takes locks of its own.
|
|
_world->system->GetBodyInterface().ActivateBody(record->body);
|
|
}
|
|
|
|
|
|
// After the steps: particles back from Jolt, then the mesh, in the node's own space.
|
|
void _writeSoft(void) {
|
|
int32_t x;
|
|
int32_t v;
|
|
|
|
for (x = 0; x < _world->softCount; x++) {
|
|
SoftRecordT *record = &_world->softs[x];
|
|
Vec3T position;
|
|
QuatT rotation;
|
|
Vec3T scale;
|
|
QuatT inverse;
|
|
|
|
if (!record->used) {
|
|
continue;
|
|
}
|
|
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation) || (nodeGetMesh(record->node) != record->mesh)) {
|
|
_releaseSoft(record);
|
|
continue;
|
|
}
|
|
if (record->body.IsInvalid()) {
|
|
continue;
|
|
}
|
|
{
|
|
JPH::BodyLockRead lock(_world->system->GetBodyLockInterface(), record->body);
|
|
|
|
if (!lock.Succeeded()) {
|
|
continue;
|
|
}
|
|
const JPH::SoftBodyMotionProperties *motion = static_cast<const JPH::SoftBodyMotionProperties *>(lock.GetBody().GetMotionProperties());
|
|
JPH::RVec3 origin = lock.GetBody().GetPosition();
|
|
|
|
for (v = 0; v < record->count; v++) {
|
|
JPH::Vec3 p = JPH::Vec3(origin) + motion->GetVertex((JPH::uint)v).mPosition;
|
|
|
|
record->positions[v * 3] = p.GetX();
|
|
record->positions[v * 3 + 1] = p.GetY();
|
|
record->positions[v * 3 + 2] = p.GetZ();
|
|
}
|
|
}
|
|
// World particles to mesh vertices in the node's frame.
|
|
nodeGetWorldTransform(record->node, &position, &rotation, &scale);
|
|
inverse = quatInverse(rotation);
|
|
if (record->kind == SOFT_ROPE) {
|
|
_ropeMesh(record, (Vec3T *)record->meshPositions);
|
|
}
|
|
for (v = 0; v < record->meshVertexCount; v++) {
|
|
Vec3T world = (record->kind == SOFT_ROPE) ? ((Vec3T *)record->meshPositions)[v] : vec3(record->positions[record->meshToSoft[v] * 3], record->positions[record->meshToSoft[v] * 3 + 1], record->positions[record->meshToSoft[v] * 3 + 2]);
|
|
Vec3T local = quatRotate(inverse, vec3Subtract(world, position));
|
|
|
|
record->meshPositions[v * 3] = local.x / SDL_max(scale.x, MIN_SCALE);
|
|
record->meshPositions[v * 3 + 1] = local.y / SDL_max(scale.y, MIN_SCALE);
|
|
record->meshPositions[v * 3 + 2] = local.z / SDL_max(scale.z, MIN_SCALE);
|
|
}
|
|
meshSetPositions(record->mesh, record->meshPositions);
|
|
}
|
|
}
|
|
|
|
|
|
RagdollRecordT *_findRagdoll(int32_t node) {
|
|
int32_t slot = (_world != nullptr) ? _indexGet(_world->ragdollOfNode, node) : NO_HANDLE;
|
|
|
|
return (slot == NO_HANDLE) ? nullptr : &_world->ragdolls[slot];
|
|
}
|
|
|
|
|
|
// The first node under (or at) node that carries a skin.
|
|
int32_t _findSkinned(int32_t node) {
|
|
int32_t x;
|
|
int32_t found;
|
|
|
|
if (nodeGetSkinJoints(node, nullptr) > 0) {
|
|
return node;
|
|
}
|
|
for (x = 0; x < nodeGetChildCount(node); x++) {
|
|
found = _findSkinned(nodeGetChild(node, x));
|
|
if (found >= 0) {
|
|
return found;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
int32_t _ragdollPartOf(RagdollRecordT *record, int32_t joint) {
|
|
int32_t x;
|
|
|
|
for (x = 0; x < record->partCount; x++) {
|
|
if (record->parts[x].joint == joint) {
|
|
return x;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
void _releaseRagdollBodies(RagdollRecordT *record) {
|
|
int32_t x;
|
|
|
|
for (x = 0; x < record->partCount; x++) {
|
|
RagdollPartT *part = &record->parts[x];
|
|
|
|
if (part->constraint != nullptr) {
|
|
_world->system->RemoveConstraint(part->constraint);
|
|
part->constraint = nullptr;
|
|
}
|
|
}
|
|
for (x = 0; x < record->partCount; x++) {
|
|
RagdollPartT *part = &record->parts[x];
|
|
|
|
if (!part->body.IsInvalid()) {
|
|
_destroyBody(part->body);
|
|
}
|
|
}
|
|
record->active = false;
|
|
}
|
|
|
|
|
|
void _releaseRagdoll(RagdollRecordT *record) {
|
|
_releaseRagdollBodies(record);
|
|
_resetRagdoll(record);
|
|
}
|
|
|
|
|
|
void _resetRagdoll(RagdollRecordT *record) {
|
|
int32_t x;
|
|
|
|
if (record->used) {
|
|
_indexSet(_world->ragdollOfNode, record->node, NO_HANDLE);
|
|
}
|
|
for (x = 0; x < MAX_RAGDOLL_PARTS; x++) {
|
|
record->parts[x].constraint = nullptr;
|
|
record->parts[x].body = JPH::BodyID();
|
|
}
|
|
record->filter = nullptr;
|
|
record->node = 0;
|
|
record->generation = 0;
|
|
record->skinned = -1;
|
|
record->partCount = 0;
|
|
record->strength = 0.0f;
|
|
record->strengthChanged = false;
|
|
record->active = false;
|
|
record->used = false;
|
|
}
|
|
|
|
|
|
// Motors toward the pose the ragdoll had when it was activated, as strong as asked.
|
|
void _steerRagdolls(void) {
|
|
int32_t x;
|
|
int32_t p;
|
|
|
|
for (x = 0; x < _world->ragdollCount; x++) {
|
|
RagdollRecordT *record = &_world->ragdolls[x];
|
|
|
|
if (!record->used || !record->active || !record->strengthChanged) {
|
|
continue;
|
|
}
|
|
record->strengthChanged = false;
|
|
for (p = 0; p < record->partCount; p++) {
|
|
RagdollPartT *part = &record->parts[p];
|
|
JPH::SwingTwistConstraint *constraint;
|
|
|
|
if (part->constraint == nullptr) {
|
|
continue;
|
|
}
|
|
constraint = static_cast<JPH::SwingTwistConstraint *>(part->constraint.GetPtr());
|
|
if (record->strength > 0.0f) {
|
|
constraint->GetSwingMotorSettings().mMaxTorqueLimit = record->strength;
|
|
constraint->GetSwingMotorSettings().mMinTorqueLimit = -record->strength;
|
|
constraint->GetTwistMotorSettings().mMaxTorqueLimit = record->strength;
|
|
constraint->GetTwistMotorSettings().mMinTorqueLimit = -record->strength;
|
|
constraint->SetTargetOrientationBS(part->rest);
|
|
constraint->SetSwingMotorState(JPH::EMotorState::Position);
|
|
constraint->SetTwistMotorState(JPH::EMotorState::Position);
|
|
} else {
|
|
constraint->SetSwingMotorState(JPH::EMotorState::Off);
|
|
constraint->SetTwistMotorState(JPH::EMotorState::Off);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// The bones' bodies drive their joint nodes: world transforms from the bodies, then locals
|
|
// against each joint's parent, parts first so children read their parents' new poses.
|
|
void _writeRagdolls(void) {
|
|
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
|
|
int32_t x;
|
|
int32_t p;
|
|
|
|
for (x = 0; x < _world->ragdollCount; x++) {
|
|
RagdollRecordT *record = &_world->ragdolls[x];
|
|
JPH::RVec3 worldPosition[MAX_RAGDOLL_PARTS];
|
|
JPH::Quat worldRotation[MAX_RAGDOLL_PARTS];
|
|
|
|
if (!record->used || !record->active) {
|
|
continue;
|
|
}
|
|
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) {
|
|
_releaseRagdoll(record);
|
|
continue;
|
|
}
|
|
for (p = 0; p < record->partCount; p++) {
|
|
RagdollPartT *part = &record->parts[p];
|
|
JPH::RVec3 position;
|
|
JPH::Quat rotation;
|
|
|
|
bodies.GetPositionAndRotation(part->body, position, rotation);
|
|
worldPosition[p] = position + rotation * part->offsetPosition;
|
|
worldRotation[p] = rotation * part->offsetRotation;
|
|
}
|
|
for (p = 0; p < record->partCount; p++) {
|
|
RagdollPartT *part = &record->parts[p];
|
|
int32_t parentNode = nodeGetParent(part->joint);
|
|
int32_t parentPart = (parentNode >= 0) ? _ragdollPartOf(record, parentNode) : -1;
|
|
JPH::RVec3 parentPosition;
|
|
JPH::Quat parentRotation;
|
|
JPH::Vec3 parentScale;
|
|
JPH::Vec3 local;
|
|
JPH::Quat localRotation;
|
|
|
|
if (parentPart >= 0) {
|
|
parentPosition = worldPosition[parentPart];
|
|
parentRotation = worldRotation[parentPart];
|
|
parentScale = record->parts[parentPart].scale;
|
|
} else if (parentNode >= 0) {
|
|
Vec3T position;
|
|
QuatT rotation;
|
|
Vec3T scale;
|
|
|
|
nodeGetWorldTransform(parentNode, &position, &rotation, &scale);
|
|
parentPosition = JPH::RVec3(position.x, position.y, position.z);
|
|
parentRotation = _fromQuat(rotation);
|
|
parentScale = JPH::Vec3(scale.x, scale.y, scale.z);
|
|
} else {
|
|
parentPosition = JPH::RVec3::sZero();
|
|
parentRotation = JPH::Quat::sIdentity();
|
|
parentScale = JPH::Vec3::sOne();
|
|
}
|
|
local = parentRotation.Conjugated() * JPH::Vec3(worldPosition[p] - parentPosition);
|
|
local = JPH::Vec3(local.GetX() / SDL_max(parentScale.GetX(), MIN_SCALE), local.GetY() / SDL_max(parentScale.GetY(), MIN_SCALE), local.GetZ() / SDL_max(parentScale.GetZ(), MIN_SCALE));
|
|
localRotation = parentRotation.Conjugated() * worldRotation[p];
|
|
nodeSetPosition(part->joint, vec3(local.GetX(), local.GetY(), local.GetZ()));
|
|
nodeSetRotation(part->joint, _toQuat(localRotation));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
VehicleRecordT *_findVehicle(int32_t node) {
|
|
int32_t slot = (_world != nullptr) ? _indexGet(_world->vehicleOfNode, node) : NO_HANDLE;
|
|
|
|
return (slot == NO_HANDLE) ? nullptr : &_world->vehicles[slot];
|
|
}
|
|
|
|
|
|
void _releaseVehicle(VehicleRecordT *record) {
|
|
if (record->constraint != nullptr) {
|
|
_world->system->RemoveStepListener(record->constraint);
|
|
_world->system->RemoveConstraint(record->constraint);
|
|
}
|
|
record->constraint = nullptr;
|
|
record->tester = nullptr;
|
|
_resetVehicle(record);
|
|
}
|
|
|
|
|
|
void _resetVehicle(VehicleRecordT *record) {
|
|
if (record->used) {
|
|
_indexSet(_world->vehicleOfNode, record->node, NO_HANDLE);
|
|
}
|
|
record->node = 0;
|
|
record->generation = 0;
|
|
record->kind = VEHICLE_CAR;
|
|
record->wheelCount = 0;
|
|
record->maxTorque = DEFAULT_ENGINE_TORQUE;
|
|
record->maxRpm = DEFAULT_ENGINE_MAX_RPM;
|
|
record->minRpm = VEHICLE_DEFAULT_MIN_RPM;
|
|
record->gearCount = 0;
|
|
record->reverseGear = -VEHICLE_DEFAULT_REVERSE_GEAR;
|
|
record->automatic = true;
|
|
record->suspensionHz = DEFAULT_SUSPENSION_HZ;
|
|
record->suspensionDamping = DEFAULT_SUSPENSION_DAMPING;
|
|
record->maxSteer = DEFAULT_STEER_DEGREES;
|
|
record->brakeTorque = DEFAULT_BRAKE_TORQUE;
|
|
record->handBrakeTorque = DEFAULT_HANDBRAKE_TORQUE;
|
|
record->antiRoll = 0.0f;
|
|
record->inputForward = 0.0f;
|
|
record->inputRight = 0.0f;
|
|
record->inputBrake = 0.0f;
|
|
record->inputHandBrake = 0.0f;
|
|
record->thrust = DEFAULT_THRUST;
|
|
record->thrustPoint = vec3(0.0f, DEFAULT_THRUST_Y, DEFAULT_THRUST_Z);
|
|
record->rudder = DEFAULT_RUDDER;
|
|
record->dirty = false;
|
|
record->used = false;
|
|
}
|
|
|
|
|
|
// Engine and gearbox settings from the recipe (a script that set no gears keeps Jolt's).
|
|
void _setDrivetrain(const VehicleRecordT *record, JPH::VehicleEngineSettings &engine, JPH::VehicleTransmissionSettings &transmission) {
|
|
int32_t v;
|
|
|
|
engine.mMaxTorque = record->maxTorque;
|
|
engine.mMaxRPM = record->maxRpm;
|
|
engine.mMinRPM = record->minRpm;
|
|
if (record->gearCount > 0) {
|
|
transmission.mGearRatios.clear();
|
|
for (v = 0; v < record->gearCount; v++) {
|
|
transmission.mGearRatios.push_back(record->gears[v]);
|
|
}
|
|
transmission.mReverseGearRatios.clear();
|
|
transmission.mReverseGearRatios.push_back(record->reverseGear);
|
|
}
|
|
transmission.mMode = record->automatic ? JPH::ETransmissionMode::Auto : JPH::ETransmissionMode::Manual;
|
|
}
|
|
|
|
|
|
// Buoyancy, drag and current for every dynamic body inside a water volume, and the surfaces
|
|
// players and boats test against this step. A volume's surface is the top of its box.
|
|
void _applyWater(float dt) {
|
|
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
|
|
JPH::Vec3 gravity = _world->system->GetGravity();
|
|
int32_t x;
|
|
size_t h;
|
|
|
|
_world->waterCount = 0;
|
|
for (x = 0; x < _world->bodyCount; x++) {
|
|
BodyRecordT *record = &_world->bodies[x];
|
|
WaterSurfaceT surface;
|
|
JPH::AllHitCollisionCollector<JPH::CollideShapeBodyCollector> hits;
|
|
|
|
if (!record->used || !record->water || !record->enabled) {
|
|
continue;
|
|
}
|
|
{
|
|
JPH::TransformedShape shape = bodies.GetTransformedShape(record->id);
|
|
JPH::AABox local = bodies.GetShape(record->id)->GetLocalBounds();
|
|
JPH::Quat rotation = bodies.GetRotation(record->id);
|
|
|
|
surface.box = shape.GetWorldSpaceBounds();
|
|
surface.normal = rotation * JPH::Vec3::sAxisY();
|
|
surface.position = bodies.GetPosition(record->id) + rotation * JPH::Vec3(0.0f, local.mMax.GetY(), 0.0f);
|
|
surface.current = JPH::Vec3(record->current.x, record->current.y, record->current.z);
|
|
}
|
|
if (_world->waterCount < MAX_WATERS) {
|
|
_world->waters[_world->waterCount++] = surface;
|
|
}
|
|
_world->system->GetBroadPhaseQuery().CollideAABox(surface.box, hits, _world->system->GetDefaultBroadPhaseLayerFilter(LAYER_MOVING), _world->system->GetDefaultLayerFilter(LAYER_MOVING));
|
|
for (h = 0; h < hits.mHits.size(); h++) {
|
|
JPH::BodyID id = hits.mHits[h];
|
|
BodyRecordT *other;
|
|
int32_t node;
|
|
|
|
if (id == record->id) {
|
|
continue;
|
|
}
|
|
{
|
|
// Rigid, dynamic and solid: soft bodies have no buoyancy (Jolt asserts on them).
|
|
JPH::BodyLockRead lock(_world->system->GetBodyLockInterface(), id);
|
|
|
|
if (!lock.Succeeded() || !lock.GetBody().IsRigidBody() || !lock.GetBody().IsDynamic() || lock.GetBody().IsSensor()) {
|
|
continue;
|
|
}
|
|
node = (int32_t)(uint32_t)lock.GetBody().GetUserData();
|
|
}
|
|
other = _find(node);
|
|
bodies.ApplyBuoyancyImpulse(id, surface.position, surface.normal, record->waterDensity * ((other != nullptr) ? other->buoyancy : DEFAULT_BUOYANCY), record->waterLinearDrag, record->waterAngularDrag, surface.current, gravity, dt);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Whether a world point is below a water surface this step; hands back the water's current.
|
|
bool _underWater(JPH::RVec3Arg point, JPH::Vec3 *current) {
|
|
int32_t x;
|
|
|
|
for (x = 0; x < _world->waterCount; x++) {
|
|
WaterSurfaceT *water = &_world->waters[x];
|
|
|
|
if (water->box.Contains(JPH::Vec3(point)) && (JPH::Vec3(point - water->position).Dot(water->normal) < 0.0f)) {
|
|
if (current != nullptr) {
|
|
*current = water->current;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// Builds (or rebuilds) the Jolt constraint from the recipe. The chassis faces -Z with Y up, like
|
|
// everything else in the scene; wheels sit where their nodes are relative to the chassis. One
|
|
// try per change: a recipe that cannot be built is left alone until a setting changes.
|
|
bool _buildVehicle(VehicleRecordT *record) {
|
|
BodyRecordT *body = _find(record->node);
|
|
JPH::VehicleConstraintSettings settings;
|
|
JPH::Vec3 forward = JPH::Vec3(0.0f, 0.0f, -1.0f);
|
|
JPH::Vec3 up = JPH::Vec3::sAxisY();
|
|
JPH::Vec3 left = up.Cross(forward);
|
|
JPH::Vec3 local[MAX_WHEELS];
|
|
float along[MAX_WHEELS];
|
|
float meanAlong = 0.0f;
|
|
float rollInertia = LEAN_INERTIA;
|
|
int32_t w;
|
|
int32_t v;
|
|
|
|
record->dirty = false;
|
|
if ((body == nullptr) || (body->type != BODY_DYNAMIC)) {
|
|
utilTrace("Physics: vehicle node %d needs a dynamic body as its chassis.", record->node);
|
|
return false;
|
|
}
|
|
if (record->wheelCount < 2) {
|
|
utilTrace("Physics: vehicle node %d needs at least two wheels.", record->node);
|
|
return false;
|
|
}
|
|
if (record->constraint != nullptr) {
|
|
_world->system->RemoveStepListener(record->constraint);
|
|
_world->system->RemoveConstraint(record->constraint);
|
|
record->constraint = nullptr;
|
|
}
|
|
// The chassis' inertia about its nose, for the motorcycle lean spring.
|
|
{
|
|
JPH::BodyLockRead lock(_world->system->GetBodyLockInterface(), body->id);
|
|
|
|
if (lock.Succeeded() && (lock.GetBody().GetMotionProperties() != nullptr)) {
|
|
float inverse = forward.Dot(lock.GetBody().GetMotionProperties()->GetLocalSpaceInverseInertia().Multiply3x3(forward));
|
|
|
|
if (inverse > MIN_INVERSE_INERTIA) {
|
|
rollInertia = 1.0f / inverse;
|
|
}
|
|
}
|
|
}
|
|
// Wheel attachment points in the chassis' frame, as recorded when the wheels were added.
|
|
for (w = 0; w < record->wheelCount; w++) {
|
|
local[w] = JPH::Vec3(record->wheels[w].rest.x, record->wheels[w].rest.y, record->wheels[w].rest.z);
|
|
along[w] = local[w].Dot(forward);
|
|
meanAlong += along[w] / (float)record->wheelCount;
|
|
}
|
|
settings.mUp = up;
|
|
settings.mForward = forward;
|
|
settings.mMaxPitchRollAngle = (record->kind == VEHICLE_MOTORCYCLE) ? JPH::DegreesToRadians(VEHICLE_MAX_TILT_DEGREES) : JPH::JPH_PI;
|
|
for (w = 0; w < record->wheelCount; w++) {
|
|
WheelRecordT *wheel = &record->wheels[w];
|
|
bool front = along[w] > meanAlong + FRONT_EPSILON;
|
|
bool steered = wheel->steeredSet ? wheel->steered : front;
|
|
bool driven = wheel->steeredSet ? wheel->driven : (record->kind != VEHICLE_MOTORCYCLE || !front);
|
|
JPH::WheelSettings *base;
|
|
|
|
if (record->kind == VEHICLE_TANK) {
|
|
JPH::WheelSettingsTV *tv = new JPH::WheelSettingsTV();
|
|
|
|
base = tv;
|
|
} else {
|
|
JPH::WheelSettingsWV *wv = new JPH::WheelSettingsWV();
|
|
|
|
wv->mMaxSteerAngle = steered ? JPH::DegreesToRadians(record->maxSteer) : 0.0f;
|
|
wv->mMaxBrakeTorque = record->brakeTorque;
|
|
wv->mMaxHandBrakeTorque = front ? 0.0f : record->handBrakeTorque;
|
|
base = wv;
|
|
}
|
|
base->mPosition = local[w];
|
|
base->mSuspensionDirection = -up;
|
|
base->mSteeringAxis = up;
|
|
base->mWheelUp = up;
|
|
base->mWheelForward = forward;
|
|
base->mSuspensionMinLength = wheel->suspension * 0.5f;
|
|
base->mSuspensionMaxLength = wheel->suspension;
|
|
base->mSuspensionSpring = JPH::SpringSettings(JPH::ESpringMode::FrequencyAndDamping, record->suspensionHz, record->suspensionDamping);
|
|
base->mRadius = wheel->radius;
|
|
base->mWidth = wheel->width;
|
|
wheel->steered = steered;
|
|
wheel->driven = driven;
|
|
settings.mWheels.push_back(base);
|
|
}
|
|
// The settings object holds the controller settings from the moment they are made, so an
|
|
// early return frees them.
|
|
if (record->kind == VEHICLE_TANK) {
|
|
JPH::TrackedVehicleControllerSettings *controller = new JPH::TrackedVehicleControllerSettings();
|
|
int32_t sides[2] = { 0, 0 };
|
|
|
|
settings.mController = controller;
|
|
_setDrivetrain(record, controller->mEngine, controller->mTransmission);
|
|
for (w = 0; w < record->wheelCount; w++) {
|
|
int32_t side = (local[w].Dot(left) > 0.0f) ? (int32_t)JPH::ETrackSide::Left : (int32_t)JPH::ETrackSide::Right;
|
|
|
|
if (sides[side] == 0) {
|
|
controller->mTracks[side].mDrivenWheel = (JPH::uint)w;
|
|
}
|
|
controller->mTracks[side].mWheels.push_back((JPH::uint)w);
|
|
sides[side]++;
|
|
}
|
|
if ((sides[0] == 0) || (sides[1] == 0)) {
|
|
utilTrace("Physics: a tank needs wheels on both sides of node %d.", record->node);
|
|
return false;
|
|
}
|
|
} else {
|
|
JPH::WheeledVehicleControllerSettings *controller = (record->kind == VEHICLE_MOTORCYCLE) ? new JPH::MotorcycleControllerSettings() : new JPH::WheeledVehicleControllerSettings();
|
|
bool paired[MAX_WHEELS];
|
|
int32_t axles = 0;
|
|
|
|
settings.mController = controller;
|
|
if (record->kind == VEHICLE_MOTORCYCLE) {
|
|
// Jolt's lean spring is tuned for one bike; a lighter or slimmer one flips with it.
|
|
JPH::MotorcycleControllerSettings *bike = static_cast<JPH::MotorcycleControllerSettings *>(controller);
|
|
|
|
bike->mLeanSpringConstant = LEAN_SPRING * rollInertia / LEAN_INERTIA;
|
|
bike->mLeanSpringDamping = LEAN_DAMPING * rollInertia / LEAN_INERTIA;
|
|
}
|
|
_setDrivetrain(record, controller->mEngine, controller->mTransmission);
|
|
// Driven wheels pair up across the chassis into differentials, one per axle; a lone wheel
|
|
// (a motorcycle's) is an axle of its own. Anti-roll bars follow the same pairs.
|
|
for (w = 0; w < record->wheelCount; w++) {
|
|
paired[w] = false;
|
|
}
|
|
for (w = 0; w < record->wheelCount; w++) {
|
|
JPH::VehicleDifferentialSettings differential;
|
|
int32_t partner = -1;
|
|
|
|
if (paired[w] || !record->wheels[w].driven) {
|
|
continue;
|
|
}
|
|
for (v = w + 1; v < record->wheelCount; v++) {
|
|
if (!paired[v] && record->wheels[v].driven && (fabsf(along[v] - along[w]) < record->wheels[w].radius) && ((local[v].Dot(left) > 0.0f) != (local[w].Dot(left) > 0.0f))) {
|
|
partner = v;
|
|
break;
|
|
}
|
|
}
|
|
paired[w] = true;
|
|
if (local[w].Dot(left) > 0.0f) {
|
|
differential.mLeftWheel = w;
|
|
differential.mRightWheel = partner;
|
|
} else {
|
|
differential.mLeftWheel = partner;
|
|
differential.mRightWheel = w;
|
|
}
|
|
if (partner >= 0) {
|
|
paired[partner] = true;
|
|
if (record->antiRoll > 0.0f) {
|
|
JPH::VehicleAntiRollBar bar;
|
|
|
|
bar.mLeftWheel = differential.mLeftWheel;
|
|
bar.mRightWheel = differential.mRightWheel;
|
|
bar.mStiffness = record->antiRoll;
|
|
settings.mAntiRollBars.push_back(bar);
|
|
}
|
|
}
|
|
controller->mDifferentials.push_back(differential);
|
|
axles++;
|
|
}
|
|
for (v = 0; v < (int32_t)controller->mDifferentials.size(); v++) {
|
|
controller->mDifferentials[(size_t)v].mEngineTorqueRatio = 1.0f / (float)SDL_max(axles, 1);
|
|
}
|
|
if (axles == 0) {
|
|
utilTrace("Physics: vehicle node %d has no driven wheel.", record->node);
|
|
return false;
|
|
}
|
|
}
|
|
{
|
|
JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), body->id);
|
|
|
|
if (!lock.Succeeded()) {
|
|
return false;
|
|
}
|
|
record->constraint = new JPH::VehicleConstraint(lock.GetBody(), settings);
|
|
}
|
|
// Wheels feel the ground with a cylinder cast (kerbs and edges); tracks with a ray, which
|
|
// still finds the ground when a heavy hull has come down on its belly.
|
|
if (record->kind == VEHICLE_TANK) {
|
|
record->tester = new JPH::VehicleCollisionTesterRay(LAYER_MOVING, up);
|
|
} else {
|
|
record->tester = new JPH::VehicleCollisionTesterCastCylinder(LAYER_MOVING, WHEEL_CAST_RADIUS);
|
|
}
|
|
record->constraint->SetVehicleCollisionTester(record->tester);
|
|
_world->system->AddConstraint(record->constraint);
|
|
_world->system->AddStepListener(record->constraint);
|
|
return true;
|
|
}
|
|
|
|
|
|
// Hands each vehicle its driver input for the coming steps, rebuilding stale ones first.
|
|
void _driveVehicles(void) {
|
|
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
|
|
int32_t x;
|
|
|
|
for (x = 0; x < _world->vehicleCount; x++) {
|
|
VehicleRecordT *record = &_world->vehicles[x];
|
|
BodyRecordT *body;
|
|
|
|
if (!record->used) {
|
|
continue;
|
|
}
|
|
// _find checks the node and its generation, which the vehicle shares with its body.
|
|
body = _find(record->node);
|
|
if (body == nullptr) {
|
|
_releaseVehicle(record);
|
|
continue;
|
|
}
|
|
if (record->kind == VEHICLE_BOAT) {
|
|
JPH::Quat rotation = bodies.GetRotation(body->id);
|
|
JPH::RVec3 point = bodies.GetPosition(body->id) + rotation * JPH::Vec3(record->thrustPoint.x, record->thrustPoint.y, record->thrustPoint.z);
|
|
JPH::Vec3 forward = rotation * JPH::Vec3(0.0f, 0.0f, -1.0f);
|
|
JPH::Vec3 up = rotation * JPH::Vec3::sAxisY();
|
|
float speed = bodies.GetLinearVelocity(body->id).Dot(forward);
|
|
|
|
record->dirty = false;
|
|
if (_underWater(point, nullptr)) {
|
|
if (record->inputForward != 0.0f) {
|
|
bodies.AddForce(body->id, forward * (record->thrust * record->inputForward), point);
|
|
}
|
|
if (record->inputRight != 0.0f) {
|
|
// The rudder bites with speed.
|
|
bodies.AddTorque(body->id, up * (-record->inputRight * record->rudder * SDL_clamp(fabsf(speed) / RUDDER_FULL_SPEED, RUDDER_MIN_BITE, 1.0f) * ((speed < 0.0f) ? -1.0f : 1.0f)));
|
|
}
|
|
if (record->inputBrake > 0.0f) {
|
|
bodies.AddForce(body->id, -bodies.GetLinearVelocity(body->id) * (record->thrust * record->inputBrake * BOAT_BRAKE_RATIO));
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (record->dirty && !_buildVehicle(record)) {
|
|
continue;
|
|
}
|
|
if (record->constraint == nullptr) {
|
|
continue;
|
|
}
|
|
if (record->kind == VEHICLE_TANK) {
|
|
JPH::TrackedVehicleController *controller = static_cast<JPH::TrackedVehicleController *>(record->constraint->GetController());
|
|
float forward = record->inputForward;
|
|
float leftRatio = 1.0f;
|
|
float rightRatio = 1.0f;
|
|
|
|
if ((forward == 0.0f) && (record->inputRight != 0.0f)) {
|
|
// Turning on the spot: the tracks run against each other, gently, or a heavy
|
|
// hull hops off the ground.
|
|
forward = fabsf(record->inputRight) * TANK_PIVOT_THROTTLE;
|
|
leftRatio = (record->inputRight > 0.0f) ? 1.0f : -1.0f;
|
|
rightRatio = -leftRatio;
|
|
} else if (record->inputRight > 0.0f) {
|
|
rightRatio = SDL_max(1.0f - record->inputRight * TANK_TURN_RATIO, TANK_TURN_MIN);
|
|
} else if (record->inputRight < 0.0f) {
|
|
leftRatio = SDL_max(1.0f + record->inputRight * TANK_TURN_RATIO, TANK_TURN_MIN);
|
|
}
|
|
controller->SetDriverInput(forward, leftRatio, rightRatio, record->inputBrake);
|
|
} else {
|
|
JPH::WheeledVehicleController *controller = static_cast<JPH::WheeledVehicleController *>(record->constraint->GetController());
|
|
|
|
controller->SetDriverInput(record->inputForward, record->inputRight, record->inputBrake, record->inputHandBrake);
|
|
}
|
|
if ((record->inputForward != 0.0f) || (record->inputRight != 0.0f) || (record->inputBrake != 0.0f)) {
|
|
bodies.ActivateBody(body->id);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Wheel nodes take the wheel transforms after the step: X is the axle, Y up.
|
|
void _poseWheels(void) {
|
|
int32_t x;
|
|
int32_t w;
|
|
|
|
for (x = 0; x < _world->vehicleCount; x++) {
|
|
VehicleRecordT *record = &_world->vehicles[x];
|
|
|
|
if (!record->used || (record->constraint == nullptr)) {
|
|
continue;
|
|
}
|
|
for (w = 0; w < record->wheelCount; w++) {
|
|
JPH::RMat44 transform;
|
|
JPH::Quat rotation;
|
|
JPH::RVec3 position;
|
|
|
|
if (!nodeValid(record->wheels[w].node) || (nodeGetGeneration(record->wheels[w].node) != record->wheels[w].generation)) {
|
|
continue;
|
|
}
|
|
transform = record->constraint->GetWheelWorldTransform((JPH::uint)w, JPH::Vec3::sAxisX(), JPH::Vec3::sAxisY());
|
|
position = transform.GetTranslation();
|
|
rotation = transform.GetQuaternion();
|
|
nodeSetWorldTransform(record->wheels[w].node, vec3((float)position.GetX(), (float)position.GetY(), (float)position.GetZ()), _toQuat(rotation));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// The triggers a player is in now against those it was in: onTrigger enter and leave.
|
|
void _playerInside(PlayerRecordT *record, const int32_t *now, int32_t count) {
|
|
int32_t x;
|
|
int32_t y;
|
|
|
|
for (x = 0; x < count; x++) {
|
|
bool had = false;
|
|
|
|
for (y = 0; y < record->insideCount; y++) {
|
|
had = had || (record->inside[y] == now[x]);
|
|
}
|
|
if (!had) {
|
|
_world->contacts->push(_triggerEvent(PHYSICS_EVENT_ENTER, now[x], record->node));
|
|
}
|
|
}
|
|
for (y = 0; y < record->insideCount; y++) {
|
|
bool still = false;
|
|
|
|
for (x = 0; x < count; x++) {
|
|
still = still || (now[x] == record->inside[y]);
|
|
}
|
|
if (!still) {
|
|
_world->contacts->push(_triggerEvent(PHYSICS_EVENT_LEAVE, record->inside[y], record->node));
|
|
}
|
|
}
|
|
for (x = 0; x < count; x++) {
|
|
record->inside[x] = now[x];
|
|
}
|
|
record->insideCount = count;
|
|
}
|
|
|
|
|
|
// Which triggers a player overlaps this frame, reported against the last frame's answer.
|
|
void _playerTriggers(PlayerRecordT *record) {
|
|
JPH::AllHitCollisionCollector<JPH::CollideShapeBodyCollector> hits;
|
|
JPH::AABox box = record->character->GetShape()->GetWorldSpaceBounds(record->character->GetCenterOfMassTransform(), JPH::Vec3::sOne());
|
|
int32_t now[MAX_PLAYER_TRIGGERS];
|
|
int32_t count = 0;
|
|
int32_t x;
|
|
size_t h;
|
|
|
|
_world->system->GetBroadPhaseQuery().CollideAABox(box, hits, _world->system->GetDefaultBroadPhaseLayerFilter(LAYER_MOVING), _world->system->GetDefaultLayerFilter(LAYER_MOVING));
|
|
for (h = 0; (h < hits.mHits.size()) && (count < MAX_PLAYER_TRIGGERS); h++) {
|
|
int32_t node = _triggerNode(hits.mHits[h]);
|
|
bool seen = false;
|
|
|
|
if (node == NO_HANDLE) {
|
|
continue;
|
|
}
|
|
// The broad phase may name a body more than once.
|
|
for (x = 0; x < count; x++) {
|
|
seen = seen || (now[x] == node);
|
|
}
|
|
if (!seen) {
|
|
now[count++] = node;
|
|
}
|
|
}
|
|
_playerInside(record, now, count);
|
|
}
|
|
|
|
|
|
// An enter or leave event for a trigger and what crossed it.
|
|
PhysicsEventT _triggerEvent(PhysicsEventTypeE type, int32_t trigger, int32_t node) {
|
|
PhysicsEventT event;
|
|
|
|
event.type = type;
|
|
event.nodeA = trigger;
|
|
event.nodeB = node;
|
|
event.point = vec3(0.0f, 0.0f, 0.0f);
|
|
event.speed = 0.0f;
|
|
return event;
|
|
}
|
|
|
|
|
|
// The node of a Jolt body that is a trigger, or NO_HANDLE.
|
|
int32_t _triggerNode(JPH::BodyID id) {
|
|
JPH::BodyLockRead lock(_world->system->GetBodyLockInterface(), id);
|
|
|
|
if (!lock.Succeeded() || !lock.GetBody().IsSensor()) {
|
|
return NO_HANDLE;
|
|
}
|
|
return (int32_t)(uint32_t)lock.GetBody().GetUserData();
|
|
}
|
|
|
|
|
|
// Players drive their nodes' positions; the script owns the rotation.
|
|
void _writePlayers(void) {
|
|
int32_t x;
|
|
Vec3T position;
|
|
QuatT rotation;
|
|
Vec3T scale;
|
|
|
|
for (x = 0; x < _world->playerCount; x++) {
|
|
PlayerRecordT *record = &_world->players[x];
|
|
JPH::RVec3 where;
|
|
|
|
if (!record->used || !record->enabled) {
|
|
continue;
|
|
}
|
|
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) {
|
|
_releasePlayer(record);
|
|
continue;
|
|
}
|
|
_playerTriggers(record);
|
|
where = record->character->GetPosition();
|
|
nodeGetWorldTransform(record->node, &position, &rotation, &scale);
|
|
nodeSetWorldTransform(record->node, vec3((float)where.GetX(), (float)where.GetY(), (float)where.GetZ()), rotation);
|
|
record->intent = vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
}
|
|
|
|
|
|
QuatT _toQuat(JPH::Quat q) {
|
|
QuatT out = { q.GetX(), q.GetY(), q.GetZ(), q.GetW() };
|
|
|
|
return out;
|
|
}
|
|
|
|
|
|
Vec3T _toVec3(JPH::Vec3 v) {
|
|
return vec3(v.GetX(), v.GetY(), v.GetZ());
|
|
}
|
|
|
|
|
|
// Jolt's trace goes to Singe's.
|
|
void _trace(const char *fmt, ...) {
|
|
char buffer[1024];
|
|
va_list args;
|
|
|
|
va_start(args, fmt);
|
|
vsnprintf(buffer, sizeof(buffer), fmt, args);
|
|
va_end(args);
|
|
utilTrace("Physics: %s", buffer);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
// ===== Bodies =====
|
|
|
|
// A force for this step, at the centre of mass or a world point.
|
|
bool bodyApplyForce(int32_t node, Vec3T force, const Vec3T *at) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if ((record == nullptr) || (record->type != BODY_DYNAMIC)) {
|
|
return false;
|
|
}
|
|
if (at != nullptr) {
|
|
_world->system->GetBodyInterface().AddForce(record->id, _fromVec3(force), JPH::RVec3(at->x, at->y, at->z));
|
|
} else {
|
|
_world->system->GetBodyInterface().AddForce(record->id, _fromVec3(force));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// An instant change of momentum, at the centre of mass or a world point.
|
|
bool bodyApplyImpulse(int32_t node, Vec3T impulse, const Vec3T *at) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if ((record == nullptr) || (record->type != BODY_DYNAMIC)) {
|
|
return false;
|
|
}
|
|
if (at != nullptr) {
|
|
_world->system->GetBodyInterface().AddImpulse(record->id, _fromVec3(impulse), JPH::RVec3(at->x, at->y, at->z));
|
|
} else {
|
|
_world->system->GetBodyInterface().AddImpulse(record->id, _fromVec3(impulse));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
bool bodyDelete(int32_t node) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
_release(record);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool bodyExists(int32_t node) {
|
|
return _find(node) != nullptr;
|
|
}
|
|
|
|
|
|
Vec3T bodyGetAngularVelocity(int32_t node) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if (record == nullptr) {
|
|
return vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
return _toVec3(_world->system->GetBodyInterface().GetAngularVelocity(record->id));
|
|
}
|
|
|
|
|
|
Vec3T bodyGetVelocity(int32_t node) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if (record == nullptr) {
|
|
return vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
return _toVec3(_world->system->GetBodyInterface().GetLinearVelocity(record->id));
|
|
}
|
|
|
|
|
|
// Asleep: a dynamic body that has come to rest (static and disabled bodies count as resting).
|
|
bool bodyIsResting(int32_t node) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
return !record->enabled || !_world->system->GetBodyInterface().IsActive(record->id);
|
|
}
|
|
|
|
|
|
// Gives the node a body with a shape sized by a, b, c (see ShapeTypeE) and the node's world scale,
|
|
// placed where the node is now. One body per node; a second call replaces the first.
|
|
bool bodyNew(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, float c) {
|
|
BodyRecordT *record;
|
|
JPH::RefConst<JPH::Shape> joltShape;
|
|
Vec3T position;
|
|
QuatT rotation;
|
|
Vec3T scale;
|
|
int32_t x;
|
|
|
|
if ((_world == nullptr) || !nodeValid(node)) {
|
|
return false;
|
|
}
|
|
bodyDelete(node);
|
|
playerDelete(node);
|
|
sceneUpdateTransforms();
|
|
nodeGetWorldTransform(node, &position, &rotation, &scale);
|
|
joltShape = _buildShape(node, type, shape, a, b, c, position, rotation, scale);
|
|
if (joltShape == nullptr) {
|
|
return false;
|
|
}
|
|
for (x = 0; x < _world->bodyCount; x++) {
|
|
if (!_world->bodies[x].used) {
|
|
break;
|
|
}
|
|
}
|
|
if (x == _world->bodyCount) {
|
|
if (_world->bodyCount == MAX_BODIES) {
|
|
utilTrace("Physics: no room for another body (%d already).", MAX_BODIES);
|
|
return false;
|
|
}
|
|
_world->bodyCount++;
|
|
}
|
|
record = &_world->bodies[x];
|
|
memset(record, 0, sizeof(*record));
|
|
{
|
|
JPH::EMotionType motion = (type == BODY_STATIC) ? JPH::EMotionType::Static : ((type == BODY_KINEMATIC) ? JPH::EMotionType::Kinematic : JPH::EMotionType::Dynamic);
|
|
JPH::ObjectLayer layer = (type == BODY_STATIC) ? LAYER_NON_MOVING : LAYER_MOVING;
|
|
JPH::BodyCreationSettings settings(joltShape, JPH::RVec3(position.x, position.y, position.z), _fromQuat(rotation), motion, layer);
|
|
|
|
settings.mFriction = DEFAULT_FRICTION;
|
|
settings.mRestitution = DEFAULT_BOUNCE;
|
|
settings.mUserData = (JPH::uint64)(uint32_t)node;
|
|
if (_world->planar) {
|
|
// Moves in X and Y, turns about Z, nothing else: a 2D game's world.
|
|
settings.mAllowedDOFs = JPH::EAllowedDOFs::Plane2D;
|
|
}
|
|
record->id = _world->system->GetBodyInterface().CreateAndAddBody(settings, JPH::EActivation::Activate);
|
|
}
|
|
if (record->id.IsInvalid()) {
|
|
utilTrace("Physics: unable to create a body (too many?).");
|
|
return false;
|
|
}
|
|
record->node = node;
|
|
record->generation = nodeGetGeneration(node);
|
|
record->type = type;
|
|
record->enabled = true;
|
|
record->used = true;
|
|
record->buoyancy = DEFAULT_BUOYANCY;
|
|
_indexSet(_world->bodyOfNode, node, x);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool bodySetAngularVelocity(int32_t node, Vec3T velocity) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if ((record == nullptr) || (record->type == BODY_STATIC)) {
|
|
return false;
|
|
}
|
|
_world->system->GetBodyInterface().SetAngularVelocity(record->id, _fromVec3(velocity));
|
|
return true;
|
|
}
|
|
|
|
|
|
// 0 stops dead, 1 bounces back with everything it arrived with.
|
|
bool bodySetBounce(int32_t node, float bounce) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
_world->system->GetBodyInterface().SetRestitution(record->id, SDL_clamp(bounce, 0.0f, 1.0f));
|
|
return true;
|
|
}
|
|
|
|
|
|
// Takes the body out of the world (it stops colliding and moving) and puts it back.
|
|
bool bodySetEnabled(int32_t node, bool enabled) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
if (enabled && !record->enabled) {
|
|
_world->system->GetBodyInterface().AddBody(record->id, JPH::EActivation::Activate);
|
|
} else if (!enabled && record->enabled) {
|
|
_world->system->GetBodyInterface().RemoveBody(record->id);
|
|
}
|
|
record->enabled = enabled;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool bodySetFriction(int32_t node, float friction) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
_world->system->GetBodyInterface().SetFriction(record->id, SDL_max(friction, 0.0f));
|
|
return true;
|
|
}
|
|
|
|
|
|
// Rescales the body's mass and inertia (dynamic bodies only).
|
|
bool bodySetMass(int32_t node, float kilograms) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if ((record == nullptr) || (record->type != BODY_DYNAMIC) || (kilograms <= 0.0f)) {
|
|
return false;
|
|
}
|
|
{
|
|
JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), record->id);
|
|
|
|
if (!lock.Succeeded()) {
|
|
return false;
|
|
}
|
|
lock.GetBody().GetMotionProperties()->ScaleToMass(kilograms);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// A trigger (Jolt sensor) reports what enters and leaves it and pushes nothing.
|
|
bool bodySetTrigger(int32_t node, bool trigger) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
{
|
|
JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), record->id);
|
|
|
|
if (!lock.Succeeded()) {
|
|
return false;
|
|
}
|
|
lock.GetBody().SetIsSensor(trigger);
|
|
}
|
|
if (!trigger) {
|
|
_world->contacts->forget(node);
|
|
}
|
|
record->trigger = trigger;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool bodySetVelocity(int32_t node, Vec3T velocity) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if ((record == nullptr) || (record->type == BODY_STATIC)) {
|
|
return false;
|
|
}
|
|
_world->system->GetBodyInterface().SetLinearVelocity(record->id, _fromVec3(velocity));
|
|
return true;
|
|
}
|
|
|
|
|
|
// ===== Joints =====
|
|
|
|
bool jointDelete(int32_t joint) {
|
|
JointRecordT *record;
|
|
|
|
if (!jointValid(joint)) {
|
|
return false;
|
|
}
|
|
record = &_world->joints[(size_t)joint];
|
|
_world->system->RemoveConstraint(record->constraint);
|
|
record->constraint = nullptr;
|
|
record->used = false;
|
|
return true;
|
|
}
|
|
|
|
|
|
// A hinge (anchor and axis), ball (anchor) or slider (axis) between two bodies, or between a body
|
|
// and the world when nodeB is -1. Anchor and axis are in world space. nodeA is Jolt's first
|
|
// body (creating them the other way round mirrors the motion); see jointSetLimits for the sign
|
|
// that implies.
|
|
int32_t jointNew(JointTypeE type, int32_t nodeA, int32_t nodeB, Vec3T anchor, Vec3T axis) {
|
|
BodyRecordT *a = _find(nodeA);
|
|
BodyRecordT *b = (nodeB == WORLD_NODE) ? nullptr : _find(nodeB);
|
|
JPH::Ref<JPH::Constraint> constraint;
|
|
JPH::Vec3 direction;
|
|
JPH::Vec3 normal;
|
|
JointRecordT record;
|
|
int32_t x;
|
|
|
|
if ((_world == nullptr) || (a == nullptr) || ((nodeB != WORLD_NODE) && (b == nullptr))) {
|
|
return NO_HANDLE;
|
|
}
|
|
direction = _fromVec3(axis).NormalizedOr(JPH::Vec3::sAxisY());
|
|
normal = direction.GetNormalizedPerpendicular();
|
|
{
|
|
JPH::BodyID ids[2] = { a->id, (b != nullptr) ? b->id : JPH::BodyID() };
|
|
JPH::BodyLockMultiWrite lock(_world->system->GetBodyLockInterface(), ids, (b != nullptr) ? 2 : 1);
|
|
JPH::Body *bodyA = lock.GetBody(0);
|
|
JPH::Body *bodyB = (b != nullptr) ? lock.GetBody(1) : &JPH::Body::sFixedToWorld;
|
|
|
|
if ((bodyA == nullptr) || (bodyB == nullptr)) {
|
|
return NO_HANDLE;
|
|
}
|
|
if (type == JOINT_HINGE) {
|
|
JPH::HingeConstraintSettings settings;
|
|
|
|
settings.mSpace = JPH::EConstraintSpace::WorldSpace;
|
|
settings.mPoint1 = JPH::RVec3(anchor.x, anchor.y, anchor.z);
|
|
settings.mPoint2 = settings.mPoint1;
|
|
settings.mHingeAxis1 = direction;
|
|
settings.mHingeAxis2 = direction;
|
|
settings.mNormalAxis1 = normal;
|
|
settings.mNormalAxis2 = normal;
|
|
constraint = settings.Create(*bodyA, *bodyB);
|
|
} else if (type == JOINT_BALL) {
|
|
JPH::PointConstraintSettings settings;
|
|
|
|
settings.mSpace = JPH::EConstraintSpace::WorldSpace;
|
|
settings.mPoint1 = JPH::RVec3(anchor.x, anchor.y, anchor.z);
|
|
settings.mPoint2 = settings.mPoint1;
|
|
constraint = settings.Create(*bodyA, *bodyB);
|
|
} else {
|
|
JPH::SliderConstraintSettings settings;
|
|
|
|
settings.mSpace = JPH::EConstraintSpace::WorldSpace;
|
|
settings.mPoint1 = JPH::RVec3(anchor.x, anchor.y, anchor.z);
|
|
settings.mPoint2 = settings.mPoint1;
|
|
settings.mSliderAxis1 = direction;
|
|
settings.mSliderAxis2 = direction;
|
|
settings.mNormalAxis1 = normal;
|
|
settings.mNormalAxis2 = normal;
|
|
constraint = settings.Create(*bodyA, *bodyB);
|
|
}
|
|
}
|
|
if (constraint == nullptr) {
|
|
return NO_HANDLE;
|
|
}
|
|
_world->system->AddConstraint(constraint);
|
|
record.constraint = constraint;
|
|
record.type = type;
|
|
record.nodeA = nodeA;
|
|
record.nodeB = nodeB;
|
|
record.used = true;
|
|
for (x = 0; x < (int32_t)_world->joints.size(); x++) {
|
|
if (!_world->joints[(size_t)x].used) {
|
|
_world->joints[(size_t)x] = record;
|
|
return x;
|
|
}
|
|
}
|
|
_world->joints.push_back(record);
|
|
return x;
|
|
}
|
|
|
|
|
|
// Limits: degrees either side of the starting angle for a hinge (positive is a right-hand turn of
|
|
// nodeA about the axis), distance along the axis for a slider (positive along it); low at or below
|
|
// 0, high at or above. Jolt measures both as its second body relative to its first, which is the
|
|
// opposite of nodeA's own motion, so the range is mirrored on the way in. A ball joint has none.
|
|
bool jointSetLimits(int32_t joint, float low, float high) {
|
|
if (!jointValid(joint)) {
|
|
return false;
|
|
}
|
|
low = SDL_min(low, 0.0f);
|
|
high = SDL_max(high, 0.0f);
|
|
if (_world->joints[(size_t)joint].type == JOINT_HINGE) {
|
|
static_cast<JPH::HingeConstraint *>(_world->joints[(size_t)joint].constraint.GetPtr())->SetLimits(JPH::DegreesToRadians(-high), JPH::DegreesToRadians(-low));
|
|
return true;
|
|
}
|
|
if (_world->joints[(size_t)joint].type == JOINT_SLIDER) {
|
|
static_cast<JPH::SliderConstraint *>(_world->joints[(size_t)joint].constraint.GetPtr())->SetLimits(-high, -low);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
bool jointValid(int32_t joint) {
|
|
return (_world != nullptr) && (joint >= 0) && (joint < (int32_t)_world->joints.size()) && _world->joints[(size_t)joint].used;
|
|
}
|
|
|
|
|
|
// ===== World =====
|
|
|
|
bool physicsAvailable(void) {
|
|
return _world != nullptr;
|
|
}
|
|
|
|
|
|
// Hands the engine up to maximum of the queued events, oldest first, and drops only those: call
|
|
// until it returns 0 to drain a busy step.
|
|
int32_t physicsGetEvents(PhysicsEventT *events, int32_t maximum) {
|
|
int32_t count;
|
|
int32_t x;
|
|
|
|
if ((_world == nullptr) || (maximum <= 0)) {
|
|
return 0;
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> guard(_world->contacts->lock);
|
|
std::vector<PhysicsEventT> &queue = _world->contacts->events;
|
|
|
|
count = SDL_min(maximum, (int32_t)queue.size());
|
|
for (x = 0; x < count; x++) {
|
|
events[x] = queue[(size_t)x];
|
|
}
|
|
queue.erase(queue.begin(), queue.begin() + count);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
|
|
// Brings Jolt up: allocators, the type factory, a job pool sized to the machine, and an empty
|
|
// world. Refuses (returning false, 3D physics unavailable) on an x86 without SSE4.1 and 4.2, the
|
|
// level the library was compiled for, rather than faulting on the first instruction.
|
|
bool physicsInit(void) {
|
|
int32_t threads;
|
|
int32_t x;
|
|
|
|
if (_world != nullptr) {
|
|
return true;
|
|
}
|
|
#if defined(JPH_USE_SSE4_2)
|
|
if (!SDL_HasSSE41() || !SDL_HasSSE42()) {
|
|
utilTrace("Physics: this CPU lacks SSE4.1/4.2; physics is unavailable.");
|
|
return false;
|
|
}
|
|
#endif
|
|
JPH::RegisterDefaultAllocator();
|
|
JPH::Trace = _trace;
|
|
JPH::Factory::sInstance = new JPH::Factory();
|
|
JPH::RegisterTypes();
|
|
_world = new WorldT();
|
|
_world->tempAllocator = new JPH::TempAllocatorImpl(TEMP_ALLOCATOR_BYTES);
|
|
threads = SDL_max(SDL_GetNumLogicalCPUCores() - 1, MIN_JOB_THREADS);
|
|
_world->jobs = new JPH::JobSystemThreadPool(JPH::cMaxPhysicsJobs, JPH::cMaxPhysicsBarriers, threads);
|
|
_world->system = new JPH::PhysicsSystem();
|
|
_world->system->Init(MAX_BODIES, 0, MAX_BODY_PAIRS, MAX_CONTACTS, _world->broadPhaseLayers, _world->objectVsBroadPhase, _world->objectPairs);
|
|
_world->contacts = new ContactListenerT();
|
|
_world->system->SetContactListener(_world->contacts);
|
|
_world->playerListener = new PlayerListenerT();
|
|
// The tables are value-initialised (all zero, so nothing is used) and then given their defaults.
|
|
_world->bodies = new BodyRecordT[MAX_BODIES]();
|
|
_world->bodyCount = 0;
|
|
_world->players = new PlayerRecordT[MAX_PLAYERS]();
|
|
_world->playerCount = MAX_PLAYERS;
|
|
for (x = 0; x < MAX_PLAYERS; x++) {
|
|
_releasePlayer(&_world->players[x]);
|
|
}
|
|
_world->vehicles = new VehicleRecordT[MAX_VEHICLES]();
|
|
_world->vehicleCount = MAX_VEHICLES;
|
|
for (x = 0; x < MAX_VEHICLES; x++) {
|
|
_resetVehicle(&_world->vehicles[x]);
|
|
}
|
|
_world->softs = new SoftRecordT[MAX_SOFT]();
|
|
_world->softCount = MAX_SOFT;
|
|
for (x = 0; x < MAX_SOFT; x++) {
|
|
_resetSoft(&_world->softs[x]);
|
|
}
|
|
_world->ragdolls = new RagdollRecordT[MAX_RAGDOLLS]();
|
|
_world->ragdollCount = MAX_RAGDOLLS;
|
|
_world->ragdollGroups = 1;
|
|
for (x = 0; x < MAX_RAGDOLLS; x++) {
|
|
_resetRagdoll(&_world->ragdolls[x]);
|
|
}
|
|
_world->enabled = true;
|
|
utilTrace("Physics: Jolt %d.%d.%d ready, %d job thread%s", JPH_VERSION_MAJOR, JPH_VERSION_MINOR, JPH_VERSION_PATCH, threads, (threads == 1) ? "" : "s");
|
|
return true;
|
|
}
|
|
|
|
|
|
void physicsQuit(void) {
|
|
int32_t x;
|
|
|
|
if (_world == nullptr) {
|
|
return;
|
|
}
|
|
for (x = 0; x < _world->bodyCount; x++) {
|
|
if (_world->bodies[x].used) {
|
|
_release(&_world->bodies[x]);
|
|
}
|
|
}
|
|
delete[] _world->bodies;
|
|
for (x = 0; x < _world->softCount; x++) {
|
|
if (_world->softs[x].used) {
|
|
_releaseSoft(&_world->softs[x]);
|
|
}
|
|
}
|
|
delete[] _world->softs;
|
|
for (x = 0; x < _world->ragdollCount; x++) {
|
|
if (_world->ragdolls[x].used) {
|
|
_releaseRagdoll(&_world->ragdolls[x]);
|
|
}
|
|
}
|
|
delete[] _world->ragdolls;
|
|
for (x = 0; x < _world->vehicleCount; x++) {
|
|
if (_world->vehicles[x].used) {
|
|
_releaseVehicle(&_world->vehicles[x]);
|
|
}
|
|
}
|
|
delete[] _world->vehicles;
|
|
for (x = 0; x < _world->playerCount; x++) {
|
|
_releasePlayer(&_world->players[x]);
|
|
}
|
|
delete[] _world->players;
|
|
delete _world->playerListener;
|
|
delete _world->system;
|
|
delete _world->contacts;
|
|
delete _world->jobs;
|
|
delete _world->tempAllocator;
|
|
delete _world;
|
|
_world = nullptr;
|
|
JPH::UnregisterTypes();
|
|
delete JPH::Factory::sInstance;
|
|
JPH::Factory::sInstance = nullptr;
|
|
#ifdef JPH_DEBUG_RENDERER
|
|
delete _renderer;
|
|
_renderer = nullptr;
|
|
#endif
|
|
}
|
|
|
|
|
|
// The nearest body along a ray (triggers included), with the hit point and surface normal.
|
|
bool physicsRaycast(Vec3T origin, Vec3T direction, float maxDistance, int32_t *node, Vec3T *point, Vec3T *normal) {
|
|
JPH::RayCastResult result;
|
|
JPH::Vec3 dir;
|
|
JPH::RVec3 hit;
|
|
|
|
if (_world == nullptr) {
|
|
return false;
|
|
}
|
|
dir = _fromVec3(direction).NormalizedOr(JPH::Vec3::sZero());
|
|
if (dir.IsNearZero()) {
|
|
return false;
|
|
}
|
|
if (maxDistance <= 0.0f) {
|
|
maxDistance = DEFAULT_RAY_DISTANCE;
|
|
}
|
|
{
|
|
JPH::RRayCast ray(JPH::RVec3(origin.x, origin.y, origin.z), dir * maxDistance);
|
|
|
|
if (!_world->system->GetNarrowPhaseQuery().CastRay(ray, result)) {
|
|
return false;
|
|
}
|
|
hit = ray.GetPointOnRay(result.mFraction);
|
|
}
|
|
*node = (int32_t)(uint32_t)_world->system->GetBodyInterface().GetUserData(result.mBodyID);
|
|
*point = vec3((float)hit.GetX(), (float)hit.GetY(), (float)hit.GetZ());
|
|
{
|
|
JPH::BodyLockRead lock(_world->system->GetBodyLockInterface(), result.mBodyID);
|
|
|
|
*normal = lock.Succeeded() ? _toVec3(lock.GetBody().GetWorldSpaceSurfaceNormal(result.mSubShapeID2, hit)) : vec3(0.0f, 1.0f, 0.0f);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// 2D mode for bodies made from now on: they move in X and Y and turn about Z only, so a 2D game
|
|
// can run its world in overlay coordinates (gravity pointing +Y then, since the overlay's Y runs
|
|
// down) and draw sprites where the nodes are. Bodies already made keep their freedom.
|
|
void physicsSet2D(bool planar) {
|
|
if (_world != nullptr) {
|
|
_world->planar = planar;
|
|
}
|
|
}
|
|
|
|
|
|
void physicsSetDebug(uint32_t mask) {
|
|
#ifdef JPH_DEBUG_RENDERER
|
|
_debugMask = mask;
|
|
#else
|
|
_debugMask = DEBUG_NONE;
|
|
if (mask != DEBUG_NONE) {
|
|
utilTrace("Physics: this build has no debug renderer.");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
// Pauses the simulation (bodies hold still) without losing it.
|
|
void physicsSetEnabled(bool enabled) {
|
|
if (_world != nullptr) {
|
|
_world->enabled = enabled;
|
|
}
|
|
}
|
|
|
|
|
|
void physicsSetGravity(Vec3T gravity) {
|
|
if (_world != nullptr) {
|
|
_world->system->SetGravity(_fromVec3(gravity));
|
|
}
|
|
}
|
|
|
|
|
|
// Once per frame, after animation: kinematic bodies are moved to their nodes, the world steps at
|
|
// a fixed rate for the time that has passed (at most a few steps, and none while the game is
|
|
// paused or physics is disabled), and dynamic bodies drive their nodes.
|
|
void physicsUpdate(bool advance) {
|
|
uint64_t now;
|
|
|
|
if (_world == nullptr) {
|
|
return;
|
|
}
|
|
now = SDL_GetTicksNS();
|
|
if (advance && _world->enabled && (_world->lastTick != 0)) {
|
|
_world->accumulator += (double)(now - _world->lastTick) / 1e9;
|
|
}
|
|
_world->lastTick = now;
|
|
if (_world->accumulator > STEP_SECONDS * MAX_STEPS_PER_FRAME) {
|
|
_world->accumulator = STEP_SECONDS * MAX_STEPS_PER_FRAME;
|
|
}
|
|
if (_world->accumulator >= STEP_SECONDS) {
|
|
_step();
|
|
}
|
|
_drawDebug();
|
|
}
|
|
|
|
|
|
bool playerDelete(int32_t node) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
_releasePlayer(record);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool playerExists(int32_t node) {
|
|
return _findPlayer(node) != nullptr;
|
|
}
|
|
|
|
|
|
// The node stood on, or -1, and the ground normal.
|
|
int32_t playerGetGround(int32_t node, Vec3T *normal) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
JPH::Vec3 n;
|
|
|
|
if ((record == nullptr) || !record->character->IsSupported()) {
|
|
if (normal != nullptr) {
|
|
*normal = vec3(0.0f, 1.0f, 0.0f);
|
|
}
|
|
return -1;
|
|
}
|
|
n = record->character->GetGroundNormal();
|
|
if (normal != nullptr) {
|
|
*normal = vec3(n.GetX(), n.GetY(), n.GetZ());
|
|
}
|
|
if (record->character->GetGroundBodyID().IsInvalid()) {
|
|
return -1;
|
|
}
|
|
return (int32_t)(uint32_t)_world->system->GetBodyInterface().GetUserData(record->character->GetGroundBodyID());
|
|
}
|
|
|
|
|
|
Vec3T playerGetVelocity(int32_t node) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
JPH::Vec3 v;
|
|
|
|
if (record == nullptr) {
|
|
return vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
v = record->character->GetLinearVelocity();
|
|
return vec3(v.GetX(), v.GetY(), v.GetZ());
|
|
}
|
|
|
|
|
|
bool playerIsOnGround(int32_t node) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
|
|
return (record != nullptr) && (record->character->GetGroundState() == JPH::CharacterBase::EGroundState::OnGround);
|
|
}
|
|
|
|
|
|
// Asks for a jump at the next step; only granted with ground underfoot.
|
|
bool playerJump(int32_t node, float speed) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
|
|
if ((record == nullptr) || (record->character->GetGroundState() != JPH::CharacterBase::EGroundState::OnGround)) {
|
|
return false;
|
|
}
|
|
record->jumpSpeed = SDL_max(0.0f, speed);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool playerMove(int32_t node, Vec3T velocity) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->intent = velocity;
|
|
return true;
|
|
}
|
|
|
|
|
|
// A character on the node, shaped like a body but standing on the node's origin.
|
|
bool playerNew(int32_t node, ShapeTypeE shape, float a, float b, float c) {
|
|
PlayerRecordT *record = nullptr;
|
|
JPH::RefConst<JPH::Shape> joltShape;
|
|
JPH::RefConst<JPH::Shape> standing;
|
|
JPH::CharacterVirtualSettings settings;
|
|
JPH::Shape::ShapeResult lifted;
|
|
JPH::AABox bounds;
|
|
Vec3T position;
|
|
QuatT rotation;
|
|
Vec3T scale;
|
|
JPH::Vec3 up;
|
|
float lift;
|
|
float extent;
|
|
int32_t x;
|
|
|
|
if ((_world == nullptr) || !nodeValid(node)) {
|
|
return false;
|
|
}
|
|
if (shape == SHAPE_MESH) {
|
|
utilTrace("Physics: a player needs a convex shape; use a hull for node %d.", node);
|
|
return false;
|
|
}
|
|
bodyDelete(node);
|
|
playerDelete(node);
|
|
sceneUpdateTransforms();
|
|
nodeGetWorldTransform(node, &position, &rotation, &scale);
|
|
joltShape = _buildShape(node, BODY_DYNAMIC, shape, a, b, c, position, rotation, scale);
|
|
if (joltShape == nullptr) {
|
|
return false;
|
|
}
|
|
for (x = 0; x < _world->playerCount; x++) {
|
|
if (!_world->players[x].used) {
|
|
record = &_world->players[x];
|
|
break;
|
|
}
|
|
}
|
|
if (record == nullptr) {
|
|
utilTrace("Physics: no room for another player (%d already).", MAX_PLAYERS);
|
|
return false;
|
|
}
|
|
// Feet at the origin: lift the shape (along up, which is against gravity) by half its height.
|
|
up = _playerUp();
|
|
bounds = joltShape->GetLocalBounds();
|
|
lift = (bounds.mMax.GetY() - bounds.mMin.GetY()) / 2.0f;
|
|
lifted = JPH::RotatedTranslatedShapeSettings(up * lift, JPH::Quat::sIdentity(), joltShape).Create();
|
|
if (lifted.HasError()) {
|
|
utilTrace("Physics: player shape: %s", lifted.GetError().c_str());
|
|
return false;
|
|
}
|
|
standing = lifted.Get();
|
|
settings.mShape = standing;
|
|
settings.mInnerBodyShape = standing;
|
|
settings.mInnerBodyLayer = LAYER_MOVING;
|
|
settings.mMaxSlopeAngle = JPH::DegreesToRadians(DEFAULT_SLOPE_DEGREES);
|
|
settings.mMaxStrength = DEFAULT_PUSH_STRENGTH;
|
|
settings.mBackFaceMode = JPH::EBackFaceMode::CollideWithBackFaces;
|
|
settings.mEnhancedInternalEdgeRemoval = true;
|
|
settings.mUp = up;
|
|
settings.mSupportingVolume = JPH::Plane(up, -SDL_max((bounds.mMax.GetY() - bounds.mMin.GetY()) * 0.25f, MIN_DIMENSION));
|
|
// Jolt's tolerances are tuned for a metre-sized capsule; a pixel-sized 2D player needs them
|
|
// scaled up with it, so they follow the shape's smallest half-extent (0.3 m gives the defaults).
|
|
extent = SDL_max(MIN_DIMENSION, (bounds.mMax - bounds.mMin).ReduceMin() / 2.0f);
|
|
settings.mCharacterPadding = extent * PADDING_PER_EXTENT;
|
|
settings.mPredictiveContactDistance = extent * PREDICTIVE_PER_EXTENT;
|
|
settings.mCollisionTolerance = extent * TOLERANCE_PER_EXTENT;
|
|
record->character = new JPH::CharacterVirtual(&settings, JPH::RVec3(position.x, position.y, position.z), JPH::Quat::sIdentity(), (JPH::uint64)(uint32_t)node, _world->system);
|
|
record->character->SetListener(_world->playerListener);
|
|
record->node = node;
|
|
record->generation = nodeGetGeneration(node);
|
|
record->extent = extent;
|
|
record->height = bounds.mMax.GetY() - bounds.mMin.GetY();
|
|
record->intent = vec3(0.0f, 0.0f, 0.0f);
|
|
record->jumpSpeed = 0.0f;
|
|
record->gravityScale = 1.0f;
|
|
record->stepHeight = DEFAULT_STEP_HEIGHT * extent / DEFAULT_EXTENT;
|
|
record->enabled = true;
|
|
record->used = true;
|
|
_indexSet(_world->playerOfNode, node, x);
|
|
return true;
|
|
}
|
|
|
|
|
|
// A disabled player neither moves nor blocks: its inner body leaves the world, and any triggers
|
|
// it stood in are left.
|
|
bool playerSetEnabled(int32_t node, bool enabled) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
JPH::BodyID inner;
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
if (enabled == record->enabled) {
|
|
return true;
|
|
}
|
|
inner = record->character->GetInnerBodyID();
|
|
if (enabled) {
|
|
if (!inner.IsInvalid()) {
|
|
_world->system->GetBodyInterface().AddBody(inner, JPH::EActivation::Activate);
|
|
}
|
|
} else {
|
|
if (!inner.IsInvalid()) {
|
|
_world->system->GetBodyInterface().RemoveBody(inner);
|
|
}
|
|
_playerInside(record, nullptr, 0);
|
|
}
|
|
record->enabled = enabled;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool playerSetGravityScale(int32_t node, float scale) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->gravityScale = scale;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool playerSetMass(int32_t node, float kilograms) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->character->SetMass(SDL_max(kilograms, MIN_MASS));
|
|
return true;
|
|
}
|
|
|
|
|
|
// Teleports; nothing in between is touched.
|
|
bool playerSetPosition(int32_t node, Vec3T position) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
Vec3T where;
|
|
QuatT rotation;
|
|
Vec3T scale;
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
|
|
record->character->SetLinearVelocity(JPH::Vec3::sZero());
|
|
nodeGetWorldTransform(node, &where, &rotation, &scale);
|
|
nodeSetWorldTransform(node, position, rotation);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool playerSetPush(int32_t node, float strength) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->character->SetMaxStrength(SDL_max(0.0f, strength));
|
|
return true;
|
|
}
|
|
|
|
|
|
bool playerSetSlope(int32_t node, float degrees) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->character->SetMaxSlopeAngle(JPH::DegreesToRadians(SDL_clamp(degrees, 0.0f, MAX_SLOPE_DEGREES)));
|
|
return true;
|
|
}
|
|
|
|
|
|
bool playerSetStep(int32_t node, float height) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->stepHeight = SDL_max(0.0f, height);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool playerSetVelocity(int32_t node, Vec3T velocity) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->character->SetLinearVelocity(JPH::Vec3(velocity.x, velocity.y, velocity.z));
|
|
record->intent = vec3(0.0f, 0.0f, 0.0f);
|
|
return true;
|
|
}
|
|
|
|
|
|
// A wheel at the wheel node's position relative to the chassis; returns its index.
|
|
int32_t vehicleAddWheel(int32_t node, int32_t wheelNode, float radius, float width, float suspension) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
WheelRecordT *wheel;
|
|
Vec3T chassisPosition;
|
|
QuatT chassisRotation;
|
|
Vec3T chassisScale;
|
|
|
|
if ((record == nullptr) || !nodeValid(wheelNode) || (record->wheelCount >= MAX_WHEELS)) {
|
|
return -1;
|
|
}
|
|
// The attachment point is the wheel node's place in the chassis' frame right now (the body's
|
|
// frame is unscaled: the chassis' scale is baked into its shape); the engine poses the node
|
|
// with suspension travel from here on, so a later rebuild must not read it back.
|
|
sceneUpdateTransforms();
|
|
nodeGetWorldTransform(node, &chassisPosition, &chassisRotation, &chassisScale);
|
|
wheel = &record->wheels[record->wheelCount];
|
|
wheel->node = wheelNode;
|
|
wheel->generation = nodeGetGeneration(wheelNode);
|
|
wheel->rest = quatRotate(quatInverse(chassisRotation), vec3Subtract(nodeGetWorldPosition(wheelNode), chassisPosition));
|
|
wheel->radius = SDL_max(radius, MIN_DIMENSION);
|
|
wheel->width = SDL_max(width, MIN_DIMENSION);
|
|
wheel->suspension = SDL_max(suspension, MIN_DIMENSION);
|
|
wheel->steered = false;
|
|
wheel->driven = true;
|
|
wheel->steeredSet = false;
|
|
record->dirty = true;
|
|
return record->wheelCount++;
|
|
}
|
|
|
|
|
|
bool vehicleDelete(int32_t node) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
_releaseVehicle(record);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool vehicleDrive(int32_t node, float forward, float right, float brake, float handBrake) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->inputForward = SDL_clamp(forward, -1.0f, 1.0f);
|
|
record->inputRight = SDL_clamp(right, -1.0f, 1.0f);
|
|
record->inputBrake = SDL_clamp(brake, 0.0f, 1.0f);
|
|
record->inputHandBrake = SDL_clamp(handBrake, 0.0f, 1.0f);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool vehicleExists(int32_t node) {
|
|
return _findVehicle(node) != nullptr;
|
|
}
|
|
|
|
|
|
int32_t vehicleGetGear(int32_t node) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if ((record == nullptr) || (record->constraint == nullptr)) {
|
|
return 0;
|
|
}
|
|
if (record->kind == VEHICLE_TANK) {
|
|
return static_cast<JPH::TrackedVehicleController *>(record->constraint->GetController())->GetTransmission().GetCurrentGear();
|
|
}
|
|
return static_cast<JPH::WheeledVehicleController *>(record->constraint->GetController())->GetTransmission().GetCurrentGear();
|
|
}
|
|
|
|
|
|
float vehicleGetRpm(int32_t node) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if ((record == nullptr) || (record->constraint == nullptr)) {
|
|
return 0.0f;
|
|
}
|
|
if (record->kind == VEHICLE_TANK) {
|
|
return static_cast<JPH::TrackedVehicleController *>(record->constraint->GetController())->GetEngine().GetCurrentRPM();
|
|
}
|
|
return static_cast<JPH::WheeledVehicleController *>(record->constraint->GetController())->GetEngine().GetCurrentRPM();
|
|
}
|
|
|
|
|
|
// Metres a second along the chassis' nose, negative in reverse.
|
|
float vehicleGetSpeed(int32_t node) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
BodyRecordT *body;
|
|
JPH::Vec3 velocity;
|
|
JPH::Quat rotation;
|
|
|
|
if ((record == nullptr) || ((body = _find(record->node)) == nullptr)) {
|
|
return 0.0f;
|
|
}
|
|
velocity = _world->system->GetBodyInterface().GetLinearVelocity(body->id);
|
|
rotation = _world->system->GetBodyInterface().GetRotation(body->id);
|
|
return velocity.Dot(rotation * JPH::Vec3(0.0f, 0.0f, -1.0f));
|
|
}
|
|
|
|
|
|
// Longitudinal slip of a wheel, 0 gripping to about 1 spinning or locked.
|
|
float vehicleGetWheelSlip(int32_t node, int32_t index) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if ((record == nullptr) || (record->constraint == nullptr) || (index < 0) || (index >= record->wheelCount) || (record->kind == VEHICLE_TANK)) {
|
|
return 0.0f;
|
|
}
|
|
return SDL_min(fabsf(static_cast<const JPH::WheelWV *>(record->constraint->GetWheel((JPH::uint)index))->mLongitudinalSlip), 1.0f);
|
|
}
|
|
|
|
|
|
bool vehicleIsWheelOnGround(int32_t node, int32_t index) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if ((record == nullptr) || (record->constraint == nullptr) || (index < 0) || (index >= record->wheelCount)) {
|
|
return false;
|
|
}
|
|
return record->constraint->GetWheel((JPH::uint)index)->HasContact();
|
|
}
|
|
|
|
|
|
// A vehicle on the node, whose dynamic body is the chassis; add wheels before driving it.
|
|
bool vehicleNew(int32_t node, VehicleKindE kind) {
|
|
VehicleRecordT *record = nullptr;
|
|
BodyRecordT *body = _find(node);
|
|
int32_t x;
|
|
|
|
if ((_world == nullptr) || (body == nullptr) || (body->type != BODY_DYNAMIC)) {
|
|
utilTrace("Physics: a vehicle needs a dynamic body on node %d first.", node);
|
|
return false;
|
|
}
|
|
if (_world->planar && (kind != VEHICLE_BOAT)) {
|
|
utilTrace("Physics: vehicles need a 3D world; a 2D car is a body with hinged wheels.");
|
|
return false;
|
|
}
|
|
vehicleDelete(node);
|
|
for (x = 0; x < _world->vehicleCount; x++) {
|
|
if (!_world->vehicles[x].used) {
|
|
record = &_world->vehicles[x];
|
|
break;
|
|
}
|
|
}
|
|
if (record == nullptr) {
|
|
utilTrace("Physics: no room for another vehicle (%d already).", MAX_VEHICLES);
|
|
return false;
|
|
}
|
|
_resetVehicle(record);
|
|
record->node = node;
|
|
record->generation = nodeGetGeneration(node);
|
|
record->kind = kind;
|
|
record->dirty = (kind != VEHICLE_BOAT);
|
|
record->used = true;
|
|
_indexSet(_world->vehicleOfNode, node, x);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool vehicleSetAntiRoll(int32_t node, float stiffness) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->antiRoll = SDL_max(0.0f, stiffness);
|
|
record->dirty = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool vehicleSetBrakes(int32_t node, float brake, float handBrake) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->brakeTorque = SDL_max(0.0f, brake);
|
|
record->handBrakeTorque = SDL_max(0.0f, handBrake);
|
|
record->dirty = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool vehicleSetEngine(int32_t node, float maxTorque, float maxRpm, float minRpm) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->maxTorque = SDL_max(MIN_ENGINE_TORQUE, maxTorque);
|
|
record->maxRpm = SDL_max(MIN_ENGINE_MAX_RPM, maxRpm);
|
|
record->minRpm = SDL_clamp(minRpm, MIN_ENGINE_RPM, record->maxRpm);
|
|
record->dirty = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool vehicleSetGears(int32_t node, const float *ratios, int32_t count, float reverse, bool automatic) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
int32_t x;
|
|
|
|
if ((record == nullptr) || (count < 1) || (count > VEHICLE_MAX_GEARS)) {
|
|
return false;
|
|
}
|
|
for (x = 0; x < count; x++) {
|
|
record->gears[x] = ratios[x];
|
|
}
|
|
record->gearCount = count;
|
|
record->reverseGear = (reverse > 0.0f) ? -reverse : reverse;
|
|
record->automatic = automatic;
|
|
record->dirty = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool vehicleSetSteering(int32_t node, float maxDegrees) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->maxSteer = SDL_clamp(maxDegrees, 0.0f, MAX_STEER_DEGREES);
|
|
record->dirty = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool vehicleSetSuspension(int32_t node, float frequency, float damping) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->suspensionHz = SDL_max(MIN_SUSPENSION_HZ, frequency);
|
|
record->suspensionDamping = SDL_max(0.0f, damping);
|
|
record->dirty = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool vehicleSetWheel(int32_t node, int32_t index, bool steered, bool driven) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if ((record == nullptr) || (index < 0) || (index >= record->wheelCount)) {
|
|
return false;
|
|
}
|
|
record->wheels[index].steered = steered;
|
|
record->wheels[index].driven = driven;
|
|
record->wheels[index].steeredSet = true;
|
|
record->dirty = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool bodySetBuoyancy(int32_t node, float factor) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->buoyancy = SDL_max(0.0f, factor);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool bodySetCurrent(int32_t node, Vec3T flow) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->current = flow;
|
|
return true;
|
|
}
|
|
|
|
|
|
// Fills a static trigger with water: bodies inside float, sink and drift; players swim.
|
|
bool bodySetWater(int32_t node, float density, float linearDrag, float angularDrag) {
|
|
BodyRecordT *record = _find(node);
|
|
|
|
if ((record == nullptr) || (record->type != BODY_STATIC)) {
|
|
utilTrace("Physics: water needs a static body on node %d.", node);
|
|
return false;
|
|
}
|
|
if (!record->trigger) {
|
|
bodySetTrigger(node, true);
|
|
}
|
|
record->water = true;
|
|
record->waterDensity = SDL_max(0.0f, density);
|
|
record->waterLinearDrag = SDL_max(0.0f, linearDrag);
|
|
record->waterAngularDrag = SDL_max(0.0f, angularDrag);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool playerIsSwimming(int32_t node) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
|
|
return (record != nullptr) && record->swimming;
|
|
}
|
|
|
|
|
|
bool playerSetSwim(int32_t node, float sinkSpeed, float drag) {
|
|
PlayerRecordT *record = _findPlayer(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->sinkSpeed = SDL_max(0.0f, sinkSpeed);
|
|
record->swimDrag = SDL_max(0.0f, drag);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool vehicleSetRudder(int32_t node, float maxTorque) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->rudder = SDL_max(0.0f, maxTorque);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool vehicleSetThrust(int32_t node, float maxForce, Vec3T point) {
|
|
VehicleRecordT *record = _findVehicle(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->thrust = SDL_max(0.0f, maxForce);
|
|
record->thrustPoint = point;
|
|
return true;
|
|
}
|
|
|
|
|
|
// Physics takes over the skeleton from where the animation left it.
|
|
bool ragdollActivate(int32_t node) {
|
|
RagdollRecordT *record = _findRagdoll(node);
|
|
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
|
|
const int32_t *joints;
|
|
int32_t jointCount;
|
|
int32_t p;
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
if (record->active) {
|
|
return true;
|
|
}
|
|
animationStop(record->node, ANIMATION_ALL_LAYERS);
|
|
sceneUpdateTransforms();
|
|
jointCount = nodeGetSkinJoints(record->skinned, &joints);
|
|
record->filter = new JPH::GroupFilterTable((JPH::uint)record->partCount);
|
|
// Bodies: a capsule along each bone, from the joint toward its children.
|
|
for (p = 0; p < record->partCount; p++) {
|
|
RagdollPartT *part = &record->parts[p];
|
|
Vec3T jointPosition;
|
|
QuatT jointRotation;
|
|
Vec3T jointScale;
|
|
JPH::Vec3 origin;
|
|
JPH::Vec3 toward = JPH::Vec3::sZero();
|
|
JPH::Vec3 direction;
|
|
JPH::Quat rotation;
|
|
JPH::RVec3 centre;
|
|
float length;
|
|
float radius;
|
|
int32_t children = 0;
|
|
int32_t c;
|
|
|
|
nodeGetWorldTransform(part->joint, &jointPosition, &jointRotation, &jointScale);
|
|
origin = JPH::Vec3(jointPosition.x, jointPosition.y, jointPosition.z);
|
|
for (c = 0; c < nodeGetChildCount(part->joint); c++) {
|
|
int32_t child = nodeGetChild(part->joint, c);
|
|
Vec3T where;
|
|
|
|
if (_ragdollPartOf(record, child) < 0) {
|
|
// A leaf joint still gives the bone its length.
|
|
bool isJoint = false;
|
|
int32_t j;
|
|
|
|
for (j = 0; j < jointCount; j++) {
|
|
isJoint = isJoint || (joints[j] == child);
|
|
}
|
|
if (!isJoint) {
|
|
continue;
|
|
}
|
|
}
|
|
where = nodeGetWorldPosition(child);
|
|
toward += JPH::Vec3(where.x, where.y, where.z);
|
|
children++;
|
|
}
|
|
if (children > 0) {
|
|
toward = toward / (float)children;
|
|
} else {
|
|
toward = origin + JPH::Vec3(0.0f, RAGDOLL_RADIUS_MIN * LEAF_BONE_RADII, 0.0f);
|
|
}
|
|
direction = toward - origin;
|
|
length = SDL_max(direction.Length(), RAGDOLL_RADIUS_MIN * MIN_BONE_RADII);
|
|
direction = (direction.LengthSq() > ZERO_LENGTH_SQ) ? direction.Normalized() : JPH::Vec3::sAxisY();
|
|
radius = (part->radius > 0.0f) ? part->radius : SDL_max(length * RAGDOLL_RADIUS_RATIO, RAGDOLL_RADIUS_MIN);
|
|
rotation = JPH::Quat::sFromTo(JPH::Vec3::sAxisY(), direction);
|
|
centre = JPH::RVec3(origin + direction * (length / 2.0f));
|
|
{
|
|
JPH::CapsuleShapeSettings capsule(SDL_max(length / 2.0f - radius, MIN_DIMENSION), radius);
|
|
JPH::BodyCreationSettings settings(capsule.Create().Get(), centre, rotation, JPH::EMotionType::Dynamic, LAYER_MOVING);
|
|
|
|
settings.mUserData = (JPH::uint64)(uint32_t)part->joint;
|
|
settings.mFriction = DEFAULT_FRICTION;
|
|
settings.mRestitution = 0.0f;
|
|
settings.mLinearDamping = RAGDOLL_LINEAR_DAMPING;
|
|
settings.mAngularDamping = RAGDOLL_ANGULAR_DAMPING;
|
|
settings.mCollisionGroup = JPH::CollisionGroup(record->filter, _world->ragdollGroups, (JPH::CollisionGroup::SubGroupID)p);
|
|
part->body = bodies.CreateAndAddBody(settings, JPH::EActivation::Activate);
|
|
}
|
|
if (part->body.IsInvalid()) {
|
|
_releaseRagdollBodies(record);
|
|
return false;
|
|
}
|
|
part->offsetPosition = rotation.Conjugated() * (origin - JPH::Vec3(centre));
|
|
part->offsetRotation = rotation.Conjugated() * _fromQuat(jointRotation);
|
|
part->scale = JPH::Vec3(jointScale.x, jointScale.y, jointScale.z);
|
|
}
|
|
_world->ragdollGroups++;
|
|
// Joints: a swing-twist cone at each joint to the parent bone, parent-child collision off.
|
|
for (p = 0; p < record->partCount; p++) {
|
|
RagdollPartT *part = &record->parts[p];
|
|
RagdollPartT *parent;
|
|
JPH::SwingTwistConstraintSettings settings;
|
|
JPH::RVec3 pivot;
|
|
JPH::Quat parentRotation;
|
|
JPH::Quat rotation;
|
|
JPH::Vec3 twist;
|
|
JPH::Vec3 plane;
|
|
|
|
if (part->parent < 0) {
|
|
continue;
|
|
}
|
|
parent = &record->parts[part->parent];
|
|
record->filter->DisableCollision((JPH::CollisionGroup::SubGroupID)p, (JPH::CollisionGroup::SubGroupID)part->parent);
|
|
bodies.GetPositionAndRotation(part->body, pivot, rotation);
|
|
parentRotation = bodies.GetRotation(parent->body);
|
|
pivot = pivot + rotation * part->offsetPosition;
|
|
twist = rotation * JPH::Vec3::sAxisY();
|
|
plane = twist.GetNormalizedPerpendicular();
|
|
settings.mSpace = JPH::EConstraintSpace::WorldSpace;
|
|
settings.mPosition1 = pivot;
|
|
settings.mPosition2 = pivot;
|
|
settings.mTwistAxis1 = twist;
|
|
settings.mTwistAxis2 = twist;
|
|
settings.mPlaneAxis1 = plane;
|
|
settings.mPlaneAxis2 = plane;
|
|
settings.mNormalHalfConeAngle = JPH::DegreesToRadians(part->swing);
|
|
settings.mPlaneHalfConeAngle = JPH::DegreesToRadians(part->swing);
|
|
settings.mTwistMinAngle = -JPH::DegreesToRadians(part->twist);
|
|
settings.mTwistMaxAngle = JPH::DegreesToRadians(part->twist);
|
|
settings.mSwingMotorSettings = JPH::MotorSettings(RAGDOLL_MOTOR_HZ, RAGDOLL_MOTOR_DAMPING);
|
|
settings.mTwistMotorSettings = JPH::MotorSettings(RAGDOLL_MOTOR_HZ, RAGDOLL_MOTOR_DAMPING);
|
|
{
|
|
JPH::BodyID pair[2] = { parent->body, part->body };
|
|
JPH::BodyLockMultiWrite lock(_world->system->GetBodyLockInterface(), pair, 2);
|
|
|
|
if ((lock.GetBody(0) == nullptr) || (lock.GetBody(1) == nullptr)) {
|
|
continue;
|
|
}
|
|
part->constraint = settings.Create(*lock.GetBody(0), *lock.GetBody(1));
|
|
}
|
|
_world->system->AddConstraint(part->constraint);
|
|
part->rest = parentRotation.Conjugated() * rotation;
|
|
}
|
|
record->active = true;
|
|
record->strengthChanged = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
// A shove on one bone, by its joint's name.
|
|
bool ragdollApplyImpulse(int32_t node, const char *joint, Vec3T impulse) {
|
|
RagdollRecordT *record = _findRagdoll(node);
|
|
int32_t p;
|
|
|
|
if ((record == nullptr) || !record->active) {
|
|
return false;
|
|
}
|
|
for (p = 0; p < record->partCount; p++) {
|
|
const char *name = nodeGetName(record->parts[p].joint);
|
|
|
|
if ((name != nullptr) && (SDL_strcasecmp(name, joint) == 0)) {
|
|
_world->system->GetBodyInterface().AddImpulse(record->parts[p].body, JPH::Vec3(impulse.x, impulse.y, impulse.z));
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// Animation may take over again; the joints keep the pose the ragdoll left them in.
|
|
bool ragdollDeactivate(int32_t node) {
|
|
RagdollRecordT *record = _findRagdoll(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
_releaseRagdollBodies(record);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ragdollDelete(int32_t node) {
|
|
RagdollRecordT *record = _findRagdoll(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
_releaseRagdoll(record);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ragdollExists(int32_t node) {
|
|
return _findRagdoll(node) != nullptr;
|
|
}
|
|
|
|
|
|
bool ragdollIsActive(int32_t node) {
|
|
RagdollRecordT *record = _findRagdoll(node);
|
|
|
|
return (record != nullptr) && record->active;
|
|
}
|
|
|
|
|
|
bool ragdollIsResting(int32_t node) {
|
|
RagdollRecordT *record = _findRagdoll(node);
|
|
int32_t p;
|
|
|
|
if ((record == nullptr) || !record->active) {
|
|
return false;
|
|
}
|
|
for (p = 0; p < record->partCount; p++) {
|
|
if (_world->system->GetBodyInterface().IsActive(record->parts[p].body)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// Derives the parts from a model instance's skin: every joint with a child joint becomes a bone,
|
|
// its parent bone the nearest ancestor joint that is one. Nothing is simulated until activated.
|
|
bool ragdollNew(int32_t node) {
|
|
RagdollRecordT *record = nullptr;
|
|
const int32_t *joints;
|
|
int32_t count;
|
|
int32_t skinned;
|
|
int32_t x;
|
|
int32_t j;
|
|
int32_t c;
|
|
|
|
if ((_world == nullptr) || !nodeValid(node)) {
|
|
return false;
|
|
}
|
|
skinned = _findSkinned(node);
|
|
if (skinned < 0) {
|
|
utilTrace("Physics: node %d has no skinned model under it for a ragdoll.", node);
|
|
return false;
|
|
}
|
|
ragdollDelete(node);
|
|
for (x = 0; x < _world->ragdollCount; x++) {
|
|
if (!_world->ragdolls[x].used) {
|
|
record = &_world->ragdolls[x];
|
|
break;
|
|
}
|
|
}
|
|
if (record == nullptr) {
|
|
utilTrace("Physics: no room for another ragdoll (%d already).", MAX_RAGDOLLS);
|
|
return false;
|
|
}
|
|
_resetRagdoll(record);
|
|
count = nodeGetSkinJoints(skinned, &joints);
|
|
for (j = 0; (j < count) && (record->partCount < MAX_RAGDOLL_PARTS); j++) {
|
|
bool hasChildJoint = false;
|
|
|
|
for (c = 0; c < nodeGetChildCount(joints[j]); c++) {
|
|
int32_t child = nodeGetChild(joints[j], c);
|
|
|
|
for (x = 0; x < count; x++) {
|
|
hasChildJoint = hasChildJoint || (joints[x] == child);
|
|
}
|
|
}
|
|
if (!hasChildJoint) {
|
|
continue;
|
|
}
|
|
record->parts[record->partCount].joint = joints[j];
|
|
record->parts[record->partCount].parent = -1;
|
|
record->parts[record->partCount].radius = 0.0f;
|
|
record->parts[record->partCount].swing = RAGDOLL_SWING_DEGREES;
|
|
record->parts[record->partCount].twist = RAGDOLL_TWIST_DEGREES;
|
|
record->partCount++;
|
|
}
|
|
if (record->partCount == 0) {
|
|
utilTrace("Physics: the skin under node %d has no bones to make a ragdoll from.", node);
|
|
return false;
|
|
}
|
|
// Parents: walk up the scene tree to the nearest joint that became a part.
|
|
for (x = 0; x < record->partCount; x++) {
|
|
int32_t up = nodeGetParent(record->parts[x].joint);
|
|
|
|
while ((up >= 0) && (record->parts[x].parent < 0)) {
|
|
record->parts[x].parent = _ragdollPartOf(record, up);
|
|
up = nodeGetParent(up);
|
|
}
|
|
}
|
|
record->node = node;
|
|
record->generation = nodeGetGeneration(node);
|
|
record->skinned = skinned;
|
|
record->used = true;
|
|
_indexSet(_world->ragdollOfNode, node, (int32_t)(record - _world->ragdolls));
|
|
return true;
|
|
}
|
|
|
|
|
|
// Tunes one bone before activation: capsule radius (0 = automatic), swing and twist limits.
|
|
bool ragdollSetJoint(int32_t node, const char *joint, float radius, float swingDegrees, float twistDegrees) {
|
|
RagdollRecordT *record = _findRagdoll(node);
|
|
int32_t p;
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
for (p = 0; p < record->partCount; p++) {
|
|
const char *name = nodeGetName(record->parts[p].joint);
|
|
|
|
if ((name != nullptr) && (SDL_strcasecmp(name, joint) == 0)) {
|
|
record->parts[p].radius = SDL_max(0.0f, radius);
|
|
record->parts[p].swing = SDL_clamp(swingDegrees, 0.0f, MAX_CONE_DEGREES);
|
|
record->parts[p].twist = SDL_clamp(twistDegrees, 0.0f, MAX_CONE_DEGREES);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// 0 goes limp; more drives every joint back toward the pose it was activated in.
|
|
bool ragdollSetStrength(int32_t node, float strength) {
|
|
RagdollRecordT *record = _findRagdoll(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->strength = SDL_max(0.0f, strength);
|
|
record->strengthChanged = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool softDelete(int32_t node) {
|
|
SoftRecordT *record = _findSoft(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
_releaseSoft(record);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool softExists(int32_t node) {
|
|
return _findSoft(node) != nullptr;
|
|
}
|
|
|
|
|
|
// The node's mesh becomes cloth or a pressure body: its vertices, welded by position, are the
|
|
// particles, in world space where the node has put them. Welding hashes each vertex's SOFT_WELD
|
|
// cell, so vertices split for their UVs or normals (the same position exactly) become one particle.
|
|
bool softNew(int32_t node, SoftKindE kind) {
|
|
SoftRecordT *record = nullptr;
|
|
const float *positions;
|
|
const uint32_t *indices;
|
|
int32_t vertexCount;
|
|
int32_t indexCount;
|
|
int32_t mesh;
|
|
Vec3T position;
|
|
QuatT rotation;
|
|
Vec3T scale;
|
|
int32_t x;
|
|
std::unordered_map<WeldKeyT, int32_t, WeldHashT> welded;
|
|
|
|
if ((_world == nullptr) || !nodeValid(node) || (kind == SOFT_ROPE)) {
|
|
return false;
|
|
}
|
|
mesh = nodeGetMesh(node);
|
|
if (!meshGetGeometry(mesh, &positions, &vertexCount, &indices, &indexCount) || (vertexCount < 3)) {
|
|
utilTrace("Physics: node %d needs a mesh to become a soft body.", node);
|
|
return false;
|
|
}
|
|
softDelete(node);
|
|
for (x = 0; x < _world->softCount; x++) {
|
|
if (!_world->softs[x].used) {
|
|
record = &_world->softs[x];
|
|
break;
|
|
}
|
|
}
|
|
if (record == nullptr) {
|
|
utilTrace("Physics: no room for another soft body (%d already).", MAX_SOFT);
|
|
return false;
|
|
}
|
|
_resetSoft(record);
|
|
sceneUpdateTransforms();
|
|
nodeGetWorldTransform(node, &position, &rotation, &scale);
|
|
record->meshToSoft = (int32_t *)SDL_calloc((size_t)vertexCount, sizeof(int32_t));
|
|
record->positions = (float *)SDL_calloc((size_t)vertexCount * 3, sizeof(float));
|
|
record->meshPositions = (float *)SDL_calloc((size_t)vertexCount * 3, sizeof(float));
|
|
if ((record->meshToSoft == nullptr) || (record->positions == nullptr) || (record->meshPositions == nullptr)) {
|
|
utilDie("Out of memory for a soft body.");
|
|
}
|
|
record->meshVertexCount = vertexCount;
|
|
welded.reserve((size_t)vertexCount);
|
|
for (x = 0; x < vertexCount; x++) {
|
|
Vec3T local = vec3(positions[x * 3] * scale.x, positions[x * 3 + 1] * scale.y, positions[x * 3 + 2] * scale.z);
|
|
Vec3T world = vec3Add(position, quatRotate(rotation, local));
|
|
WeldKeyT key = { (int32_t)floorf(world.x / SOFT_WELD), (int32_t)floorf(world.y / SOFT_WELD), (int32_t)floorf(world.z / SOFT_WELD) };
|
|
std::unordered_map<WeldKeyT, int32_t, WeldHashT>::iterator it = welded.find(key);
|
|
int32_t found;
|
|
|
|
if (it != welded.end()) {
|
|
found = it->second;
|
|
} else {
|
|
found = record->count++;
|
|
record->positions[found * 3] = world.x;
|
|
record->positions[found * 3 + 1] = world.y;
|
|
record->positions[found * 3 + 2] = world.z;
|
|
welded[key] = found;
|
|
}
|
|
record->meshToSoft[x] = found;
|
|
}
|
|
record->node = node;
|
|
record->generation = nodeGetGeneration(node);
|
|
record->kind = kind;
|
|
record->mesh = mesh;
|
|
record->used = true;
|
|
_indexSet(_world->softOfNode, node, (int32_t)(record - _world->softs));
|
|
if (!_buildSoft(record)) {
|
|
_releaseSoft(record);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// A rope from the node's position to a world point: particles along it, a tube mesh round them
|
|
// that becomes the node's mesh (keeping its material).
|
|
bool softNewRope(int32_t node, Vec3T end, int32_t segments, float radius) {
|
|
SoftRecordT *record = nullptr;
|
|
SceneVertexT *vertices;
|
|
uint32_t *indices;
|
|
Vec3T *rings;
|
|
Vec3T start;
|
|
Vec3T position;
|
|
QuatT rotation;
|
|
Vec3T scale;
|
|
QuatT inverse;
|
|
int32_t indexCount;
|
|
int32_t count;
|
|
int32_t mesh;
|
|
int32_t x;
|
|
int32_t side;
|
|
|
|
if ((_world == nullptr) || !nodeValid(node) || (segments < 1) || (radius <= 0.0f)) {
|
|
return false;
|
|
}
|
|
softDelete(node);
|
|
for (x = 0; x < _world->softCount; x++) {
|
|
if (!_world->softs[x].used) {
|
|
record = &_world->softs[x];
|
|
break;
|
|
}
|
|
}
|
|
if (record == nullptr) {
|
|
utilTrace("Physics: no room for another soft body (%d already).", MAX_SOFT);
|
|
return false;
|
|
}
|
|
_resetSoft(record);
|
|
sceneUpdateTransforms();
|
|
nodeGetWorldTransform(node, &position, &rotation, &scale);
|
|
start = position;
|
|
count = segments + 1;
|
|
indexCount = segments * ROPE_SIDES * ROPE_INDICES_PER_QUAD;
|
|
record->count = count;
|
|
record->meshVertexCount = count * ROPE_SIDES;
|
|
record->positions = (float *)SDL_calloc((size_t)count * 3, sizeof(float));
|
|
record->meshPositions = (float *)SDL_calloc((size_t)record->meshVertexCount * 3, sizeof(float));
|
|
record->meshToSoft = (int32_t *)SDL_calloc((size_t)record->meshVertexCount, sizeof(int32_t));
|
|
vertices = (SceneVertexT *)SDL_calloc((size_t)record->meshVertexCount, sizeof(SceneVertexT));
|
|
indices = (uint32_t *)SDL_calloc((size_t)indexCount, sizeof(uint32_t));
|
|
rings = (Vec3T *)SDL_calloc((size_t)record->meshVertexCount, sizeof(Vec3T));
|
|
if ((record->positions == nullptr) || (record->meshPositions == nullptr) || (record->meshToSoft == nullptr) || (vertices == nullptr) || (indices == nullptr) || (rings == nullptr)) {
|
|
utilDie("Out of memory for a rope.");
|
|
}
|
|
for (x = 0; x < count; x++) {
|
|
float t = (float)x / (float)segments;
|
|
|
|
record->positions[x * 3] = start.x + (end.x - start.x) * t;
|
|
record->positions[x * 3 + 1] = start.y + (end.y - start.y) * t;
|
|
record->positions[x * 3 + 2] = start.z + (end.z - start.z) * t;
|
|
}
|
|
record->ropeRadius = radius;
|
|
record->kind = SOFT_ROPE;
|
|
// The tube, in the node's frame, with the rope's length running along V.
|
|
_ropeMesh(record, rings);
|
|
inverse = quatInverse(rotation);
|
|
for (x = 0; x < count; x++) {
|
|
for (side = 0; side < ROPE_SIDES; side++) {
|
|
int32_t v = x * ROPE_SIDES + side;
|
|
Vec3T local = quatRotate(inverse, vec3Subtract(rings[v], position));
|
|
|
|
vertices[v].position[0] = local.x / SDL_max(scale.x, MIN_SCALE);
|
|
vertices[v].position[1] = local.y / SDL_max(scale.y, MIN_SCALE);
|
|
vertices[v].position[2] = local.z / SDL_max(scale.z, MIN_SCALE);
|
|
vertices[v].uv[0] = (float)side / (float)ROPE_SIDES;
|
|
vertices[v].uv[1] = (float)x / (float)segments;
|
|
vertices[v].weights[0] = 1.0f;
|
|
record->meshToSoft[v] = x;
|
|
}
|
|
}
|
|
for (x = 0; x < segments; x++) {
|
|
for (side = 0; side < ROPE_SIDES; side++) {
|
|
uint32_t a = (uint32_t)(x * ROPE_SIDES + side);
|
|
uint32_t b = (uint32_t)(x * ROPE_SIDES + (side + 1) % ROPE_SIDES);
|
|
uint32_t c = a + ROPE_SIDES;
|
|
uint32_t d = b + ROPE_SIDES;
|
|
uint32_t *tri = &indices[(x * ROPE_SIDES + side) * ROPE_INDICES_PER_QUAD];
|
|
|
|
tri[0] = a;
|
|
tri[1] = c;
|
|
tri[2] = b;
|
|
tri[3] = b;
|
|
tri[4] = c;
|
|
tri[5] = d;
|
|
}
|
|
}
|
|
sceneComputeNormals(vertices, record->meshVertexCount, indices, indexCount);
|
|
mesh = meshNewVertices(vertices, record->meshVertexCount, indices, indexCount, false);
|
|
SDL_free(vertices);
|
|
SDL_free(indices);
|
|
SDL_free(rings);
|
|
if (mesh == NO_HANDLE) {
|
|
_releaseSoft(record);
|
|
return false;
|
|
}
|
|
nodeSetMesh(node, mesh, nodeGetMaterial(node));
|
|
record->node = node;
|
|
record->generation = nodeGetGeneration(node);
|
|
record->mesh = mesh;
|
|
record->used = true;
|
|
_indexSet(_world->softOfNode, node, (int32_t)(record - _world->softs));
|
|
if (!_buildSoft(record)) {
|
|
_releaseSoft(record);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// Holds the particle nearest a world point where it is, or to a node from now on.
|
|
bool softPin(int32_t node, Vec3T point, int32_t follow) {
|
|
SoftRecordT *record = _findSoft(node);
|
|
int32_t vertex;
|
|
int32_t x;
|
|
|
|
if ((record == nullptr) || (record->pinCount >= MAX_SOFT_PINS)) {
|
|
return false;
|
|
}
|
|
vertex = _nearestSoftVertex(record, point);
|
|
if (vertex < 0) {
|
|
return false;
|
|
}
|
|
for (x = 0; x < record->pinCount; x++) {
|
|
if (record->pins[x].vertex == vertex) {
|
|
record->pins[x].follow = follow;
|
|
return true;
|
|
}
|
|
}
|
|
record->pins[record->pinCount].vertex = vertex;
|
|
record->pins[record->pinCount].follow = follow;
|
|
record->pinCount++;
|
|
if (!record->body.IsInvalid()) {
|
|
JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), record->body);
|
|
|
|
if (lock.Succeeded()) {
|
|
JPH::SoftBodyMotionProperties *motion = static_cast<JPH::SoftBodyMotionProperties *>(lock.GetBody().GetMotionProperties());
|
|
|
|
motion->GetVertex((JPH::uint)vertex).mInvMass = 0.0f;
|
|
motion->GetVertex((JPH::uint)vertex).mVelocity = JPH::Vec3::sZero();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// Damping is a live setting of the body; nothing is rebuilt.
|
|
bool softSetDamping(int32_t node, float damping) {
|
|
SoftRecordT *record = _findSoft(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->damping = SDL_max(0.0f, damping);
|
|
if (!record->body.IsInvalid()) {
|
|
{
|
|
JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), record->body);
|
|
|
|
if (lock.Succeeded()) {
|
|
lock.GetBody().GetMotionProperties()->SetLinearDamping(record->damping);
|
|
}
|
|
}
|
|
// Outside the lock: activating takes locks of its own.
|
|
_world->system->GetBodyInterface().ActivateBody(record->body);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// Mass is shared out over the particles in place; nothing is rebuilt.
|
|
bool softSetMass(int32_t node, float kilograms) {
|
|
SoftRecordT *record = _findSoft(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->mass = SDL_max(MIN_MASS, kilograms);
|
|
_softSetMasses(record);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool softSetPressure(int32_t node, float pressure) {
|
|
SoftRecordT *record = _findSoft(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->pressure = SDL_max(0.0f, pressure);
|
|
if (!record->body.IsInvalid()) {
|
|
{
|
|
JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), record->body);
|
|
|
|
if (lock.Succeeded()) {
|
|
static_cast<JPH::SoftBodyMotionProperties *>(lock.GetBody().GetMotionProperties())->SetPressure(record->pressure);
|
|
}
|
|
}
|
|
// Outside the lock: activating takes locks of its own.
|
|
_world->system->GetBodyInterface().ActivateBody(record->body);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// Stretch and bend stiffness, 0 to 1; the constraints are remade around the current shape.
|
|
bool softSetStiffness(int32_t node, float stretch, float bend) {
|
|
SoftRecordT *record = _findSoft(node);
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
record->stretch = SDL_clamp(stretch, 0.0f, 1.0f);
|
|
record->bend = SDL_clamp(bend, 0.0f, 1.0f);
|
|
if (!_buildSoft(record)) {
|
|
_releaseSoft(record);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// Lets the particle nearest a world point go, giving it its share of the mass back.
|
|
bool softUnpin(int32_t node, Vec3T point) {
|
|
SoftRecordT *record = _findSoft(node);
|
|
int32_t vertex;
|
|
int32_t x;
|
|
|
|
if (record == nullptr) {
|
|
return false;
|
|
}
|
|
vertex = _nearestSoftVertex(record, point);
|
|
for (x = 0; x < record->pinCount; x++) {
|
|
if (record->pins[x].vertex == vertex) {
|
|
record->pins[x] = record->pins[record->pinCount - 1];
|
|
record->pinCount--;
|
|
_softSetMasses(record);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|