normal_heights/
lib.rs

1use image::{DynamicImage, GrayImage, RgbImage};
2
3// Why 6.0? Because that was the whole number that gave the closest results to
4// the topographic map and normal map I was using as reference material.
5// Considering my primary intent for creating this library is to create
6// alternatives to those two files to use in the program they came with, it
7// seemed like a good idea to match them, at least approximately.
8pub const DEFAULT_STRENGTH: f32 = 6.0;
9
10struct AdjPixels {
11    nw: f32, n : f32, ne: f32,
12     w: f32,           e: f32,
13    sw: f32, s : f32, se: f32,
14}
15
16impl AdjPixels {
17    /// edge pixels are duplicated when necessary
18    #[allow(clippy::many_single_char_names, clippy::absurd_extreme_comparisons)]
19    fn new(x: u32, y: u32, img: &GrayImage) -> Self {
20        let n = if y <= 0 { 0 } else { y-1 };
21        let s = if y >= (img.height()-1) { img.height()-1 } else { y+1 };
22        let w = if x <= 0 { 0 } else { x-1 };
23        let e = if x >= (img.width()-1) { img.width()-1 } else { x+1 };
24
25        AdjPixels {
26            nw: fetch_pixel(n,w,img),
27            n : fetch_pixel(n,x,img),
28            ne: fetch_pixel(n,e,img),
29             w: fetch_pixel(y,w,img),
30
31             e: fetch_pixel(y,e,img),
32            sw: fetch_pixel(s,w,img),
33            s : fetch_pixel(s,x,img),
34            se: fetch_pixel(s,e,img),
35        }
36    }
37
38    /// Calculates the normals along the x-axis. Usually used for the red
39    /// channel after normalization..
40    fn x_normals(&self) -> f32 {
41        -(       self.se-self.sw
42          + 2.0*(self.e -self.w )
43          +      self.ne-self.nw
44        )
45    }
46
47    /// Calculates the normals along the y-axis. Usually used for the green
48    /// channel after normalization.
49    fn y_normals(&self) -> f32 {
50        -(       self.nw-self.sw
51          + 2.0*(self.n -self.s )
52          +      self.ne-self.se
53        )
54    }
55}
56
57/// Fetches the pixel at (x,y) and returns its value as an f32 scaled to between
58/// 0.0 and 1.0. Coordinate parameters are reversed from usual to better match
59///   compass directions.
60fn fetch_pixel(y: u32, x: u32, img: &GrayImage) -> f32 {
61    (img.get_pixel(x,y)[0] as f32)/255.0
62}
63
64/// Creates the normal mapping from the given image with
65/// [DEFAULT_STRENGTH](constant.DEFAULT_STRENGTH.html)
66pub fn map_normals(img: &DynamicImage) -> RgbImage {
67    map_normals_with_strength(img, DEFAULT_STRENGTH)
68}
69
70/// Creates the normal mapping from the given image with the given strength.
71pub fn map_normals_with_strength(img: &DynamicImage, strength: f32) -> RgbImage {
72    let img = img.clone().into_luma8();
73    let mut normal_map = RgbImage::new(img.width(), img.height());
74
75    for (x, y, p) in normal_map.enumerate_pixels_mut() {
76        let mut new_p = [0.0, 0.0, 0.0];
77        let s = AdjPixels::new(x,y,&img);
78
79        new_p[0] = s.x_normals();
80        new_p[1] = s.y_normals();
81        new_p[2] = 1.0/strength;
82
83        let new_p = scale_normalized_to_0_to_1(&normalize(new_p));
84
85        p[0] = (new_p[0]*255.0) as u8;
86        p[1] = (new_p[1]*255.0) as u8;
87        p[2] = (new_p[2]*255.0) as u8;
88    }
89    normal_map
90}
91
92fn normalize(v: [f32;3]) -> [f32;3] {
93    let v_mag = (v[0]*v[0] + v[1]*v[1] + v[2]*v[2]).sqrt();
94    [v[0]/v_mag, v[1]/v_mag, v[2]/v_mag]
95}
96
97fn scale_normalized_to_0_to_1(v: &[f32;3]) -> [f32;3] {
98    [
99        v[0] * 0.5 + 0.5,
100        v[1] * 0.5 + 0.5,
101        v[2] * 0.5 + 0.5,
102    ]
103}
104
105#[cfg(test)]
106mod tests {
107    use super::map_normals_with_strength;
108
109    #[test]
110    fn shapes_bmp_regression_test() {
111        let height_map = image::open("./samples/shapes.bmp").unwrap();
112        let test_normal = map_normals_with_strength(&height_map, 3.14);
113        let reference_normal = image::open("./samples/shapes_normal_strength_3.14.png").unwrap().into_rgb8();
114        assert_eq!(reference_normal.width(), test_normal.width());
115        assert_eq!(reference_normal.height(), test_normal.height());
116        for (ref_pixel, test_pixel) in reference_normal.pixels().zip(test_normal.pixels()) {
117            assert_eq!(ref_pixel, test_pixel);
118        }
119    }
120
121    #[test]
122    fn world_regression_test() {
123        let height_map = image::open("./samples/gebco_08_rev_elev_1080x540.png").unwrap();
124        let test_normal = map_normals_with_strength(&height_map, 6.0);
125        let reference_normal = image::open("./samples/gebco_08_rev_elev_1080x540_normal.png").unwrap().into_rgb8();
126        assert_eq!(reference_normal.width(), test_normal.width());
127        assert_eq!(reference_normal.height(), test_normal.height());
128        for (ref_pixel, test_pixel) in reference_normal.pixels().zip(test_normal.pixels()) {
129            assert_eq!(ref_pixel, test_pixel);
130        }
131    }
132}