1use crate::Terrain;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Layer {
16 Shaded,
18 Height,
20 Flow,
22 Rivers,
24 Lakes,
26 Ocean,
28}
29
30impl Layer {
31 pub const ALL: [Layer; 6] = [
33 Layer::Shaded,
34 Layer::Height,
35 Layer::Flow,
36 Layer::Rivers,
37 Layer::Lakes,
38 Layer::Ocean,
39 ];
40
41 pub fn name(self) -> &'static str {
44 match self {
45 Layer::Shaded => "shaded",
46 Layer::Height => "height",
47 Layer::Flow => "flow",
48 Layer::Rivers => "rivers",
49 Layer::Lakes => "lakes",
50 Layer::Ocean => "ocean",
51 }
52 }
53}
54
55impl std::fmt::Display for Layer {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.write_str(self.name())
58 }
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct UnknownLayer(pub String);
64
65impl std::fmt::Display for UnknownLayer {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(
68 f,
69 "unknown layer '{}' (expected one of: shaded, height, flow, rivers, lakes, ocean)",
70 self.0
71 )
72 }
73}
74
75impl std::error::Error for UnknownLayer {}
76
77impl std::str::FromStr for Layer {
78 type Err = UnknownLayer;
79
80 fn from_str(s: &str) -> Result<Self, Self::Err> {
81 Layer::ALL
82 .into_iter()
83 .find(|l| l.name() == s)
84 .ok_or_else(|| UnknownLayer(s.to_string()))
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
90pub enum RenderStyle {
91 #[default]
94 Shaded,
95 Greyscale,
97}
98
99type Rgb = [f32; 3];
100
101fn lerp(a: Rgb, b: Rgb, t: f32) -> Rgb {
102 [
103 a[0] + (b[0] - a[0]) * t,
104 a[1] + (b[1] - a[1]) * t,
105 a[2] + (b[2] - a[2]) * t,
106 ]
107}
108
109fn ramp(stops: &[(f32, Rgb)], t: f32) -> Rgb {
111 let t = t.clamp(0.0, 1.0);
112 for pair in stops.windows(2) {
113 let (t0, c0) = pair[0];
114 let (t1, c1) = pair[1];
115 if t <= t1 {
116 let f = if t1 > t0 { (t - t0) / (t1 - t0) } else { 0.0 };
117 return lerp(c0, c1, f.clamp(0.0, 1.0));
118 }
119 }
120 stops.last().unwrap().1
121}
122
123const WATER_RAMP: &[(f32, Rgb)] = &[
124 (0.0, [0.05, 0.15, 0.30]), (0.7, [0.15, 0.35, 0.55]),
126 (1.0, [0.30, 0.55, 0.75]), ];
128
129const LAND_RAMP: &[(f32, Rgb)] = &[
130 (0.00, [0.80, 0.73, 0.55]), (0.04, [0.55, 0.65, 0.35]), (0.30, [0.25, 0.45, 0.22]), (0.55, [0.45, 0.42, 0.36]), (0.75, [0.60, 0.58, 0.55]), (0.90, [0.93, 0.93, 0.95]), (1.00, [1.00, 1.00, 1.00]), ];
138
139const RIVER_COLOR: Rgb = [0.20, 0.45, 0.70];
140const LAKE_COLOR: Rgb = [0.17, 0.40, 0.62];
141
142pub fn to_rgb(terrain: &Terrain, style: RenderStyle) -> Vec<u8> {
144 let w = terrain.width();
145 let h = terrain.height();
146 let sea = terrain.sea_level;
147 let mut data = Vec::with_capacity(w * h * 3);
148
149 for y in 0..h {
150 for x in 0..w {
151 let v = terrain.heightmap[(x, y)];
152
153 let rgb = match style {
154 RenderStyle::Greyscale => {
155 let g = v.clamp(0.0, 1.0);
156 [g, g, g]
157 }
158 RenderStyle::Shaded => {
159 if terrain.rivers[(x, y)] == 1 {
160 let shade = 0.8 + 0.2 * hillshade(terrain, x, y);
163 [
164 RIVER_COLOR[0] * shade,
165 RIVER_COLOR[1] * shade,
166 RIVER_COLOR[2] * shade,
167 ]
168 } else if terrain.lakes[(x, y)] == 1 {
169 LAKE_COLOR
170 } else if terrain.ocean[(x, y)] == 1 {
171 ramp(WATER_RAMP, if sea > 0.0 { v / sea } else { 0.0 })
172 } else {
173 let t = (v - sea) / (1.0 - sea);
174 let base = ramp(LAND_RAMP, t);
175 let shade = 0.55 + 0.45 * hillshade(terrain, x, y);
176 [base[0] * shade, base[1] * shade, base[2] * shade]
177 }
178 }
179 };
180
181 data.push((rgb[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
182 data.push((rgb[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
183 data.push((rgb[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8);
184 }
185 }
186
187 data
188}
189
190fn hillshade(terrain: &Terrain, x: usize, y: usize) -> f32 {
193 let w = terrain.width();
194 let h = terrain.height();
195 let hm = &terrain.heightmap;
196
197 let xl = x.saturating_sub(1);
198 let xr = (x + 1).min(w - 1);
199 let yu = y.saturating_sub(1);
200 let yd = (y + 1).min(h - 1);
201
202 let z_scale = 0.15 * w.max(h) as f32;
204
205 let dzdx = (hm[(xr, y)] - hm[(xl, y)]) / (xr - xl).max(1) as f32 * z_scale;
206 let dzdy = (hm[(x, yd)] - hm[(x, yu)]) / (yd - yu).max(1) as f32 * z_scale;
207
208 let light = [-0.5f32, -0.5, std::f32::consts::FRAC_1_SQRT_2];
210
211 let norm = 1.0 / (dzdx * dzdx + dzdy * dzdy + 1.0).sqrt();
212 let dot = (-dzdx * light[0] - dzdy * light[1] + light[2]) * norm;
213
214 dot.clamp(0.0, 1.0)
215}
216
217#[cfg(feature = "png")]
218mod png_output {
219 use std::fs::File;
220 use std::io::BufWriter;
221 use std::path::Path;
222
223 use super::{to_rgb, RenderStyle};
224 use crate::Terrain;
225
226 #[derive(Debug)]
228 pub enum RenderError {
229 Io(std::io::Error),
230 Png(png::EncodingError),
231 }
232
233 impl std::fmt::Display for RenderError {
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 match self {
236 RenderError::Io(e) => write!(f, "io error: {e}"),
237 RenderError::Png(e) => write!(f, "png encoding error: {e}"),
238 }
239 }
240 }
241
242 impl std::error::Error for RenderError {}
243
244 impl From<std::io::Error> for RenderError {
245 fn from(e: std::io::Error) -> Self {
246 RenderError::Io(e)
247 }
248 }
249
250 impl From<png::EncodingError> for RenderError {
251 fn from(e: png::EncodingError) -> Self {
252 RenderError::Png(e)
253 }
254 }
255
256 pub fn save_layer_png<P: AsRef<Path>>(
260 terrain: &Terrain,
261 layer: super::Layer,
262 path: P,
263 ) -> Result<(), RenderError> {
264 use super::Layer;
265
266 let (w, h) = (terrain.width() as u32, terrain.height() as u32);
267
268 match layer {
269 Layer::Shaded => save_png(terrain, path, RenderStyle::Shaded),
270 Layer::Height => write_gray16(&terrain.heightmap, w, h, path),
271 Layer::Flow => write_gray16(&terrain.flow, w, h, path),
272 Layer::Rivers => write_mask(&terrain.rivers, w, h, path),
273 Layer::Lakes => write_mask(&terrain.lakes, w, h, path),
274 Layer::Ocean => write_mask(&terrain.ocean, w, h, path),
275 }
276 }
277
278 fn write_gray16<P: AsRef<Path>>(
280 grid: &crate::Grid<f32>,
281 w: u32,
282 h: u32,
283 path: P,
284 ) -> Result<(), RenderError> {
285 let mut data = Vec::with_capacity(grid.len() * 2);
286 for v in grid.iter() {
287 let q = (v.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16;
288 data.extend_from_slice(&q.to_be_bytes());
289 }
290 write_png(
291 path,
292 w,
293 h,
294 png::ColorType::Grayscale,
295 png::BitDepth::Sixteen,
296 &data,
297 )
298 }
299
300 fn write_mask<P: AsRef<Path>>(
302 grid: &crate::Grid<u8>,
303 w: u32,
304 h: u32,
305 path: P,
306 ) -> Result<(), RenderError> {
307 let data: Vec<u8> = grid.iter().map(|v| if *v == 0 { 0 } else { 255 }).collect();
308 write_png(
309 path,
310 w,
311 h,
312 png::ColorType::Grayscale,
313 png::BitDepth::Eight,
314 &data,
315 )
316 }
317
318 fn write_png<P: AsRef<Path>>(
319 path: P,
320 w: u32,
321 h: u32,
322 color: png::ColorType,
323 depth: png::BitDepth,
324 data: &[u8],
325 ) -> Result<(), RenderError> {
326 let file = File::create(path)?;
327 let mut encoder = png::Encoder::new(BufWriter::new(file), w, h);
328 encoder.set_color(color);
329 encoder.set_depth(depth);
330 encoder.write_header()?.write_image_data(data)?;
331 Ok(())
332 }
333
334 pub fn save_png<P: AsRef<Path>>(
336 terrain: &Terrain,
337 path: P,
338 style: RenderStyle,
339 ) -> Result<(), RenderError> {
340 let data = to_rgb(terrain, style);
341
342 let file = File::create(path)?;
343 let writer = BufWriter::new(file);
344
345 let mut encoder =
346 png::Encoder::new(writer, terrain.width() as u32, terrain.height() as u32);
347 encoder.set_color(png::ColorType::Rgb);
348 encoder.set_depth(png::BitDepth::Eight);
349
350 let mut png_writer = encoder.write_header()?;
351 png_writer.write_image_data(&data)?;
352
353 Ok(())
354 }
355}
356
357#[cfg(feature = "png")]
358pub use png_output::{save_layer_png, save_png, RenderError};
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::Terrain;
364
365 fn small_terrain() -> Terrain {
366 Terrain::builder().size(32, 32).seed(1).generate()
367 }
368
369 #[test]
370 fn rgb_buffer_has_expected_size() {
371 let t = small_terrain();
372 assert_eq!(to_rgb(&t, RenderStyle::Shaded).len(), 32 * 32 * 3);
373 assert_eq!(to_rgb(&t, RenderStyle::Greyscale).len(), 32 * 32 * 3);
374 }
375
376 #[test]
377 fn rendering_is_deterministic() {
378 let a = to_rgb(&small_terrain(), RenderStyle::Shaded);
379 let b = to_rgb(&small_terrain(), RenderStyle::Shaded);
380 assert_eq!(a, b);
381 }
382
383 #[test]
384 fn greyscale_encodes_elevation() {
385 let t = small_terrain();
386 let rgb = to_rgb(&t, RenderStyle::Greyscale);
387 let expected = (t.heightmap[(0, 0)].clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
388 assert_eq!(rgb[0], expected);
389 assert_eq!(rgb[1], expected);
390 assert_eq!(rgb[2], expected);
391 }
392}