veloren_world/
column.rs

1use crate::{
2    CONFIG, IndexRef,
3    all::ForestKind,
4    sim::{Cave, Path, RiverKind, SimChunk, WorldSim, local_cells},
5    site::SpawnRules,
6    util::{RandomField, RandomPerm, Sampler},
7};
8use common::{
9    calendar::{Calendar, CalendarEvent},
10    terrain::{
11        CoordinateConversions, TerrainChunkSize, quadratic_nearest_point, river_spline_coeffs,
12        uniform_idx_as_vec2, vec2_as_uniform_idx,
13    },
14    vol::RectVolSize,
15};
16use noise::NoiseFn;
17use rand::seq::SliceRandom;
18use serde::Deserialize;
19use std::ops::{Add, Div, Mul, Sub};
20use tracing::error;
21use vek::*;
22
23pub struct ColumnGen<'a> {
24    pub sim: &'a WorldSim,
25}
26
27#[derive(Deserialize)]
28pub struct Colors {
29    pub cold_grass: (f32, f32, f32),
30    pub warm_grass: (f32, f32, f32),
31    pub dark_grass: (f32, f32, f32),
32    pub wet_grass: (f32, f32, f32),
33    pub cold_stone: (f32, f32, f32),
34    pub hot_stone: (f32, f32, f32),
35    pub warm_stone: (f32, f32, f32),
36    pub beach_sand: (f32, f32, f32),
37    pub desert_sand: (f32, f32, f32),
38    pub snow: (f32, f32, f32),
39    pub snow_moss: (f32, f32, f32),
40
41    pub stone_col: (u8, u8, u8),
42
43    pub dirt_low: (f32, f32, f32),
44    pub dirt_high: (f32, f32, f32),
45
46    pub snow_high: (f32, f32, f32),
47    pub warm_stone_high: (f32, f32, f32),
48
49    pub grass_high: (f32, f32, f32),
50    pub tropical_high: (f32, f32, f32),
51    pub mesa_layers: Vec<(f32, f32, f32)>,
52}
53
54/// Generalised power function, pushes values in the range 0-1 to extremes.
55fn power(x: f64, t: f64) -> f64 {
56    if x < 0.5 {
57        (2.0 * x).powf(t) / 2.0
58    } else {
59        1.0 - (-2.0 * x + 2.0).powf(t) / 2.0
60    }
61}
62
63impl<'a> ColumnGen<'a> {
64    pub fn new(sim: &'a WorldSim) -> Self { Self { sim } }
65}
66
67impl<'a> Sampler<'a> for ColumnGen<'a> {
68    type Index = (Vec2<i32>, IndexRef<'a>, Option<&'a Calendar>);
69    type Sample = Option<ColumnSample<'a>>;
70
71    fn get(&self, (wpos, index, calendar): Self::Index) -> Option<ColumnSample<'a>> {
72        let wposf = wpos.map(|e| e as f64);
73        let chunk_pos = wpos.wpos_to_cpos();
74
75        let sim = &self.sim;
76
77        // let turb = Vec2::new(
78        //     sim.gen_ctx.turb_x_nz.get((wposf.div(48.0)).into_array()) as f32,
79        //     sim.gen_ctx.turb_y_nz.get((wposf.div(48.0)).into_array()) as f32,
80        // ) * 12.0;
81        let wposf_turb = wposf; // + turb.map(|e| e as f64);
82
83        let chaos = sim.get_interpolated(wpos, |chunk| chunk.chaos)?;
84        let temp = sim.get_interpolated(wpos, |chunk| chunk.temp)?;
85        let humidity = sim.get_interpolated(wpos, |chunk| chunk.humidity)?;
86        let rockiness = sim.get_interpolated(wpos, |chunk| chunk.rockiness)?;
87        let tree_density = sim.get_interpolated(wpos, |chunk| chunk.tree_density)?;
88        let spawn_rate = sim.get_interpolated(wpos, |chunk| chunk.spawn_rate)?;
89        let near_water =
90            sim.get_interpolated(
91                wpos,
92                |chunk| if chunk.river.near_water() { 1.0 } else { 0.0 },
93            )?;
94        let water_vel = sim.get_interpolated(wpos, |chunk| {
95            if chunk.river.river_kind.is_some() {
96                chunk.river.velocity
97            } else {
98                Vec3::zero()
99            }
100        })?;
101        let alt = sim.get_interpolated_monotone(wpos, |chunk| chunk.alt)?;
102        let surface_veg = sim.get_interpolated_monotone(wpos, |chunk| chunk.surface_veg)?;
103        let sim_chunk = sim.get(chunk_pos)?;
104        let neighbor_coef = TerrainChunkSize::RECT_SIZE.map(|e| e as f64);
105        let my_chunk_idx = vec2_as_uniform_idx(self.sim.map_size_lg(), chunk_pos);
106        let neighbor_river_data =
107            local_cells(self.sim.map_size_lg(), my_chunk_idx).filter_map(|neighbor_idx: usize| {
108                let neighbor_pos = uniform_idx_as_vec2(self.sim.map_size_lg(), neighbor_idx);
109                let neighbor_chunk = sim.get(neighbor_pos)?;
110                Some((neighbor_pos, neighbor_chunk, &neighbor_chunk.river))
111            });
112        let spawn_rules = sim_chunk
113            .sites
114            .iter()
115            .map(|site| index.sites[*site].spawn_rules(wpos))
116            .fold(SpawnRules::default(), |a, b| a.combine(b));
117
118        const SAMP_RES: i32 = 8;
119        let altx0 = sim.get_interpolated(wpos - Vec2::new(1, 0) * SAMP_RES, |chunk| chunk.alt);
120        let altx1 = sim.get_interpolated(wpos + Vec2::new(1, 0) * SAMP_RES, |chunk| chunk.alt);
121        let alty0 = sim.get_interpolated(wpos - Vec2::new(0, 1) * SAMP_RES, |chunk| chunk.alt);
122        let alty1 = sim.get_interpolated(wpos + Vec2::new(0, 1) * SAMP_RES, |chunk| chunk.alt);
123        let gradient =
124            altx0
125                .zip(altx1)
126                .zip_with(alty0.zip(alty1), |(altx0, altx1), (alty0, alty1)| {
127                    Vec2::new(altx1 - altx0, alty1 - alty0)
128                        .map(f32::abs)
129                        .magnitude()
130                        / SAMP_RES as f32
131                });
132
133        let wposf3d = Vec3::new(wposf.x, wposf.y, alt as f64);
134
135        let marble_small = (sim.gen_ctx.hill_nz.get((wposf3d.div(3.0)).into_array()) as f32)
136            .powi(3)
137            .add(1.0)
138            .mul(0.5);
139        let marble_mid = (sim.gen_ctx.hill_nz.get((wposf3d.div(12.0)).into_array()) as f32)
140            .mul(0.75)
141            .add(1.0)
142            .mul(0.5);
143        //.add(marble_small.sub(0.5).mul(0.25));
144        let marble = (sim.gen_ctx.hill_nz.get((wposf3d.div(48.0)).into_array()) as f32)
145            .mul(0.75)
146            .add(1.0)
147            .mul(0.5);
148        let marble_mixed = marble
149            .add(marble_mid.sub(0.5).mul(0.5))
150            .add(marble_small.sub(0.5).mul(0.25));
151
152        let lake_width = (TerrainChunkSize::RECT_SIZE.x as f64 * 2.0f64.sqrt()) + 6.0;
153        let neighbor_river_data = neighbor_river_data
154            .map(|(posj, chunkj, river)| {
155                let kind = match river.river_kind {
156                    Some(kind) => kind,
157                    None => {
158                        return (posj, chunkj, river, None);
159                    },
160                };
161                let downhill_pos = if let Some(pos) = chunkj.downhill {
162                    pos
163                } else {
164                    match kind {
165                        RiverKind::River { .. } => {
166                            error!(?river, ?posj, "What?");
167                            panic!("How can a river have no downhill?");
168                        },
169                        RiverKind::Lake { .. } => {
170                            return (posj, chunkj, river, None);
171                        },
172                        RiverKind::Ocean => posj,
173                    }
174                };
175                let downhill_wpos = downhill_pos.map(|e| e as f64);
176                let downhill_pos = downhill_pos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| {
177                    e.div_euclid(sz as i32)
178                });
179                let neighbor_wpos = posj.map(|e| e as f64) * neighbor_coef + neighbor_coef * 0.5;
180                let direction = neighbor_wpos - downhill_wpos;
181                let river_width_min = if let RiverKind::River { cross_section } = kind {
182                    cross_section.x as f64
183                } else {
184                    lake_width
185                };
186                let downhill_chunk = sim.get(downhill_pos).expect("How can this not work?");
187                let coeffs = river_spline_coeffs(
188                    neighbor_wpos,
189                    chunkj.river.spline_derivative,
190                    downhill_wpos,
191                );
192                let (direction, coeffs, downhill_chunk, river_t, river_pos, river_dist) = match kind
193                {
194                    RiverKind::River { .. } => {
195                        if let Some((t, pt, dist)) = quadratic_nearest_point(
196                            &coeffs,
197                            wposf,
198                            Vec2::new(neighbor_wpos, downhill_wpos),
199                        ) {
200                            let (t, pt, dist) = if dist > wposf.distance_squared(neighbor_wpos) {
201                                (0.0, neighbor_wpos, wposf.distance_squared(neighbor_wpos))
202                            } else if dist > wposf.distance_squared(downhill_wpos) {
203                                (1.0, downhill_wpos, wposf.distance_squared(downhill_wpos))
204                            } else {
205                                (t, pt, dist)
206                            };
207                            (direction, coeffs, downhill_chunk, t, pt, dist.sqrt())
208                        } else {
209                            let ndist = wposf.distance_squared(neighbor_wpos);
210                            let ddist = wposf.distance_squared(downhill_wpos);
211                            let (closest_pos, closest_dist, closest_t) = if ndist <= ddist {
212                                (neighbor_wpos, ndist, 0.0)
213                            } else {
214                                (downhill_wpos, ddist, 1.0)
215                            };
216                            (
217                                direction,
218                                coeffs,
219                                downhill_chunk,
220                                closest_t,
221                                closest_pos,
222                                closest_dist.sqrt(),
223                            )
224                        }
225                    },
226                    RiverKind::Lake { neighbor_pass_pos } => {
227                        let pass_dist = neighbor_pass_pos
228                            .map2(
229                                neighbor_wpos
230                                    .map2(TerrainChunkSize::RECT_SIZE, |f, g| (f as i32, g as i32)),
231                                |e, (f, g)| ((e - f) / g).abs(),
232                            )
233                            .reduce_partial_max();
234                        let spline_derivative = river.spline_derivative;
235                        let neighbor_pass_pos = if pass_dist <= 1 {
236                            neighbor_pass_pos
237                        } else {
238                            downhill_wpos.map(|e| e as i32)
239                        };
240                        let pass_dist = neighbor_pass_pos
241                            .map2(
242                                neighbor_wpos
243                                    .map2(TerrainChunkSize::RECT_SIZE, |f, g| (f as i32, g as i32)),
244                                |e, (f, g)| ((e - f) / g).abs(),
245                            )
246                            .reduce_partial_max();
247                        if pass_dist > 1 {
248                            return (posj, chunkj, river, None);
249                        }
250                        let neighbor_pass_wpos =
251                            neighbor_pass_pos.map(|e| e as f64) + neighbor_coef * 0.5;
252                        let neighbor_pass_pos = neighbor_pass_pos.wpos_to_cpos();
253                        let coeffs = river_spline_coeffs(
254                            neighbor_wpos,
255                            spline_derivative,
256                            neighbor_pass_wpos,
257                        );
258                        let direction = neighbor_wpos - neighbor_pass_wpos;
259
260                        // Lakes get a special distance function to avoid cookie-cutter edges
261                        if matches!(
262                            downhill_chunk.river.river_kind,
263                            Some(RiverKind::Lake { .. } | RiverKind::Ocean)
264                        ) {
265                            let water_chunk = posj.map(|e| e as f64);
266                            let lake_width_noise =
267                                sim.gen_ctx.small_nz.get(wposf.div(32.0).into_array());
268                            let water_aabr = Aabr {
269                                min: water_chunk * neighbor_coef + 4.0 - lake_width_noise * 8.0,
270                                max: (water_chunk + 1.0) * neighbor_coef - 4.0
271                                    + lake_width_noise * 8.0,
272                            };
273                            let pos = water_aabr.projected_point(wposf);
274                            (
275                                direction,
276                                coeffs,
277                                sim.get(neighbor_pass_pos).expect("Must already work"),
278                                0.5,
279                                pos,
280                                pos.distance(wposf),
281                            )
282                        } else if let Some((t, pt, dist)) = quadratic_nearest_point(
283                            &coeffs,
284                            wposf,
285                            Vec2::new(neighbor_wpos, neighbor_pass_wpos),
286                        ) {
287                            (
288                                direction,
289                                coeffs,
290                                sim.get(neighbor_pass_pos).expect("Must already work"),
291                                t,
292                                pt,
293                                dist.sqrt(),
294                            )
295                        } else {
296                            let ndist = wposf.distance_squared(neighbor_wpos);
297                            /* let ddist = wposf.distance_squared(neighbor_pass_wpos); */
298                            let (closest_pos, closest_dist, closest_t) = /*if ndist <= ddist */ {
299                                (neighbor_wpos, ndist, 0.0)
300                            } /* else {
301                                (neighbor_pass_wpos, ddist, 1.0)
302                            } */;
303                            (
304                                direction,
305                                coeffs,
306                                sim.get(neighbor_pass_pos).expect("Must already work"),
307                                closest_t,
308                                closest_pos,
309                                closest_dist.sqrt(),
310                            )
311                        }
312                    },
313                    RiverKind::Ocean => {
314                        let water_chunk = posj.map(|e| e as f64);
315                        let lake_width_noise =
316                            sim.gen_ctx.small_nz.get(wposf.div(32.0).into_array());
317                        let water_aabr = Aabr {
318                            min: water_chunk * neighbor_coef + 4.0 - lake_width_noise * 8.0,
319                            max: (water_chunk + 1.0) * neighbor_coef - 4.0 + lake_width_noise * 8.0,
320                        };
321                        let pos = water_aabr.projected_point(wposf);
322                        (
323                            direction,
324                            coeffs,
325                            sim.get(posj).expect("Must already work"),
326                            0.5,
327                            pos,
328                            pos.distance(wposf),
329                        )
330                    },
331                };
332                let river_width_max = if let Some(RiverKind::River { cross_section }) =
333                    downhill_chunk.river.river_kind
334                {
335                    // Harmless hack that prevents a river growing wildly outside its bounds to
336                    // create water walls
337                    (cross_section.x as f64).min(river_width_min * 1.75)
338                } else if let Some(RiverKind::River { cross_section }) = chunkj.river.river_kind {
339                    Lerp::lerp(cross_section.x as f64, lake_width, 0.5)
340                } else {
341                    // 0.5 prevents rivers pooling into lakes having extremely wide bounds, creating
342                    // water walls
343                    lake_width * 0.5
344                };
345                let river_width_noise =
346                    (sim.gen_ctx.small_nz.get((river_pos.div(16.0)).into_array()))
347                        .clamp(-1.0, 1.0)
348                        .mul(0.5)
349                        .sub(0.5);
350                let river_width = Lerp::lerp(
351                    river_width_min,
352                    river_width_max,
353                    river_t.clamped(0.0, 1.0).powf(3.0),
354                );
355
356                let river_width = river_width.max(2.0) * (1.0 + river_width_noise * 0.3);
357                // To find the distance, we just evaluate the quadratic equation at river_t and
358                // see if it's within width (but we should be able to use it for a
359                // lot more, and this probably isn't the very best approach anyway
360                // since it will bleed out). let river_pos = coeffs.x * river_t *
361                // river_t + coeffs.y * river_t + coeffs.z;
362                // let river_width = 32.0f64;
363                let res = Vec2::new(0.0, (river_dist - (river_width * 0.5).max(1.0)).max(0.0));
364                (
365                    posj,
366                    chunkj,
367                    river,
368                    Some((
369                        direction,
370                        res,
371                        river_width,
372                        (river_t, (river_pos, coeffs), downhill_chunk),
373                    )),
374                )
375            })
376            .collect::<Vec<_>>();
377
378        debug_assert!(sim_chunk.water_alt >= CONFIG.sea_level);
379
380        /// A type that makes managing surface altitude weighting much simpler.
381        #[derive(Default)]
382        struct WeightedSum<T> {
383            sum: T,
384            weight: T,
385            min: Option<T>,
386            max: Option<T>,
387        }
388        impl WeightedSum<f32> {
389            /// Add a weight to the sum.
390            fn with(self, value: f32, weight: f32) -> Self {
391                Self {
392                    sum: self.sum + value * weight,
393                    weight: self.weight + weight,
394                    ..self
395                }
396            }
397
398            /// Add an upper bound to the result.
399            fn with_min(self, min: f32) -> Self {
400                Self {
401                    min: Some(self.min.unwrap_or(min).min(min)),
402                    ..self
403                }
404            }
405
406            /// Add a lower bound to the result.
407            fn with_max(self, max: f32) -> Self {
408                Self {
409                    max: Some(self.max.unwrap_or(max).max(max)),
410                    ..self
411                }
412            }
413
414            /// Evaluate the weighted sum, if weightings were applied.
415            fn eval(&self) -> Option<f32> {
416                if self.weight > 0.0 {
417                    let res = self.sum / self.weight;
418                    let res = self.min.map_or(res, |m| m.min(res));
419                    let res = self.max.map_or(res, |m| m.max(res));
420                    Some(res)
421                } else {
422                    None
423                }
424            }
425
426            /// Evaluate the weighted sum, or use a default value if no
427            /// weightings were applied.
428            fn eval_or(&self, default: f32) -> f32 {
429                let res = if self.weight > 0.0 {
430                    self.sum / self.weight
431                } else {
432                    default
433                };
434                let res = self.min.map_or(res, |m| m.min(res));
435                self.max.map_or(res, |m| m.max(res))
436            }
437        }
438
439        /// Determine whether a river should become a waterfall
440        fn is_waterfall(
441            chunk_pos: Vec2<i32>,
442            river_chunk: &SimChunk,
443            downhill_chunk: &SimChunk,
444        ) -> bool {
445            // Waterfalls are rare, so use some hacky RNG seeded with the position to
446            // reflect that. Additionally, the river must experience a rapid
447            // change in elevation. Pooling into a lake produces a rapid.
448            // TODO: Find a better way to produce rapids along the course of a river?
449            (RandomField::new(3119).chance(chunk_pos.with_z(0), 0.1)
450                || matches!(
451                    downhill_chunk.river.river_kind,
452                    Some(RiverKind::Lake { .. })
453                ))
454                && (river_chunk.water_alt > downhill_chunk.water_alt + 0.0)
455        }
456
457        /// Determine the altitude of a river based on the altitude of the
458        /// spline ends and a tweening factor.
459        fn river_water_alt(a: f32, b: f32, t: f32, is_waterfall: bool) -> f32 {
460            let t = if is_waterfall {
461                // Waterfalls bias the water altitude toward extremes
462                power(t as f64, 3.0 + (a - b).clamped(0.0, 16.0) as f64) as f32
463            } else {
464                t
465            };
466            Lerp::lerp(a, b, t)
467        }
468
469        // Use this to temporarily alter the sea level
470        let base_sea_level = CONFIG.sea_level - 1.0 + 0.01;
471
472        // What's going on here?
473        //
474        // We're iterating over nearby bodies of water and calculating a weighted sum
475        // for the river water level, the lake water level, and the 'unbounded
476        // water level' (the maximum water body altitude, which we use later to
477        // prevent water walls). In doing so, we also apply various clamping strategies
478        // to catch lots of nasty edge cases, as well as calculating the
479        // distance to the nearest body of water.
480        //
481        // The clamping strategies employed prevent very specific, annoying artifacts
482        // such as 'water walls' (vertical columns of water that are physically
483        // implausible) and 'backflows' (regions where a body of water appears to
484        // flow upstream due to irregular humps along its course).
485        //
486        // It is incredibly difficult to explain exactly what every part of this code is
487        // doing without visual examples. Needless to say, any changes to this
488        // code *at all* should be very ruggedly tested to ensure that
489        // they do not result in artifacts, even in edge cases. The exact configuration
490        // of this code is the product of hundreds of hours of testing and
491        // refinement and I ask that you do not take that effort lightly.
492        let (
493            river_water_level,
494            in_river,
495            lake_water_level,
496            lake_dist,
497            water_dist,
498            unbounded_water_level,
499        ) = neighbor_river_data.iter().copied().fold(
500            (
501                WeightedSum::default().with_max(base_sea_level),
502                false,
503                WeightedSum::default().with_max(base_sea_level),
504                10000.0f32,
505                None,
506                WeightedSum::default().with_max(base_sea_level),
507            ),
508            |(
509                mut river_water_level,
510                mut in_river,
511                lake_water_level,
512                mut lake_dist,
513                water_dist,
514                mut unbounded_water_level,
515            ),
516             (river_chunk_idx, river_chunk, river, dist_info)| match (
517                river.river_kind,
518                dist_info,
519            ) {
520                (
521                    Some(kind),
522                    Some((_, _, river_width, (river_t, (river_pos, _), downhill_chunk))),
523                ) => {
524                    // Distance from river center
525                    let river_dist = river_pos.distance(wposf);
526                    // Distance from edge of river
527                    let river_edge_dist = (river_dist - river_width * 0.5).max(0.0) as f32;
528                    // 0.0 = not near river, 1.0 = in middle of river
529                    let near_center = ((river_dist / (river_width * 0.5)) as f32)
530                        .min(1.0)
531                        .mul(std::f32::consts::PI)
532                        .cos()
533                        .add(1.0)
534                        .mul(0.5);
535
536                    match kind {
537                        RiverKind::River { .. } => {
538                            // Alt of river water *is* the alt of land (ignoring gorge, which gets
539                            // applied later)
540                            let river_water_alt = river_water_alt(
541                                river_chunk.alt.max(river_chunk.water_alt),
542                                downhill_chunk.alt.max(downhill_chunk.water_alt),
543                                river_t as f32,
544                                is_waterfall(river_chunk_idx, river_chunk, downhill_chunk),
545                            );
546
547                            river_water_level =
548                                river_water_level.with(river_water_alt, near_center);
549
550                            if river_edge_dist <= 0.0 {
551                                in_river = true;
552                            }
553                        },
554                        // Slightly wider threshold is chosen in case the lake bounds are a bit
555                        // wrong
556                        RiverKind::Lake { .. } | RiverKind::Ocean => {
557                            let lake_water_alt = if matches!(kind, RiverKind::Ocean) {
558                                base_sea_level
559                            } else {
560                                river_water_alt(
561                                    river_chunk.alt.max(river_chunk.water_alt),
562                                    downhill_chunk.alt.max(downhill_chunk.water_alt),
563                                    river_t as f32,
564                                    is_waterfall(river_chunk_idx, river_chunk, downhill_chunk),
565                                )
566                            };
567
568                            if river_edge_dist > 0.0 && river_width > lake_width * 0.99 {
569                                let unbounded_water_alt = lake_water_alt
570                                    - ((river_edge_dist - 8.0).max(0.0) / 5.0).powf(2.0);
571                                unbounded_water_level = unbounded_water_level
572                                    .with(unbounded_water_alt, 1.0 / (1.0 + river_edge_dist * 5.0));
573                                //.with_max(unbounded_water_alt);
574                            }
575
576                            river_water_level = river_water_level.with(lake_water_alt, near_center);
577
578                            lake_dist = lake_dist.min(river_edge_dist);
579
580                            // Lake border prevents a lake failing to propagate its altitude to
581                            // nearby rivers
582                            let off = 0.0;
583                            let len = 3.0;
584                            if river_edge_dist <= off {
585                                // lake_water_level = lake_water_level
586                                //     // Make sure the closest lake is prioritised
587                                //     .with(lake_water_alt, near_center + 0.1 / (1.0 +
588                                // river_edge_dist));     // .with_min(lake_water_alt);
589                                //
590                                river_water_level = river_water_level.with_min(
591                                    lake_water_alt
592                                        + ((((river_dist - river_width * 0.5) as f32 + len - off)
593                                            .max(0.0))
594                                            / len)
595                                            .powf(1.5)
596                                            * 32.0,
597                                );
598                            }
599                        },
600                    };
601
602                    let river_edge_dist_unclamped = (river_dist - river_width * 0.5) as f32;
603                    let water_dist = Some(
604                        water_dist
605                            .unwrap_or(river_edge_dist_unclamped)
606                            .min(river_edge_dist_unclamped),
607                    );
608
609                    (
610                        river_water_level,
611                        in_river,
612                        lake_water_level,
613                        lake_dist,
614                        water_dist,
615                        unbounded_water_level,
616                    )
617                },
618                (_, _) => (
619                    river_water_level,
620                    in_river,
621                    lake_water_level,
622                    lake_dist,
623                    water_dist,
624                    unbounded_water_level,
625                ),
626            },
627        );
628        let unbounded_water_level = unbounded_water_level.eval_or(base_sea_level);
629        // Calculate a final, canonical altitude for the water in this column by
630        // combining and clamping the attributes we found while iterating over
631        // nearby bodies of water.
632        let water_level = match (
633            river_water_level.eval(),
634            lake_water_level
635                .eval()
636                .filter(|_| lake_dist <= 0.0 || in_river),
637        ) {
638            (Some(r), Some(l)) => r.max(l),
639            (r, l) => r.or(l).unwrap_or(base_sea_level).max(unbounded_water_level),
640        }
641        .max(base_sea_level);
642
643        let riverless_alt = alt;
644
645        // What's going on here?
646        //
647        // Now that we've figured out the altitude of the water in this column, we can
648        // determine the altitude of the river banks. This initially appears
649        // somewhat backward (surely the river basin determines the water level?)
650        // but it is necessary to prevent backflows. Here, the surface of the water is
651        // king because we require global information to determine it without
652        // backflows. The river banks simply reflect the will of the water. We care
653        // much less about a river bank that's slightly rugged and irregular than we do
654        // about the surface of the water itself being rugged and irregular (and
655        // hence physically implausible). From that perspective, it makes sense
656        // that we determine river banks after the water level because it is the one
657        // that we are most at liberty to screw up.
658        //
659        // Similar to the iteration above, we perform a fold over nearby bodies of water
660        // and use the distance to the water to come up wight a weighted sum for
661        // the altitude. The way we determine this altitude differs somewhat
662        // between rivers, lakes, and the ocean and also whether we are *inside* said
663        // bodies of water or simply near their edge.
664        //
665        // As with the previous iteration, a lot of this code is extremely delicate and
666        // has been carefully designed to handle innumeral edge cases. Please
667        // test any changes to this code extremely well to avoid regressions: some
668        // edge cases are very rare indeed!
669        let alt = neighbor_river_data.into_iter().fold(
670            WeightedSum::default().with(riverless_alt, 1.0),
671            |alt, (river_chunk_idx, river_chunk, river, dist_info)| match (
672                river.river_kind,
673                dist_info,
674            ) {
675                (
676                    Some(kind),
677                    Some((_, _, river_width, (river_t, (river_pos, _), downhill_chunk))),
678                ) => {
679                    // Distance from river center
680                    let river_dist = river_pos.distance(wposf);
681                    // Distance from edge of river
682                    let river_edge_dist = (river_dist - river_width * 0.5).max(0.0) as f32;
683
684                    let water_alt = match kind {
685                        RiverKind::River { cross_section } => {
686                            // Alt of river water *is* the alt of land
687                            let river_water_alt = river_water_alt(
688                                river_chunk.alt.max(river_chunk.water_alt),
689                                downhill_chunk.alt.max(downhill_chunk.water_alt),
690                                river_t as f32,
691                                is_waterfall(river_chunk_idx, river_chunk, downhill_chunk),
692                            );
693                            Some((river_water_alt, cross_section.y, None))
694                        },
695                        RiverKind::Lake { .. } | RiverKind::Ocean => {
696                            let lake_water_alt = if matches!(kind, RiverKind::Ocean) {
697                                base_sea_level
698                            } else {
699                                river_water_alt(
700                                    river_chunk.alt.max(river_chunk.water_alt),
701                                    downhill_chunk.alt.max(downhill_chunk.water_alt),
702                                    river_t as f32,
703                                    is_waterfall(river_chunk_idx, river_chunk, downhill_chunk),
704                                )
705                            };
706
707                            let depth = water_level
708                                - Lerp::lerp(
709                                    riverless_alt.min(water_level),
710                                    water_level - 4.0,
711                                    0.5,
712                                );
713
714                            let min_alt = Lerp::lerp(
715                                riverless_alt,
716                                lake_water_alt,
717                                ((river_dist / (river_width * 0.5) - 0.5) * 2.0).clamped(0.0, 1.0)
718                                    as f32,
719                            );
720
721                            Some((
722                                lake_water_alt,
723                                // TODO: The depth given to us by the erosion code is technically
724                                // correct, but it also
725                                // looks terrible. Come up with a good solution to this.
726                                /* river_width as f32 * 0.15 */
727                                depth,
728                                Some(min_alt),
729                            ))
730                        },
731                    };
732
733                    const BANK_STRENGTH: f32 = 100.0;
734                    if let Some((water_alt, water_depth, min_alt)) = water_alt {
735                        if river_edge_dist <= 0.0 {
736                            const MIN_DEPTH: f32 = 1.0;
737                            let near_center = ((river_dist / (river_width * 0.5)) as f32)
738                                .min(1.0)
739                                .mul(std::f32::consts::PI)
740                                .cos()
741                                .add(1.0)
742                                .mul(0.5);
743                            // Waterfalls 'boost' the depth of the river to prevent artifacts. This
744                            // is also necessary when rivers become very
745                            // steep without explicitly being waterfalls.
746                            // TODO: Come up with a more principled way of doing this without
747                            // guessing magic numbers
748                            let waterfall_boost =
749                                if is_waterfall(river_chunk_idx, river_chunk, downhill_chunk) {
750                                    (river_chunk.alt - downhill_chunk.alt).max(0.0).powf(2.0)
751                                        * (1.0 - (river_t as f32 - 0.5).abs() * 2.0).powf(3.5)
752                                        / 20.0
753                                } else {
754                                    // Handle very steep rivers gracefully
755                                    (river_chunk.alt - downhill_chunk.alt).max(0.0) * 2.0
756                                        / TerrainChunkSize::RECT_SIZE.x as f32
757                                };
758                            let riverbed_depth =
759                                near_center * water_depth + MIN_DEPTH + waterfall_boost;
760                            // Handle rivers debouching into the ocean nicely by 'flattening' their
761                            // bottom
762                            let riverbed_alt = (water_alt - riverbed_depth)
763                                .max(riverless_alt.min(base_sea_level - MIN_DEPTH));
764                            alt.with(
765                                min_alt.unwrap_or(riverbed_alt).min(riverbed_alt),
766                                near_center * BANK_STRENGTH,
767                            )
768                            .with_min(min_alt.unwrap_or(riverbed_alt).min(riverbed_alt))
769                        } else {
770                            const GORGE: f32 = 0.25;
771                            const BANK_SCALE: f32 = 24.0;
772                            // Weighting of this riverbank on nearby terrain (higher when closer to
773                            // the river). This 'pulls' the riverbank
774                            // toward the river's altitude to make sure that we get a smooth
775                            // transition from normal terrain to the water.
776                            let weight = Lerp::lerp(
777                                BANK_STRENGTH
778                                    / (1.0
779                                        + (river_edge_dist - 3.0).max(0.0) * BANK_STRENGTH
780                                            / BANK_SCALE),
781                                0.0,
782                                power((river_edge_dist / BANK_SCALE).clamped(0.0, 1.0) as f64, 2.0)
783                                    as f32,
784                            );
785                            let alt = alt.with(water_alt + GORGE, weight);
786
787                            let alt = if matches!(kind, RiverKind::Ocean) {
788                                alt
789                            } else if (0.0..1.5).contains(&river_edge_dist)
790                                && water_dist.is_some_and(|d| d >= 0.0)
791                            {
792                                alt.with_max(water_alt + GORGE)
793                            } else {
794                                alt
795                            };
796
797                            if matches!(kind, RiverKind::Ocean) {
798                                alt
799                            } else if lake_dist > 0.0 && water_level < unbounded_water_level {
800                                alt.with_max(unbounded_water_level)
801                            } else {
802                                alt
803                            }
804                        }
805                    } else {
806                        alt
807                    }
808                },
809                (_, _) => alt,
810            },
811        );
812        let alt = alt
813            .eval_or(riverless_alt)
814            .max(if water_dist.is_none_or(|d| d > 0.0) {
815                // Terrain below sea level breaks things, so force it to never happen
816                base_sea_level + 0.5
817            } else {
818                f32::MIN
819            });
820
821        let riverless_alt_delta = (sim.gen_ctx.small_nz.get(
822            (wposf_turb.div(200.0 * (32.0 / TerrainChunkSize::RECT_SIZE.x as f64))).into_array(),
823        ) as f32)
824            .clamp(-1.0, 1.0)
825            .abs()
826            .mul(3.0)
827            + (sim.gen_ctx.small_nz.get(
828                (wposf_turb.div(400.0 * (32.0 / TerrainChunkSize::RECT_SIZE.x as f64)))
829                    .into_array(),
830            ) as f32)
831                .clamp(-1.0, 1.0)
832                .abs()
833                .mul(3.0);
834
835        // Cliffs
836        let cliff_factor = (alt
837            + self.sim.gen_ctx.hill_nz.get(wposf.div(64.0).into_array()) as f32 * 8.0
838            + self.sim.gen_ctx.hill_nz.get(wposf.div(350.0).into_array()) as f32 * 128.0)
839            .rem_euclid(200.0)
840            / 64.0
841            - 1.0;
842        let cliff_scale =
843            ((self.sim.gen_ctx.hill_nz.get(wposf.div(128.0).into_array()) as f32 * 1.5 + 0.75)
844                + self.sim.gen_ctx.hill_nz.get(wposf.div(48.0).into_array()) as f32 * 0.1)
845                .clamped(0.0, 1.0)
846                .powf(2.0);
847        let cliff_height = sim.get_interpolated(wpos, |chunk| chunk.cliff_height)? * cliff_scale;
848        let cliff = if cliff_factor < 0.0 {
849            cliff_factor.abs().powf(1.5)
850        } else {
851            0.0
852        } * (1.0 - near_water * 3.0).max(0.0).powi(2);
853        let cliff_offset = cliff * cliff_height;
854        let riverless_alt_delta = riverless_alt_delta + (cliff - 0.5) * cliff_height;
855        let basement_sub_alt =
856            sim.get_interpolated_monotone(wpos, |chunk| chunk.basement.sub(chunk.alt))?;
857
858        let warp_factor = water_dist.map_or(1.0, |d| ((d - 0.0) / 64.0).clamped(0.0, 1.0));
859
860        // NOTE: To disable warp, uncomment this line.
861        // let warp_factor = 0.0;
862
863        let warp_factor = warp_factor * spawn_rules.max_warp;
864
865        let surface_rigidity = 1.0 - temp.max(0.0) * (1.0 - tree_density);
866        let surface_rigidity =
867            surface_rigidity.max(((basement_sub_alt + 3.0) / 1.5).clamped(0.0, 2.0));
868        let warp = ((marble_mid * 0.2 + marble * 0.8) * 2.0 - 1.0)
869            * (10.0 + rockiness * 15.0)
870            * gradient.unwrap_or(0.0).min(1.0)
871            * surface_rigidity
872            * warp_factor;
873
874        let mesa = 1.0f32
875            .min(30.0 / (1.0 + -basement_sub_alt.min(0.0)))
876            .min(1.0 - humidity * 4.0)
877            .min(temp)
878            .min(Lerp::lerp(-0.4, 1.0, gradient.unwrap_or(0.0)).max(0.0))
879            .max(0.0);
880
881        let riverless_alt_delta = Lerp::lerp(0.0, riverless_alt_delta, warp_factor);
882        let alt = alt + riverless_alt_delta + warp;
883        let basement = alt + basement_sub_alt;
884        // Adjust this to make rock placement better
885        let rock_density = rockiness
886            + water_dist
887                .filter(|wd| *wd > 2.0)
888                .map(|wd| (1.0 - wd / 32.0).clamped(0.0, 1.0).powf(0.5) * 10.0)
889                .unwrap_or(0.0);
890
891        // Columns near water have a more stable temperature and so get pushed towards
892        // the average (0)
893        let temp = Lerp::lerp(
894            Lerp::lerp(temp, 0.0, 0.1),
895            temp,
896            water_dist
897                .map(|water_dist| water_dist / 20.0)
898                .unwrap_or(1.0)
899                .clamped(0.0, 1.0),
900        );
901        // Columns near water get a humidity boost
902        let humidity = Lerp::lerp(
903            Lerp::lerp(humidity, 1.0, 0.25),
904            humidity,
905            water_dist
906                .map(|water_dist| water_dist / 20.0)
907                .unwrap_or(1.0)
908                .clamped(0.0, 1.0),
909        );
910
911        // Colours
912        let Colors {
913            cold_grass,
914            warm_grass,
915            dark_grass,
916            wet_grass,
917            cold_stone,
918            hot_stone,
919            warm_stone,
920            beach_sand,
921            desert_sand,
922            snow,
923            snow_moss,
924            stone_col,
925            dirt_low,
926            dirt_high,
927            snow_high,
928            warm_stone_high,
929            grass_high,
930            tropical_high,
931            mesa_layers,
932        } = &index.colors.column;
933
934        let cold_grass = (*cold_grass).into();
935        let warm_grass = (*warm_grass).into();
936        let dark_grass = (*dark_grass).into();
937        let wet_grass = (*wet_grass).into();
938        let cold_stone = (*cold_stone).into();
939        let hot_stone = (*hot_stone).into();
940        let warm_stone: Rgb<f32> = (*warm_stone).into();
941        let beach_sand = (*beach_sand).into();
942        let desert_sand = (*desert_sand).into();
943        let snow = (*snow).into();
944        let snow_moss = (*snow_moss).into();
945        let stone_col = (*stone_col).into();
946        let dirt_low: Rgb<f32> = (*dirt_low).into();
947        let dirt_high = (*dirt_high).into();
948        let snow_high = (*snow_high).into();
949        let warm_stone_high = (*warm_stone_high).into();
950        let grass_high = (*grass_high).into();
951        let tropical_high = (*tropical_high).into();
952
953        let dirt = Lerp::lerp(dirt_low, dirt_high, marble_mixed);
954        let tundra = Lerp::lerp(snow, snow_high, 0.4 + marble_mixed * 0.6);
955        let dead_tundra = Lerp::lerp(warm_stone, warm_stone_high, marble_mixed);
956        let cliff = Rgb::lerp(cold_stone, hot_stone, marble_mixed);
957
958        let grass = Rgb::lerp(
959            cold_grass,
960            warm_grass,
961            marble_mixed
962                .sub(0.5)
963                .add(1.0.sub(humidity).mul(0.5))
964                .powf(1.5),
965        );
966        let snow_moss = Rgb::lerp(snow_moss, cold_grass, 0.4 + marble_mixed.powf(1.5) * 0.6);
967        let moss = Rgb::lerp(dark_grass, cold_grass, marble_mixed.powf(1.5));
968        let rainforest = Rgb::lerp(wet_grass, warm_grass, marble_mixed.powf(1.5));
969        let sand = Rgb::lerp(beach_sand, desert_sand, marble_mixed);
970
971        let tropical = Rgb::lerp(
972            Rgb::lerp(
973                grass,
974                grass_high,
975                marble_small
976                    .sub(0.5)
977                    .mul(0.2)
978                    .add(0.75.mul(1.0.sub(humidity)))
979                    .powf(0.667),
980            ),
981            tropical_high,
982            marble_mixed.powf(1.5).sub(0.5).mul(4.0),
983        );
984
985        // For below desert humidity, we are always sand or rock, depending on altitude
986        // and temperature.
987        let ground = Lerp::lerp(
988            Lerp::lerp(
989                dead_tundra,
990                sand,
991                temp.sub(CONFIG.snow_temp)
992                    .div(CONFIG.desert_temp.sub(CONFIG.snow_temp))
993                    .mul(0.5),
994            ),
995            dirt,
996            humidity
997                .sub(CONFIG.desert_hum)
998                .div(CONFIG.forest_hum.sub(CONFIG.desert_hum))
999                .mul(1.0),
1000        );
1001
1002        let sub_surface_color = Lerp::lerp(cliff, ground, alt.sub(basement).mul(0.25));
1003
1004        // From desert to forest humidity, we go from tundra to dirt to grass to moss to
1005        // sand, depending on temperature.
1006        let ground = Rgb::lerp(
1007            ground,
1008            Rgb::lerp(
1009                Rgb::lerp(
1010                    Rgb::lerp(
1011                        Rgb::lerp(
1012                            tundra,
1013                            // snow_temp to temperate_temp
1014                            dirt,
1015                            temp.sub(CONFIG.snow_temp)
1016                                .div(CONFIG.temperate_temp.sub(CONFIG.snow_temp))
1017                                /*.sub((marble - 0.5) * 0.05)
1018                                .mul(256.0)*/
1019                                .mul(1.0),
1020                        ),
1021                        // temperate_temp to tropical_temp
1022                        grass,
1023                        temp.sub(CONFIG.temperate_temp)
1024                            .div(CONFIG.tropical_temp.sub(CONFIG.temperate_temp))
1025                            .mul(4.0),
1026                    ),
1027                    // tropical_temp to desert_temp
1028                    moss,
1029                    temp.sub(CONFIG.tropical_temp)
1030                        .div(CONFIG.desert_temp.sub(CONFIG.tropical_temp))
1031                        .mul(1.0),
1032                ),
1033                // above desert_temp
1034                sand,
1035                temp.sub(CONFIG.desert_temp)
1036                    .div(1.0 - CONFIG.desert_temp)
1037                    .mul(4.0),
1038            ),
1039            humidity
1040                .sub(CONFIG.desert_hum)
1041                .div(CONFIG.forest_hum.sub(CONFIG.desert_hum))
1042                .mul(1.25),
1043        );
1044        // From forest to jungle humidity, we go from snow to dark grass to grass to
1045        // tropics to sand depending on temperature.
1046        let ground = Rgb::lerp(
1047            ground,
1048            Rgb::lerp(
1049                Rgb::lerp(
1050                    Rgb::lerp(
1051                        snow_moss,
1052                        // temperate_temp to tropical_temp
1053                        grass,
1054                        temp.sub(CONFIG.temperate_temp)
1055                            .div(CONFIG.tropical_temp.sub(CONFIG.temperate_temp))
1056                            .mul(4.0),
1057                    ),
1058                    // tropical_temp to desert_temp
1059                    tropical,
1060                    temp.sub(CONFIG.tropical_temp)
1061                        .div(CONFIG.desert_temp.sub(CONFIG.tropical_temp))
1062                        .mul(1.0),
1063                ),
1064                // above desert_temp
1065                sand,
1066                temp.sub(CONFIG.desert_temp)
1067                    .div(1.0 - CONFIG.desert_temp)
1068                    .mul(4.0),
1069            ),
1070            humidity
1071                .sub(CONFIG.forest_hum)
1072                .div(CONFIG.jungle_hum.sub(CONFIG.forest_hum))
1073                .mul(1.0),
1074        );
1075        // From jungle humidity upwards, we go from snow to grass to rainforest to
1076        // tropics to sand.
1077        let ground = Rgb::lerp(
1078            ground,
1079            Rgb::lerp(
1080                Rgb::lerp(
1081                    Rgb::lerp(
1082                        snow_moss,
1083                        // temperate_temp to tropical_temp
1084                        rainforest,
1085                        temp.sub(CONFIG.temperate_temp)
1086                            .div(CONFIG.tropical_temp.sub(CONFIG.temperate_temp))
1087                            .mul(4.0),
1088                    ),
1089                    // tropical_temp to desert_temp
1090                    tropical,
1091                    temp.sub(CONFIG.tropical_temp)
1092                        .div(CONFIG.desert_temp.sub(CONFIG.tropical_temp))
1093                        .mul(4.0),
1094                ),
1095                // above desert_temp
1096                sand,
1097                temp.sub(CONFIG.desert_temp)
1098                    .div(1.0 - CONFIG.desert_temp)
1099                    .mul(4.0),
1100            ),
1101            humidity.sub(CONFIG.jungle_hum).mul(1.0),
1102        );
1103
1104        // Snow covering
1105        let thematic_snow = calendar.is_some_and(|c| c.is_event(CalendarEvent::Christmas));
1106        let snow_factor = temp
1107            .sub(if thematic_snow {
1108                CONFIG.tropical_temp
1109            } else {
1110                CONFIG.snow_temp
1111            })
1112            .max(-humidity.sub(CONFIG.desert_hum))
1113            .mul(4.0)
1114            .max(-0.25)
1115            // 'Simulate' avalanches moving snow from areas with high gradients to areas with high flux
1116            .add((gradient.unwrap_or(0.0) - 0.5).max(0.0) * 0.1)
1117            // .add(-flux * 0.003 * gradient.unwrap_or(0.0))
1118            .add(((marble - 0.5) / 0.5) * 0.25)
1119            .add(((marble_mid - 0.5) / 0.5) * 0.125)
1120            .add(((marble_small - 0.5) / 0.5) * 0.0625);
1121        let snow_cover = snow_factor <= 0.0;
1122        let (alt, ground, sub_surface_color) = if snow_cover && alt > water_level {
1123            // Allow snow cover.
1124            (
1125                alt + 1.0 - snow_factor.max(0.0),
1126                Rgb::lerp(snow, ground, snow_factor),
1127                Lerp::lerp(sub_surface_color, ground, alt.sub(basement).mul(0.15)),
1128            )
1129        } else {
1130            (alt, ground, sub_surface_color)
1131        };
1132
1133        // Make river banks not have grass
1134        let ground = water_dist
1135            .map(|wd| Lerp::lerp(sub_surface_color, ground, (wd / 3.0).clamped(0.0, 1.0)))
1136            .unwrap_or(ground);
1137
1138        let (sub_surface_color, ground, alt, basement) = if mesa > 0.0 {
1139            let marble_big = (sim.gen_ctx.hill_nz.get((wposf3d.div(128.0)).into_array()) as f32)
1140                .mul(0.75)
1141                .add(1.0)
1142                .mul(0.5);
1143            let cliff_scale = 130.0;
1144            let cliff2_scale = 50.0;
1145            let cliff_offset = |scale: f32| {
1146                let x = (alt * 0.95 * (1.0 / scale) + marble_mixed * 0.07).fract() - 0.75;
1147                if x > 0.0 { x * 3.0 * scale } else { -x * scale }
1148            };
1149            let mesa_alt = alt
1150                + Lerp::lerp(
1151                    cliff_offset(cliff_scale),
1152                    cliff_offset(cliff2_scale),
1153                    ((marble_big - 0.5) * 3.0 + (marble_mixed - 0.5) * 0.0).clamped(-0.5, 0.5)
1154                        + 0.5,
1155                ) * 0.9;
1156            let alt = Lerp::lerp(alt, mesa_alt, mesa.powf(2.0) * warp_factor);
1157
1158            let idx = alt * 0.35 + (alt * 0.35 + marble * 10.0).sin();
1159            let mesa_color = Lerp::lerp(
1160                Rgb::from(
1161                    mesa_layers
1162                        .choose(&mut RandomPerm::new(idx as u32))
1163                        .copied()
1164                        .unwrap_or_default(),
1165                ),
1166                Rgb::from(
1167                    mesa_layers
1168                        .choose(&mut RandomPerm::new(idx as u32 + 1))
1169                        .copied()
1170                        .unwrap_or_default(),
1171                ),
1172                idx.fract(),
1173            );
1174
1175            let sub_surface_color = Lerp::lerp(sub_surface_color, mesa_color, mesa.powf(0.25));
1176
1177            let basement = Lerp::lerp(
1178                basement,
1179                alt,
1180                (mesa * (marble_mixed - 0.35) * 1.5).clamped(0.0, 1.0) * warp_factor,
1181            );
1182
1183            (sub_surface_color, ground, alt, basement)
1184        } else {
1185            (sub_surface_color, ground, alt, basement)
1186        };
1187
1188        // Ground under thick trees should be receive less sunlight and so often become
1189        // dirt
1190        let ground = Lerp::lerp(ground, sub_surface_color, marble_mid * tree_density);
1191
1192        let path = if spawn_rules.paths {
1193            sim.get_nearest_path(wpos)
1194        } else {
1195            None
1196        };
1197        let cave = sim.get_nearest_cave(wpos);
1198
1199        let ice_depth = if snow_factor < -0.25
1200            && water_vel.magnitude_squared() < (0.1f32 + marble_mid * 0.2).powi(2)
1201        {
1202            let cliff = (sim.gen_ctx.hill_nz.get((wposf3d.div(180.0)).into_array()) as f32)
1203                .add((marble_mid - 0.5) * 0.2)
1204                .abs()
1205                .powi(3)
1206                .mul(32.0);
1207            let cliff_ctrl = (sim.gen_ctx.hill_nz.get((wposf3d.div(128.0)).into_array()) as f32)
1208                .sub(0.4)
1209                .add((marble_mid - 0.5) * 0.2)
1210                .mul(32.0)
1211                .clamped(0.0, 1.0);
1212
1213            (((1.0 - Lerp::lerp(marble, Lerp::lerp(marble_mid, marble_small, 0.25), 0.5)) * 5.0
1214                - 1.5)
1215                .max(0.0)
1216                + cliff * cliff_ctrl)
1217                .min((water_level - alt).max(0.0))
1218        } else {
1219            0.0
1220        };
1221
1222        Some(ColumnSample {
1223            alt,
1224            riverless_alt,
1225            basement,
1226            chaos,
1227            water_level,
1228            warp_factor,
1229            surface_color: Rgb::lerp(
1230                sub_surface_color,
1231                Rgb::lerp(
1232                    // Beach
1233                    Rgb::lerp(cliff, sand, alt.sub(basement).mul(0.25)),
1234                    // Land
1235                    ground,
1236                    ((alt - base_sea_level) / 12.0).clamped(0.0, 1.0),
1237                ),
1238                surface_veg,
1239            ),
1240            sub_surface_color,
1241            // No growing directly on bedrock.
1242            // And, no growing on sites that don't want them TODO: More precise than this when we
1243            // apply trees as a post-processing layer
1244            tree_density: if spawn_rules.trees {
1245                Lerp::lerp(0.0, tree_density, alt.sub(2.0).sub(basement).mul(0.5))
1246            } else {
1247                0.0
1248            },
1249            forest_kind: sim_chunk.forest_kind,
1250            marble,
1251            marble_mid,
1252            marble_small,
1253            rock_density: if spawn_rules.trees { rock_density } else { 0.0 },
1254            temp,
1255            humidity,
1256            spawn_rate,
1257            stone_col,
1258            water_dist,
1259            gradient,
1260            path,
1261            cave,
1262            snow_cover,
1263            cliff_offset,
1264            cliff_height,
1265            water_vel,
1266            ice_depth,
1267
1268            chunk: sim_chunk,
1269        })
1270    }
1271}
1272
1273#[derive(Clone)]
1274pub struct ColumnSample<'a> {
1275    pub alt: f32,
1276    pub riverless_alt: f32,
1277    pub basement: f32,
1278    pub chaos: f32,
1279    pub water_level: f32,
1280    pub warp_factor: f32,
1281    pub surface_color: Rgb<f32>,
1282    pub sub_surface_color: Rgb<f32>,
1283    pub tree_density: f32,
1284    pub forest_kind: ForestKind,
1285    pub marble: f32,
1286    pub marble_mid: f32,
1287    pub marble_small: f32,
1288    pub rock_density: f32,
1289    pub temp: f32,
1290    pub humidity: f32,
1291    pub spawn_rate: f32,
1292    pub stone_col: Rgb<u8>,
1293    pub water_dist: Option<f32>,
1294    pub gradient: Option<f32>,
1295    pub path: Option<(f32, Vec2<f32>, Path, Vec2<f32>)>,
1296    pub cave: Option<(f32, Vec2<f32>, Cave, Vec2<f32>)>,
1297    pub snow_cover: bool,
1298    pub cliff_offset: f32,
1299    pub cliff_height: f32,
1300    pub water_vel: Vec3<f32>,
1301    pub ice_depth: f32,
1302
1303    pub chunk: &'a SimChunk,
1304}
1305
1306impl ColumnSample<'_> {
1307    pub fn get_info(&self) -> ColInfo {
1308        ColInfo {
1309            alt: self.alt,
1310            basement: self.basement,
1311            cliff_offset: self.cliff_offset,
1312            cliff_height: self.cliff_height,
1313        }
1314    }
1315}
1316
1317// For a version of ColumnSample that can easily be moved around. Feel free to
1318// add non reference fields as needed.
1319#[derive(Clone, Default)]
1320pub struct ColInfo {
1321    pub alt: f32,
1322    pub basement: f32,
1323    pub cliff_offset: f32,
1324    pub cliff_height: f32,
1325}