A physics and collision detection library for the C3 language.
Collision Detection
- GJK (Gilbert-Johnson-Keerthi) narrow-phase collision
- EPA (Expanding Polytope Algorithm) for contact points and penetration depth
- Spatial hashing for broad-phase optimization
- BVH (Bounding Volume Hierarchy) for triangle mesh collision
Physics Simulation
- Rigid body dynamics with mass and inertia
- XPBD position-based solver with friction and restitution
- Multi-contact manifolds for stable stacking and mesh contacts
- Swept CCD — fast bodies don't tunnel through static geometry
- Gravity, linear/angular damping, and sleep states
- Generic 6-DOF joint constraints with limits
- XPBD soft bodies with distance, volume and bending constraints
Collision Shapes
Sphere- Sphere with radiusCapsule- Capsule with height and radiusCylinder- Cylinder with height and top/bottom radiusAabb3- Axis-aligned bounding boxMesh- Triangle mesh with BVH accelerationCompoundShape- Multiple convex pieces combinedHeightmap- Terrain collision with ray casting
Advanced
- CoACD convex decomposition for concave meshes
- Parallel constraint solving with thread pool
- Collision filtering between body pairs
- Deterministic lockstep support: bit-identical simulation across machines, state hashing, and snapshot/restore for rollback netcode.
import collision;
fn void main() {
// Create physics world with default settings
PhysicsWorld world = collision::create_world();
defer world.free();
// Create a static ground plane
world.add_body(body: {
.id = 0,
.is_static = true,
.material = { .dynamic_friction = 0.5, .restitution = 0.3 },
.collider = {
.shape = &&collision::aabb_from_half({10, 10, 0.5}),
.translation = {0, 0, -0.5},
}
})!!;
// Create a dynamic sphere
world.add_body(body: {
.id = 1,
.mass = 1.0,
.material = { .dynamic_friction = 0.5, .restitution = 0.8 },
.collider = {
.shape = &&(Sphere){ .radius = 0.5 },
.translation = {0, 0, 5},
}
})!!;
// Game loop
while (running) {
world.run_step(1.0 / 60.0); // Fixed 60Hz timestep
// Get body position after simulation
if (try Rigidbody* ball = world.find_body(1)) {
Vec3f pos = ball.collider.translation;
}
}
}Rigidbody* body = world.find_body(1);
// Apply impulse at center of mass
body.apply_linear_impulse({0, 0, 10});
// Apply angular impulse (torque)
body.apply_angular_impulse({0, 1, 0});
// Apply impulse at specific point (creates both linear and angular motion)
body.apply_impulse({5, 0, 0}, point: {0, 0.5, 0});// Sphere
Sphere sphere = { .radius = 1.0 };
// Capsule (pill shape), centered at origin, axis +y. height is the
// cylindrical section only (KHR_implicit_shapes convention), so this one
// spans y in [-1.5, 1.5].
Capsule capsule = { .height = 2.0, .radius_top = 0.5, .radius_bottom = 0.5 };
// Box from its full size, centered at origin
Aabb3 box = collision::aabb_from_half({2, 2, 2}); // 2x2x2 box
// Cylinder, centered at origin, axis +y (caps at y = ±height/2)
Cylinder cylinder = { .height = 2.0, .radius_top = 0.5, .radius_bottom = 0.5 };
// Triangle mesh with BVH
Mesh mesh = collision::create_mesh(vertices, indices);
mesh.build_bvh();
defer mesh.free();SpatialHash3D spatial_map = { .cell_size = 2.0 };
defer spatial_map.free();
// Insert AABBs
spatial_map.insert(aabb1, id: 0)!!;
spatial_map.insert(aabb2, id: 1)!!;
// Get potentially colliding pairs
spatial_map.@get_pairs(; Pair pair) {
// pair.a and pair.b are IDs of nearby objects
// Perform narrow-phase collision here
};// Check collision between two shapes
TransformedShape shape_a = { .shape = &sphere, .translation = pos_a };
TransformedShape shape_b = { .shape = &box, .translation = pos_b };
CollisionInfo info = collision::check_collision(&shape_a, &shape_b);
if (info.collided) {
Vec3f normal = info.normal;
float depth = info.depth;
}For concave meshes, decompose into convex pieces:
Mesh concave_mesh = collision::create_mesh(vertices, indices);
MeshList pieces = concave_mesh.decompose(collision::COACD_FAST);
defer pieces.release();
// Create compound shape from pieces
CompoundShape compound;
foreach (piece : pieces) {
compound.add_piece(&piece);
}create_world takes the settings and seats the world's storage in one call.
Start from DEFAULT_PHYSICS_WORLD and change what you need, so the fields you
do not name keep their defaults:
PhysicsWorld config = collision::DEFAULT_PHYSICS_WORLD;
config.gravity = {0, 0, -9.8}; // Gravity vector (library convention is z-up)
config.spatial_map.cell_size = 2.0; // Broad-phase cell size
config.sleep_timer = 5.0; // Seconds before bodies sleep
config.linear_dampening = 0.9; // Velocity damping per step
config.angular_dampening = 0.9;
config.sleep_delta = 0.1; // Velocity threshold for sleep
config.ccd_enabled = true; // Swept CCD for fast-moving bodies
config.thread_count = 0; // Solver worker threads; 0 = auto (CPU cores - 1)
PhysicsWorld world = collision::create_world(config);
defer world.free();It takes an allocator too — create_world(config, my_allocator) — which owns
every container in the world until free. Assigning DEFAULT_PHYSICS_WORLD
on its own gives you the settings and not the storage, and a world whose
containers were never seated binds each of them to the temp allocator on first
use: invisible until a pool is released a frame later. PhysicsWorld.init is
the same thing for a world you already have.
Triangle meshes are treated as one-sided: contacts always push bodies out along the triangle face normal, so meshes need consistent (outward) winding.
The simulation is bit-deterministic: two machines that create the same bodies in the same order and step with the same inputs produce bit-identical worlds, so multiplayer games can send only player inputs over the wire. The full contract.
- Build with
-O3or below (default--fp-math=strict);-O4/-O5enable fast math and break bit-equality - Create bodies in the same order on every peer, step with a fixed,
bit-identical
dt(accumulator pattern), and apply inputs at agreed simulation ticks only
const float TICK_DT = 1.0f / 60.0f;
float accumulator;
// Game loop: fixed simulation ticks, never the variable frame time
accumulator += frame_time;
while (accumulator >= TICK_DT) {
apply_player_inputs(&world, current_tick); // same inputs on every peer
world.run_step(TICK_DT, step_count: 4);
accumulator -= TICK_DT;
current_tick++;
}
// Desync detection: exchange hashes with peers (cheap, every frame if you like)
ulong hash = world.state_hash();
// Rollback netcode: snapshot, simulate ahead, restore and replay on a
// mispredicted input — the replay is bit-exact
WorldSnapshot snap = world.snapshot();
defer snap.free();
// ... simulate ahead, receive authoritative inputs ...
world.restore(&snap)!!;c3c test # every test, leak-tracked
c3c test --test-noleak # faster, and blind to ownership bugsTests live in test/, one module per area (collision_tests::world,
::softbody, ::voxel, ::hull, …) and are listed under test-sources in
project.json, so c3c test is the only thing that builds them.