rustial_engine/terrain/
hillshade.rs1use crate::tile_source::DecodedImage;
4use rustial_math::{tile_bounds_world, ElevationGrid, TileId};
5use std::sync::Arc;
6
7#[derive(Debug, Clone)]
18pub struct PreparedHillshadeRaster {
19 pub tile: TileId,
21 pub generation: u64,
23 pub image: DecodedImage,
25}
26
27pub fn prepare_hillshade_raster(
29 elevation: &ElevationGrid,
30 vertical_exaggeration: f64,
31 generation: u64,
32) -> PreparedHillshadeRaster {
33 let width = elevation.width.max(1);
34 let height = elevation.height.max(1);
35 let mut data = vec![0u8; width as usize * height as usize * 4];
36
37 let bounds = tile_bounds_world(&elevation.tile);
38 let step_x = if width > 1 {
39 (bounds.max.position.x - bounds.min.position.x) / (width - 1) as f64
40 } else {
41 1.0
42 };
43 let step_y = if height > 1 {
44 (bounds.max.position.y - bounds.min.position.y) / (height - 1) as f64
45 } else {
46 1.0
47 };
48
49 for y in 0..height {
50 for x in 0..width {
51 let idx = (y * width + x) as usize;
52 let sample = |sx: i32, sy: i32| -> f64 {
53 let xx = sx.clamp(0, width.saturating_sub(1) as i32) as u32;
54 let yy = sy.clamp(0, height.saturating_sub(1) as i32) as u32;
55 elevation.data[(yy * width + xx) as usize] as f64 * vertical_exaggeration
56 };
57
58 let left = sample(x as i32 - 1, y as i32);
59 let right = sample(x as i32 + 1, y as i32);
60 let up = sample(x as i32, y as i32 - 1);
61 let down = sample(x as i32, y as i32 + 1);
62
63 let dzdx = ((right - left) / (2.0 * step_x.max(1e-6))) as f32;
64 let dzdy = ((up - down) / (2.0 * step_y.max(1e-6))) as f32;
65
66 let nx = -dzdx;
67 let ny = -dzdy;
68 let nz = 1.0f32;
69 let len = (nx * nx + ny * ny + nz * nz).sqrt().max(1e-6);
70 let normal = [nx / len, ny / len, nz / len];
71
72 let o = idx * 4;
73 data[o] = encode_signed_unit(normal[0]);
74 data[o + 1] = encode_signed_unit(normal[1]);
75 data[o + 2] = ((normal[2].clamp(0.0, 1.0) * 255.0).round()) as u8;
76 data[o + 3] = 255;
77 }
78 }
79
80 PreparedHillshadeRaster {
81 tile: elevation.tile,
82 generation,
83 image: DecodedImage {
84 width,
85 height,
86 data: Arc::new(data),
87 },
88 }
89}
90
91#[inline]
92fn encode_signed_unit(v: f32) -> u8 {
93 ((((v.clamp(-1.0, 1.0) * 0.5) + 0.5) * 255.0).round()) as u8
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use rustial_math::ElevationGrid;
100
101 #[test]
102 fn flat_grid_encodes_upward_normal() {
103 let grid = ElevationGrid::flat(TileId::new(2, 1, 1), 2, 2);
104 let raster = prepare_hillshade_raster(&grid, 1.0, 7);
105 assert_eq!(raster.generation, 7);
106 assert_eq!(raster.image.width, 2);
107 assert_eq!(raster.image.height, 2);
108 for px in raster.image.data.chunks_exact(4) {
109 assert!((px[0] as i32 - 128).abs() <= 1);
110 assert!((px[1] as i32 - 128).abs() <= 1);
111 assert!(px[2] >= 254);
112 assert_eq!(px[3], 255);
113 }
114 }
115}