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