Skip to main content

3d/
3d.rs

1//! 3D: meshes, materials, a sun, and thousands of instanced models.
2//!
3//!   `cargo run --release --example 3d --features 3d`
4//!
5//! - Drag with the left mouse button to orbit, wheel to zoom.
6//! - Space: pause the spinning.  Escape: quit.
7//! - `CUBES=100000` (environment) spins that many cubes in a field.
8//! - `BENCH_SECONDS=10` goes fullscreen (`BENCH_WINDOWED=1`: not) with vsync off, measures that many seconds after a
9//!   3 second warmup, prints the averages as JSON and quits (the same
10//!   measurement as the Unity version of this scene).
11
12use ponko::*;
13
14/// Turns around its own Y axis.
15struct Spin(f32);
16
17/// The orbiting camera: angles around the target and distance.
18struct Orbit {
19    yaw: f32,
20    pitch: f32,
21    distance: f32,
22}
23
24fn spin(ctx: &mut Ctx) {
25    let dt = ctx.dt();
26    ctx.world.query::<(&mut Transform3D, &Spin)>().for_each(|(t, s)| {
27        t.rotation *= Quat::from_rotation_y(s.0 * dt);
28    });
29}
30
31fn orbit(ctx: &mut Ctx) {
32    if ctx.key_pressed(Key::Space) {
33        if ctx.is_paused() { ctx.resume() } else { ctx.pause() }
34    }
35    if ctx.key_pressed(Key::Escape) {
36        ctx.quit();
37    }
38    let drag = if ctx.mouse_down(MouseButton::Left) {
39        ctx.mouse_delta()
40    } else {
41        Vec2::ZERO
42    };
43    let wheel = ctx.mouse_wheel().y;
44    let o = ctx.world.resource_mut::<Orbit>();
45    o.yaw -= drag.x * 0.005;
46    o.pitch = (o.pitch + drag.y * 0.005).clamp(0.05, 1.5);
47    o.distance = (o.distance * 0.9f32.powf(wheel)).clamp(3.0, 400.0);
48    let (yaw, pitch, distance) = (o.yaw, o.pitch, o.distance);
49    let offset = vec3(yaw.sin() * pitch.cos(), pitch.sin(), yaw.cos() * pitch.cos()) * distance;
50    ctx.camera3d.position = offset;
51    ctx.camera3d.look_at(Vec3::ZERO);
52}
53
54/// Frame times summed after the warmup.
55struct Bench {
56    seconds: f64,
57    start: Option<std::time::Instant>,
58    last: Option<std::time::Instant>,
59    frames: u64,
60    frame_ms: f64,
61    render_cpu_ms: f64,
62    /// Frame time minus waiting for the display: the work.
63    busy_ms: f64,
64    /// GPU time of the frame's passes.
65    gpu_ms: f64,
66}
67
68fn bench(ctx: &mut Ctx) {
69    let now = std::time::Instant::now();
70    let cpu = ctx.render_stats().cpu_ms as f64;
71    let gpu = ctx.render_stats().gpu_ms.unwrap_or(0.0) as f64;
72    let wait = ctx.display_wait_ms() as f64;
73    let b = ctx.world.resource_mut::<Bench>();
74    let start = *b.start.get_or_insert(now);
75    let elapsed = now.duration_since(start).as_secs_f64();
76    let last = b.last.replace(now);
77    let measuring = last.is_some() && elapsed > 3.0;
78    let first = measuring && b.frames == 0;
79    if let (true, Some(last)) = (measuring, last) {
80        b.frames += 1;
81        b.frame_ms += now.duration_since(last).as_secs_f64() * 1000.0;
82        b.render_cpu_ms += cpu;
83        b.gpu_ms += gpu;
84        b.busy_ms += now.duration_since(last).as_secs_f64() * 1000.0 - wait;
85    }
86    let done = elapsed > 3.0 + b.seconds;
87    let (frames, frame_ms, render_cpu_ms, busy_ms, gpu_ms) =
88        (b.frames, b.frame_ms, b.render_cpu_ms, b.busy_ms, b.gpu_ms);
89    if first && let Ok(path) = std::env::var("BENCH_SHOT") {
90        ctx.save_screenshot(path);
91    }
92    if done {
93        let n = frames as f64;
94        println!(
95            "{{\"engine\":\"crabby\",\"frames\":{frames},\"fps\":{:.3},\"frame_ms\":{:.3},\"render_cpu_ms\":{:.3},\"busy_ms\":{:.3},\"gpu_ms\":{:.3}}}",
96            n / (frame_ms / 1000.0),
97            frame_ms / n,
98            render_cpu_ms / n,
99            busy_ms / n,
100            gpu_ms / n
101        );
102        ctx.quit();
103    }
104}
105
106fn main() {
107    let bench_seconds: Option<f64> = std::env::var("BENCH_SECONDS").ok().and_then(|s| s.parse().ok());
108    let cubes: usize = std::env::var("CUBES").ok().and_then(|n| n.parse().ok()).unwrap_or(400);
109    let mut game = Game::new("Crabby Engine: 3D", 1280, 720).system(orbit);
110    game = if bench_seconds.is_some() {
111        // Spin every frame, as Unity's Update does, so both do the same work.
112        let fullscreen = std::env::var_os("BENCH_WINDOWED").is_none();
113        game.vsync(false).fullscreen(fullscreen).system(spin).system(bench)
114    } else {
115        game.sim_system(spin)
116    };
117    game.run(move |ctx| {
118        ctx.clear(Color::hex(0x9CC4E4)); // sky
119        ctx.camera3d.far = 2000.0;
120        ctx.lighting.sun_direction = vec3(-0.8, -0.6, -0.4); // late afternoon: long shadows
121        if let Some(seconds) = bench_seconds {
122            ctx.world.insert_resource(Bench {
123                seconds,
124                start: None,
125                last: None,
126                frames: 0,
127                frame_ms: 0.0,
128                render_cpu_ms: 0.0,
129                busy_ms: 0.0,
130                gpu_ms: 0.0,
131            });
132        }
133        ctx.world.insert_resource(Orbit {
134            yaw: 0.6,
135            pitch: 0.45,
136            distance: 18.0,
137        });
138
139        let grass = ctx.create_material(MaterialDesc {
140            roughness: 0.95,
141            ..MaterialDesc::color(Color::hex(0x5E8C4A))
142        });
143        let ground = ctx.create_mesh(&MeshData::plane(400.0), grass);
144        ctx.world.spawn((Transform3D::default(), Model::new(ground)));
145
146        // Spheres: rough to smooth left to right, plastic in front, metal behind.
147        let ball = MeshData::sphere(0.45, 32);
148        for row in 0..2 {
149            for col in 0..6 {
150                let material = ctx.create_material(MaterialDesc {
151                    roughness: 0.05 + col as f32 * 0.19,
152                    metallic: row as f32,
153                    ..MaterialDesc::color(if row == 0 {
154                        Color::hex(0xD8453B)
155                    } else {
156                        Color::hex(0xE8C070)
157                    })
158                });
159                let mesh = ctx.create_mesh(&ball, material);
160                let at = vec3(col as f32 * 1.1 - 2.75, 0.45, row as f32 * -1.2);
161                ctx.world.spawn((Transform3D::at(at), Model::new(mesh)));
162            }
163        }
164
165        // A field of spinning cubes, one mesh, tinted per cube.
166        let white = ctx.default_material();
167        let cube = ctx.create_mesh(&MeshData::cube(0.6), white);
168        let side = (cubes as f32).sqrt().ceil() as usize;
169        let mut rng = Rng::new(3);
170        for i in 0..cubes {
171            let (x, z) = ((i % side) as f32, (i / side) as f32);
172            let at = vec3((x - side as f32 / 2.0) * 1.6, 0.3, -6.0 - z * 1.6);
173            let tint = Color::rgb(rng.range(0.3, 1.0), rng.range(0.3, 1.0), rng.range(0.3, 1.0));
174            ctx.world.spawn((
175                Transform3D::at(at).rotated(Quat::from_rotation_y(rng.range(0.0, 6.3))),
176                Model::new(cube).with_color(tint),
177                Spin(rng.range(-2.0, 2.0)),
178            ));
179        }
180    });
181}