veloren_world/sim/
map.rs

1use crate::{
2    CONFIG, IndexRef,
3    column::ColumnSample,
4    sim::{RiverKind, WorldSim},
5    site::SiteKind,
6};
7use common::{
8    terrain::{
9        CoordinateConversions, NEIGHBOR_DELTA, TerrainChunkSize,
10        map::{Connection, ConnectionKind, MapConfig, MapSample},
11        vec2_as_uniform_idx,
12    },
13    vol::RectVolSize,
14};
15use std::f64;
16use vek::*;
17
18/// A sample function that grabs the connections at a chunk.
19///
20/// Currently this just supports rivers, but ideally it can be extended past
21/// that.
22///
23/// A sample function that grabs surface altitude at a column.
24/// (correctly reflecting settings like is_basement and is_water).
25///
26/// The altitude produced by this function at a column corresponding to a
27/// particular chunk should be identical to the altitude produced by
28/// sample_pos at that chunk.
29///
30/// You should generally pass a closure over this function into generate
31/// when constructing a map for the first time.
32/// However, if repeated construction is needed, or alternate base colors
33/// are to be used for some reason, one should pass a custom function to
34/// generate instead (e.g. one that just looks up the height in a cached
35/// array).
36pub fn sample_wpos(config: &MapConfig, sampler: &WorldSim, wpos: Vec2<i32>) -> f32 {
37    let MapConfig {
38        focus,
39        gain,
40
41        is_basement,
42        is_water,
43        ..
44    } = *config;
45
46    (sampler
47        .get_wpos(wpos)
48        .map(|s| {
49            if is_basement { s.basement } else { s.alt }.max(if is_water {
50                s.water_alt
51            } else {
52                -f32::INFINITY
53            })
54        })
55        .unwrap_or(CONFIG.sea_level)
56        - focus.z as f32)
57        / gain
58}
59
60/// Samples a MapSample at a chunk.
61///
62/// You should generally pass a closure over this function into generate
63/// when constructing a map for the first time.
64/// However, if repeated construction is needed, or alternate base colors
65/// are to be used for some reason, one should pass a custom function to
66/// generate instead (e.g. one that just looks up the color in a cached
67/// array).
68// NOTE: Deliberately not putting Rgb colors here in the config file; they
69// aren't hot reloaded anyway, and for various reasons they're probably not a
70// good idea to update in that way (for example, we currently want water colors
71// to match voxygen's).  Eventually we'll fix these sorts of issues in some
72// other way.
73pub fn sample_pos(
74    config: &MapConfig,
75    sampler: &WorldSim,
76    index: IndexRef,
77    samples: Option<&[Option<ColumnSample>]>,
78    pos: Vec2<i32>,
79) -> MapSample {
80    let map_size_lg = config.map_size_lg();
81    let MapConfig {
82        focus,
83        gain,
84
85        is_basement,
86        is_water,
87        is_ice,
88        is_shaded,
89        is_temperature,
90        is_humidity,
91        // is_debug,
92        ..
93    } = *config;
94
95    let true_sea_level = (CONFIG.sea_level as f64 - focus.z) / gain as f64;
96
97    let (
98        chunk_idx,
99        alt,
100        basement,
101        water_alt,
102        humidity,
103        temperature,
104        downhill,
105        river_kind,
106        spline_derivative,
107        is_path,
108        is_bridge,
109    ) = sampler
110        .get(pos)
111        .map(|sample| {
112            (
113                Some(vec2_as_uniform_idx(map_size_lg, pos)),
114                sample.alt,
115                sample.basement,
116                sample.water_alt,
117                sample.humidity,
118                sample.temp,
119                sample.downhill,
120                sample.river.river_kind,
121                sample.river.spline_derivative,
122                sample.path.0.is_way(),
123                sample
124                    .sites
125                    .iter()
126                    .any(|site| match &index.sites.get(*site).kind {
127                        SiteKind::Bridge(bridge) => {
128                            if let Some(plot) =
129                                bridge.wpos_tile(TerrainChunkSize::center_wpos(pos)).plot
130                            {
131                                matches!(bridge.plot(plot).kind, crate::site2::PlotKind::Bridge(_))
132                            } else {
133                                false
134                            }
135                        },
136                        _ => false,
137                    }),
138            )
139        })
140        .unwrap_or((
141            None,
142            CONFIG.sea_level,
143            CONFIG.sea_level,
144            CONFIG.sea_level,
145            0.0,
146            0.0,
147            None,
148            None,
149            Vec2::zero(),
150            false,
151            false,
152        ));
153
154    let humidity = humidity.clamp(0.0, 1.0);
155    let temperature = temperature.clamp(-1.0, 1.0) * 0.5 + 0.5;
156    let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
157    let column_data = samples
158        .and_then(|samples| {
159            chunk_idx
160                .and_then(|chunk_idx| samples.get(chunk_idx))
161                .and_then(Option::as_ref)
162        })
163        .map(|sample| {
164            // TODO: Eliminate the redundancy between this and the block renderer.
165            let alt = sample.alt;
166            let basement = sample.basement;
167            let grass_depth = (1.5 + 2.0 * sample.chaos).min(alt - basement);
168            let wposz = if is_basement { basement } else { alt };
169            let rgb = if is_basement && wposz < alt - grass_depth {
170                Lerp::lerp(
171                    sample.sub_surface_color,
172                    sample.stone_col.map(|e| e as f32 / 255.0),
173                    (alt - grass_depth - wposz) * 0.15,
174                )
175                .map(|e| e as f64)
176            } else {
177                Lerp::lerp(
178                    sample.sub_surface_color,
179                    sample.surface_color,
180                    ((wposz - (alt - grass_depth)) / grass_depth).sqrt(),
181                )
182                .map(|e| e as f64)
183            };
184
185            (rgb, alt, sample.ice_depth)
186        });
187
188    let downhill_wpos = downhill.unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32));
189    let alt = if is_basement {
190        basement
191    } else {
192        column_data.map_or(alt, |(_, alt, _)| alt)
193    };
194
195    let true_water_alt = (alt.max(water_alt) as f64 - focus.z) / gain as f64;
196    let true_alt = (alt as f64 - focus.z) / gain as f64;
197    let water_depth = (true_water_alt - true_alt).clamp(0.0, 1.0);
198    let alt = true_alt.clamp(0.0, 1.0);
199
200    let water_color_factor = 2.0;
201    let g_water = 32.0 * water_color_factor;
202    let b_water = 64.0 * water_color_factor;
203    let default_rgb = Rgb::new(
204        if is_shaded || is_temperature {
205            1.0
206        } else {
207            0.0
208        },
209        if is_shaded { 1.0 } else { alt },
210        if is_shaded || is_humidity { 1.0 } else { 0.0 },
211    );
212    let column_rgb = column_data.map(|(rgb, _, _)| rgb).unwrap_or(default_rgb);
213    let mut connections = [None; 8];
214    let mut has_connections = false;
215    // TODO: Support non-river connections.
216    // TODO: Support multiple connections.
217    let river_width = river_kind.map(|river| match river {
218        RiverKind::River { cross_section } => cross_section.x,
219        RiverKind::Lake { .. } | RiverKind::Ocean => TerrainChunkSize::RECT_SIZE.x as f32,
220    });
221    if let (Some(river_width), true) = (river_width, is_water) {
222        let downhill_pos = downhill_wpos.wpos_to_cpos();
223        NEIGHBOR_DELTA
224            .iter()
225            .zip(connections.iter_mut())
226            .filter(|&(&offset, _)| downhill_pos - pos == Vec2::from(offset))
227            .for_each(|(_, connection)| {
228                has_connections = true;
229                *connection = Some(Connection {
230                    kind: ConnectionKind::River,
231                    spline_derivative,
232                    width: river_width,
233                });
234            });
235    };
236    let rgb = if is_water && is_ice && column_data.is_some_and(|(_, _, ice_depth)| ice_depth > 0.0)
237    {
238        CONFIG.ice_color
239    } else {
240        match (river_kind, (is_water, true_alt >= true_sea_level)) {
241            (_, (false, _)) | (None, (_, true)) | (Some(RiverKind::River { .. }), _) => {
242                let (r, g, b) = (
243                    (column_rgb.r
244                        * if is_temperature {
245                            temperature as f64
246                        } else {
247                            column_rgb.r
248                        })
249                    .sqrt(),
250                    column_rgb.g,
251                    (column_rgb.b
252                        * if is_humidity {
253                            humidity as f64
254                        } else {
255                            column_rgb.b
256                        })
257                    .sqrt(),
258                );
259                Rgb::new((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
260            },
261            (None | Some(RiverKind::Lake { .. } | RiverKind::Ocean), _) => Rgb::new(
262                0,
263                ((g_water - water_depth * g_water) * 1.0) as u8,
264                ((b_water - water_depth * b_water) * 1.0) as u8,
265            ),
266        }
267    };
268    // TODO: Make principled.
269    let rgb = if is_bridge {
270        Rgb::new(0x80, 0x80, 0x80)
271    } else if is_path {
272        Rgb::new(0x37, 0x29, 0x23)
273    } else {
274        rgb
275    };
276
277    MapSample {
278        rgb: Rgb::new(rgb.r, rgb.g, rgb.b),
279        alt: if is_water {
280            true_alt.max(true_water_alt)
281        } else {
282            true_alt
283        },
284        downhill_wpos,
285        connections: if has_connections {
286            Some(connections)
287        } else {
288            None
289        },
290    }
291}