Skip to main content

banished_terrain/
surface.rs

1//! Packed per-cell surface data, uploaded as the terrain's `size × size` image and kept at
2//! MapComponent.
3//!
4//! The image is BGRA8 (format 5), so in memory each texel is the bytes `[dirt, rock, light, snow]`
5//! and the terrain pixel shader (`TerrainMaterial_ps`, sampler `transitionMap`) reads them as `.z`,
6//! `.y`, `.x`, `.w`:
7//! - Dirt (`.z`, lerps grass → dirt): 255 below height -0.5, otherwise squared Perlin noise;
8//!   raised towards 255 above height 8.
9//! - Rock (`.y`, lerps → triplanar rock): from the cell normal's up component (0 when nearly flat,
10//!   255 when steep); raised towards 255 above height 7.
11//! - Light (`.x`, multiplies the ambient term): one minus the mean facing of a radius-10
12//!   neighbourhood, scaled by the smoothed normal's up component.
13//! - Snow (`.w`): left 0 here.
14//!
15//! Float operations follow the disassembly order so results are bit-exact.
16//!
17//! Original: `FUN_1400d4910`, `FUN_1400b1c50`.
18// +0x140
19
20use super::Generator;
21use banished_core::noise::PerlinNoise;
22
23const NOISE_SCALE: f32 = 0.25;
24const NOISE_OCTAVES: f32 = 4.0;
25const LOW: f32 = -0.5;
26const FLAT: f32 = 0.999;
27const STEEP: f32 = 0.85;
28/// `DAT_1401787f0`, the f32 difference `0.999 - 0.85`.
29const SLOPE_RANGE: f32 = FLAT - STEEP;
30const R_HEIGHT: f32 = 8.0;
31const G_HEIGHT: f32 = 7.0;
32const HEIGHT_RAMP: f32 = 3.0;
33const NORMAL_Z: f32 = 2.0;
34
35type Vec3 = [f32; 3];
36
37/// `Math::Vector3::Normalize`: multiply by `1 / length`; a zero vector is returned unchanged.
38fn normalize(v: Vec3) -> Vec3 {
39    let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
40    if len == 0.0 {
41        return v;
42    }
43    let inv = 1.0 / len;
44    [v[0] * inv, v[1] * inv, v[2] * inv]
45}
46
47fn dot(a: Vec3, b: Vec3) -> f32 {
48    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
49}
50
51/// `cvttss2si` to 64 bits, keeping the low byte.
52fn byte(v: f32) -> u32 {
53    (v as i64 as u64 & 0xff) as u32
54}
55
56/// `clamp((h - from) / 3, 0, 1) * 255` as a byte.
57fn height_ramp(h: f32, from: f32) -> u32 {
58    // comiss order in the original keeps NaN, as `clamp` does.
59    let t = ((h - from) / HEIGHT_RAMP).clamp(0.0, 1.0);
60    byte(t * 255.0)
61}
62
63struct Cell {
64    pos: Vec3,
65    normal: Vec3,
66}
67
68impl Generator {
69    fn sample(&self, x: i32, y: i32) -> f32 {
70        let hi = self.size;
71        let x = x.clamp(0, hi);
72        let y = y.clamp(0, hi);
73        self.grid[(y * (hi + 1) + x) as usize]
74    }
75
76    /// `FUN_1400d4620`: the normal from central differences, with 2 as the up component.
77    fn cell_normal(&self, x: i32, y: i32) -> Vec3 {
78        let dx = self.sample(x + 1, y) - self.sample(x - 1, y);
79        let dy = self.sample(x, y + 1) - self.sample(x, y - 1);
80        normalize([-dx, -dy, NORMAL_Z])
81    }
82
83    /// Build the packed surface image (`dirt | rock << 8 | light << 16`). Consumes two random
84    /// values, so it must run after `run` exactly as the game does.
85    ///
86    /// Original: `FUN_1400d4910`.
87    pub fn surface_map(&mut self) -> Vec<u32> {
88        let seed_y = self.rng.rand_tol(0xffffff);
89        let seed_x = self.rng.rand_tol(0xffffff);
90        let noise = PerlinNoise::new(NOISE_SCALE, NOISE_OCTAVES, seed_x, seed_y);
91        let n = self.size;
92        let mut cells = Vec::with_capacity((n * n) as usize);
93        for y in 0..n {
94            for x in 0..n {
95                cells.push(Cell {
96                    pos: [x as f32, y as f32, self.sample(x, y)],
97                    normal: self.cell_normal(x, y),
98                });
99            }
100        }
101
102        // Each texel depends only on the finished cells (no Random), so the rows are computed
103        // on several threads (native); every row is written by one thread, in the same order of
104        // operations as one pass.
105        let mut out = vec![0u32; (n * n) as usize];
106        let mut rows: Vec<(i32, &mut [u32])> = out
107            .chunks_mut(n as usize)
108            .enumerate()
109            .map(|(y, row)| (y as i32, row))
110            .collect();
111        banished_formats::par::map_mut(&mut rows, |(y, row)| {
112            for x in 0..n {
113                row[x as usize] = surface_texel(&cells, &noise, n, x, *y);
114            }
115        });
116        out
117    }
118}
119
120/// One texel of `surface_map`: dirt from the noise below `LOW`, rock from the slope, both
121/// raised by height, then the light (occlusion over a radius of 10 cells times the smoothed
122/// normal's z).
123fn surface_texel(cells: &[Cell], noise: &PerlinNoise, n: i32, x: i32, y: i32) -> u32 {
124    let c = &cells[(y * n + x) as usize];
125    let h = c.pos[2];
126    let up = c.normal[2];
127    let mut r = if LOW <= h {
128        let v = noise.generate(x as f32 * NOISE_SCALE, y as f32 * NOISE_SCALE);
129        let v = v * 0.5 + 0.5;
130        byte(v * v * 255.0)
131    } else {
132        0xff
133    };
134    let mut g = if FLAT > up && up > STEEP {
135        byte((1.0 - (up - STEEP) / SLOPE_RANGE) * 255.0)
136    } else if STEEP >= up {
137        0xff
138    } else {
139        0
140    };
141    if h > R_HEIGHT {
142        r = r.max(height_ramp(h, R_HEIGHT));
143    }
144    if h > G_HEIGHT {
145        g = g.max(height_ramp(h, G_HEIGHT));
146    }
147    let mut occlusion = 0.0f32;
148    let mut count = 0.0f32;
149    let mut smooth = [0.0f32; 3];
150    // The original visits rows -10..=9 but columns -10..=10.
151    for dy in -10..10 {
152        let ny = y + dy;
153        if ny < 0 || ny >= n {
154            continue;
155        }
156        for dx in -10..=10 {
157            let nx = x + dx;
158            if nx < 0 || nx >= n || dx * dx + dy * dy > 100 {
159                continue;
160            }
161            let o = &cells[(ny * n + nx) as usize];
162            let dir = normalize([
163                o.pos[0] - c.pos[0],
164                o.pos[1] - c.pos[1],
165                o.pos[2] - c.pos[2],
166            ]);
167            let d = dot(c.normal, dir);
168            occlusion += if d > 0.0 { d } else { 0.0 };
169            count += 1.0;
170            if dx.abs() <= 1 && dy.abs() <= 1 {
171                for (s, n) in smooth.iter_mut().zip(o.normal) {
172                    *s += n;
173                }
174            }
175        }
176    }
177    let smooth = normalize(smooth);
178    let b = byte((1.0 - occlusion / count) * smooth[2] * 255.0);
179    ((g & 0xff) << 8 | (r & 0xff)) | b << 16
180}