feat(physics): phase 16 - ludic.physics over Jolt Physics v5.6.0
Our own shim (native/shim/jph_shim.cpp) over Jolt built from the pinned tag with cross-platform determinism and no fp contraction: shapes as handles (box, sphere, capsule, cylinder, dome, offset, heightfield, mesh), still/kinematic/dynamic bodies by id with real removal, rays, top_at and push (what CharacterGround asks), overlaps, buoyancy against the PhysWater port, contacts recorded in C and drained as PHYS_HIT / PHYS_SPLASH facts. Fixed 1/60 steps, at most four a frame. Nine tests against the real library, including a pile run twice to the bit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5d3a799e33
commit
72dbdca1f3
21 changed files with 1152 additions and 0 deletions
64
packages/ludic.physics/native/shim/jph_bodies.inl
Normal file
64
packages/ludic.physics/native/shim/jph_bodies.inl
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// jph_bodies.inl - bodies by id: added, removed, moved, pushed, read back
|
||||
|
||||
// motion 0 still, 1 kinematic, 2 dynamic; layer 0 ground, 1 static, 2 moving; mass 0 = from shape
|
||||
JPH_SHIM int jph_body_add(void *w, void *shape, float x, float y, float z, float yaw, int motion, int layer,
|
||||
float mass, float friction, float bounce) {
|
||||
World *ww = static_cast<World *>(w);
|
||||
EMotionType mt = motion == 2 ? EMotionType::Dynamic : (motion == 1 ? EMotionType::Kinematic : EMotionType::Static);
|
||||
BodyCreationSettings st(static_cast<Shape *>(shape), RVec3(x, y, z), yaw_q(yaw), mt, ObjectLayer(layer));
|
||||
st.mFriction = friction;
|
||||
st.mRestitution = bounce;
|
||||
if (mt == EMotionType::Dynamic && mass > 0.0f) {
|
||||
st.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
|
||||
st.mMassPropertiesOverride.mMass = mass;
|
||||
}
|
||||
EActivation act = mt == EMotionType::Static ? EActivation::DontActivate : EActivation::Activate;
|
||||
BodyID id = ww->sys.GetBodyInterface().CreateAndAddBody(st, act);
|
||||
if (id.IsInvalid()) return -1;
|
||||
return int(id.GetIndexAndSequenceNumber());
|
||||
}
|
||||
|
||||
JPH_SHIM void jph_body_remove(void *w, int id) {
|
||||
BodyInterface &bi = static_cast<World *>(w)->sys.GetBodyInterface();
|
||||
bi.RemoveBody(bid(id));
|
||||
bi.DestroyBody(bid(id));
|
||||
}
|
||||
|
||||
// position, rotation (x, y, z, w) and velocity into out[10]
|
||||
JPH_SHIM int jph_body_read(void *w, int id, float *out) {
|
||||
BodyInterface &bi = static_cast<World *>(w)->sys.GetBodyInterface();
|
||||
if (!bi.IsAdded(bid(id))) return -1;
|
||||
RVec3 p; Quat q;
|
||||
bi.GetPositionAndRotation(bid(id), p, q);
|
||||
Vec3 v = bi.GetLinearVelocity(bid(id));
|
||||
float vals[10] = { float(p.GetX()), float(p.GetY()), float(p.GetZ()), q.GetX(), q.GetY(), q.GetZ(), q.GetW(),
|
||||
v.GetX(), v.GetY(), v.GetZ() };
|
||||
for (int i = 0; i < 10; i++) out[i] = vals[i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
JPH_SHIM void jph_body_place(void *w, int id, float x, float y, float z, float yaw) {
|
||||
static_cast<World *>(w)->sys.GetBodyInterface().SetPositionAndRotation(bid(id), RVec3(x, y, z), yaw_q(yaw), EActivation::Activate);
|
||||
}
|
||||
JPH_SHIM void jph_body_impulse(void *w, int id, float x, float y, float z) {
|
||||
static_cast<World *>(w)->sys.GetBodyInterface().AddImpulse(bid(id), Vec3(x, y, z));
|
||||
}
|
||||
JPH_SHIM void jph_body_velocity(void *w, int id, float x, float y, float z) {
|
||||
static_cast<World *>(w)->sys.GetBodyInterface().SetLinearVelocity(bid(id), Vec3(x, y, z));
|
||||
}
|
||||
// a kinematic body driven to where it should be after dt, pushing what it meets
|
||||
JPH_SHIM void jph_body_target(void *w, int id, float x, float y, float z, float yaw, float dt) {
|
||||
static_cast<World *>(w)->sys.GetBodyInterface().MoveKinematic(bid(id), RVec3(x, y, z), yaw_q(yaw), dt);
|
||||
}
|
||||
JPH_SHIM int jph_body_awake(void *w, int id) { return static_cast<World *>(w)->sys.GetBodyInterface().IsActive(bid(id)) ? 1 : 0; }
|
||||
|
||||
// water under a body: the surface's height and normal, how buoyant, the drags and the current
|
||||
JPH_SHIM int jph_body_buoyancy(void *w, int id, float sy, float nx, float ny, float nz, float buoy, float lin, float ang,
|
||||
float cx, float cz, float dt) {
|
||||
World *ww = static_cast<World *>(w);
|
||||
BodyInterface &bi = ww->sys.GetBodyInterface();
|
||||
RVec3 p = bi.GetPosition(bid(id));
|
||||
bool in = bi.ApplyBuoyancyImpulse(bid(id), RVec3(p.GetX(), sy, p.GetZ()), Vec3(nx, ny, nz), buoy, lin, ang,
|
||||
Vec3(cx, 0.0f, cz), ww->sys.GetGravity(), dt);
|
||||
return in ? 1 : 0;
|
||||
}
|
||||
81
packages/ludic.physics/native/shim/jph_queries.inl
Normal file
81
packages/ludic.physics/native/shim/jph_queries.inl
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// jph_queries.inl - what is there: a ray, the top a body could step onto, overlaps, a push out
|
||||
|
||||
// a ray from o along d (its length the reach) against the layers in mask; the body id or -1, and
|
||||
// out[7] the fraction, the point and the normal
|
||||
JPH_SHIM int jph_ray(void *w, float ox, float oy, float oz, float dx, float dy, float dz, int mask, float *out) {
|
||||
World *ww = static_cast<World *>(w);
|
||||
RRayCast ray{ RVec3(ox, oy, oz), Vec3(dx, dy, dz) };
|
||||
RayCastResult hit;
|
||||
MaskFilter f(mask);
|
||||
if (!ww->sys.GetNarrowPhaseQuery().CastRay(ray, hit, {}, f)) return -1;
|
||||
RVec3 p = ray.GetPointOnRay(hit.mFraction);
|
||||
Vec3 n = Vec3::sAxisY();
|
||||
BodyLockRead lock(ww->sys.GetBodyLockInterface(), hit.mBodyID);
|
||||
if (lock.Succeeded()) n = lock.GetBody().GetWorldSpaceSurfaceNormal(hit.mSubShapeID2, p);
|
||||
float vals[7] = { hit.mFraction, float(p.GetX()), float(p.GetY()), float(p.GetZ()), n.GetX(), n.GetY(), n.GetZ() };
|
||||
for (int i = 0; i < 7; i++) out[i] = vals[i];
|
||||
return int(hit.mBodyID.GetIndexAndSequenceNumber());
|
||||
}
|
||||
|
||||
// a ball of radius r let down from (x, from_y, z) at most `depth`: the height of what it lands
|
||||
// on, or -FLT_MAX when it lands on nothing in mask
|
||||
JPH_SHIM float jph_top_at(void *w, float x, float z, float r, float from_y, float depth, int mask) {
|
||||
World *ww = static_cast<World *>(w);
|
||||
SphereShape ball(r);
|
||||
ball.SetEmbedded();
|
||||
RShapeCast cast = RShapeCast::sFromWorldTransform(&ball, Vec3::sOne(), RMat44::sTranslation(RVec3(x, from_y, z)),
|
||||
Vec3(0.0f, -depth, 0.0f));
|
||||
ClosestHitCollisionCollector<CastShapeCollector> hit;
|
||||
MaskFilter f(mask);
|
||||
ww->sys.GetNarrowPhaseQuery().CastShape(cast, ShapeCastSettings(), RVec3::sZero(), hit, {}, f);
|
||||
if (!hit.HadHit()) return -FLT_MAX;
|
||||
return float(hit.mHit.mContactPointOn2.GetY());
|
||||
}
|
||||
|
||||
// the bodies (in mask) a ball at (x, y, z) touches, up to cap into ids; returns how many
|
||||
JPH_SHIM int jph_overlap(void *w, float x, float y, float z, float r, int mask, int *ids, int cap) {
|
||||
World *ww = static_cast<World *>(w);
|
||||
SphereShape ball(r);
|
||||
ball.SetEmbedded();
|
||||
AllHitCollisionCollector<CollideShapeCollector> hits;
|
||||
MaskFilter f(mask);
|
||||
ww->sys.GetNarrowPhaseQuery().CollideShape(&ball, Vec3::sOne(), RMat44::sTranslation(RVec3(x, y, z)),
|
||||
CollideShapeSettings(), RVec3::sZero(), hits, {}, f);
|
||||
hits.Sort();
|
||||
int n = 0;
|
||||
for (const CollideShapeResult &h : hits.mHits) {
|
||||
int id = int(h.mBodyID2.GetIndexAndSequenceNumber());
|
||||
bool seen = false;
|
||||
for (int i = 0; i < n; i++) seen = seen || ids[i] == id;
|
||||
if (!seen && n < cap) ids[n++] = id;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// an upright body r wide, from y0 to y1, at (x, z): how far to move it in x and z to stand clear
|
||||
// of what it overlaps in mask, into out[2]; returns how many things it overlapped
|
||||
JPH_SHIM int jph_push(void *w, float x, float z, float r, float y0, float y1, int mask, float *out) {
|
||||
World *ww = static_cast<World *>(w);
|
||||
float half = (y1 - y0) * 0.5f;
|
||||
CylinderShape body(half, r, 0.0f);
|
||||
body.SetEmbedded();
|
||||
AllHitCollisionCollector<CollideShapeCollector> hits;
|
||||
MaskFilter f(mask);
|
||||
CollideShapeSettings cs;
|
||||
cs.mBackFaceMode = EBackFaceMode::CollideWithBackFaces;
|
||||
ww->sys.GetNarrowPhaseQuery().CollideShape(&body, Vec3::sOne(), RMat44::sTranslation(RVec3(x, y0 + half, z)),
|
||||
cs, RVec3::sZero(), hits, {}, f);
|
||||
hits.Sort();
|
||||
float px = 0.0f, pz = 0.0f;
|
||||
for (const CollideShapeResult &h : hits.mHits) {
|
||||
Vec3 a = h.mPenetrationAxis;
|
||||
Vec3 flat(a.GetX(), 0.0f, a.GetZ());
|
||||
if (flat.LengthSq() < 1.0e-8f) continue;
|
||||
flat = flat.Normalized() * h.mPenetrationDepth;
|
||||
px -= flat.GetX();
|
||||
pz -= flat.GetZ();
|
||||
}
|
||||
out[0] = px;
|
||||
out[1] = pz;
|
||||
return int(hits.mHits.size());
|
||||
}
|
||||
42
packages/ludic.physics/native/shim/jph_shapes.inl
Normal file
42
packages/ludic.physics/native/shim/jph_shapes.inl
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// jph_shapes.inl - shapes, each a handle the package keeps and frees; null when Jolt refused it
|
||||
|
||||
JPH_SHIM void *jph_shape_box(float hx, float hy, float hz) {
|
||||
return keep(BoxShapeSettings(Vec3(hx, hy, hz), 0.02f).Create());
|
||||
}
|
||||
JPH_SHIM void *jph_shape_sphere(float r) { return keep(SphereShapeSettings(r).Create()); }
|
||||
JPH_SHIM void *jph_shape_capsule(float half_h, float r) { return keep(CapsuleShapeSettings(half_h, r).Create()); }
|
||||
JPH_SHIM void *jph_shape_cylinder(float half_h, float r) {
|
||||
float cr = r < 0.05f ? r * 0.5f : 0.02f;
|
||||
return keep(CylinderShapeSettings(half_h, r, cr).Create());
|
||||
}
|
||||
|
||||
// a dome r across and h tall standing on y = 0: the cap of a sphere whose centre is below
|
||||
JPH_SHIM void *jph_shape_dome(float r, float h) {
|
||||
float big = (r * r + h * h) / (2.0f * h);
|
||||
RefConst<Shape> s = SphereShapeSettings(big).Create().Get();
|
||||
return keep(RotatedTranslatedShapeSettings(Vec3(0.0f, h - big, 0.0f), Quat::sIdentity(), s).Create());
|
||||
}
|
||||
|
||||
// a shape moved and turned about y, inside its body
|
||||
JPH_SHIM void *jph_shape_offset(void *s, float x, float y, float z, float yaw) {
|
||||
return keep(RotatedTranslatedShapeSettings(Vec3(x, y, z), yaw_q(yaw), static_cast<Shape *>(s)).Create());
|
||||
}
|
||||
|
||||
// n x n heights (row z, column x) at ox, oz, cell metres apart; FLT_MAX is a hole
|
||||
JPH_SHIM void *jph_shape_heightfield(const float *h, int n, float ox, float oz, float cell) {
|
||||
HeightFieldShapeSettings st(h, Vec3(ox, 0.0f, oz), Vec3(cell, 1.0f, cell), uint32(n));
|
||||
return keep(st.Create());
|
||||
}
|
||||
|
||||
// a triangle mesh for a dock or a cabin: nv vertices (x, y, z) and nt triangles (i, j, k)
|
||||
JPH_SHIM void *jph_shape_mesh(const float *v, int nv, const int *tri, int nt) {
|
||||
VertexList vs;
|
||||
vs.reserve(nv);
|
||||
for (int i = 0; i < nv; i++) vs.push_back(Float3(v[i * 3], v[i * 3 + 1], v[i * 3 + 2]));
|
||||
IndexedTriangleList ts;
|
||||
ts.reserve(nt);
|
||||
for (int i = 0; i < nt; i++) ts.push_back(IndexedTriangle(tri[i * 3], tri[i * 3 + 1], tri[i * 3 + 2]));
|
||||
return keep(MeshShapeSettings(vs, ts).Create());
|
||||
}
|
||||
|
||||
JPH_SHIM void jph_shape_free(void *s) { if (s) static_cast<Shape *>(s)->Release(); }
|
||||
126
packages/ludic.physics/native/shim/jph_shim.cpp
Normal file
126
packages/ludic.physics/native/shim/jph_shim.cpp
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
// jph_shim.cpp - ludic.physics' door into Jolt Physics (phase 16). It keeps the shim rules of
|
||||
// packages/README.md: int, long, float and opaque handles cross; no struct by value, no callback
|
||||
// into Ludic - contacts are recorded during the step and drained after it; errors are return
|
||||
// codes. Jolt's job threads stay in here.
|
||||
#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/Collision/Shape/BoxShape.h>
|
||||
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
|
||||
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
|
||||
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
|
||||
#include <Jolt/Physics/Collision/Shape/HeightFieldShape.h>
|
||||
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
||||
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
|
||||
#include <Jolt/Physics/Collision/RayCast.h>
|
||||
#include <Jolt/Physics/Collision/CastResult.h>
|
||||
#include <Jolt/Physics/Collision/ShapeCast.h>
|
||||
#include <Jolt/Physics/Collision/CollideShape.h>
|
||||
#include <Jolt/Physics/Collision/CollisionCollectorImpl.h>
|
||||
#include <Jolt/Physics/Body/BodyCreationSettings.h>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <cfloat>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define JPH_SHIM extern "C" __declspec(dllexport)
|
||||
#else
|
||||
#define JPH_SHIM extern "C" __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
using namespace JPH;
|
||||
|
||||
// ---- layers: the ground, the still things on it, and what moves --------------------------
|
||||
namespace {
|
||||
const ObjectLayer L_GROUND = 0, L_STATIC = 1, L_MOVING = 2, L_COUNT = 3;
|
||||
const BroadPhaseLayer BP_STILL(0), BP_MOVING(1);
|
||||
|
||||
class BPLayers final : public BroadPhaseLayerInterface {
|
||||
public:
|
||||
uint GetNumBroadPhaseLayers() const override { return 2; }
|
||||
BroadPhaseLayer GetBroadPhaseLayer(ObjectLayer l) const override { return l == L_MOVING ? BP_MOVING : BP_STILL; }
|
||||
#if defined(JPH_EXTERNAL_PROFILE) || defined(JPH_PROFILE_ENABLED)
|
||||
const char *GetBroadPhaseLayerName(BroadPhaseLayer l) const override { return l == BP_MOVING ? "moving" : "still"; }
|
||||
#endif
|
||||
};
|
||||
class ObjVsBP final : public ObjectVsBroadPhaseLayerFilter {
|
||||
public:
|
||||
bool ShouldCollide(ObjectLayer l, BroadPhaseLayer bp) const override { return l == L_MOVING || bp == BP_MOVING; }
|
||||
};
|
||||
class ObjPairs final : public ObjectLayerPairFilter {
|
||||
public:
|
||||
bool ShouldCollide(ObjectLayer a, ObjectLayer b) const override { return a == L_MOVING || b == L_MOVING; }
|
||||
};
|
||||
// a query's layers as a bit mask (1 ground, 2 static, 4 moving)
|
||||
class MaskFilter final : public ObjectLayerFilter {
|
||||
public:
|
||||
explicit MaskFilter(int m) : mask(m) {}
|
||||
bool ShouldCollide(ObjectLayer l) const override { return (mask >> l) & 1; }
|
||||
int mask;
|
||||
};
|
||||
|
||||
// ---- contacts: recorded on Jolt's threads, drained on the game's -------------------------
|
||||
const int C_CAP = 512;
|
||||
struct Contact { uint32 a, b; float x, y, z, speed; };
|
||||
|
||||
class Contacts final : public ContactListener {
|
||||
public:
|
||||
void OnContactAdded(const Body &b1, const Body &b2, const ContactManifold &m, ContactSettings &) override {
|
||||
Vec3 n = m.mWorldSpaceNormal;
|
||||
float speed = (b2.GetLinearVelocity() - b1.GetLinearVelocity()).Dot(n);
|
||||
RVec3 p = m.GetWorldSpaceContactPointOn1(0);
|
||||
std::lock_guard<std::mutex> g(lock);
|
||||
if (count >= C_CAP) { lost++; return; }
|
||||
items[count++] = { b1.GetID().GetIndexAndSequenceNumber(), b2.GetID().GetIndexAndSequenceNumber(),
|
||||
float(p.GetX()), float(p.GetY()), float(p.GetZ()), std::abs(speed) };
|
||||
}
|
||||
std::mutex lock;
|
||||
Contact items[C_CAP];
|
||||
int count = 0;
|
||||
int lost = 0;
|
||||
};
|
||||
|
||||
struct World {
|
||||
World(int max_bodies, int threads)
|
||||
: temp(16 * 1024 * 1024), jobs(cMaxPhysicsJobs, cMaxPhysicsBarriers, threads) {
|
||||
sys.Init(max_bodies, 0, max_bodies, max_bodies * 4, bpl, ovb, olp);
|
||||
sys.SetContactListener(&contacts);
|
||||
}
|
||||
TempAllocatorImpl temp;
|
||||
JobSystemThreadPool jobs;
|
||||
BPLayers bpl;
|
||||
ObjVsBP ovb;
|
||||
ObjPairs olp;
|
||||
PhysicsSystem sys;
|
||||
Contacts contacts;
|
||||
};
|
||||
|
||||
bool g_ready = false;
|
||||
BodyID bid(int id) { return BodyID(uint32(id)); }
|
||||
Quat yaw_q(float yaw) { return Quat::sRotation(Vec3::sAxisY(), yaw); }
|
||||
void *keep(const ShapeSettings::ShapeResult &r) {
|
||||
if (r.HasError()) return nullptr;
|
||||
Shape *s = const_cast<Shape *>(r.Get().GetPtr());
|
||||
s->AddRef();
|
||||
return s;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Jolt's allocator, factory and types, once per process; 0 when ready
|
||||
JPH_SHIM int jph_init(void) {
|
||||
if (g_ready) return 0;
|
||||
RegisterDefaultAllocator();
|
||||
Factory::sInstance = new Factory();
|
||||
RegisterTypes();
|
||||
g_ready = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#include "jph_world.inl"
|
||||
#include "jph_shapes.inl"
|
||||
#include "jph_bodies.inl"
|
||||
#include "jph_queries.inl"
|
||||
49
packages/ludic.physics/native/shim/jph_world.inl
Normal file
49
packages/ludic.physics/native/shim/jph_world.inl
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// jph_world.inl - a physics world: made, stepped, emptied of its contacts, freed
|
||||
|
||||
// a world for up to max_bodies; threads 0 means the machine's cores less one, at most four
|
||||
JPH_SHIM void *jph_world_new(int max_bodies, int threads) {
|
||||
if (jph_init() != 0) return nullptr;
|
||||
if (threads <= 0) {
|
||||
int hw = int(std::thread::hardware_concurrency()) - 1;
|
||||
threads = hw < 1 ? 1 : (hw > 4 ? 4 : hw);
|
||||
}
|
||||
World *w = new World(max_bodies, threads);
|
||||
w->sys.SetGravity(Vec3(0.0f, -9.81f, 0.0f));
|
||||
return w;
|
||||
}
|
||||
|
||||
JPH_SHIM void jph_world_free(void *w) { delete static_cast<World *>(w); }
|
||||
|
||||
JPH_SHIM void jph_world_gravity(void *w, float y) { static_cast<World *>(w)->sys.SetGravity(Vec3(0.0f, y, 0.0f)); }
|
||||
|
||||
// one step of dt seconds in `sub` collision steps; 0 when Jolt reports nothing wrong
|
||||
JPH_SHIM int jph_world_step(void *w, float dt, int sub) {
|
||||
World *ww = static_cast<World *>(w);
|
||||
EPhysicsUpdateError e = ww->sys.Update(dt, sub < 1 ? 1 : sub, &ww->temp, &ww->jobs);
|
||||
return e == EPhysicsUpdateError::None ? 0 : -int(e);
|
||||
}
|
||||
|
||||
// after adding many still bodies at once, before the first query
|
||||
JPH_SHIM void jph_world_optimize(void *w) { static_cast<World *>(w)->sys.OptimizeBroadPhase(); }
|
||||
|
||||
JPH_SHIM int jph_world_bodies(void *w) { return int(static_cast<World *>(w)->sys.GetNumBodies()); }
|
||||
|
||||
// up to cap contacts into ids (a, b pairs) and vals (x, y, z, speed); returns how many
|
||||
JPH_SHIM int jph_contacts_drain(void *w, int *ids, float *vals, int cap) {
|
||||
Contacts &c = static_cast<World *>(w)->contacts;
|
||||
std::lock_guard<std::mutex> g(c.lock);
|
||||
int n = c.count < cap ? c.count : cap;
|
||||
for (int i = 0; i < n; i++) {
|
||||
ids[i * 2] = int(c.items[i].a);
|
||||
ids[i * 2 + 1] = int(c.items[i].b);
|
||||
vals[i * 4] = c.items[i].x;
|
||||
vals[i * 4 + 1] = c.items[i].y;
|
||||
vals[i * 4 + 2] = c.items[i].z;
|
||||
vals[i * 4 + 3] = c.items[i].speed;
|
||||
}
|
||||
for (int i = n; i < c.count; i++) c.items[i - n] = c.items[i];
|
||||
c.count -= n;
|
||||
return n;
|
||||
}
|
||||
|
||||
JPH_SHIM int jph_contacts_lost(void *w) { return static_cast<World *>(w)->contacts.lost; }
|
||||
Loading…
Add table
Add a link
Reference in a new issue