1028 lines
32 KiB
C++
1028 lines
32 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/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 <mutex>
|
|
#include <vector>
|
|
extern "C" {
|
|
#include "util.h"
|
|
#include "scene.h"
|
|
}
|
|
#include "physics.h"
|
|
|
|
|
|
#define MAX_BODIES 4096
|
|
#define MAX_BODY_PAIRS 4096
|
|
#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 DEFAULT_FRICTION 0.5f
|
|
#define DEFAULT_BOUNCE 0.1f
|
|
#define NO_HANDLE -1
|
|
#define MAX_EVENTS 512 // Per frame; the rest of a busy step is dropped
|
|
#define DEFAULT_RAY_DISTANCE 1000.0f
|
|
#define WORLD_NODE -1 // A joint's other side fixed to the world
|
|
#define DEGREES_TO_RADIANS(d) ((d) * (3.14159265358979323846f / 180.0f))
|
|
|
|
|
|
// 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;
|
|
};
|
|
|
|
|
|
struct WorldT;
|
|
extern WorldT *_world;
|
|
bool _isTrigger(JPH::BodyID id);
|
|
|
|
|
|
// Collects contacts from Jolt's job threads; the engine drains it after the step.
|
|
class ContactListenerT final : public JPH::ContactListener {
|
|
public:
|
|
std::mutex lock;
|
|
std::vector<PhysicsEventT> events;
|
|
|
|
void OnContactAdded(const JPH::Body &a, const JPH::Body &b, const JPH::ContactManifold &manifold, JPH::ContactSettings &settings) override {
|
|
PhysicsEventT event;
|
|
JPH::Vec3 relative;
|
|
|
|
(void)settings;
|
|
event.nodeA = (int32_t)(uint32_t)a.GetUserData();
|
|
event.nodeB = (int32_t)(uint32_t)b.GetUserData();
|
|
event.point = vec3((float)manifold.GetWorldSpaceContactPointOn1(0).GetX(), (float)manifold.GetWorldSpaceContactPointOn1(0).GetY(), (float)manifold.GetWorldSpaceContactPointOn1(0).GetZ());
|
|
relative = a.GetLinearVelocity() - b.GetLinearVelocity();
|
|
event.speed = fabsf(relative.Dot(manifold.mWorldSpaceNormal));
|
|
if (a.IsSensor() || b.IsSensor()) {
|
|
event.type = PHYSICS_EVENT_ENTER;
|
|
if (b.IsSensor()) {
|
|
// The trigger comes first.
|
|
int32_t swap = event.nodeA;
|
|
|
|
event.nodeA = event.nodeB;
|
|
event.nodeB = swap;
|
|
}
|
|
} else {
|
|
event.type = PHYSICS_EVENT_COLLISION;
|
|
}
|
|
push(event);
|
|
}
|
|
|
|
void OnContactRemoved(const JPH::SubShapeIDPair &pair) override;
|
|
|
|
void push(const PhysicsEventT &event) {
|
|
std::lock_guard<std::mutex> guard(lock);
|
|
|
|
if (events.size() < MAX_EVENTS) {
|
|
events.push_back(event);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// 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;
|
|
};
|
|
|
|
|
|
struct WorldT {
|
|
JPH::TempAllocatorImpl *tempAllocator;
|
|
JPH::JobSystemThreadPool *jobs;
|
|
BroadPhaseLayersT broadPhaseLayers;
|
|
ObjectVsBroadPhaseFilterT objectVsBroadPhase;
|
|
ObjectPairFilterT objectPairs;
|
|
JPH::PhysicsSystem *system;
|
|
ContactListenerT *contacts;
|
|
BodyRecordT *bodies;
|
|
int32_t bodyCount;
|
|
JointRecordT *joints;
|
|
int32_t jointCount;
|
|
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;
|
|
|
|
|
|
// A contact ending only matters for triggers: the pair is reported as left. This runs inside
|
|
// Jolt's step on a job thread, where taking a body lock deadlocks, so the lock-free interface
|
|
// reads the user data; the trigger flag comes from our records.
|
|
void ContactListenerT::OnContactRemoved(const JPH::SubShapeIDPair &pair) {
|
|
PhysicsEventT event;
|
|
JPH::uint64 userA = _world->system->GetBodyInterfaceNoLock().GetUserData(pair.GetBody1ID());
|
|
JPH::uint64 userB = _world->system->GetBodyInterfaceNoLock().GetUserData(pair.GetBody2ID());
|
|
bool sensorA = _isTrigger(pair.GetBody1ID());
|
|
bool sensorB = _isTrigger(pair.GetBody2ID());
|
|
|
|
if (!sensorA && !sensorB) {
|
|
return;
|
|
}
|
|
event.type = PHYSICS_EVENT_LEAVE;
|
|
event.nodeA = (int32_t)(uint32_t)(sensorA ? userA : userB);
|
|
event.nodeB = (int32_t)(uint32_t)(sensorA ? userB : userA);
|
|
event.point = vec3(0.0f, 0.0f, 0.0f);
|
|
event.speed = 0.0f;
|
|
push(event);
|
|
}
|
|
|
|
|
|
JPH::RefConst<JPH::Shape> _buildMeshShape(int32_t node, ShapeTypeE shape, Vec3T position, QuatT rotation);
|
|
void _collectGeometry(int32_t node, const Mat4T *toBody, JPH::Array<JPH::Vec3> &points, JPH::IndexedTriangleList &triangles, JPH::VertexList &vertices);
|
|
BodyRecordT *_find(int32_t node);
|
|
JPH::Quat _fromQuat(QuatT q);
|
|
JPH::Vec3 _fromVec3(Vec3T v);
|
|
bool _isTrigger(JPH::BodyID id);
|
|
void _release(BodyRecordT *record);
|
|
QuatT _toQuat(JPH::Quat q);
|
|
Vec3T _toVec3(JPH::Vec3 v);
|
|
void _trace(const char *fmt, ...);
|
|
|
|
|
|
// 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, 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.
|
|
void _collectGeometry(int32_t node, const Mat4T *toBody, 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]));
|
|
|
|
points.push_back(JPH::Vec3(v.x, v.y, v.z));
|
|
vertices.push_back(JPH::Float3(v.x, v.y, v.z));
|
|
}
|
|
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, points, triangles, vertices);
|
|
}
|
|
}
|
|
|
|
|
|
// 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) {
|
|
int32_t x;
|
|
|
|
if (_world == nullptr) {
|
|
return nullptr;
|
|
}
|
|
for (x = 0; x < _world->bodyCount; x++) {
|
|
BodyRecordT *record = &_world->bodies[x];
|
|
|
|
if (!record->used || (record->node != node)) {
|
|
continue;
|
|
}
|
|
if (!nodeValid(node) || (nodeGetGeneration(node) != record->generation)) {
|
|
_release(record);
|
|
return nullptr;
|
|
}
|
|
return record;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
// Whether a Jolt body is one of our triggers (by record, so a body being destroyed is safe).
|
|
bool _isTrigger(JPH::BodyID id) {
|
|
int32_t x;
|
|
|
|
for (x = 0; x < _world->bodyCount; x++) {
|
|
if (_world->bodies[x].used && (_world->bodies[x].id == id)) {
|
|
return _world->bodies[x].trigger;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// Takes the body out of the world (its joints first) and frees its slot.
|
|
void _release(BodyRecordT *record) {
|
|
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
|
|
int32_t x;
|
|
|
|
for (x = 0; x < _world->jointCount; x++) {
|
|
if (_world->joints[x].used && ((_world->joints[x].nodeA == record->node) || (_world->joints[x].nodeB == record->node))) {
|
|
jointDelete(x);
|
|
}
|
|
}
|
|
if (record->enabled) {
|
|
bodies.RemoveBody(record->id);
|
|
}
|
|
bodies.DestroyBody(record->id);
|
|
memset(record, 0, sizeof(*record));
|
|
}
|
|
|
|
|
|
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;
|
|
float radius;
|
|
float height;
|
|
int32_t x;
|
|
|
|
if ((_world == nullptr) || !nodeValid(node)) {
|
|
return false;
|
|
}
|
|
bodyDelete(node);
|
|
sceneUpdateTransforms();
|
|
nodeGetWorldTransform(node, &position, &rotation, &scale);
|
|
switch (shape) {
|
|
case SHAPE_BOX:
|
|
joltShape = 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);
|
|
break;
|
|
case SHAPE_SPHERE:
|
|
joltShape = new JPH::SphereShape(SDL_max(a * SDL_max(scale.x, SDL_max(scale.y, scale.z)), MIN_DIMENSION));
|
|
break;
|
|
case SHAPE_CAPSULE:
|
|
radius = SDL_max(a * SDL_max(scale.x, scale.z), MIN_DIMENSION);
|
|
height = SDL_max(b * scale.y, MIN_DIMENSION);
|
|
joltShape = new JPH::CapsuleShape(SDL_max(height / 2.0f - radius, MIN_DIMENSION), radius);
|
|
break;
|
|
case SHAPE_CYLINDER:
|
|
radius = SDL_max(a * SDL_max(scale.x, scale.z), MIN_DIMENSION);
|
|
height = SDL_max(b * scale.y, MIN_DIMENSION);
|
|
joltShape = new JPH::CylinderShape(height / 2.0f, radius);
|
|
break;
|
|
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 false;
|
|
}
|
|
joltShape = _buildMeshShape(node, shape, position, rotation);
|
|
if (joltShape == nullptr) {
|
|
return false;
|
|
}
|
|
break;
|
|
default:
|
|
utilTrace("Physics: unknown shape %d.", (int32_t)shape);
|
|
return false;
|
|
}
|
|
for (x = 0; x < _world->bodyCount; x++) {
|
|
if (!_world->bodies[x].used) {
|
|
break;
|
|
}
|
|
}
|
|
if (x == _world->bodyCount) {
|
|
_world->bodies = (BodyRecordT *)SDL_realloc(_world->bodies, sizeof(BodyRecordT) * (size_t)(_world->bodyCount + 1));
|
|
if (_world->bodies == nullptr) {
|
|
utilDie("Out of memory allocating a physics body.");
|
|
}
|
|
_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;
|
|
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);
|
|
}
|
|
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) {
|
|
if (!jointValid(joint)) {
|
|
return false;
|
|
}
|
|
_world->system->RemoveConstraint(_world->joints[joint].constraint);
|
|
_world->joints[joint].constraint = nullptr;
|
|
_world->joints[joint].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;
|
|
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);
|
|
for (x = 0; x < _world->jointCount; x++) {
|
|
if (!_world->joints[x].used) {
|
|
break;
|
|
}
|
|
}
|
|
if (x == _world->jointCount) {
|
|
JointRecordT *grown = new JointRecordT[_world->jointCount + 1];
|
|
|
|
for (int32_t y = 0; y < _world->jointCount; y++) {
|
|
grown[y] = _world->joints[y];
|
|
}
|
|
delete[] _world->joints;
|
|
_world->joints = grown;
|
|
_world->jointCount++;
|
|
}
|
|
_world->joints[x].constraint = constraint;
|
|
_world->joints[x].type = type;
|
|
_world->joints[x].nodeA = nodeA;
|
|
_world->joints[x].nodeB = nodeB;
|
|
_world->joints[x].used = true;
|
|
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[joint].type == JOINT_HINGE) {
|
|
((JPH::HingeConstraint *)_world->joints[joint].constraint.GetPtr())->SetLimits(DEGREES_TO_RADIANS(-high), DEGREES_TO_RADIANS(-low));
|
|
return true;
|
|
}
|
|
if (_world->joints[joint].type == JOINT_SLIDER) {
|
|
((JPH::SliderConstraint *)_world->joints[joint].constraint.GetPtr())->SetLimits(-high, -low);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
bool jointValid(int32_t joint) {
|
|
return (_world != nullptr) && (joint >= 0) && (joint < _world->jointCount) && _world->joints[joint].used;
|
|
}
|
|
|
|
|
|
// ===== World =====
|
|
|
|
bool physicsAvailable(void) {
|
|
return _world != nullptr;
|
|
}
|
|
|
|
|
|
// Hands the engine the events the last step produced (up to maximum) and clears them.
|
|
int32_t physicsGetEvents(PhysicsEventT *events, int32_t maximum) {
|
|
int32_t count = 0;
|
|
|
|
if (_world == nullptr) {
|
|
return 0;
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> guard(_world->contacts->lock);
|
|
|
|
while ((count < maximum) && (count < (int32_t)_world->contacts->events.size())) {
|
|
events[count] = _world->contacts->events[(size_t)count];
|
|
count++;
|
|
}
|
|
_world->contacts->events.clear();
|
|
}
|
|
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;
|
|
|
|
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->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]);
|
|
}
|
|
}
|
|
SDL_free(_world->bodies);
|
|
delete[] _world->joints;
|
|
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;
|
|
}
|
|
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
|
|
// 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;
|
|
int32_t steps = 0;
|
|
int32_t x;
|
|
Vec3T position;
|
|
QuatT rotation;
|
|
Vec3T scale;
|
|
|
|
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) {
|
|
return;
|
|
}
|
|
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)) {
|
|
_world->system->Update((float)STEP_SECONDS, 1, _world->tempAllocator, _world->jobs);
|
|
_world->accumulator -= STEP_SECONDS;
|
|
steps++;
|
|
}
|
|
// 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));
|
|
}
|
|
}
|
|
}
|