FUNDAMENTALS
Setting Up the Physics World
Every physics simulation starts with a world. The physics world manages gravity, collision detection, and constraint solving for all physics objects in your scene.
raku_init();
raku_renderer_create_window(1280, 720, "Physics World", true);
raku_renderer_create_camera("main", 60.0f, 0.1f, 500.0f);
float gravity[] = { 0.0f, -9.81f, 0.0f };
raku_physics_create_world(gravity);
raku_physics_set_fixed_timestep(1.0f / 60.0f);
raku_physics_set_max_substeps(4);
Tip: For XR applications, use a fixed timestep of 1/90s to match common headset refresh rates. This prevents physics jitter in VR.
CORE CONCEPTS
Creating Your First Rigidbody
A rigidbody is any object that responds to physics forces: gravity, collisions, impulses. You control its mass, friction, and bounciness.
raku_scene_create("physics_demo");
raku_scene_add_model("crate.glb", 0.0f, 5.0f, 0.0f);
raku_physics_add_rigidbody("crate.glb", 2.0f);
raku_physics_set_friction("crate.glb", 0.6f);
raku_physics_set_restitution("crate.glb", 0.3f);
raku_physics_set_damping("crate.glb", 0.1f, 0.05f);
Apply forces and impulses to move objects:
float force[] = { 0.0f, 0.0f, -10.0f };
raku_physics_apply_force("crate.glb", force);
float impulse[] = { 0.0f, 50.0f, 0.0f };
raku_physics_apply_impulse("crate.glb", impulse);
float torque[] = { 0.0f, 5.0f, 0.0f };
raku_physics_apply_torque("crate.glb", torque);
COLLISION
Adding Collision Shapes and Responses
Every physics object needs a collider shape. Raku supports boxes, spheres, capsules, convex hulls, and triangle meshes.
raku_physics_add_collider("crate.glb", "box");
raku_physics_add_collider("ball.glb", "sphere");
raku_physics_add_collider("npc.glb", "capsule");
raku_physics_add_collider("rock.glb", "convex");
raku_physics_add_static_collider("level.glb", "mesh");
Register callbacks for collision events:
void on_collision(RakuCollisionEvent* event) {
raku_log(0, "Collision: %s hit %s at force %.2f",
event->tag_a, event->tag_b, event->impact_force);
if (event->impact_force > 5.0f) {
raku_audio_play_spatial("impact_hard",
event->contact_point[0],
event->contact_point[1],
event->contact_point[2]);
}
}
raku_physics_on_collision("crate.glb", on_collision);
raku_physics_on_collision_global(on_collision);
Use collision layers to control which objects interact:
#define LAYER_DEFAULT 0x01
#define LAYER_PLAYER 0x02
#define LAYER_PICKUP 0x04
#define LAYER_TRIGGER 0x08
raku_physics_set_collision_layer("crate.glb", LAYER_PICKUP);
raku_physics_set_collision_mask("crate.glb", LAYER_DEFAULT | LAYER_PLAYER);
raku_physics_set_trigger("zone.glb", true);
raku_physics_on_trigger_enter("zone.glb", on_zone_enter);
raku_physics_on_trigger_exit("zone.glb", on_zone_exit);
QUERIES
Raycasting for Object Picking
Raycasting shoots an invisible ray into the scene and returns what it hits. Essential for mouse picking, laser pointers, line-of-sight checks, and gaze interaction.
float origin[] = { 0.0f, 1.5f, 0.0f };
float direction[] = { 0.0f, 0.0f, -1.0f };
RakuRayHit hit;
if (raku_physics_raycast(origin, direction, 100.0f, &hit)) {
raku_log(0, "Hit: %s at distance %.2f", hit.tag, hit.distance);
raku_log(0, " Position: (%.2f, %.2f, %.2f)",
hit.point[0], hit.point[1], hit.point[2]);
raku_log(0, " Normal: (%.2f, %.2f, %.2f)",
hit.normal[0], hit.normal[1], hit.normal[2]);
}
Cast against specific layers or cast multiple rays:
if (raku_physics_raycast_layer(origin, direction, 100.0f, LAYER_PICKUP, &hit)) {
raku_scene_set_outline(hit.tag, true);
}
RakuRayHit hits[16];
int count = raku_physics_raycast_all(origin, direction, 100.0f, hits, 16);
for (int i = 0; i < count; i++) {
raku_log(0, "Hit %d: %s at %.2f", i, hits[i].tag, hits[i].distance);
}
if (raku_physics_spherecast(origin, direction, 0.5f, 10.0f, &hit)) {
}
CONSTRAINTS
Creating Joints (Hinges, Springs, and More)
Joints connect two rigid bodies together with constraints. Use them to build doors, swinging platforms, chains, ragdolls, and mechanical contraptions.
raku_scene_add_model("door.glb", 2.0f, 1.5f, 0.0f);
raku_physics_add_rigidbody("door.glb", 5.0f);
raku_physics_add_collider("door.glb", "box");
RakuJointConfig hinge = {
.type = "hinge",
.body_a = "door_frame.glb",
.body_b = "door.glb",
.anchor = { 2.0f, 1.5f, 0.5f },
.axis = { 0.0f, 1.0f, 0.0f },
.limit_min = 0.0f,
.limit_max = 90.0f,
};
raku_physics_create_joint(&hinge);
Spring joints for bouncy platforms:
raku_scene_add_model("platform.glb", 0.0f, 2.0f, -3.0f);
raku_physics_add_rigidbody("platform.glb", 1.0f);
raku_physics_add_collider("platform.glb", "box");
RakuJointConfig spring = {
.type = "spring",
.body_a = "ceiling.glb",
.body_b = "platform.glb",
.anchor = { 0.0f, 4.0f, -3.0f },
.spring_stiffness = 200.0f,
.spring_damping = 5.0f,
.spring_rest_length = 2.0f,
};
raku_physics_create_joint(&spring);
More joint types:
RakuJointConfig fixed = {
.type = "fixed",
.body_a = "wall.glb",
.body_b = "shelf.glb",
.break_force = 500.0f,
};
raku_physics_create_joint(&fixed);
RakuJointConfig ball = {
.type = "ball",
.body_a = "torso.glb",
.body_b = "upper_arm.glb",
.anchor = { 0.3f, 1.4f, 0.0f },
.cone_angle = 60.0f,
};
raku_physics_create_joint(&ball);
RakuJointConfig slider = {
.type = "slider",
.body_a = "rail.glb",
.body_b = "elevator.glb",
.axis = { 0.0f, 1.0f, 0.0f },
.limit_min = 0.0f,
.limit_max = 5.0f,
};
raku_physics_create_joint(&slider);
PERFORMANCE
Performance Optimization Tips
Physics can be expensive. Follow these guidelines to keep your simulation running smoothly, especially on mobile XR headsets.
- Use simple colliders — Prefer boxes and spheres over convex hulls. Triangle meshes should only be used for static geometry.
- Sleep idle objects — The engine automatically sleeps objects that stop moving. Avoid waking them unnecessarily.
- Limit active rigidbodies — Keep under 200 active rigidbodies on Quest-class hardware, under 500 on desktop.
- Use collision layers — Reduce the number of collision pairs by grouping objects into layers.
- Fixed timestep matters — Smaller timesteps are more accurate but more expensive. 1/60s is a good default.
raku_physics_set_sleeping_enabled(true);
raku_physics_set_sleep_threshold_linear(0.05f);
raku_physics_set_sleep_threshold_angular(0.05f);
raku_physics_set_sleep_time(0.5f);
RakuPhysicsStats stats;
raku_physics_get_stats(&stats);
raku_log(0, "Active bodies: %d, Contacts: %d, Step time: %.2fms",
stats.active_bodies, stats.contact_count, stats.step_time_ms);
COMPLETE EXAMPLE
Complete Example: Physics-Based Puzzle Room
A self-contained puzzle room where the player must stack crates to reach a button, open a hinge door, and cross a spring bridge. Demonstrates rigidbodies, colliders, joints, triggers, and raycasting all working together.
raku_init();
raku_renderer_create_window(1280, 720, "Puzzle Room", true);
raku_renderer_create_camera("main", 60.0f, 0.1f, 200.0f);
raku_renderer_set_camera_position("main", 0.0f, 3.0f, 8.0f);
raku_audio_init(32, false);
raku_audio_load("sounds/impact.wav", "impact");
raku_audio_load("sounds/door_open.wav", "door_open");
raku_audio_load("sounds/win.wav", "win");
float gravity[] = { 0.0f, -9.81f, 0.0f };
raku_physics_create_world(gravity);
raku_scene_create("puzzle");
raku_scene_add_model("room_floor.glb", 0, 0, 0);
raku_physics_add_static_collider("room_floor.glb", "box");
raku_scene_add_model("room_walls.glb", 0, 0, 0);
raku_physics_add_static_collider("room_walls.glb", "mesh");
for (int i = 0; i < 3; i++) {
char tag[32];
sprintf(tag, "crate_%d", i);
raku_scene_add_model("crate.glb", -2.0f + i * 1.5f, 0.5f, 0.0f);
raku_physics_add_rigidbody(tag, 3.0f);
raku_physics_add_collider(tag, "box");
raku_physics_set_friction(tag, 0.8f);
}
raku_scene_add_model("button_platform.glb", 4.0f, 3.0f, 0.0f);
raku_physics_add_static_collider("button_platform.glb", "box");
raku_physics_set_trigger("button_platform.glb", true);
raku_scene_add_model("heavy_door.glb", 6.0f, 1.5f, 0.0f);
raku_physics_add_rigidbody("heavy_door.glb", 20.0f);
raku_physics_add_collider("heavy_door.glb", "box");
raku_physics_set_kinematic("heavy_door.glb", true);
RakuJointConfig door_hinge = {
.type = "hinge",
.body_a = "room_walls.glb",
.body_b = "heavy_door.glb",
.anchor = { 6.0f, 1.5f, -1.0f },
.axis = { 0, 1, 0 },
.limit_min = 0, .limit_max = 120,
};
raku_physics_create_joint(&door_hinge);
for (int i = 0; i < 5; i++) {
char plank[32];
sprintf(plank, "plank_%d", i);
raku_scene_add_model("plank.glb", 8.0f + i * 1.0f, 2.0f, 0.0f);
raku_physics_add_rigidbody(plank, 0.5f);
raku_physics_add_collider(plank, "box");
RakuJointConfig plank_spring = {
.type = "spring",
.body_a = "room_walls.glb",
.body_b = plank,
.anchor = { 8.0f + i * 1.0f, 4.0f, 0.0f },
.spring_stiffness = 100.0f,
.spring_damping = 3.0f,
.spring_rest_length = 2.0f,
};
raku_physics_create_joint(&plank_spring);
}
void on_button_pressed(RakuTriggerEvent* event) {
raku_physics_set_kinematic("heavy_door.glb", false);
float push[] = { -15.0f, 0.0f, 0.0f };
raku_physics_apply_impulse("heavy_door.glb", push);
raku_audio_play("door_open", false);
}
raku_physics_on_trigger_enter("button_platform.glb", on_button_pressed);
void on_impact(RakuCollisionEvent* event) {
if (event->impact_force > 3.0f) {
raku_audio_play_spatial("impact",
event->contact_point[0], event->contact_point[1], event->contact_point[2]);
}
}
raku_physics_on_collision_global(on_impact);
raku_scene_start_loop();
raku_shutdown();