flock-collision

CPU 2D/3D collision detection and rigid-body physics for the Flock engine

flock-collision

CPU collision detection and rigid-body resolution for the Flock ECS engine — 2D and 3D, with box / circle-sphere / capsule shapes and full rotational dynamics. GPU acceleration is intentionally out of scope here and will live in a separate library.

Prerequisites

Flock and its native stack — see the Flock prerequisites (brew install sdl3 sdl3_image sdl3_ttf, wgpu-native via ../wgpu-cr). Crystal >= 1.16. The physics core itself is headless (no GPU); only running the demos needs the window/render stack.

Install / build

flock-collision is a sibling shard: its entry point pulls Flock in by relative path (../flock), so no shards install is needed.

crystal spec                     # headless physics tests
crystal run examples/demo2d.cr   # 2D: stacking / bouncing bodies
crystal run examples/demo3d.cr   # 3D: falling / resting crates

As a dependency of your own project, add it to shard.yml and require "flock-collision".

Quick start (2D)

require "flock-collision"

app = Flock::App.new
app.add_plugin(Flock::Collision::Physics2DPlugin.new(gravity: Flock::Vec2.new(0, -20)))

app.add_startup do |_world, cmd|
  cmd.spawn(                                       # static floor
    Flock::Transform2D.at(0, 0),
    Flock::Collision::Collider2D.box(10, 0.5),
    Flock::Collision::RigidBody2D.static(friction: 0.6))
  cmd.spawn(                                       # a bouncing box
    Flock::Transform2D.at(0, 5),
    Flock::Collision::Collider2D.box(0.5, 0.5),
    Flock::Collision::RigidBody2D.dynamic(mass: 1.0, inertia: 0.17, restitution: 0.4))
end

app.add_system(Flock::Schedule::Update) do |world, _cmd|
  world.each_event(Flock::Collision::Contact2D) do |c|
    # c.a, c.b, c.normal, c.penetration, c.point
  end
end

3D is symmetric: Physics3DPlugin, Collider3D (.sphere/.box/.capsule), RigidBody3D (.sphere/.box/.dynamic/.static), Contact3D. In 3D the rotation authority is RigidBody3D.orientation (a quaternion); the step writes each dynamic entity's Transform3D.position + matrix_override.

Colliders carry layer/mask bitfields for filtering and a sensor flag (reports contacts but is not resolved).

Ray casting

hit = Flock::Collision.raycast2d(world, origin, direction, max_distance, mask)
hit = Flock::Collision.raycast3d(world, origin, direction, max_distance, mask)
# => RayHit* { entity, point, normal, distance } or nil

Pipeline (per fixed step)

  1. Integrate forces (gravity) into velocities
  2. Broadphase — uniform spatial hash
  3. Narrowphase — exact tests → contact manifolds (+ Contact* events)
  4. Islands + sleeping — group touching bodies; wake islands touched by motion
  5. Resolve velocities — warm-started sequential impulse: restitution + Coulomb friction
  6. Positional correction — Baumgarte / slop
  7. Integrate velocities into positions + orientation, then update sleep timers

Feature coverage

2D 3D
shapes circle, box, capsule sphere, box, capsule
rotation full full (quaternion)
narrowphase all pairs, rotated all pairs, rotated
box manifold 2 points (clipping) up to 4 points (face clipping)
friction yes yes
warm starting yes yes
sleeping / islands yes yes
joints distance, spring, pin distance, spring, pin
continuous collision yes (vs static) yes (vs static)
broadphase spatial hash / SAP spatial hash / SAP
ray cast circle, box, capsule sphere, box, capsule

Joints, CCD, broadphase

phys = app.world.resource(Flock::Collision::Physics2D)
phys.add_joint(Flock::Collision::Joint2D.pin(anchor, bob))          # distance / spring / pin
# fast bodies opt into continuous collision:
Flock::Collision::RigidBody2D.dynamic(mass: 1.0, ccd: true)
# choose the broadphase (default :hash):
Flock::Collision::Physics2DPlugin.new                               # ... then
phys.broadphase = Flock::Collision::SweepPrune2D.new                # or pass broadphase: :sweep

Known limits / TODO

  • 3D box-box uses face clipping (multi-point); sphere/capsule contacts and edge-edge box contacts are single-point.
  • Deep capsule-box penetration is resolved exactly with GJK+EPA (also available directly as Flock::Collision::GJK3D.penetration for any convex support pair).
  • Sleeping wakes down a stack one layer per step; disable with allow_sleeping: false. Jointed bodies do not sleep.
  • CCD (swept) handles fast bodies vs static and moving bodies; 3D revolute motors and limits are not provided (2D revolute has both).

Layout

src/flock/collision/
  # shared
  shape2d/3d  collider2d/3d  rigid_body2d/3d  contact2d/3d
  aabb2d/3d   broadphase2d/3d  obb2d/3d
  narrowphase2d/3d  solver2d/3d  physics2d/3d  physics2d/3d_plugin
  raycast2d/3d

License

MIT — see LICENSE.

Repository

flock-collision

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 1
  • about 7 hours ago
  • August 19, 2026
License

MIT License

Links
Synced at

Wed, 19 Aug 2026 12:18:25 GMT

Languages