Skip to main content

terrain/
terrain.rs

1//! Heightfield terrain: 5 x 5 tiles of 256 m (129 x 129 heights, 2 m
2//! apart), drawn coarser with distance, with grass, dirt and rock blended
3//! by a weight map, a lake, and an underwater screen effect.
4//!
5//!   `cargo run --release --example terrain --features 3d`
6//!
7//! WASD moves, Q and E go down and up, the arrows turn, U switches the
8//! underwater effect. `SHOT=path.png` saves the third frame and quits;
9//! `VIEW=high|low|under|cliff` picks where the camera starts, `HOUR=18.5` the
10//! time of day. `BENCH=1` prints the frame's cost after 300 frames and quits.
11
12use ponko::*;
13
14const TILES: i32 = 5;
15const N: usize = 129;
16const TILE: f32 = 256.0;
17const WATER: f32 = 2.0;
18
19/// Smooth value noise, 0 to 1.
20fn noise(x: f32, z: f32) -> f32 {
21    tiling_noise(x, z, i32::MAX)
22}
23
24/// Value noise that repeats every `period` cells, for textures that tile.
25fn tiling_noise(x: f32, z: f32, period: i32) -> f32 {
26    let hash = |i: i32, j: i32| {
27        let (i, j) = (i.rem_euclid(period), j.rem_euclid(period));
28        let h = (i.wrapping_mul(374_761_393) ^ j.wrapping_mul(668_265_263)) as u32;
29        let h = (h ^ (h >> 13)).wrapping_mul(1_274_126_177);
30        (h >> 8) as f32 / (1u32 << 24) as f32
31    };
32    let (i, j) = (x.floor() as i32, z.floor() as i32);
33    let (fx, fz) = (x - x.floor(), z - z.floor());
34    let (sx, sz) = (fx * fx * (3.0 - 2.0 * fx), fz * fz * (3.0 - 2.0 * fz));
35    let top = hash(i, j) + (hash(i + 1, j) - hash(i, j)) * sx;
36    let bottom = hash(i, j + 1) + (hash(i + 1, j + 1) - hash(i, j + 1)) * sx;
37    top + (bottom - top) * sz
38}
39
40/// Hills: a few octaves of noise, flattened in the valleys.
41fn height(x: f32, z: f32) -> f32 {
42    let mut h = 0.0;
43    let (mut scale, mut amp) = (1.0 / 300.0, 40.0);
44    for _ in 0..5 {
45        h += (noise(x * scale, z * scale) - 0.5) * amp;
46        scale *= 2.1;
47        amp *= 0.45;
48    }
49    h + 6.0
50}
51
52/// A ground layer: `base` color with two scales of noise in it.
53fn layer(base: [f32; 3], grain: f32) -> Vec<u8> {
54    let size = 256;
55    (0..size * size)
56        .flat_map(|i| {
57            let (x, y) = ((i % size) as f32, (i / size) as f32);
58            // Periods that divide the 256 pixels, so the layer tiles.
59            let n = tiling_noise(x / 16.0, y / 16.0, 16) * 0.6 + tiling_noise(x / 4.0, y / 4.0, 64) * 0.4;
60            let shade = 1.0 + (n - 0.5) * grain;
61            let c = base.map(|v| ((v * shade).clamp(0.0, 1.0) * 255.0) as u8);
62            [c[0], c[1], c[2], 255]
63        })
64        .collect()
65}
66
67fn ground(ctx: &mut Ctx) -> Material {
68    let layers = [
69        layer([0.24, 0.42, 0.16], 0.6),
70        layer([0.40, 0.31, 0.22], 0.7),
71        layer([0.46, 0.45, 0.42], 0.9),
72        layer([0.70, 0.62, 0.45], 0.4),
73    ];
74    let refs: Vec<&[u8]> = layers.iter().map(Vec::as_slice).collect();
75    let layers = ctx.create_texture_array(256, 256, &refs, TextureKind::Color);
76    // One weight map over the whole world, 2 m a pixel.
77    let cells = (TILES as usize * (N - 1)) / 2;
78    let area = TILES as f32 * TILE;
79    let origin = -area / 2.0;
80    let weights: Vec<u8> = (0..cells * cells)
81        .flat_map(|i| {
82            let x = origin + (i % cells) as f32 * area / cells as f32;
83            let z = origin + (i / cells) as f32 * area / cells as f32;
84            let h = height(x, z);
85            let slope = (height(x + 2.0, z) - height(x - 2.0, z)).hypot(height(x, z + 2.0) - height(x, z - 2.0)) / 4.0;
86            let sand = (1.0 - (h - WATER).abs() / 1.5).clamp(0.0, 1.0);
87            let rock = ((slope - 0.45) * 4.0).clamp(0.0, 1.0);
88            let dirt = ((h - 18.0) / 8.0).clamp(0.0, 1.0) * (1.0 - rock);
89            let grass = (1.0 - sand - rock - dirt).max(0.0);
90            [grass, dirt, rock, sand].map(|w| (w * 255.0) as u8)
91        })
92        .collect();
93    let weights = ctx.create_texture_array(cells as u32, cells as u32, &[&weights], TextureKind::Data);
94    let desc = TerrainMaterialDesc {
95        layer_size: [5.0, 4.0, 7.0, 3.0, 4.0, 4.0, 4.0, 4.0],
96        ..TerrainMaterialDesc::new(layers, weights, vec2(origin, origin), vec2(area, area))
97    };
98    ctx.create_terrain_material(&desc)
99}
100
101/// The lake: waves, the bottom seen through it, and the hills mirrored in
102/// it (custom_texture0, drawn by a target camera below the water).
103/// custom[0]: wave height, wave length, speed, clarity.
104const LAKE: &str = "
105fn vertex(v: Vertex) -> Vertex {
106    var out = v;
107    let p = v.position.xz / material.custom[0].y;
108    let t = frame.time.x * material.custom[0].z;
109    out.position.y += (sin(p.x + t) + sin(p.y * 1.3 + t * 0.8)) * material.custom[0].x;
110    return out;
111}
112
113fn surface(s: Surface) -> Surface {
114    var out = s;
115    let p = s.world.xz / material.custom[0].y;
116    let t = frame.time.x * material.custom[0].z;
117    let slope = vec2<f32>(cos(p.x + t), cos(p.y * 1.3 + t * 0.8) * 1.3) * material.custom[0].x / material.custom[0].y;
118    let fine = vec2<f32>(noise2(s.world.xz * 3.0 + t), noise2(s.world.zx * 3.0 - t)) - 0.5;
119    out.normal = normalize(vec3<f32>(-slope.x - fine.x * 0.15, 1.0, -slope.y - fine.y * 0.15));
120
121    let bent = s.screen + out.normal.xz * 25.0;
122    let depth = max(scene_distance(bent) - distance(s.world, frame.camera.xyz), 0.0);
123    let seen = exp(-depth * material.custom[0].w);
124    let fresnel = mix(0.02, 1.0, pow(1.0 - max(dot(out.normal, s.view), 0.0), 5.0));
125
126    // The mirror image, upside down, nudged by the ripples.
127    let uv = s.screen * frame.screen.zw;
128    let mirrored = vec2<f32>(uv.x, 1.0 - uv.y) + out.normal.xz * 0.008;
129    let reflected = textureSample(custom_texture0, material_sampler, mirrored).rgb;
130
131    out.emissive = scene_color(bent) * seen * (1.0 - fresnel) + reflected * fresnel;
132    out.albedo = s.albedo * (1.0 - seen) * (1.0 - fresnel);
133    out.alpha = 1.0;
134    out.roughness = 0.05;
135    return out;
136}";
137
138const UNDERWATER: &str = "
139fn effect(p: Pixel) -> vec3<f32> {
140    if frame.camera.y > material.custom[0].x {
141        return p.color;
142    }
143    let deep = vec3<f32>(0.02, 0.12, 0.18);
144    let fade = 1.0 - exp(-p.distance * 0.05);
145    return mix(p.color * vec3<f32>(0.6, 0.9, 1.0), deep, fade);
146}";
147
148fn main() {
149    let view = std::env::var("VIEW").unwrap_or_default();
150    let hour = std::env::var("HOUR").ok().and_then(|h| h.parse().ok()).unwrap_or(16.0);
151    // The first spot nearest the middle where the ground fits.
152    let spot = |fits: &dyn Fn(f32) -> bool| {
153        (0..200 * 200)
154            .map(|i| vec2((i % 200) as f32 * 4.0 - 400.0, (i / 200) as f32 * 4.0 - 400.0))
155            .filter(|p| fits(height(p.x, p.y)))
156            .min_by(|a, b| a.length().total_cmp(&b.length()))
157            .unwrap_or(Vec2::ZERO)
158    };
159    let (start, (mut yaw, mut pitch)) = match view.as_str() {
160        "low" => {
161            let p = spot(&|h| h > WATER + 4.0);
162            (vec3(p.x, height(p.x, p.y) + 1.8, p.y), (0.6f32, -0.08f32))
163        }
164        "under" => {
165            let p = spot(&|h| h < WATER - 2.0 && h > WATER - 5.0);
166            (vec3(p.x, WATER - 1.0, p.y), (0.6, -0.6))
167        }
168        "cliff" => {
169            // The steepest ground near the middle, seen from 30 m off its foot.
170            let slope = |p: Vec2| {
171                (height(p.x + 2.0, p.y) - height(p.x - 2.0, p.y)).hypot(height(p.x, p.y + 2.0) - height(p.x, p.y - 2.0))
172            };
173            let steep = (0..200 * 200)
174                .map(|i| vec2((i % 200) as f32 * 4.0 - 400.0, (i / 200) as f32 * 4.0 - 400.0))
175                .filter(|p| height(p.x, p.y) > WATER + 2.0)
176                .max_by(|a, b| slope(*a).total_cmp(&slope(*b)))
177                .unwrap_or(Vec2::ZERO);
178            // Downhill, away from the face.
179            let down = -vec2(
180                height(steep.x + 2.0, steep.y) - height(steep.x - 2.0, steep.y),
181                height(steep.x, steep.y + 2.0) - height(steep.x, steep.y - 2.0),
182            )
183            .normalize_or_zero();
184            let eye = steep + down * 30.0;
185            let at = vec3(eye.x, height(eye.x, eye.y).max(WATER) + 6.0, eye.y);
186            let to = vec3(steep.x, height(steep.x, steep.y), steep.y) - at;
187            (at, (f32::atan2(-to.x, -to.z), (to.y / to.length()).asin()))
188        }
189        _ => (vec3(-300.0, 90.0, 300.0), (-0.8, -0.25)),
190    };
191    let mut parked: Vec<ScreenEffect> = Vec::new();
192    let bench = std::env::var("BENCH").is_ok();
193    Game::new("Crabby Engine: terrain", 1280, 720)
194        .vsync(!bench)
195        .fullscreen(bench)
196        .system(move |ctx: &mut Ctx| {
197            if ctx.key_pressed(Key::Escape) {
198                ctx.quit();
199            }
200            let dt = ctx.dt();
201            if ctx.key_down(Key::Left) {
202                yaw += dt;
203            }
204            if ctx.key_down(Key::Right) {
205                yaw -= dt;
206            }
207            if ctx.key_down(Key::Up) {
208                pitch = (pitch + dt).min(1.4);
209            }
210            if ctx.key_down(Key::Down) {
211                pitch = (pitch - dt).max(-1.4);
212            }
213            let rotation = Quat::from_rotation_y(yaw) * Quat::from_rotation_x(pitch);
214            let forward = rotation * Vec3::NEG_Z;
215            let right = rotation * Vec3::X;
216            let speed = 60.0 * dt;
217            let mut go = Vec3::ZERO;
218            for (key, dir) in [
219                (Key::W, forward),
220                (Key::S, -forward),
221                (Key::D, right),
222                (Key::A, -right),
223                (Key::E, Vec3::Y),
224                (Key::Q, Vec3::NEG_Y),
225            ] {
226                if ctx.key_down(key) {
227                    go += dir;
228                }
229            }
230            ctx.camera3d.position += go * speed;
231            ctx.camera3d.rotation = rotation;
232            // The lake's mirror follows the camera.
233            if let Some(mirror) = ctx.target_cameras.first().map(|c| c.target) {
234                ctx.target_cameras[0] = TargetCamera::reflection(ctx.camera3d, mirror, WATER);
235            }
236            if ctx.key_pressed(Key::U) {
237                std::mem::swap(&mut ctx.effects, &mut parked);
238            }
239            if ctx.frame() == 3
240                && let Ok(path) = std::env::var("SHOT")
241            {
242                ctx.save_screenshot(path);
243            }
244            if ctx.frame() == 5 && std::env::var("SHOT").is_ok() {
245                ctx.quit();
246            }
247            if ctx.frame() == 300 {
248                let stats = ctx.render_stats();
249                println!("{stats:?} models drawn: {}", ctx.models_drawn());
250                if bench {
251                    ctx.quit();
252                }
253            }
254        })
255        .run(move |ctx| {
256            ctx.post.bloom = 0.1;
257            ctx.lighting.sun_direction = Sky::sun_direction_at(hour);
258            ctx.lighting.shadow_distance = 400.0;
259            ctx.camera3d.far = 3000.0;
260            ctx.sky.fog_density = 0.0015;
261            ctx.sky.fog_height = 0.0;
262            ctx.sky.fog_falloff = 60.0;
263
264            let ground = ground(ctx);
265            let half = TILES / 2;
266            for tz in -half..=half {
267                for tx in -half..=half {
268                    let corner = vec3(tx as f32 * TILE - TILE / 2.0, 0.0, tz as f32 * TILE - TILE / 2.0);
269                    let heights: Vec<f32> = (0..N * N)
270                        .map(|i| height(corner.x + (i % N) as f32 * 2.0, corner.z + (i / N) as f32 * 2.0))
271                        .collect();
272                    let tile = ctx.create_terrain_tile(&heights, TILE, ground);
273                    ctx.world.spawn((Transform3D::at(corner), tile));
274                }
275            }
276
277            // Half the window's size is plenty for a rippled mirror.
278            let size = ctx.window_size();
279            let mirror = ctx.create_render_target((size.x as u32 / 2).max(1), (size.y as u32 / 2).max(1));
280            ctx.target_cameras
281                .push(TargetCamera::reflection(ctx.camera3d, mirror, WATER));
282            let water = ctx.create_shader("lake", LAKE);
283            let lake = ctx.create_material(MaterialDesc {
284                shader: Some(water),
285                blend: Blend::Transparent,
286                custom_textures: [Some(mirror), None],
287                custom: [vec4(0.06, 1.5, 1.2, 1.2), Vec4::ZERO, Vec4::ZERO, Vec4::ZERO],
288                ..MaterialDesc::color(Color::hex(0x1E4D6B))
289            });
290            let surface = ctx.create_mesh(&MeshData::plane(TILES as f32 * TILE), lake);
291            ctx.world
292                .spawn((Transform3D::at(vec3(0.0, WATER, 0.0)), Model::new(surface)));
293
294            let underwater = ctx.create_effect("underwater", UNDERWATER);
295            let level = ctx.create_material(MaterialDesc {
296                custom: [vec4(WATER, 0.0, 0.0, 0.0), Vec4::ZERO, Vec4::ZERO, Vec4::ZERO],
297                ..Default::default()
298            });
299            ctx.effects.push(ScreenEffect::new(underwater).with_material(level));
300
301            ctx.camera3d.position = start;
302        });
303}