use crate::tile_source::DecodedImage;
use rustial_math::{tile_bounds_world, ElevationGrid, TileId};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct PreparedHillshadeRaster {
pub tile: TileId,
pub generation: u64,
pub image: DecodedImage,
}
pub fn prepare_hillshade_raster(
elevation: &ElevationGrid,
vertical_exaggeration: f64,
generation: u64,
) -> PreparedHillshadeRaster {
let width = elevation.width.max(1);
let height = elevation.height.max(1);
let mut data = vec![0u8; width as usize * height as usize * 4];
let bounds = tile_bounds_world(&elevation.tile);
let step_x = if width > 1 {
(bounds.max.position.x - bounds.min.position.x) / (width - 1) as f64
} else {
1.0
};
let step_y = if height > 1 {
(bounds.max.position.y - bounds.min.position.y) / (height - 1) as f64
} else {
1.0
};
for y in 0..height {
for x in 0..width {
let idx = (y * width + x) as usize;
let sample = |sx: i32, sy: i32| -> f64 {
let xx = sx.clamp(0, width.saturating_sub(1) as i32) as u32;
let yy = sy.clamp(0, height.saturating_sub(1) as i32) as u32;
elevation.data[(yy * width + xx) as usize] as f64 * vertical_exaggeration
};
let left = sample(x as i32 - 1, y as i32);
let right = sample(x as i32 + 1, y as i32);
let up = sample(x as i32, y as i32 - 1);
let down = sample(x as i32, y as i32 + 1);
let dzdx = ((right - left) / (2.0 * step_x.max(1e-6))) as f32;
let dzdy = ((up - down) / (2.0 * step_y.max(1e-6))) as f32;
let nx = -dzdx;
let ny = -dzdy;
let nz = 1.0f32;
let len = (nx * nx + ny * ny + nz * nz).sqrt().max(1e-6);
let normal = [nx / len, ny / len, nz / len];
let o = idx * 4;
data[o] = encode_signed_unit(normal[0]);
data[o + 1] = encode_signed_unit(normal[1]);
data[o + 2] = ((normal[2].clamp(0.0, 1.0) * 255.0).round()) as u8;
data[o + 3] = 255;
}
}
PreparedHillshadeRaster {
tile: elevation.tile,
generation,
image: DecodedImage {
width,
height,
data: Arc::new(data),
},
}
}
#[inline]
fn encode_signed_unit(v: f32) -> u8 {
((((v.clamp(-1.0, 1.0) * 0.5) + 0.5) * 255.0).round()) as u8
}
#[cfg(test)]
mod tests {
use super::*;
use rustial_math::ElevationGrid;
#[test]
fn flat_grid_encodes_upward_normal() {
let grid = ElevationGrid::flat(TileId::new(2, 1, 1), 2, 2);
let raster = prepare_hillshade_raster(&grid, 1.0, 7);
assert_eq!(raster.generation, 7);
assert_eq!(raster.image.width, 2);
assert_eq!(raster.image.height, 2);
for px in raster.image.data.chunks_exact(4) {
assert!((px[0] as i32 - 128).abs() <= 1);
assert!((px[1] as i32 - 128).abs() <= 1);
assert!(px[2] >= 254);
assert_eq!(px[3], 255);
}
}
}