#include "physics.h" #include "../core/core.h" #include "../serialization/stream.h" #include namespace unify { using namespace phys; PhysicsWorld::PhysicsWorld() { bodies_.reserve(1024); order_.reserve(1024); events_.reserve(512); constraints_.reserve(512); } void PhysicsWorld::set_mass(Body& b, const BodyDef& def) { b.mass = b.inv_mass = b.inertia = b.inv_inertia = 0; if (b.type != BodyType::Dynamic) return; float m; if (std::isinf(def.mass)) { // "infinite mass": immovable dynamic body b.mass = INFINITY; return; } else if (def.mass > 0 && std::isfinite(def.mass)) { m = def.mass; } else { m = def.density * b.shape.area(); } if (!(m > 0) || !std::isfinite(m)) { // zero/negative/NaN mass: fall back to unit mass, never divide by 0 UNIFY_LOG_WARN("PHYSICS", "body has non-positive mass (%g); using 1.0", double(m)); m = 1.0f; } b.mass = m; b.inv_mass = 1.0f / m; b.inertia = m * b.shape.inertia_per_mass(); bool rotation_locked = b.fixed_rotation || b.shape.type == ShapeType::AABB; b.inv_inertia = (rotation_locked || b.inertia <= 0) ? 0.0f : 1.0f / b.inertia; } PhysicsBodyHandle PhysicsWorld::create_body(const BodyDef& def) { PhysicsBodyHandle h = handles_.allocate(); if (h.index() >= bodies_.size()) bodies_.resize(h.index() + 1); Body& b = bodies_[h.index()]; b = Body{}; b.type = def.type; b.shape = def.shape; b.position = def.position; b.angle = def.shape.type == ShapeType::AABB ? 0.0f : def.angle; b.rot = Rot(b.angle); b.velocity = def.velocity; b.angular_velocity = def.angular_velocity; b.friction = def.friction; b.restitution = def.restitution; b.gravity_scale = def.gravity_scale; b.linear_damping = def.linear_damping; b.angular_damping = def.angular_damping; b.fixed_rotation = def.fixed_rotation || def.shape.type == ShapeType::AABB; b.bullet = def.bullet; b.sensor = def.sensor; b.can_sleep = def.can_sleep; b.category = def.category; b.mask = def.mask; b.user = def.user; b.awake = def.type != BodyType::Static; set_mass(b, def); if (b.fixed_rotation) b.angular_velocity = 0; b.aabb = b.shape.bounds(b.position, b.rot); b.position0 = b.position; b.angle0 = b.angle; // Insert into SAP order at the right place so the order stays sorted and deterministic. auto it = order_.begin(); while (it != order_.end() && bodies_[*it].aabb.min.x <= b.aabb.min.x) ++it; order_.insert(it, h.index()); return h; } bool PhysicsWorld::destroy_body(PhysicsBodyHandle h) { if (!handles_.valid(h)) return false; // End any contacts so gameplay sees matching End events. for (auto it = contacts_.begin(); it != contacts_.end();) { if (it->second.a == h || it->second.b == h) { if (it->second.touching) { const Body& ba = bodies_[it->second.a.index()]; const Body& bb = bodies_[it->second.b.index()]; events_.push_back({it->second.sensor ? ContactEventType::TriggerEnd : ContactEventType::End, it->second.a, it->second.b, ba.user, bb.user, {}, {}, 0}); // Wake the survivor so it does not float where the destroyed body was. PhysicsBodyHandle other = it->second.a == h ? it->second.b : it->second.a; if (bodies_[other.index()].type == BodyType::Dynamic) { bodies_[other.index()].awake = true; bodies_[other.index()].sleep_time = 0; } } it = contacts_.erase(it); } else { ++it; } } for (size_t i = 0; i < order_.size(); i++) if (order_[i] == h.index()) { order_.erase(order_.begin() + long(i)); break; } handles_.release(h); return true; } void PhysicsWorld::wake(PhysicsBodyHandle h) { if (Body* b = get(h)) if (b->type != BodyType::Static) { b->awake = true; b->sleep_time = 0; } } void PhysicsWorld::apply_force(PhysicsBodyHandle h, Vec2 f) { Body* b = get(h); if (!b || b->type != BodyType::Dynamic || !finite(f)) return; b->force += f; wake(h); } void PhysicsWorld::apply_impulse(PhysicsBodyHandle h, Vec2 impulse) { Body* b = get(h); if (!b || b->type != BodyType::Dynamic || !finite(impulse)) return; b->velocity += b->inv_mass * impulse; wake(h); } void PhysicsWorld::set_velocity(PhysicsBodyHandle h, Vec2 v) { Body* b = get(h); if (!b || b->type == BodyType::Static || !finite(v)) return; b->velocity = v; if (length_sq(v) > 0) wake(h); } void PhysicsWorld::set_transform(PhysicsBodyHandle h, Vec2 p, float angle) { Body* b = get(h); if (!b || !finite(p) || !std::isfinite(angle)) return; b->position = p; b->angle = b->fixed_rotation ? b->angle : angle; b->rot = Rot(b->angle); b->aabb = b->shape.bounds(b->position, b->rot); wake(h); } // ------------------------------------------------------------------------------ step void PhysicsWorld::update_aabbs() { for (uint32_t i : order_) { Body& b = bodies_[i]; b.aabb = b.shape.bounds(b.position, b.rot); } // Insertion sort: O(n) for the nearly-sorted order that frame coherence produces, // and stable, so equal keys keep a deterministic order. for (size_t i = 1; i < order_.size(); i++) { uint32_t key = order_[i]; float x = bodies_[key].aabb.min.x; size_t j = i; while (j > 0 && bodies_[order_[j - 1]].aabb.min.x > x) { order_[j] = order_[j - 1]; j--; } order_[j] = key; } } static bool should_collide(const Body& a, const Body& b) { if ((a.category & b.mask) == 0 || (b.category & a.mask) == 0) return false; if (a.type != BodyType::Dynamic && b.type != BodyType::Dynamic) return a.sensor || b.sensor ? (a.type == BodyType::Kinematic || b.type == BodyType::Kinematic) : false; return true; } void PhysicsWorld::broadphase() { for (auto& kv : contacts_) kv.second.seen = false; stats.pairs = 0; for (size_t i = 0; i < order_.size(); i++) { const Body& a = bodies_[order_[i]]; for (size_t j = i + 1; j < order_.size(); j++) { const Body& b = bodies_[order_[j]]; if (b.aabb.min.x > a.aabb.max.x) break; if (a.aabb.max.y < b.aabb.min.y || b.aabb.max.y < a.aabb.min.y) continue; if (!should_collide(a, b)) continue; bool active_a = a.awake && a.type != BodyType::Static; bool active_b = b.awake && b.type != BodyType::Static; PhysicsBodyHandle ha = handles_.at(order_[i]), hb = handles_.at(order_[j]); uint64_t key = pair_key(ha, hb); auto it = contacts_.find(key); if (!active_a && !active_b) { // both resting/static: keep the cached contact as-is if (it != contacts_.end()) it->second.seen = true; continue; } stats.pairs++; if (it == contacts_.end()) { Contact c; // Polygon-vs-circle manifolds are defined polygon-first. bool swap = a.shape.type == ShapeType::Circle && b.shape.type != ShapeType::Circle; c.a = swap ? hb : ha; c.b = swap ? ha : hb; c.sensor = a.sensor || b.sensor; c.friction = std::sqrt(a.friction * b.friction); c.restitution = std::max(a.restitution, b.restitution); it = contacts_.emplace(key, c).first; } it->second.seen = true; narrowphase(it->second); } } } void PhysicsWorld::narrowphase(Contact& c) { Manifold old = c.manifold; collide(bodies_[c.a.index()], bodies_[c.b.index()], c.manifold); // Warm start: carry impulses across steps for points with the same feature id. for (int i = 0; i < c.manifold.count; i++) { ManifoldPoint& p = c.manifold.points[i]; p.normal_impulse = p.tangent_impulse = 0; for (int j = 0; j < old.count; j++) if (old.points[j].id == p.id) { p.normal_impulse = old.points[j].normal_impulse; p.tangent_impulse = old.points[j].tangent_impulse; break; } } } void PhysicsWorld::solve(float dt) { constraints_.clear(); for (auto& kv : contacts_) { Contact& c = kv.second; if (c.sensor || c.manifold.count == 0) continue; Body& a = bodies_[c.a.index()]; Body& b = bodies_[c.b.index()]; if (!(a.awake && a.type == BodyType::Dynamic) && !(b.awake && b.type == BodyType::Dynamic)) continue; WorldManifold wm = world_manifold(c.manifold, a, b); VelocityConstraint vc{}; vc.c = &c; vc.normal = wm.normal; vc.count = c.manifold.count; Vec2 tangent = cross(wm.normal, 1.0f); for (int i = 0; i < vc.count; i++) { VelocityPoint& p = vc.p[i]; p.ra = wm.points[i] - a.position; p.rb = wm.points[i] - b.position; float rna = cross(p.ra, wm.normal), rnb = cross(p.rb, wm.normal); float kn = a.inv_mass + b.inv_mass + a.inv_inertia * rna * rna + b.inv_inertia * rnb * rnb; p.normal_mass = kn > 0 ? 1.0f / kn : 0; float rta = cross(p.ra, tangent), rtb = cross(p.rb, tangent); float kt = a.inv_mass + b.inv_mass + a.inv_inertia * rta * rta + b.inv_inertia * rtb * rtb; p.tangent_mass = kt > 0 ? 1.0f / kt : 0; Vec2 dv = b.velocity + cross(b.angular_velocity, p.rb) - a.velocity - cross(a.angular_velocity, p.ra); float vrel = dot(wm.normal, dv); p.velocity_bias = vrel < -kVelocityThreshold ? -c.restitution * vrel : 0; } constraints_.push_back(vc); } // Warm start. for (auto& vc : constraints_) { Body& a = bodies_[vc.c->a.index()]; Body& b = bodies_[vc.c->b.index()]; Vec2 tangent = cross(vc.normal, 1.0f); for (int i = 0; i < vc.count; i++) { const ManifoldPoint& mp = vc.c->manifold.points[i]; Vec2 P = mp.normal_impulse * vc.normal + mp.tangent_impulse * tangent; a.velocity -= a.inv_mass * P; a.angular_velocity -= a.inv_inertia * cross(vc.p[i].ra, P); b.velocity += b.inv_mass * P; b.angular_velocity += b.inv_inertia * cross(vc.p[i].rb, P); } } for (int it = 0; it < velocity_iterations; it++) { for (auto& vc : constraints_) { Body& a = bodies_[vc.c->a.index()]; Body& b = bodies_[vc.c->b.index()]; Vec2 tangent = cross(vc.normal, 1.0f); float friction = vc.c->friction; // Friction first so the normal (non-penetration) solve has the last word. for (int i = 0; i < vc.count; i++) { VelocityPoint& p = vc.p[i]; ManifoldPoint& mp = vc.c->manifold.points[i]; Vec2 dv = b.velocity + cross(b.angular_velocity, p.rb) - a.velocity - cross(a.angular_velocity, p.ra); float lambda = p.tangent_mass * -dot(dv, tangent); float max_f = friction * mp.normal_impulse; float next = clampf(mp.tangent_impulse + lambda, -max_f, max_f); lambda = next - mp.tangent_impulse; mp.tangent_impulse = next; Vec2 P = lambda * tangent; a.velocity -= a.inv_mass * P; a.angular_velocity -= a.inv_inertia * cross(p.ra, P); b.velocity += b.inv_mass * P; b.angular_velocity += b.inv_inertia * cross(p.rb, P); } for (int i = 0; i < vc.count; i++) { VelocityPoint& p = vc.p[i]; ManifoldPoint& mp = vc.c->manifold.points[i]; Vec2 dv = b.velocity + cross(b.angular_velocity, p.rb) - a.velocity - cross(a.angular_velocity, p.ra); float vn = dot(dv, vc.normal); float lambda = -p.normal_mass * (vn - p.velocity_bias); float next = std::max(mp.normal_impulse + lambda, 0.0f); lambda = next - mp.normal_impulse; mp.normal_impulse = next; Vec2 P = lambda * vc.normal; a.velocity -= a.inv_mass * P; a.angular_velocity -= a.inv_inertia * cross(p.ra, P); b.velocity += b.inv_mass * P; b.angular_velocity += b.inv_inertia * cross(p.rb, P); } } } // Integrate positions (dynamic and kinematic), clamping absurd translations. for (uint32_t i : order_) { Body& b = bodies_[i]; if (b.type == BodyType::Static || !b.awake) continue; Vec2 move = dt * b.velocity; float ls = length_sq(move); if (ls > kMaxTranslation * kMaxTranslation) b.velocity *= kMaxTranslation / std::sqrt(ls); b.position += dt * b.velocity; if (!b.fixed_rotation) { b.angle += dt * b.angular_velocity; b.rot = Rot(b.angle); } } // Position solver: push resolving penetration directly (no energy added to velocities). for (int it = 0; it < position_iterations; it++) { float min_sep = 0; for (auto& vc : constraints_) { Contact& c = *vc.c; Body& a = bodies_[c.a.index()]; Body& b = bodies_[c.b.index()]; const Manifold& m = c.manifold; float ma = a.inv_mass, mb = b.inv_mass, ia = a.inv_inertia, ib = b.inv_inertia; for (int j = 0; j < m.count; j++) { Vec2 normal, point; float sep; float ra = a.shape.radius, rb = b.shape.radius; if (m.type == Manifold::Circles) { Vec2 pa = a.position + a.rot.apply(m.local_point), pb = b.position + b.rot.apply(m.points[0].local_point); Vec2 d = pb - pa; normal = length_sq(d) > FLT_EPSILON * FLT_EPSILON ? normalize(d) : Vec2{0, 1}; point = 0.5f * (pa + pb); sep = dot(d, normal) - ra - rb; } else if (m.type == Manifold::FaceA) { normal = a.rot.apply(m.local_normal); Vec2 plane = a.position + a.rot.apply(m.local_point); Vec2 clip = b.position + b.rot.apply(m.points[j].local_point); sep = dot(clip - plane, normal) - ra - rb; point = clip; } else { normal = b.rot.apply(m.local_normal); Vec2 plane = b.position + b.rot.apply(m.local_point); Vec2 clip = a.position + a.rot.apply(m.points[j].local_point); sep = dot(clip - plane, normal) - ra - rb; point = clip; normal = -normal; } Vec2 rA = point - a.position, rB = point - b.position; min_sep = std::min(min_sep, sep); float C = clampf(kBaumgarte * (sep + kLinearSlop), -kMaxLinearCorrection, 0.0f); float rna = cross(rA, normal), rnb = cross(rB, normal); float K = ma + mb + ia * rna * rna + ib * rnb * rnb; float impulse = K > 0 ? -C / K : 0; Vec2 P = impulse * normal; a.position -= ma * P; b.position += mb * P; if (ia > 0) { a.angle -= ia * cross(rA, P); a.rot = Rot(a.angle); } if (ib > 0) { b.angle += ib * cross(rB, P); b.rot = Rot(b.angle); } } } if (min_sep >= -3.0f * kLinearSlop) break; } } // Continuous collision: conservative advancement along each fast body's sweep against // static/kinematic geometry (and everything, for bullets). Stops the body at first touch. void PhysicsWorld::continuous(float) { for (uint32_t i : order_) { Body& b = bodies_[i]; if (b.type != BodyType::Dynamic || !b.awake || b.sensor) continue; Vec2 motion = b.position - b.position0; float travel = length(motion) + std::fabs(b.angle - b.angle0) * b.shape.max_extent(); if (!b.bullet && travel < 0.5f * b.shape.min_extent()) continue; Rot r0(b.angle0); // Swept bounds from the pre-step pose to the integrated pose (b.aabb is still pre-step here). AABB swept = b.shape.bounds(b.position0, r0).merged(b.shape.bounds(b.position, b.rot)); float toi = 1.0f; for (uint32_t k : order_) { if (k == i) continue; const Body& o = bodies_[k]; if (o.sensor || !should_collide(b, o)) continue; if (o.type == BodyType::Dynamic && !b.bullet) continue; AABB oswept = o.shape.bounds(o.position0, Rot(o.angle0)).merged(o.shape.bounds(o.position, o.rot)); if (!swept.overlaps(oswept)) continue; // Measured on core shapes: stop where the skins overlap by ~3 slop, so the next // step's narrowphase is guaranteed a manifold and the solver removes the approach. const float target = std::max(kLinearSlop, b.shape.radius + o.shape.radius - 3.0f * kLinearSlop); const float tolerance = 0.25f * kLinearSlop; float d0 = shape_core_distance(b.shape, b.position0, r0, o.shape, o.position0, Rot(o.angle0)); if (d0 <= target + tolerance) continue; // already touching at the start: discrete contacts handle it float bound = length(motion - (o.position - o.position0)) + std::fabs(b.angle - b.angle0) * b.shape.max_extent() + std::fabs(o.angle - o.angle0) * o.shape.max_extent(); if (bound < 1e-9f) continue; float t = 0; for (int iter = 0; iter < 32; iter++) { Vec2 pb = lerp(b.position0, b.position, t), po = lerp(o.position0, o.position, t); float d = shape_core_distance(b.shape, pb, Rot(lerp(b.angle0, b.angle, t)), o.shape, po, Rot(lerp(o.angle0, o.angle, t))); if (d <= target + tolerance) break; t += (d - target) / bound; if (t >= toi) break; } if (t < toi) toi = t; } if (toi < 1.0f) { b.position = lerp(b.position0, b.position, toi); b.angle = lerp(b.angle0, b.angle, toi); b.rot = Rot(b.angle); b.aabb = b.shape.bounds(b.position, b.rot); stats.ccd_hits++; } } } void PhysicsWorld::update_sleep(float dt) { const float lin2 = kLinearSleepTolerance * kLinearSleepTolerance; const float ang2 = kAngularSleepTolerance * kAngularSleepTolerance; island_parent_.resize(bodies_.size()); for (uint32_t i = 0; i < bodies_.size(); i++) island_parent_[i] = i; auto find = [&](uint32_t x) { while (island_parent_[x] != x) { island_parent_[x] = island_parent_[island_parent_[x]]; x = island_parent_[x]; } return x; }; for (uint32_t i : order_) { Body& b = bodies_[i]; if (b.type != BodyType::Dynamic || !b.awake) continue; if (!b.can_sleep || length_sq(b.velocity) > lin2 || b.angular_velocity * b.angular_velocity > ang2) b.sleep_time = 0; else b.sleep_time += dt; } for (auto& kv : contacts_) { const Contact& c = kv.second; if (!c.touching || c.sensor) continue; Body& a = bodies_[c.a.index()]; Body& b = bodies_[c.b.index()]; // A moving kinematic body keeps whatever it touches awake. if (a.type == BodyType::Kinematic && length_sq(a.velocity) > 0) b.sleep_time = 0; if (b.type == BodyType::Kinematic && length_sq(b.velocity) > 0) a.sleep_time = 0; if (a.type == BodyType::Dynamic && b.type == BodyType::Dynamic) { uint32_t ra = find(c.a.index()), rb = find(c.b.index()); if (ra != rb) island_parent_[std::max(ra, rb)] = std::min(ra, rb); } } std::vector& min_sleep = island_min_; min_sleep.assign(bodies_.size(), FLT_MAX); for (uint32_t i : order_) { const Body& b = bodies_[i]; if (b.type != BodyType::Dynamic) continue; float t = b.awake ? b.sleep_time : kTimeToSleep; // sleeping members count as ready uint32_t r = find(i); min_sleep[r] = std::min(min_sleep[r], t); } for (uint32_t i : order_) { Body& b = bodies_[i]; if (b.type != BodyType::Dynamic) continue; if (min_sleep[find(i)] >= kTimeToSleep) { if (b.awake) { b.awake = false; b.velocity = {}; b.angular_velocity = 0; } } else if (!b.awake) { b.awake = true; b.sleep_time = 0; } } } void PhysicsWorld::guard_failures() { for (uint32_t i : order_) { Body& b = bodies_[i]; if (finite(b.position) && std::isfinite(b.angle) && finite(b.velocity) && std::isfinite(b.angular_velocity)) continue; // Numeric failure (e.g. degenerate input). Roll the body back rather than letting NaN // propagate into transforms, contacts and the ECS. UNIFY_LOG_ERROR("PHYSICS", "non-finite state on body %u; rolled back", i); b.position = b.position0; b.angle = b.angle0; b.rot = Rot(b.angle); b.velocity = {}; b.angular_velocity = 0; b.force = {}; b.torque = 0; stats.failures++; for (auto& kv : contacts_) if (kv.second.a.index() == i || kv.second.b.index() == i) for (int k = 0; k < kv.second.manifold.count; k++) kv.second.manifold.points[k].normal_impulse = kv.second.manifold.points[k].tangent_impulse = 0; } } void PhysicsWorld::step(float dt) { events_.clear(); if (!(dt > 0)) return; stats.ccd_hits = 0; for (uint32_t i : order_) { Body& b = bodies_[i]; b.position0 = b.position; b.angle0 = b.angle; b.velocity0 = b.velocity; b.angular_velocity0 = b.angular_velocity; if (b.type != BodyType::Dynamic || !b.awake) continue; b.velocity += dt * (b.gravity_scale * gravity + b.inv_mass * b.force); b.angular_velocity += dt * b.inv_inertia * b.torque; b.velocity *= 1.0f / (1.0f + dt * b.linear_damping); b.angular_velocity *= 1.0f / (1.0f + dt * b.angular_damping); } update_aabbs(); broadphase(); // Touching state before the solve, so impulses from this step accompany the events. std::vector& was = was_touching_; was.clear(); for (auto& kv : contacts_) was.push_back(kv.second.touching ? 1 : 0); { for (auto& kv : contacts_) { Contact& c = kv.second; if (c.seen) { const Body& a = bodies_[c.a.index()]; const Body& b = bodies_[c.b.index()]; bool active = (a.awake && a.type != BodyType::Static) || (b.awake && b.type != BodyType::Static); if (active) c.touching = c.manifold.count > 0; } else { c.touching = false; } } } solve(dt); continuous(dt); guard_failures(); update_sleep(dt); for (uint32_t i : order_) { bodies_[i].force = {}; bodies_[i].torque = 0; } // Events in deterministic (pair-key) order; drop contacts whose AABBs no longer overlap. size_t k = 0; stats.contacts = stats.touching = 0; for (auto it = contacts_.begin(); it != contacts_.end(); k++) { Contact& c = it->second; bool before = was[k] != 0; const Body& a = bodies_[c.a.index()]; const Body& b = bodies_[c.b.index()]; ContactEvent ev{}; ev.a = c.a; ev.b = c.b; ev.user_a = a.user; ev.user_b = b.user; if (c.touching) { WorldManifold wm = world_manifold(c.manifold, a, b); ev.normal = wm.normal; ev.point = wm.points[0]; for (int i = 0; i < c.manifold.count; i++) ev.impulse += c.manifold.points[i].normal_impulse; } bool asleep = !(a.awake && a.type != BodyType::Static) && !(b.awake && b.type != BodyType::Static); if (c.touching && !before) { ev.type = c.sensor ? ContactEventType::TriggerBegin : ContactEventType::Begin; events_.push_back(ev); } else if (!c.touching && before) { ev.type = c.sensor ? ContactEventType::TriggerEnd : ContactEventType::End; events_.push_back(ev); } else if (c.touching && !c.sensor && !asleep) { ev.type = ContactEventType::Persist; events_.push_back(ev); } if (!c.seen) { it = contacts_.erase(it); continue; } stats.contacts++; if (c.touching) stats.touching++; ++it; } stats.bodies = handles_.alive(); stats.awake = 0; for (uint32_t i : order_) if (bodies_[i].awake && bodies_[i].type == BodyType::Dynamic) stats.awake++; } // ------------------------------------------------------------------------------ queries static bool ray_shape(const Body& b, Vec2 p1, Vec2 p2, float& fraction, Vec2& normal) { Vec2 d = p2 - p1; if (b.shape.type == ShapeType::Circle) { Vec2 s = p1 - b.position; float r = b.shape.radius; float bb = dot(s, d), cc = dot(s, s) - r * r, rr = dot(d, d); float sigma = bb * bb - rr * cc; if (sigma < 0 || rr < FLT_EPSILON) return false; float a = -(bb + std::sqrt(sigma)); if (a < 0 || a > rr) return false; fraction = a / rr; normal = normalize(s + fraction * d); return true; } Vec2 lp1 = b.rot.apply_inv(p1 - b.position), lp2 = b.rot.apply_inv(p2 - b.position), ld = lp2 - lp1; float lower = 0, upper = 1; int index = -1; for (int i = 0; i < b.shape.count; i++) { // Expand faces by the polygon skin so rays hit where contacts happen. float num = dot(b.shape.normals[i], b.shape.vertices[i] - lp1) + b.shape.radius; float den = dot(b.shape.normals[i], ld); if (den == 0) { if (num < 0) return false; continue; } if (den < 0 && num < lower * den) { lower = num / den; index = i; } else if (den > 0 && num < upper * den) { upper = num / den; } if (upper < lower) return false; } if (index < 0) return false; fraction = lower; normal = b.rot.apply(b.shape.normals[index]); return true; } bool PhysicsWorld::raycast(Vec2 from, Vec2 to, uint32_t mask, RayHit& out, PhysicsBodyHandle ignore) const { bool hit = false; out.fraction = 1; for (uint32_t i : order_) { const Body& b = bodies_[i]; PhysicsBodyHandle h = handles_.at(i); if (h == ignore || b.sensor || (b.category & mask) == 0) continue; float f; Vec2 n; if (ray_shape(b, from, to, f, n) && f < out.fraction) { out = {h, from + f * (to - from), n, f}; hit = true; } } return hit; } void PhysicsWorld::query_aabb(const AABB& box, std::vector& out) const { out.clear(); for (uint32_t i : order_) if (bodies_[i].aabb.overlaps(box)) out.push_back(handles_.at(i)); std::sort(out.begin(), out.end()); } // ------------------------------------------------------------------------------ save state static void write_shape(BinaryWriter& w, const Shape& s) { w.u8(uint8_t(s.type)); w.f32(s.radius); w.u8(uint8_t(s.count)); for (int i = 0; i < s.count; i++) { w.vec2(s.vertices[i]); w.vec2(s.normals[i]); } } static Shape read_shape(BinaryReader& r) { Shape s; s.type = ShapeType(r.u8()); s.radius = r.f32(); s.count = r.u8(); if (s.count > kMaxPolygonVertices) throw SerializationError("polygon vertex count out of range"); for (int i = 0; i < s.count; i++) { s.vertices[i] = r.vec2(); s.normals[i] = r.vec2(); } return s; } void PhysicsWorld::serialize(BinaryWriter& w) const { auto hs = handles_.save(); w.u32(uint32_t(hs.gens.size())); for (size_t i = 0; i < hs.gens.size(); i++) { w.u8(hs.gens[i]); w.u8(hs.live[i]); } w.u32(uint32_t(hs.free.size())); for (uint32_t f : hs.free) w.u32(f); w.vec2(gravity); w.u32(uint32_t(order_.size())); for (uint32_t i : order_) { const Body& b = bodies_[i]; w.u32(i); w.u8(uint8_t(b.type)); write_shape(w, b.shape); w.vec2(b.position); w.f32(b.angle); w.vec2(b.velocity); w.f32(b.angular_velocity); w.vec2(b.force); w.f32(b.torque); w.f32(b.mass); w.f32(b.inv_mass); w.f32(b.inertia); w.f32(b.inv_inertia); w.f32(b.friction); w.f32(b.restitution); w.f32(b.gravity_scale); w.f32(b.linear_damping); w.f32(b.angular_damping); w.u8(uint8_t(b.fixed_rotation | b.bullet << 1 | b.sensor << 2 | b.can_sleep << 3 | b.awake << 4)); w.f32(b.sleep_time); w.u32(b.category); w.u32(b.mask); w.u32(b.user); } // Contacts carry warm-starting impulses; without them a restored world diverges. w.u32(uint32_t(contacts_.size())); for (auto& kv : contacts_) { const Contact& c = kv.second; w.u64(kv.first); w.u32(c.a.bits); w.u32(c.b.bits); w.u8(uint8_t(c.touching | c.sensor << 1)); w.f32(c.friction); w.f32(c.restitution); const Manifold& m = c.manifold; w.u8(uint8_t(m.type)); w.vec2(m.local_normal); w.vec2(m.local_point); w.u8(uint8_t(m.count)); for (int i = 0; i < m.count; i++) { w.vec2(m.points[i].local_point); w.f32(m.points[i].normal_impulse); w.f32(m.points[i].tangent_impulse); w.u32(m.points[i].id); } } } void PhysicsWorld::deserialize(BinaryReader& r) { clear(); HandlePool::State hs; uint32_t n = r.u32(); hs.gens.resize(n); hs.live.resize(n); for (uint32_t i = 0; i < n; i++) { hs.gens[i] = r.u8(); hs.live[i] = r.u8(); if (hs.live[i]) hs.alive++; } uint32_t nf = r.u32(); hs.free.resize(nf); for (auto& f : hs.free) f = r.u32(); handles_.load(hs); bodies_.assign(n, Body{}); gravity = r.vec2(); uint32_t count = r.u32(); for (uint32_t k = 0; k < count; k++) { uint32_t i = r.u32(); if (i >= n) throw SerializationError("body index out of range"); Body& b = bodies_[i]; b.type = BodyType(r.u8()); b.shape = read_shape(r); b.position = r.vec2(); b.angle = r.f32(); b.velocity = r.vec2(); b.angular_velocity = r.f32(); b.force = r.vec2(); b.torque = r.f32(); b.mass = r.f32(); b.inv_mass = r.f32(); b.inertia = r.f32(); b.inv_inertia = r.f32(); b.friction = r.f32(); b.restitution = r.f32(); b.gravity_scale = r.f32(); b.linear_damping = r.f32(); b.angular_damping = r.f32(); uint8_t f = r.u8(); b.fixed_rotation = f & 1; b.bullet = f & 2; b.sensor = f & 4; b.can_sleep = f & 8; b.awake = f & 16; b.sleep_time = r.f32(); b.category = r.u32(); b.mask = r.u32(); b.user = r.u32(); b.rot = Rot(b.angle); b.aabb = b.shape.bounds(b.position, b.rot); b.position0 = b.position; b.angle0 = b.angle; order_.push_back(i); } uint32_t nc = r.u32(); for (uint32_t k = 0; k < nc; k++) { uint64_t key = r.u64(); Contact c; c.a.bits = r.u32(); c.b.bits = r.u32(); uint8_t f = r.u8(); c.touching = f & 1; c.sensor = f & 2; c.friction = r.f32(); c.restitution = r.f32(); Manifold& m = c.manifold; m.type = Manifold::Type(r.u8()); m.local_normal = r.vec2(); m.local_point = r.vec2(); m.count = r.u8(); if (m.count > 2) throw SerializationError("manifold point count out of range"); for (int i = 0; i < m.count; i++) { m.points[i].local_point = r.vec2(); m.points[i].normal_impulse = r.f32(); m.points[i].tangent_impulse = r.f32(); m.points[i].id = r.u32(); } if (!handles_.valid(c.a) || !handles_.valid(c.b)) throw SerializationError("contact references a dead body"); contacts_.emplace(key, c); } } void PhysicsWorld::clear() { handles_.clear(); bodies_.clear(); order_.clear(); contacts_.clear(); events_.clear(); constraints_.clear(); stats = {}; } } // namespace unify