Goose

28_physics_boxes.goose

// Thousands of boxes raining into a walled arena: the physics module
// (Box3D, docs/stdlib.md) moves them, the gfx module draws them.
//
// Boxes of every size and color fall from high above the whole arena. They
// tumble onto two pyramids and each other, and a long arm turning in the
// middle sweeps the heap around. Once the arena holds as many boxes as asked
// for, the oldest are lifted back up, so the rain never stops. Box3D steps
// the world on as many worker threads as the machine has, up to eight.
//
// Each frame the bodies' transforms come back in one call and go straight
// into an instance buffer: one unit cube drawn once per box, moved, turned
// and colored in the vertex shader, and lit by a sun whose shadow map is
// drawn from the same buffer first. The shaders are GLSL, written below as
// """ strings that embed_shader compiles along with the program.
//
// Space sets off an explosion under the heap; drag with the left mouse
// button to look around and use the wheel to zoom; Up and Down pour faster
// and slower; Escape quits.
//
//   goose 28_physics_boxes.goose
//   goose 28_physics_boxes.goose -- --boxes 20000
//
// `--frames N` stops after N frames and saves the last as physics_boxes.png;
// `--rate N` drops N boxes a frame. Built from the generated C, the program
// links both native layers, whose paths goose prints as response files:
//   goose -o physics_boxes.c 28_physics_boxes.goose
//   cl physics_boxes.c @<goose --gfx-link msvc> @<goose --physics-link msvc>
//   cc physics_boxes.c -o physics_boxes @<goose --gfx-link cc> @<goose --physics-link cc>

import std;
import vec;
import math;
import os;
import gfx;
import physics;

// A corner of the unit cube, with its face's normal.
struct Vertex { pos: float3, normal: float3 }
// One box as the vertex shader reads it, per instance: where, how big, how
// turned, what color.
struct Instance { position: float3, half: float3, rotation: physics::Quat, color: u8[4] }
// The uniform blocks of the shaders: two column-major matrices, and the
// light, each already laid out as std140 wants.
struct Scene { view_proj: gfx::mat4, light_view_proj: gfx::mat4 }
struct Light { sun: float4, eye: float4, haze: float4 }
// What a falling box looks like, by its body.
struct Look { half: float3, color: u8[4] }

let ARENA: f32 = 18.0;          // half the floor's width, inside the walls
let SHADOW_SIZE = 2048;
// The rain falls from a grid of cells over the arena, which it visits in a
// scattered order that comes back to a cell only after all the others:
// by then that cell's last box has fallen well clear, so no box starts
// inside another.
let CELLS = 24;
let CELL: f32 = 1.3;
let DT: f32 = 1.0 / 60.0;

fn f3(x: f32) -> float3 { float3 { x, x, x } }

// The unit cube: four corners and two triangles a face, counterclockwise
// seen from outside.
fn cube(verts: Vertex[>..]&, indices: u16[>..]&) {
    let axes: float3[3] = [float3 { 1.0, 0.0, 0.0 }, float3 { 0.0, 1.0, 0.0 },
                           float3 { 0.0, 0.0, 1.0 }];
    for f in 6 {
        let sign: f32 = if f % 2 == 0 { 1.0 } else { -1.0 };
        let n = axes[f / 2] * f3(sign);
        let u = axes[(f / 2 + 1) % 3] * f3(sign);
        let v = cross(n, u);
        let base = verts.len as u16;
        let corners: float2[4] = [float2 { -1.0, -1.0 }, float2 { 1.0, -1.0 },
                                  float2 { 1.0, 1.0 }, float2 { -1.0, 1.0 }];
        for c in corners { verts.push(Vertex { pos: n + u * f3(c.x) + v * f3(c.y), normal: n }); }
        indices.append([base, base + 1, base + 2, base, base + 2, base + 3]);
    }
}

// A bright color from around the color wheel, h from 0 to 1.
fn hue(h: f32) -> u8[4] {
    let r = clamp(abs(h * 6.0 - 3.0) - 1.0, 0.0, 1.0);
    let g = clamp(2.0 - abs(h * 6.0 - 2.0), 0.0, 1.0);
    let b = clamp(2.0 - abs(h * 6.0 - 4.0), 0.0, 1.0);
    [round(50.0 + 190.0 * r) as u8, round(50.0 + 190.0 * g) as u8, round(50.0 + 190.0 * b) as u8,
     255]
}

// A random number from lo to hi; the f64 rounds to the nearest f32.
fn flt(r: rng&, lo: f32, hi: f32) -> f32 { lo + (hi - lo) * r.rand_flt() as f32 }

// Where the n-th box starts: 263 shares no factor with the 576 cells, so
// stepping by it visits them all before any twice.
fn raindrop(n: i64, r: rng&) -> float3 {
    let c = n * 263 % (CELLS * CELLS);
    let middle = (CELLS - 1) as f32 * 0.5;
    float3 { (c % CELLS - middle) * CELL + flt(r, -0.15, 0.15), flt(r, 24.0, 25.0),
             (c / CELLS - middle) * CELL + flt(r, -0.15, 0.15) }
}

// --- shaders -----------------------------------------------------------------

// The first part of both vertex shaders: a corner of the cube from vertex
// buffer 0, an Instance from buffer 1, the Scene, and where that corner of
// that box is in the world. Both shaders read the same buffers, so both
// declare every input, which is what the pipeline lays the buffers out by.
let box_input = """
    layout(location = 0) in vec3 a_pos;
    layout(location = 1) in vec3 a_normal;
    layout(location = 2) in vec3 i_position;
    layout(location = 3) in vec3 i_half;
    layout(location = 4) in vec4 i_rotation;
    layout(location = 5) in vec4 i_color;
    layout(set = 1, binding = 0) uniform Scene {
        mat4 view_proj;
        mat4 light_view_proj;
    };

    // Rotates v by the unit quaternion q.
    vec3 rotate_q(vec4 q, vec3 v) {
        vec3 t = 2.0 * cross(q.xyz, v);
        return v + q.w * t + cross(q.xyz, t);
    }

    vec3 world_corner() {
        return i_position + rotate_q(i_rotation, a_pos * i_half);
    }
    """;

// A box seen from the camera, with what its light needs.
let boxes_vert = """
    layout(location = 0) out vec3 v_world;
    layout(location = 1) out vec3 v_normal;
    layout(location = 2) out vec4 v_color;
    layout(location = 3) out vec4 v_shadow;
    layout(location = 4) out vec3 v_local;
    void main() {
        vec3 world = world_corner();
        v_world = world;
        v_normal = rotate_q(i_rotation, a_normal);
        v_color = i_color;
        v_shadow = light_view_proj * vec4(world, 1.0);
        // Where on the face this corner is, from -1 to 1, for darkening edges.
        v_local = a_pos * 2.0;
        gl_Position = view_proj * vec4(world, 1.0);
    }
    """;

// The boxes' light: the sun, softened by a shadow map read through hardware
// depth comparison, sky light from above, darker edges so that boxes in a
// heap stay apart, and haze with distance.
let boxes_frag = """
    layout(location = 0) in vec3 v_world;
    layout(location = 1) in vec3 v_normal;
    layout(location = 2) in vec4 v_color;
    layout(location = 3) in vec4 v_shadow;
    layout(location = 4) in vec3 v_local;
    layout(location = 0) out vec4 result;
    layout(set = 2, binding = 0) uniform sampler2DShadow shadow_map;
    layout(set = 3, binding = 0) uniform Light {
        vec4 sun;       // xyz: toward the sun
        vec4 eye;
        vec4 haze;      // rgb: its color, w: density
    };

    float sunlit(vec4 s) {
        vec3 p = s.xyz / s.w;
        vec2 uv = vec2(p.x * 0.5 + 0.5, 0.5 - p.y * 0.5);
        if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) return 1.0;
        float texel = 1.0 / 2048.0;
        float lit = 0.0;
        lit += texture(shadow_map, vec3(uv + vec2(-texel, -texel), p.z));
        lit += texture(shadow_map, vec3(uv + vec2(texel, -texel), p.z));
        lit += texture(shadow_map, vec3(uv + vec2(-texel, texel), p.z));
        lit += texture(shadow_map, vec3(uv + vec2(texel, texel), p.z));
        return lit * 0.25;
    }

    void main() {
        vec3 n = normalize(v_normal);
        float diffuse = max(dot(n, sun.xyz), 0.0) * sunlit(v_shadow);
        float sky = 0.35 + 0.25 * n.y;
        // The two coordinates across this face nearest 1 in size make an edge.
        vec3 a = abs(v_local);
        float across = max(max(min(a.x, a.y), min(a.y, a.z)), min(a.x, a.z));
        float edge = 1.0 - 0.35 * smoothstep(0.85, 1.0, across);
        vec3 light = vec3(1.0, 0.95, 0.85) * diffuse + vec3(0.55, 0.65, 0.8) * sky;
        vec3 color = v_color.rgb * light * edge;
        float dist = length(eye.xyz - v_world);
        result = vec4(mix(color, haze.rgb, 1.0 - exp(-dist * haze.w)), 1.0);
    }
    """;

// A box seen from the sun, for the shadow map: only its depth.
let shadow_vert = """
    void main() {
        gl_Position = light_view_proj * vec4(world_corner(), 1.0);
    }
    """;

fn main() {
    var frames = -1;
    var max_boxes = 10000;
    var rate = 25;
    let argv = args();
    var a = 1;
    while a + 1 < argv.len {
        let n, ok = parse_int(argv[a + 1]);
        if ok && argv[a] == "--frames" { frames = n; }
        if ok && argv[a] == "--boxes" { max_boxes = max(n, 1); }
        if ok && argv[a] == "--rate" { rate = max(n, 0); }
        a += 2;
    }
    guard gfx::open("Goose: boxes", 1280, 720, gfx::WINDOW_RESIZABLE) else {
        write_stderr(str("gfx: ", gfx::error(), "\n"));
        return;
    }

    // --- the world -----------------------------------------------------------
    let world = physics::create_world(physics::WorldDef {
        workers: min(hardware_threads(), 8) as i32,
        capacity: physics::Capacity { dynamic_bodies: max_boxes as i32 } });
    // The floor and four walls, all shapes of one static body; the arena's
    // pieces are drawn as boxes too.
    var props: Instance[>..] = [];
    let arena = physics::create_body(world, physics::BodyDef {});
    let wall_color: u8[4] = [150, 155, 165, 255];
    let pieces: float3[10] = [
        float3 { 0.0, -0.5, 0.0 }, float3 { ARENA + 1.0, 0.5, ARENA + 1.0 },
        float3 { 0.0, 1.5, ARENA + 0.5 }, float3 { ARENA + 1.0, 1.5, 0.5 },
        float3 { 0.0, 1.5, -ARENA - 0.5 }, float3 { ARENA + 1.0, 1.5, 0.5 },
        float3 { ARENA + 0.5, 1.5, 0.0 }, float3 { 0.5, 1.5, ARENA + 1.0 },
        float3 { -ARENA - 0.5, 1.5, 0.0 }, float3 { 0.5, 1.5, ARENA + 1.0 }];
    for i in 5 {
        let at = pieces[i * 2];
        let half = pieces[i * 2 + 1];
        physics::create_box_shape(arena, physics::ShapeDef {}, half,
                                  physics::Transform { p: at, q: physics::quat_identity });
        props.push(Instance { position: at, half: half, rotation: physics::quat_identity,
                              color: if i == 0 { [96, 104, 112, 255] } else { wall_color } });
    }

    // The arm: kinematic, so it turns at its own speed whatever it hits.
    let arm_half = float3 { ARENA * 0.55, 0.6, 0.35 };
    let arm = physics::create_body(world, physics::BodyDef {
        body_type: physics::KINEMATIC, position: float3 { 0.0, 0.6, 0.0 },
        angular_velocity: float3 { 0.0, 0.5, 0.0 } });
    physics::create_box_shape(arm, physics::ShapeDef {}, arm_half);

    // Two pyramids for the pour to bury and the arm to knock down.
    var bodies: physics::Body[>..] = [];
    var looks: Look[>..] = [];
    for p in 2 {
        let center = if p == 0 { float3 { 8.0, 0.0, 5.0 } } else { float3 { -7.0, 0.0, -8.0 } };
        let rows = 8;
        for row in rows {
            for k in rows - row {
                let at = center + float3 { (k - (rows - row - 1) * 0.5) * 1.02,
                                           0.5 + row * 1.0, 0.0 };
                let b = physics::create_body(world, physics::BodyDef { body_type: physics::DYNAMIC,
                                                                       position: at });
                physics::create_box_shape(b, physics::ShapeDef {}, f3(0.5));
                bodies.push(b);
                let shade = (180 + row * 9) as u8;
                looks.push(Look { half: f3(0.5), color: if p == 0 { [shade, shade, 90, 255] }
                                                        else { [90, shade, shade, 255] } });
            }
        }
    }
    let pyramid_boxes = bodies.len;

    // --- drawing -------------------------------------------------------------
    var verts: Vertex[>..] = [];
    var indices: u16[>..] = [];
    cube(verts, indices);
    let cube_vb = gfx::vertex_buffer(bytes_of(verts));
    let cube_ib = gfx::index_buffer(bytes_of(indices));
    // Room for every instance there will ever be: filled from the start
    // each frame.
    let one: Instance[1] = [default<Instance>()];
    let none: u8[>..] = [];
    let instance_bytes = bytes_of(one).len;
    let inst_vb = gfx::buffer(gfx::BUFFER_VERTEX,
                              (props.len + 1 + pyramid_boxes + max_boxes) * instance_bytes, none);
    // Corners from buffer 0, the rest per instance from buffer 1; the color
    // comes as four bytes.
    var desc = gfx::PipelineDesc { cull: gfx::CULL_BACK, depth_test: true, depth_write: true,
                                   instance_location: 2 };
    desc.formats[5] = gfx::VERTEX_UBYTE4_NORM;
    let boxes_pipe = gfx::pipeline(embed_shader("vert", box_input, boxes_vert),
                                   embed_shader("frag", boxes_frag), desc);
    var shadow_desc = gfx::PipelineDesc { cull: gfx::CULL_FRONT, depth_test: true,
                                          depth_write: true, instance_location: 2,
                                          depth_bias: 1.0, depth_bias_slope: 1.5 };
    shadow_desc.formats[5] = gfx::VERTEX_UBYTE4_NORM;
    // A pass with only depth still needs a fragment shader: an empty one.
    let shadow_pipe = gfx::pipeline(embed_shader("vert", box_input, shadow_vert),
                                    embed_shader("frag", "void main() { }"), shadow_desc);
    let shadow_map = gfx::depth_target(SHADOW_SIZE, SHADOW_SIZE);
    let shadow_cmp = gfx::shadow_sampler();

    // The sun, and a view of the whole arena from it.
    let sun = normalize(float3 { 0.45, 1.0, 0.3 });
    let up = float3 { 0.0, 1.0, 0.0 };
    let light_view_proj = gfx::mul(gfx::ortho(-30.0, 30.0, -30.0, 30.0, 1.0, 100.0),
                                   gfx::look_at(sun * f3(50.0), float3_0, up));

    // --- the pour ------------------------------------------------------------
    var r = rng { 2026 };
    var instances: Instance[>..] = [];
    var transforms: physics::Transform[>..] = [];
    var dropped = 0;
    var oldest = 0;
    var yaw: f32 = 0.6;
    var pitch: f32 = 0.45;
    var distance: f32 = 48.0;
    var drawn = 0;
    var stats_time: f64 = 0.0;
    var stats_frames = 0;
    while gfx::frame() {
        if gfx::key_pressed("Escape") { break; }
        if gfx::key_pressed("Up") { rate = min(rate * 2, 400); }
        if gfx::key_pressed("Down") { rate = rate / 2; }
        if gfx::key_pressed("Space") {
            physics::explode(world, physics::ExplosionDef { position: float3 { 0.0, -1.0, 0.0 },
                                                            radius: 14.0, falloff: 6.0,
                                                            impulse_per_area: 60.0 });
        }
        if gfx::mouse_down(gfx::MOUSE_LEFT) {
            let d = gfx::mouse_delta();
            yaw += d.x * 0.005;
            pitch = clamp(pitch + d.y * 0.005, 0.05, 1.45);
        } else {
            yaw += 0.001;
        }
        distance = clamp(distance - gfx::mouse_wheel() * 2.0, 10.0, 120.0);

        // New boxes, and once there are enough, the oldest lifted back up.
        for k in rate {
            let at = raindrop(dropped, r);
            let turn = physics::quat_axis_angle(float3 { flt(r, -1.0, 1.0), flt(r, -1.0, 1.0),
                                                         flt(r, -1.0, 1.0) } + f3(0.01),
                                                flt(r, 0.0, 3.14));
            let velocity = float3 { flt(r, -0.3, 0.3), flt(r, -5.0, -3.0), flt(r, -0.3, 0.3) };
            let spin = float3 { flt(r, -3.0, 3.0), flt(r, -3.0, 3.0), flt(r, -3.0, 3.0) };
            let h = dropped * 0.0007 + flt(r, 0.0, 0.05);
            let look = Look { half: float3 { flt(r, 0.18, 0.45), flt(r, 0.18, 0.45),
                                             flt(r, 0.18, 0.45) },
                              color: hue(h - floor(h)) };
            if bodies.len - pyramid_boxes < max_boxes {
                let b = physics::create_body(world, physics::BodyDef {
                    body_type: physics::DYNAMIC, position: at, rotation: turn,
                    linear_velocity: velocity, angular_velocity: spin });
                physics::create_box_shape(b, physics::ShapeDef {}, look.half);
                bodies.push(b);
                looks.push(look);
            } else {
                // The oldest box, back up; it keeps its shape.
                let i = pyramid_boxes + oldest;
                oldest = (oldest + 1) % max_boxes;
                physics::set_transform(bodies[i], at, turn);
                physics::set_linear_velocity(bodies[i], velocity);
                physics::set_angular_velocity(bodies[i], spin);
                physics::set_awake(bodies[i], true);
                looks[i].color = look.color;
            }
            dropped++;
        }

        let before = clock();
        physics::step(world, DT, 4);
        stats_time += clock() - before;
        stats_frames++;

        // Every body's transform in one call, into the instances.
        transforms.resize(bodies.len, physics::transform_identity);
        physics::transforms(bodies, transforms);
        // A box knocked over a wall has nothing under it: it rains again.
        for i in bodies.len {
            if transforms[i].p.y < -10.0 {
                transforms[i].p = raindrop(dropped, r);
                dropped++;
                physics::set_transform(bodies[i], transforms[i].p, transforms[i].q);
                physics::set_linear_velocity(bodies[i], float3 { 0.0, -4.0, 0.0 });
            }
        }
        instances.clear();
        instances.append(props);
        let arm_at = physics::transform(arm);
        instances.push(Instance { position: arm_at.p, half: arm_half, rotation: arm_at.q,
                                  color: [230, 90, 60, 255] });
        for i in bodies.len {
            instances.push(Instance { position: transforms[i].p, half: looks[i].half,
                                      rotation: transforms[i].q, color: looks[i].color });
        }
        gfx::update_buffer(inst_vb, bytes_of(instances));

        let size = gfx::screen_size();
        let aspect = size.x as f32 / size.y;
        let eye = float3 { distance * cos(pitch) * cos(yaw), distance * sin(pitch),
                           distance * cos(pitch) * sin(yaw) };
        let scene = Scene {
            view_proj: gfx::mul(gfx::perspective(0.9, aspect, 0.5, 300.0),
                                gfx::look_at(eye, float3 { 0.0, 2.0, 0.0 }, up)),
            light_view_proj: light_view_proj };

        // The shadow map, then the boxes lit and shadowed.
        gfx::begin_pass(gfx::PassDesc { depth: shadow_map });
        gfx::bind(shadow_pipe);
        gfx::bind_vertex_buffer(0, cube_vb, 0);
        gfx::bind_vertex_buffer(1, inst_vb, 0);
        gfx::bind_index_buffer(cube_ib, 2);
        gfx::uniforms(gfx::VERTEX, 0, scene);
        gfx::draw_indexed_instanced(indices.len, instances.len);
        gfx::end_pass();

        gfx::begin_screen(float4 { 0.62, 0.72, 0.84, 1.0 });
        gfx::bind(boxes_pipe);
        gfx::bind_vertex_buffer(0, cube_vb, 0);
        gfx::bind_vertex_buffer(1, inst_vb, 0);
        gfx::bind_index_buffer(cube_ib, 2);
        gfx::bind_texture(gfx::FRAGMENT, 0, shadow_map, shadow_cmp);
        gfx::uniforms(gfx::VERTEX, 0, scene);
        gfx::uniforms(gfx::FRAGMENT, 0, Light { sun: float4 { sun.x, sun.y, sun.z, 0.0 },
                                                eye: float4 { eye.x, eye.y, eye.z, 1.0 },
                                                haze: float4 { 0.62, 0.72, 0.84, 0.006 } });
        gfx::draw_indexed_instanced(indices.len, instances.len);
        gfx::end_pass();

        if stats_frames == 30 {
            gfx::set_title(str("Goose: ", bodies.len - pyramid_boxes, " boxes, ",
                               physics::awake_body_count(world), " awake, a step takes ",
                               round(stats_time / 30.0 * 1000.0) as i64, " ms on ",
                               physics::worker_count(world), " threads (",
                               rate, " a frame; Space, mouse, Up/Down)"));
            stats_time = 0.0;
            stats_frames = 0;
        }
        drawn++;
        if drawn == frames { break; }
    }

    // How the last frame left things: every box still in the arena, the
    // pyramids knocked about, the arm turned.
    physics::transforms(bodies, transforms);
    var inside = true;
    var toppled = 0;
    for i in bodies.len {
        let p = transforms[i].p;
        inside = inside && abs(p.x) < ARENA + 1.0 && abs(p.z) < ARENA + 1.0 && p.y > -0.1;
    }
    for i in pyramid_boxes {
        if length(physics::linear_velocity(bodies[i])) > 0.01 { toppled++; }
    }
    let arm_turn = physics::rotation(arm);
    let saved = gfx::screenshot("physics_boxes.png");
    print("drew ", drawn, " frames; dropped ", dropped, " boxes; all in the arena: ", inside,
          "; the arm turned ", round(2.0 * acos(arm_turn.w) * 57.29578) as i64,
          " degrees; saved physics_boxes.png: ", saved);
    write_stderr(str(toppled, " of ", pyramid_boxes, " pyramid boxes moving\n"));
    physics::destroy(world);
    gfx::close();
}