Skip to main content

veloren_world/
column.rs

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