🎮 GDC 2026 Open Beta — Pro features FREE through June 30, 2026 | Try It Now →
INTERMEDIATE TUTORIAL

Building Interactive Physics Worlds

Rigid bodies, collisions, raycasting, joints, and optimization. Build a physics-based puzzle room from scratch.

← All Tutorials

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); // Create physics world with Earth gravity float gravity[] = { 0.0f, -9.81f, 0.0f }; raku_physics_create_world(gravity); // Configure physics timestep (default: 1/60s fixed step) raku_physics_set_fixed_timestep(1.0f / 60.0f); // Set maximum substeps per frame (prevents spiral of death) 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.

// Add a 3D model to the scene raku_scene_create("physics_demo"); raku_scene_add_model("crate.glb", 0.0f, 5.0f, 0.0f); // Make it a dynamic rigid body (mass in kg) raku_physics_add_rigidbody("crate.glb", 2.0f); // Set material properties raku_physics_set_friction("crate.glb", 0.6f); // 0 = ice, 1 = sandpaper raku_physics_set_restitution("crate.glb", 0.3f); // 0 = no bounce, 1 = superball // Set linear and angular damping (simulates air resistance) raku_physics_set_damping("crate.glb", 0.1f, 0.05f);

Apply forces and impulses to move objects:

// Apply a continuous force (call every frame for sustained push) float force[] = { 0.0f, 0.0f, -10.0f }; raku_physics_apply_force("crate.glb", force); // Apply an impulse (one-time kick, like an explosion) float impulse[] = { 0.0f, 50.0f, 0.0f }; raku_physics_apply_impulse("crate.glb", impulse); // Apply torque for rotation 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.

// Box collider (best for crates, walls, platforms) raku_physics_add_collider("crate.glb", "box"); // Sphere collider (best for balls, round objects) raku_physics_add_collider("ball.glb", "sphere"); // Capsule collider (best for characters, pillars) raku_physics_add_collider("npc.glb", "capsule"); // Convex hull (auto-generated from mesh, max 256 vertices) raku_physics_add_collider("rock.glb", "convex"); // Static triangle mesh (for complex level geometry, static only) 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); // Play a sound based on 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]); } } // Register for collisions on a specific object raku_physics_on_collision("crate.glb", on_collision); // Or register for all collisions in the world raku_physics_on_collision_global(on_collision);

Use collision layers to control which objects interact:

// Define collision layers #define LAYER_DEFAULT 0x01 #define LAYER_PLAYER 0x02 #define LAYER_PICKUP 0x04 #define LAYER_TRIGGER 0x08 // Set an object's collision layer raku_physics_set_collision_layer("crate.glb", LAYER_PICKUP); // Set which layers this object collides with raku_physics_set_collision_mask("crate.glb", LAYER_DEFAULT | LAYER_PLAYER); // Triggers: detect overlap without physical response 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.

// Simple raycast: origin, direction, max distance 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:

// Raycast against specific layers only if (raku_physics_raycast_layer(origin, direction, 100.0f, LAYER_PICKUP, &hit)) { // Only hits objects on the PICKUP layer raku_scene_set_outline(hit.tag, true); } // Raycast all: returns every object along the ray 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); } // Sphere cast: thicker ray, useful for character movement if (raku_physics_spherecast(origin, direction, 0.5f, 10.0f, &hit)) { // Detected an obstacle within a 0.5m radius tube }
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.

// Hinge joint: a door that swings on an axis 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", // static anchor .body_b = "door.glb", // swinging door .anchor = { 2.0f, 1.5f, 0.5f }, .axis = { 0.0f, 1.0f, 0.0f }, // rotate around Y axis .limit_min = 0.0f, // degrees .limit_max = 90.0f, }; raku_physics_create_joint(&hinge);

Spring joints for bouncy platforms:

// Spring joint: a platform that bounces 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, // N/m .spring_damping = 5.0f, // damping ratio .spring_rest_length = 2.0f, // meters }; raku_physics_create_joint(&spring);

More joint types:

// Fixed joint: welds two objects together (breakable) RakuJointConfig fixed = { .type = "fixed", .body_a = "wall.glb", .body_b = "shelf.glb", .break_force = 500.0f, // breaks if force exceeds 500N }; raku_physics_create_joint(&fixed); // Ball-socket joint: free rotation around a point (ragdoll shoulders) RakuJointConfig ball = { .type = "ball", .body_a = "torso.glb", .body_b = "upper_arm.glb", .anchor = { 0.3f, 1.4f, 0.0f }, .cone_angle = 60.0f, // limit rotation to a cone }; raku_physics_create_joint(&ball); // Slider joint: movement along one axis (elevator, piston) 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, // 5 meters of travel }; 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.
// Enable physics sleeping (on by default, shown for reference) raku_physics_set_sleeping_enabled(true); // Configure sleep thresholds raku_physics_set_sleep_threshold_linear(0.05f); // m/s raku_physics_set_sleep_threshold_angular(0.05f); // rad/s raku_physics_set_sleep_time(0.5f); // seconds idle before sleeping // Query physics stats for profiling 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.

// === Physics Puzzle Room === 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); // Audio for impact sounds 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"); // Physics world float gravity[] = { 0.0f, -9.81f, 0.0f }; raku_physics_create_world(gravity); // Scene setup raku_scene_create("puzzle"); // --- Room shell (static geometry) --- 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"); // --- Stackable crates --- 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); } // --- Pressure button (trigger zone at height) --- 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); // --- Hinge door (unlocked by button) --- 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); // locked initially 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); // --- Spring bridge --- 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); } // --- Collision and trigger callbacks --- void on_button_pressed(RakuTriggerEvent* event) { // Unlock the door when a crate lands on the button 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); // --- Start the game loop --- raku_scene_start_loop(); raku_shutdown();
Next steps: Add hand tracking to grab crates in VR using the XR tutorial, or add spatial audio with the Spatial Audio tutorial.

Ready for More?

Explore the full Physics API (108 functions) in the documentation, or continue with another tutorial.