Skip to main content

streaming/
streaming.rs

1//! An endless landscape streamed in around the camera: 256 m terrain
2//! tiles built on worker threads ([`Job`]), uploaded a couple a frame,
3//! and freed again once they're far behind ([`CellStreamer`]).
4//!
5//!   `cargo run --release --example streaming --features 3d`
6//!
7//! The camera flies on by itself; the arrows steer, W and S change speed,
8//! Q and E go down and up. `SHOT=path.png` saves frame 400 and quits.
9//! `BENCH=1` flies at top speed for 3000 frames, prints what the streaming cost on the
10//! game's thread, and quits.
11
12use std::collections::HashMap;
13
14use ponko::*;
15
16const N: usize = 129;
17const TILE: f32 = 256.0;
18/// Tiles within this distance load; beyond `KEEP` they're freed.
19const LOAD: f32 = 1400.0;
20const KEEP: f32 = 1700.0;
21/// How many tile jobs start, and finished tiles upload, per frame.
22const STARTS: usize = 4;
23const UPLOADS: usize = 2;
24
25fn noise(x: f32, z: f32) -> f32 {
26    let hash = |i: i32, j: i32| {
27        let h = (i.wrapping_mul(374_761_393) ^ j.wrapping_mul(668_265_263)) as u32;
28        let h = (h ^ (h >> 13)).wrapping_mul(1_274_126_177);
29        (h >> 8) as f32 / (1u32 << 24) as f32
30    };
31    let (i, j) = (x.floor() as i32, z.floor() as i32);
32    let (fx, fz) = (x - x.floor(), z - z.floor());
33    let (sx, sz) = (fx * fx * (3.0 - 2.0 * fx), fz * fz * (3.0 - 2.0 * fz));
34    let top = hash(i, j) + (hash(i + 1, j) - hash(i, j)) * sx;
35    let bottom = hash(i, j + 1) + (hash(i + 1, j + 1) - hash(i, j + 1)) * sx;
36    top + (bottom - top) * sz
37}
38
39/// Rolling hills with mountain ranges through them.
40fn height(x: f32, z: f32) -> f32 {
41    let mut h = 0.0;
42    let (mut scale, mut amp) = (1.0 / 700.0, 130.0);
43    for _ in 0..6 {
44        h += (noise(x * scale, z * scale) - 0.5) * amp;
45        scale *= 2.1;
46        amp *= 0.45;
47    }
48    let ridge = 1.0 - (noise(x / 1800.0 + 7.3, z / 1800.0 - 2.1) - 0.5).abs() * 2.0;
49    h + ridge.powi(3) * 90.0
50}
51
52/// The ground's colors from its height and slope: grass, rock, snow.
53const GROUND: &str = "
54fn surface(s: Surface) -> Surface {
55    var out = s;
56    let steep = 1.0 - s.normal.y;
57    let grain = noise2(s.world.xz * 0.35) * 0.5 + noise2(s.world.xz * 0.05) * 0.5;
58    let grass = mix(vec3<f32>(0.20, 0.36, 0.13), vec3<f32>(0.33, 0.42, 0.18), grain);
59    let rock = mix(vec3<f32>(0.33, 0.31, 0.29), vec3<f32>(0.48, 0.46, 0.43), grain);
60    var c = mix(grass, rock, smoothstep(0.18, 0.32, steep));
61    let snow_line = 95.0 + grain * 20.0;
62    c = mix(c, vec3<f32>(0.92, 0.94, 0.97), smoothstep(snow_line, snow_line + 8.0, s.world.y) * (1.0 - smoothstep(0.35, 0.55, steep)));
63    out.albedo = c;
64    out.roughness = 0.9;
65    return out;
66}";
67
68/// A cell's tile: still being built, or in the world.
69enum Tile {
70    Building(Job<TerrainTileData>),
71    Live(Entity, TerrainTile),
72}
73
74/// Heights for the tile whose corner is `corner`, 2 m apart.
75fn tile_data(corner: Vec3) -> TerrainTileData {
76    let heights: Vec<f32> = (0..N * N)
77        .map(|i| height(corner.x + (i % N) as f32 * 2.0, corner.z + (i / N) as f32 * 2.0))
78        .collect();
79    TerrainTileData::new(&heights, TILE)
80}
81
82fn main() {
83    let bench = std::env::var("BENCH").is_ok();
84    let shot = std::env::var("SHOT").ok();
85    let mut cells = CellStreamer::new(TILE, LOAD, KEEP);
86    let mut tiles: HashMap<(i32, i32), Tile> = HashMap::new();
87    let mut ground: Option<Material> = None;
88    let (mut yaw, mut speed, mut altitude) = (0.4f32, if bench { 600.0f32 } else { 120.0 }, 90.0f32);
89    // The streaming's cost on the game's thread, for BENCH.
90    let (mut uploaded, mut freed, mut upload_time, mut worst_upload) = (0usize, 0usize, 0.0f64, 0.0f64);
91    Game::new("Crabby Engine: streaming", 1280, 720)
92        .vsync(!bench)
93        .system(move |ctx: &mut Ctx| {
94            if ctx.key_pressed(Key::Escape) {
95                ctx.quit();
96            }
97            let dt = ctx.dt();
98            if ctx.key_down(Key::Left) {
99                yaw += dt * 0.8;
100            }
101            if ctx.key_down(Key::Right) {
102                yaw -= dt * 0.8;
103            }
104            if ctx.key_down(Key::W) {
105                speed = (speed + 80.0 * dt).min(600.0);
106            }
107            if ctx.key_down(Key::S) {
108                speed = (speed - 80.0 * dt).max(0.0);
109            }
110            if ctx.key_down(Key::E) {
111                altitude += 40.0 * dt;
112            }
113            if ctx.key_down(Key::Q) {
114                altitude = (altitude - 40.0 * dt).max(5.0);
115            }
116            // Fly on, keeping above the hills ahead.
117            let rotation = Quat::from_rotation_y(yaw);
118            let mut at = ctx.camera3d.position + rotation * Vec3::NEG_Z * speed * dt;
119            let ahead = at + rotation * Vec3::NEG_Z * 120.0;
120            let floor = height(at.x, at.z).max(height(ahead.x, ahead.z));
121            at.y += (floor + altitude - at.y) * (dt * 1.5).min(1.0);
122            ctx.camera3d.position = at;
123            ctx.camera3d.rotation = rotation * Quat::from_rotation_x(-0.22);
124
125            // Which tiles to start and drop.
126            cells.update(at, STARTS);
127            for &cell in cells.to_load() {
128                let corner = cells.corner(cell);
129                tiles.insert(cell, Tile::Building(Job::spawn(move || tile_data(corner))));
130            }
131            for cell in cells.to_drop() {
132                // A job still building is dropped with its result.
133                if let Some(Tile::Live(entity, tile)) = tiles.remove(cell) {
134                    ctx.world.despawn(entity);
135                    ctx.free_terrain_tile(tile);
136                    freed += 1;
137                }
138            }
139            // Finished tiles go on the GPU, a few a frame.
140            let ground = *ground.get_or_insert_with(|| {
141                let shader = ctx.create_shader("ground", GROUND);
142                ctx.create_material(MaterialDesc {
143                    shader: Some(shader),
144                    ..MaterialDesc::color(Color::WHITE)
145                })
146            });
147            let mut uploads = 0;
148            for (&cell, tile) in tiles.iter_mut() {
149                if uploads == UPLOADS {
150                    break;
151                }
152                if let Tile::Building(job) = tile
153                    && let Some(data) = job.take()
154                {
155                    let start = Instant::now();
156                    let made = ctx.upload_terrain_tile(&data, ground);
157                    let entity = ctx.world.spawn((Transform3D::at(cells.corner(cell)), made));
158                    let took = start.elapsed().as_secs_f64() * 1000.0;
159                    upload_time += took;
160                    worst_upload = worst_upload.max(took);
161                    uploaded += 1;
162                    uploads += 1;
163                    *tile = Tile::Live(entity, made);
164                }
165            }
166
167            if ctx.frame() == 400
168                && let Some(path) = &shot
169            {
170                ctx.save_screenshot(path);
171            }
172            if ctx.frame() == 402 && shot.is_some() {
173                ctx.quit();
174            }
175            if bench && ctx.frame() == 3000 {
176                let live = tiles.values().filter(|t| matches!(t, Tile::Live(..))).count();
177                println!(
178                    "{uploaded} tiles uploaded ({:.2} ms each, worst {worst_upload:.2} ms), {freed} freed, {live} live; {:?}",
179                    upload_time / uploaded.max(1) as f64,
180                    ctx.render_stats()
181                );
182                ctx.quit();
183            }
184        })
185        .run(move |ctx| {
186            ctx.post.bloom = 0.08;
187            ctx.lighting.sun_direction = Sky::sun_direction_at(15.5);
188            ctx.lighting.shadow_distance = 500.0;
189            ctx.camera3d.far = LOAD + TILE;
190            // Fog thick enough that tiles appear behind it, not at an edge.
191            ctx.sky.fog_density = 0.0007;
192            ctx.sky.fog_height = 40.0;
193            ctx.sky.fog_falloff = 90.0;
194            ctx.camera3d.position = vec3(0.0, height(0.0, 0.0) + 60.0, 0.0);
195        });
196}