Skip to main content

steel_worldgen/
utils.rs

1use crate::density::traits::ColumnCache;
2use crate::density::traits::NoiseSettings;
3use crate::{
4    density::DimensionNoises,
5    noise::{Aquifer, AquiferResult, preliminary_surface_level},
6};
7use steel_math::lerp;
8
9/// `getBaseHeight(WORLD_SURFACE_WG)`-compatible height scan. Uses
10/// `preliminary_surface_level + 16` as an upper bound to avoid scanning empty
11/// upper atmosphere, and the cell-based iterator so 8-corner density
12/// evaluations are shared across Y values in each cell.
13///
14/// Exposed for `GenerationContext`.
15pub(crate) fn column_base_height<N: DimensionNoises>(
16    cache: &mut N::ColumnCache,
17    noises: &N,
18    aquifer: &mut Aquifer<N>,
19    x: i32,
20    z: i32,
21    ocean_floor: bool,
22) -> i32 {
23    let estimate = preliminary_surface_level::<N>(noises, cache, x, z);
24    let max_y = (estimate + 16).min(N::Settings::MIN_Y + N::Settings::HEIGHT - 1);
25    iterate_noise_column_capped::<N>(cache, noises, aquifer, x, z, max_y, ocean_floor)
26}
27/// Single-point `getInterpolatedDensity`. Exposed for `GenerationContext`.
28pub(crate) fn column_interpolated_density<N: DimensionNoises>(
29    cache: &mut N::ColumnCache,
30    noises: &N,
31    x: i32,
32    y: i32,
33    z: i32,
34    cell_w: i32,
35    cell_h: i32,
36) -> f64 {
37    interpolated_density::<N>(cache, noises, x, y, z, cell_w, cell_h)
38}
39/// Finds the highest solid block below air in a single base-noise column.
40///
41/// Used by structure placement probes such as nether fossils. This preserves the
42/// same base terrain classification as repeated `column_state` calls, but shares
43/// the eight cell-corner density evaluations across adjacent Y positions.
44#[expect(
45    clippy::too_many_lines,
46    reason = "keeps the vanilla density interpolation flow in one readable pass"
47)]
48pub(crate) fn find_solid_block_below_air<N: DimensionNoises>(
49    cache: &mut N::ColumnCache,
50    noises: &N,
51    aquifer: &mut Aquifer<N>,
52    block_x: i32,
53    block_z: i32,
54    start_y: i32,
55    min_solid_y: i32,
56) -> Option<i32> {
57    const MAX_INTERP: usize = 16;
58
59    if start_y <= min_solid_y {
60        return None;
61    }
62
63    let cell_w = N::Settings::CELL_WIDTH;
64    let cell_h = N::Settings::CELL_HEIGHT;
65    let min_y = N::Settings::MIN_Y;
66    let height = N::Settings::HEIGHT;
67    let cell_min_y = min_y.div_euclid(cell_h);
68    let cell_count_y = height.div_euclid(cell_h);
69
70    let cell_x = block_x.div_euclid(cell_w);
71    let cell_z = block_z.div_euclid(cell_w);
72    let factor_x = f64::from(block_x.rem_euclid(cell_w)) / f64::from(cell_w);
73    let factor_z = f64::from(block_z.rem_euclid(cell_w)) / f64::from(cell_w);
74    let x0 = cell_x * cell_w;
75    let x1 = x0 + cell_w;
76    let z0 = cell_z * cell_w;
77    let z1 = z0 + cell_w;
78
79    let interp_count = N::interpolated_count();
80
81    let mut c000 = [0.0f64; MAX_INTERP];
82    let mut c100 = [0.0f64; MAX_INTERP];
83    let mut c010 = [0.0f64; MAX_INTERP];
84    let mut c110 = [0.0f64; MAX_INTERP];
85    let mut c001 = [0.0f64; MAX_INTERP];
86    let mut c101 = [0.0f64; MAX_INTERP];
87    let mut c011 = [0.0f64; MAX_INTERP];
88    let mut c111 = [0.0f64; MAX_INTERP];
89    let mut interpolated = [0.0f64; MAX_INTERP];
90
91    macro_rules! fill {
92        ($out:expr, $ex:expr, $ey:expr, $ez:expr, $blended:expr) => {{
93            cache.ensure($ex, $ez, noises);
94            noises.fill_cell_corner_densities(
95                &mut *cache,
96                $ex,
97                $ey,
98                $ez,
99                $blended,
100                &mut $out[..interp_count],
101            );
102        }};
103    }
104
105    let max_cell_y_idx = {
106        let raw = start_y.div_euclid(cell_h) - cell_min_y;
107        raw.clamp(0, cell_count_y - 1)
108    };
109    let min_cell_y_idx = {
110        let raw = min_solid_y.div_euclid(cell_h) - cell_min_y;
111        raw.clamp(0, cell_count_y - 1)
112    };
113
114    let mut above_is_air = false;
115    let mut have_above = false;
116    let mut blended_scratch = [0.0_f64; 2];
117
118    for cell_y_idx in (min_cell_y_idx..=max_cell_y_idx).rev() {
119        let y0 = (cell_min_y + cell_y_idx) * cell_h;
120        let y1 = y0 + cell_h;
121        let ys = [y0, y1];
122
123        noises.compute_noise_column(x0, &ys, z0, &mut blended_scratch);
124        let b000 = blended_scratch[0];
125        let b010 = blended_scratch[1];
126        noises.compute_noise_column(x1, &ys, z0, &mut blended_scratch);
127        let b100 = blended_scratch[0];
128        let b110 = blended_scratch[1];
129        noises.compute_noise_column(x0, &ys, z1, &mut blended_scratch);
130        let b001 = blended_scratch[0];
131        let b011 = blended_scratch[1];
132        noises.compute_noise_column(x1, &ys, z1, &mut blended_scratch);
133        let b101 = blended_scratch[0];
134        let b111 = blended_scratch[1];
135
136        fill!(c000, x0, y0, z0, b000);
137        fill!(c100, x1, y0, z0, b100);
138        fill!(c010, x0, y1, z0, b010);
139        fill!(c110, x1, y1, z0, b110);
140        fill!(c001, x0, y0, z1, b001);
141        fill!(c101, x1, y0, z1, b101);
142        fill!(c011, x0, y1, z1, b011);
143        fill!(c111, x1, y1, z1, b111);
144
145        let top_y_in_cell = if cell_y_idx == max_cell_y_idx {
146            (start_y - y0).clamp(0, cell_h - 1)
147        } else {
148            cell_h - 1
149        };
150        let bottom_y_in_cell = if cell_y_idx == min_cell_y_idx {
151            (min_solid_y - y0).clamp(0, cell_h - 1)
152        } else {
153            0
154        };
155
156        for y_in_cell in (bottom_y_in_cell..=top_y_in_cell).rev() {
157            let pos_y = y0 + y_in_cell;
158            let factor_y = f64::from(y_in_cell) / f64::from(cell_h);
159
160            for ch in 0..interp_count {
161                let d00 = lerp(factor_y, c000[ch], c010[ch]);
162                let d10 = lerp(factor_y, c100[ch], c110[ch]);
163                let d01 = lerp(factor_y, c001[ch], c011[ch]);
164                let d11 = lerp(factor_y, c101[ch], c111[ch]);
165                let d0 = lerp(factor_x, d00, d10);
166                let d1 = lerp(factor_x, d01, d11);
167                interpolated[ch] = lerp(factor_z, d0, d1);
168            }
169
170            let density = noises.combine_interpolated(
171                &mut *cache,
172                &interpolated[..interp_count],
173                0,
174                pos_y,
175                0,
176            );
177            let state = aquifer.compute_substance(noises, block_x, pos_y, block_z, density);
178            let is_air = matches!(state, AquiferResult::Air);
179            let is_solid = matches!(state, AquiferResult::Solid);
180
181            if have_above && above_is_air && is_solid {
182                return Some(pos_y);
183            }
184
185            above_is_air = is_air;
186            have_above = true;
187        }
188    }
189
190    None
191}
192/// Matches vanilla's `iterateNoiseColumn`: iterates by Y cells, evaluating
193/// inner density functions at 8 cell corners, trilinearly interpolating each
194/// channel independently, then applying outer operations (squeeze, min, etc.)
195/// per-block via `combine_interpolated`.
196///
197/// Returns getBaseHeight (= getFirstFreeHeight = first Y above surface).
198pub(crate) fn iterate_noise_column_with_aquifer<N: DimensionNoises>(
199    cache: &mut N::ColumnCache,
200    noises: &N,
201    aquifer: &mut Aquifer<N>,
202    block_x: i32,
203    block_z: i32,
204    ocean_floor: bool,
205) -> i32 {
206    let max_y = N::Settings::MIN_Y + N::Settings::HEIGHT - 1;
207    iterate_noise_column_capped::<N>(cache, noises, aquifer, block_x, block_z, max_y, ocean_floor)
208}
209
210/// Same as `iterate_noise_column_with_aquifer` but only scans Y values up to
211/// `max_y_inclusive`. Used by `base_height` with an estimate from
212/// `preliminary_surface_level + 16` to skip empty upper atmosphere — reducing
213/// cell-corner density evaluations from O(height) to `O(estimate_depth)`.
214#[expect(
215    clippy::too_many_lines,
216    reason = "inlines 8-corner density buffers + interpolation to match vanilla's iterateNoiseColumn fast path"
217)]
218fn iterate_noise_column_capped<N: DimensionNoises>(
219    cache: &mut N::ColumnCache,
220    noises: &N,
221    aquifer: &mut Aquifer<N>,
222    block_x: i32,
223    block_z: i32,
224    max_y_inclusive: i32,
225    ocean_floor: bool,
226) -> i32 {
227    // Corner channel buffers for 8 cell corners
228    const MAX_INTERP: usize = 16;
229
230    let cell_w = N::Settings::CELL_WIDTH;
231    let cell_h = N::Settings::CELL_HEIGHT;
232    let min_y = N::Settings::MIN_Y;
233    let height = N::Settings::HEIGHT;
234    let cell_min_y = min_y.div_euclid(cell_h);
235    let cell_count_y = height.div_euclid(cell_h);
236
237    let cell_x = block_x.div_euclid(cell_w);
238    let cell_z = block_z.div_euclid(cell_w);
239    let factor_x = f64::from(block_x.rem_euclid(cell_w)) / f64::from(cell_w);
240    let factor_z = f64::from(block_z.rem_euclid(cell_w)) / f64::from(cell_w);
241    let x0 = cell_x * cell_w;
242    let x1 = x0 + cell_w;
243    let z0 = cell_z * cell_w;
244    let z1 = z0 + cell_w;
245
246    let interp_count = N::interpolated_count();
247
248    let mut c000 = [0.0f64; MAX_INTERP];
249    let mut c100 = [0.0f64; MAX_INTERP];
250    let mut c010 = [0.0f64; MAX_INTERP];
251    let mut c110 = [0.0f64; MAX_INTERP];
252    let mut c001 = [0.0f64; MAX_INTERP];
253    let mut c101 = [0.0f64; MAX_INTERP];
254    let mut c011 = [0.0f64; MAX_INTERP];
255    let mut c111 = [0.0f64; MAX_INTERP];
256    let mut interpolated = [0.0f64; MAX_INTERP];
257
258    macro_rules! fill {
259        ($out:expr, $ex:expr, $ey:expr, $ez:expr, $blended:expr) => {{
260            cache.ensure($ex, $ez, noises);
261            noises.fill_cell_corner_densities(
262                &mut *cache,
263                $ex,
264                $ey,
265                $ez,
266                $blended,
267                &mut $out[..interp_count],
268            );
269        }};
270    }
271
272    // Topmost cell containing max_y_inclusive.
273    let max_cell_y_idx = {
274        let raw = max_y_inclusive.div_euclid(cell_h) - cell_min_y;
275        raw.clamp(0, cell_count_y - 1)
276    };
277    // Top Y-within-cell for the topmost cell.
278    let top_cell_top_y_in_cell =
279        (max_y_inclusive - (cell_min_y + max_cell_y_idx) * cell_h).clamp(0, cell_h - 1);
280
281    // Precompute blended noise per corner (x, z) × two Y levels per cell.
282    let mut blended_scratch = [0.0_f64; 2];
283    for cell_y_idx in (0..=max_cell_y_idx).rev() {
284        let y0 = (cell_min_y + cell_y_idx) * cell_h;
285        let y1 = y0 + cell_h;
286        let ys = [y0, y1];
287
288        // `compute_noise_column` gives us the blended noise values at (x0,z0),
289        // (x1,z0), (x0,z1), (x1,z1) for this Y pair. Query each corner once.
290        noises.compute_noise_column(x0, &ys, z0, &mut blended_scratch);
291        let b000 = blended_scratch[0];
292        let b010 = blended_scratch[1];
293        noises.compute_noise_column(x1, &ys, z0, &mut blended_scratch);
294        let b100 = blended_scratch[0];
295        let b110 = blended_scratch[1];
296        noises.compute_noise_column(x0, &ys, z1, &mut blended_scratch);
297        let b001 = blended_scratch[0];
298        let b011 = blended_scratch[1];
299        noises.compute_noise_column(x1, &ys, z1, &mut blended_scratch);
300        let b101 = blended_scratch[0];
301        let b111 = blended_scratch[1];
302
303        // Evaluate inner functions at 8 cell corners (all channels)
304        fill!(c000, x0, y0, z0, b000);
305        fill!(c100, x1, y0, z0, b100);
306        fill!(c010, x0, y1, z0, b010);
307        fill!(c110, x1, y1, z0, b110);
308        fill!(c001, x0, y0, z1, b001);
309        fill!(c101, x1, y0, z1, b101);
310        fill!(c011, x0, y1, z1, b011);
311        fill!(c111, x1, y1, z1, b111);
312
313        // For the topmost cell, start from the Y within cell that corresponds
314        // to `max_y_inclusive`. For lower cells, start at cell_h - 1.
315        let top_y_in_cell = if cell_y_idx == max_cell_y_idx {
316            top_cell_top_y_in_cell
317        } else {
318            cell_h - 1
319        };
320
321        // Iterate Y within cell from top to bottom
322        for y_in_cell in (0..=top_y_in_cell).rev() {
323            let pos_y = (cell_min_y + cell_y_idx) * cell_h + y_in_cell;
324            let factor_y = f64::from(y_in_cell) / f64::from(cell_h);
325
326            // Trilinearly interpolate each channel independently
327            for ch in 0..interp_count {
328                let d00 = lerp(factor_y, c000[ch], c010[ch]);
329                let d10 = lerp(factor_y, c100[ch], c110[ch]);
330                let d01 = lerp(factor_y, c001[ch], c011[ch]);
331                let d11 = lerp(factor_y, c101[ch], c111[ch]);
332                let d0 = lerp(factor_x, d00, d10);
333                let d1 = lerp(factor_x, d01, d11);
334                interpolated[ch] = lerp(factor_z, d0, d1);
335            }
336
337            // Apply outer operations (squeeze, min, etc.) per-block
338            let density = noises.combine_interpolated(
339                &mut *cache,
340                &interpolated[..interp_count],
341                0,
342                pos_y,
343                0,
344            );
345
346            // Use aquifer to determine block state (matches vanilla's getInterpolatedState)
347            let opaque = match aquifer.compute_substance(noises, block_x, pos_y, block_z, density) {
348                AquiferResult::Solid => true,
349                AquiferResult::Fluid(_) => !ocean_floor,
350                AquiferResult::Air => false,
351            };
352
353            if opaque {
354                return pos_y + 1;
355            }
356        }
357    }
358    min_y
359}
360
361/// Evaluate terrain density at a single block position using cell-based
362/// interpolation matching vanilla's `NoiseChunk`: inner functions at 8 cell
363/// corners, trilinear interpolation per channel, then outer operations.
364fn interpolated_density<N: DimensionNoises>(
365    cache: &mut N::ColumnCache,
366    noises: &N,
367    x: i32,
368    y: i32,
369    z: i32,
370    cell_w: i32,
371    cell_h: i32,
372) -> f64 {
373    const MAX_INTERP: usize = 16;
374
375    let cx = x.div_euclid(cell_w);
376    let cy = y.div_euclid(cell_h);
377    let cz = z.div_euclid(cell_w);
378    let fx = f64::from(x.rem_euclid(cell_w)) / f64::from(cell_w);
379    let fy = f64::from(y.rem_euclid(cell_h)) / f64::from(cell_h);
380    let fz = f64::from(z.rem_euclid(cell_w)) / f64::from(cell_w);
381
382    let x0 = cx * cell_w;
383    let x1 = x0 + cell_w;
384    let y0 = cy * cell_h;
385    let y1 = y0 + cell_h;
386    let z0 = cz * cell_w;
387    let z1 = z0 + cell_w;
388
389    let interp_count = N::interpolated_count();
390
391    let mut c000 = [0.0f64; MAX_INTERP];
392    let mut c100 = [0.0f64; MAX_INTERP];
393    let mut c010 = [0.0f64; MAX_INTERP];
394    let mut c110 = [0.0f64; MAX_INTERP];
395    let mut c001 = [0.0f64; MAX_INTERP];
396    let mut c101 = [0.0f64; MAX_INTERP];
397    let mut c011 = [0.0f64; MAX_INTERP];
398    let mut c111 = [0.0f64; MAX_INTERP];
399    let mut interpolated = [0.0f64; MAX_INTERP];
400
401    macro_rules! fill {
402        ($out:expr, $ex:expr, $ey:expr, $ez:expr, $blended:expr) => {{
403            cache.ensure($ex, $ez, noises);
404            noises.fill_cell_corner_densities(
405                &mut *cache,
406                $ex,
407                $ey,
408                $ez,
409                $blended,
410                &mut $out[..interp_count],
411            );
412        }};
413    }
414
415    // Precompute blended noise at each corner (x, z) for the two cell Y levels.
416    let ys = [y0, y1];
417    let mut blended_scratch = [0.0_f64; 2];
418    noises.compute_noise_column(x0, &ys, z0, &mut blended_scratch);
419    let (b000, b010) = (blended_scratch[0], blended_scratch[1]);
420    noises.compute_noise_column(x1, &ys, z0, &mut blended_scratch);
421    let (b100, b110) = (blended_scratch[0], blended_scratch[1]);
422    noises.compute_noise_column(x0, &ys, z1, &mut blended_scratch);
423    let (b001, b011) = (blended_scratch[0], blended_scratch[1]);
424    noises.compute_noise_column(x1, &ys, z1, &mut blended_scratch);
425    let (b101, b111) = (blended_scratch[0], blended_scratch[1]);
426
427    fill!(c000, x0, y0, z0, b000);
428    fill!(c100, x1, y0, z0, b100);
429    fill!(c010, x0, y1, z0, b010);
430    fill!(c110, x1, y1, z0, b110);
431    fill!(c001, x0, y0, z1, b001);
432    fill!(c101, x1, y0, z1, b101);
433    fill!(c011, x0, y1, z1, b011);
434    fill!(c111, x1, y1, z1, b111);
435
436    for ch in 0..interp_count {
437        let d00 = lerp(fy, c000[ch], c010[ch]);
438        let d10 = lerp(fy, c100[ch], c110[ch]);
439        let d01 = lerp(fy, c001[ch], c011[ch]);
440        let d11 = lerp(fy, c101[ch], c111[ch]);
441        let d0 = lerp(fx, d00, d10);
442        let d1 = lerp(fx, d01, d11);
443        interpolated[ch] = lerp(fz, d0, d1);
444    }
445
446    noises.combine_interpolated(&mut *cache, &interpolated[..interp_count], 0, y, 0)
447}