veloren_common/terrain/
mod.rs

1pub mod biome;
2pub mod block;
3pub mod chonk;
4pub mod map;
5pub mod site;
6pub mod sprite;
7pub mod structure;
8
9use std::ops::{Add, Mul};
10
11// Reexports
12pub use self::{
13    biome::BiomeKind,
14    block::{Block, BlockKind},
15    map::MapSizeLg,
16    site::SiteKindMeta,
17    sprite::{SpriteCfg, SpriteKind, StructureSprite, UnlockKind},
18    structure::{Structure, StructuresGroup},
19};
20use hashbrown::HashMap;
21use roots::find_roots_cubic;
22use serde::{Deserialize, Serialize};
23
24use crate::{
25    vol::{ReadVol, RectVolSize},
26    volumes::vol_grid_2d::VolGrid2d,
27};
28use vek::*;
29
30// TerrainChunkSize
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct TerrainChunkSize;
34
35/// Base two logarithm of the number of blocks along either horizontal axis of
36/// a chunk.
37///
38/// NOTE: (1 << CHUNK_SIZE_LG) is guaranteed to fit in a u32.
39///
40/// NOTE: A lot of code assumes that the two dimensions are equal, so we make it
41/// explicit here.
42///
43/// NOTE: It is highly unlikely that a value greater than 5 will work, as many
44/// frontend optimizations rely on being able to pack chunk horizontal
45/// dimensions into 5 bits each.
46pub const TERRAIN_CHUNK_BLOCKS_LG: u32 = 5;
47
48impl RectVolSize for TerrainChunkSize {
49    const RECT_SIZE: Vec2<u32> = Vec2 {
50        x: (1 << TERRAIN_CHUNK_BLOCKS_LG),
51        y: (1 << TERRAIN_CHUNK_BLOCKS_LG),
52    };
53}
54
55impl TerrainChunkSize {
56    #[inline(always)]
57    /// Convert dimensions in terms of chunks into dimensions in terms of blocks
58    /// ```
59    /// use vek::*;
60    /// use veloren_common::terrain::TerrainChunkSize;
61    ///
62    /// assert_eq!(TerrainChunkSize::blocks(Vec2::new(3, 2)), Vec2::new(96, 64));
63    /// ```
64    pub fn blocks(chunks: Vec2<u32>) -> Vec2<u32> { chunks * Self::RECT_SIZE }
65
66    /// Calculate the world position (i.e. in blocks) at the center of this
67    /// chunk
68    /// ```
69    /// use vek::*;
70    /// use veloren_common::terrain::TerrainChunkSize;
71    ///
72    /// assert_eq!(
73    ///     TerrainChunkSize::center_wpos(Vec2::new(0, 2)),
74    ///     Vec2::new(16, 80)
75    /// );
76    /// ```
77    pub fn center_wpos(chunk_pos: Vec2<i32>) -> Vec2<i32> {
78        chunk_pos * Self::RECT_SIZE.as_::<i32>() + Self::RECT_SIZE.as_::<i32>() / 2
79    }
80}
81
82pub trait CoordinateConversions {
83    fn wpos_to_cpos(&self) -> Self;
84    fn cpos_to_wpos(&self) -> Self;
85    fn cpos_to_wpos_center(&self) -> Self;
86}
87
88impl CoordinateConversions for Vec2<i32> {
89    #[inline]
90    fn wpos_to_cpos(&self) -> Self {
91        self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.div_euclid(sz as i32))
92    }
93
94    #[inline]
95    fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as i32) }
96
97    #[inline]
98    fn cpos_to_wpos_center(&self) -> Self {
99        self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
100            e * sz as i32 + sz as i32 / 2
101        })
102    }
103}
104
105impl CoordinateConversions for Vec2<f32> {
106    #[inline]
107    fn wpos_to_cpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e / sz as f32) }
108
109    #[inline]
110    fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as f32) }
111
112    #[inline]
113    fn cpos_to_wpos_center(&self) -> Self {
114        self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
115            e * sz as f32 + sz as f32 / 2.0
116        })
117    }
118}
119
120impl CoordinateConversions for Vec2<f64> {
121    #[inline]
122    fn wpos_to_cpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e / sz as f64) }
123
124    #[inline]
125    fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as f64) }
126
127    #[inline]
128    fn cpos_to_wpos_center(&self) -> Self {
129        self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
130            e * sz as f64 + sz as f64 / 2.0
131        })
132    }
133}
134
135// TerrainChunkMeta
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct TerrainChunkMeta {
139    name: Option<String>,
140    biome: BiomeKind,
141    alt: f32,
142    tree_density: f32,
143    contains_river: bool,
144    near_water: bool,
145    river_velocity: Vec3<f32>,
146    approx_chunk_terrain_normal: Option<Vec3<f32>>,
147    rockiness: f32,
148    cliff_height: f32,
149    temp: f32,
150    humidity: f32,
151    site: Option<SiteKindMeta>,
152    tracks: Vec<CubicBezier3<f32>>,
153    debug_points: Vec<Vec3<f32>>,
154    debug_lines: Vec<LineSegment3<f32>>,
155    sprite_cfgs: HashMap<Vec3<i32>, SpriteCfg>,
156}
157
158impl TerrainChunkMeta {
159    pub fn new(
160        name: Option<String>,
161        biome: BiomeKind,
162        alt: f32,
163        tree_density: f32,
164        contains_river: bool,
165        near_water: bool,
166        river_velocity: Vec3<f32>,
167        temp: f32,
168        humidity: f32,
169        site: Option<SiteKindMeta>,
170        approx_chunk_terrain_normal: Option<Vec3<f32>>,
171        rockiness: f32,
172        cliff_height: f32,
173    ) -> Self {
174        Self {
175            name,
176            biome,
177            alt,
178            tree_density,
179            contains_river,
180            near_water,
181            river_velocity,
182            temp,
183            humidity,
184            site,
185            tracks: Vec::new(),
186            debug_points: Vec::new(),
187            debug_lines: Vec::new(),
188            sprite_cfgs: HashMap::default(),
189            approx_chunk_terrain_normal,
190            rockiness,
191            cliff_height,
192        }
193    }
194
195    pub fn void() -> Self {
196        Self {
197            name: None,
198            biome: BiomeKind::Void,
199            alt: 0.0,
200            tree_density: 0.0,
201            contains_river: false,
202            near_water: false,
203            river_velocity: Vec3::zero(),
204            temp: 0.0,
205            humidity: 0.0,
206            site: None,
207            tracks: Vec::new(),
208            debug_points: Vec::new(),
209            debug_lines: Vec::new(),
210            sprite_cfgs: HashMap::default(),
211            approx_chunk_terrain_normal: None,
212            rockiness: 0.0,
213            cliff_height: 0.0,
214        }
215    }
216
217    pub fn name(&self) -> Option<&str> { self.name.as_deref() }
218
219    pub fn biome(&self) -> BiomeKind { self.biome }
220
221    /// Altitude in blocks
222    pub fn alt(&self) -> f32 { self.alt }
223
224    pub fn tree_density(&self) -> f32 { self.tree_density }
225
226    pub fn contains_river(&self) -> bool { self.contains_river }
227
228    pub fn near_water(&self) -> bool { self.near_water }
229
230    pub fn river_velocity(&self) -> Vec3<f32> { self.river_velocity }
231
232    pub fn site(&self) -> Option<SiteKindMeta> { self.site }
233
234    /// Temperature from 0 to 1 (possibly -1 to 1)
235    pub fn temp(&self) -> f32 { self.temp }
236
237    pub fn humidity(&self) -> f32 { self.humidity }
238
239    pub fn tracks(&self) -> &[CubicBezier3<f32>] { &self.tracks }
240
241    pub fn add_track(&mut self, bezier: CubicBezier3<f32>) { self.tracks.push(bezier); }
242
243    pub fn debug_points(&self) -> &[Vec3<f32>] { &self.debug_points }
244
245    pub fn add_debug_point(&mut self, point: Vec3<f32>) { self.debug_points.push(point); }
246
247    pub fn debug_lines(&self) -> &[LineSegment3<f32>] { &self.debug_lines }
248
249    pub fn add_debug_line(&mut self, line: LineSegment3<f32>) { self.debug_lines.push(line); }
250
251    pub fn sprite_cfg_at(&self, rpos: Vec3<i32>) -> Option<&SpriteCfg> {
252        self.sprite_cfgs.get(&rpos)
253    }
254
255    pub fn set_sprite_cfg_at(&mut self, rpos: Vec3<i32>, sprite_cfg: SpriteCfg) {
256        self.sprite_cfgs.insert(rpos, sprite_cfg);
257    }
258
259    pub fn approx_chunk_terrain_normal(&self) -> Option<Vec3<f32>> {
260        self.approx_chunk_terrain_normal
261    }
262
263    pub fn rockiness(&self) -> f32 { self.rockiness }
264
265    pub fn cliff_height(&self) -> f32 { self.cliff_height }
266}
267
268// Terrain type aliases
269
270pub type TerrainChunk = chonk::Chonk<Block, TerrainChunkSize, TerrainChunkMeta>;
271pub type TerrainGrid = VolGrid2d<TerrainChunk>;
272
273const TERRAIN_GRID_SEARCH_DIST: i32 = 63;
274
275impl TerrainGrid {
276    /// Find a location suitable for spawning an entity near the given
277    /// position (but in the same chunk).
278    pub fn find_ground(&self, pos: Vec3<i32>) -> Vec3<i32> {
279        self.try_find_ground(pos).unwrap_or(pos)
280    }
281
282    pub fn is_space(&self, pos: Vec3<i32>) -> bool {
283        (0..2).all(|z| {
284            self.get(pos + Vec3::unit_z() * z)
285                .map_or(true, |b| !b.is_solid())
286        })
287    }
288
289    pub fn try_find_space(&self, pos: Vec3<i32>) -> Option<Vec3<i32>> {
290        (0..TERRAIN_GRID_SEARCH_DIST * 2 + 1)
291            .map(|i| if i % 2 == 0 { i } else { -i } / 2)
292            .map(|z_diff| pos + Vec3::unit_z() * z_diff)
293            .find(|pos| self.is_space(*pos))
294    }
295
296    pub fn try_find_ground(&self, pos: Vec3<i32>) -> Option<Vec3<i32>> {
297        (0..TERRAIN_GRID_SEARCH_DIST * 2 + 1)
298            .map(|i| if i % 2 == 0 { i } else { -i } / 2)
299            .map(|z_diff| pos + Vec3::unit_z() * z_diff)
300            .find(|pos| {
301                self.get(pos - Vec3::unit_z()).is_ok_and(|b| b.is_filled()) && self.is_space(*pos)
302            })
303    }
304
305    pub fn get_interpolated<T, F>(&self, pos: Vec2<i32>, mut f: F) -> Option<T>
306    where
307        T: Copy + Default + Add<Output = T> + Mul<f32, Output = T>,
308        F: FnMut(&TerrainChunk) -> T,
309    {
310        let pos = pos.as_::<f64>().wpos_to_cpos();
311
312        let cubic = |a: T, b: T, c: T, d: T, x: f32| -> T {
313            let x2 = x * x;
314
315            // Catmull-Rom splines
316            let co0 = a * -0.5 + b * 1.5 + c * -1.5 + d * 0.5;
317            let co1 = a + b * -2.5 + c * 2.0 + d * -0.5;
318            let co2 = a * -0.5 + c * 0.5;
319            let co3 = b;
320
321            co0 * x2 * x + co1 * x2 + co2 * x + co3
322        };
323
324        let mut x = [T::default(); 4];
325
326        for (x_idx, j) in (-1..3).enumerate() {
327            let y0 = f(self.get_key(pos.map2(Vec2::new(j, -1), |e, q| e.max(0.0) as i32 + q))?);
328            let y1 = f(self.get_key(pos.map2(Vec2::new(j, 0), |e, q| e.max(0.0) as i32 + q))?);
329            let y2 = f(self.get_key(pos.map2(Vec2::new(j, 1), |e, q| e.max(0.0) as i32 + q))?);
330            let y3 = f(self.get_key(pos.map2(Vec2::new(j, 2), |e, q| e.max(0.0) as i32 + q))?);
331
332            x[x_idx] = cubic(y0, y1, y2, y3, pos.y.fract() as f32);
333        }
334
335        Some(cubic(x[0], x[1], x[2], x[3], pos.x.fract() as f32))
336    }
337
338    pub fn sprite_cfg_at(&self, wpos: Vec3<i32>) -> Option<&SpriteCfg> {
339        let chunk = self.pos_chunk(wpos)?;
340        let sprite_chunk_pos = TerrainGrid::chunk_offs(wpos);
341
342        chunk.meta().sprite_cfg_at(sprite_chunk_pos)
343    }
344}
345
346impl TerrainChunk {
347    /// Generate an all-water chunk at a specific sea level.
348    pub fn water(sea_level: i32) -> TerrainChunk {
349        TerrainChunk::new(
350            sea_level,
351            Block::water(SpriteKind::Empty),
352            Block::air(SpriteKind::Empty),
353            TerrainChunkMeta::void(),
354        )
355    }
356
357    /// Find the highest or lowest accessible position within the chunk
358    pub fn find_accessible_pos(&self, spawn_wpos: Vec2<i32>, ascending: bool) -> Vec3<f32> {
359        let min_z = self.get_min_z();
360        let max_z = self.get_max_z();
361
362        let pos = Vec3::new(
363            spawn_wpos.x,
364            spawn_wpos.y,
365            if ascending { min_z } else { max_z },
366        );
367        (0..(max_z - min_z))
368            .map(|z_diff| {
369                if ascending {
370                    pos + Vec3::unit_z() * z_diff
371                } else {
372                    pos - Vec3::unit_z() * z_diff
373                }
374            })
375            .find(|test_pos| {
376                let chunk_relative_xy = test_pos
377                    .xy()
378                    .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.rem_euclid(sz as i32));
379                self.get(
380                    Vec3::new(chunk_relative_xy.x, chunk_relative_xy.y, test_pos.z)
381                        - Vec3::unit_z(),
382                )
383                .is_ok_and(|b| b.is_filled())
384                    && (0..3).all(|z| {
385                        self.get(
386                            Vec3::new(chunk_relative_xy.x, chunk_relative_xy.y, test_pos.z)
387                                + Vec3::unit_z() * z,
388                        )
389                        .map_or(true, |b| !b.is_solid())
390                    })
391            })
392            .unwrap_or(pos)
393            .map(|e| e as f32)
394            + 0.5
395    }
396}
397
398// Terrain helper functions used across multiple crates.
399
400/// Computes the position Vec2 of a SimChunk from an index, where the index was
401/// generated by uniform_noise.
402///
403/// NOTE: Dimensions obey constraints on [map::MapConfig::map_size_lg].
404#[inline(always)]
405pub fn uniform_idx_as_vec2(map_size_lg: MapSizeLg, idx: usize) -> Vec2<i32> {
406    let x_mask = (1 << map_size_lg.vec().x) - 1;
407    Vec2::new((idx & x_mask) as i32, (idx >> map_size_lg.vec().x) as i32)
408}
409
410/// Computes the index of a Vec2 of a SimChunk from a position, where the index
411/// is generated by uniform_noise.  NOTE: Both components of idx should be
412/// in-bounds!
413#[inline(always)]
414pub fn vec2_as_uniform_idx(map_size_lg: MapSizeLg, idx: Vec2<i32>) -> usize {
415    ((idx.y as usize) << map_size_lg.vec().x) | idx.x as usize
416}
417
418// NOTE: want to keep this such that the chunk index is in ascending order!
419pub const NEIGHBOR_DELTA: [(i32, i32); 8] = [
420    (-1, -1),
421    (0, -1),
422    (1, -1),
423    (-1, 0),
424    (1, 0),
425    (-1, 1),
426    (0, 1),
427    (1, 1),
428];
429
430/// Iterate through all cells adjacent to a chunk.
431#[inline(always)]
432pub fn neighbors(map_size_lg: MapSizeLg, posi: usize) -> impl Clone + Iterator<Item = usize> {
433    let pos = uniform_idx_as_vec2(map_size_lg, posi);
434    let world_size = map_size_lg.chunks();
435    NEIGHBOR_DELTA
436        .iter()
437        .map(move |&(x, y)| Vec2::new(pos.x + x, pos.y + y))
438        .filter(move |pos| {
439            pos.x >= 0 && pos.y >= 0 && pos.x < world_size.x as i32 && pos.y < world_size.y as i32
440        })
441        .map(move |pos| vec2_as_uniform_idx(map_size_lg, pos))
442}
443
444pub fn river_spline_coeffs(
445    // _sim: &WorldSim,
446    chunk_pos: Vec2<f64>,
447    spline_derivative: Vec2<f32>,
448    downhill_pos: Vec2<f64>,
449) -> Vec3<Vec2<f64>> {
450    let dxy = downhill_pos - chunk_pos;
451    // Since all splines have been precomputed, we don't have to do that much work
452    // to evaluate the spline.  The spline is just ax^2 + bx + c = 0, where
453    //
454    // a = dxy - chunk.river.spline_derivative
455    // b = chunk.river.spline_derivative
456    // c = chunk_pos
457    let spline_derivative = spline_derivative.map(|e| e as f64);
458    Vec3::new(dxy - spline_derivative, spline_derivative, chunk_pos)
459}
460
461/// Find the nearest point from a quadratic spline to this point (in terms of t,
462/// the "distance along the curve" by which our spline is parameterized).  Note
463/// that if t < 0.0 or t >= 1.0, we probably shouldn't be considered "on the
464/// curve"... hopefully this works out okay and gives us what we want (a
465/// river that extends outwards tangent to a quadratic curve, with width
466/// configured by distance along the line).
467pub fn quadratic_nearest_point(
468    spline: &Vec3<Vec2<f64>>,
469    point: Vec2<f64>,
470    _line: Vec2<Vec2<f64>>, // Used for alternative distance functions below
471) -> Option<(f64, Vec2<f64>, f64)> {
472    //let eval_at = |t: f64| spline.x * t * t + spline.y * t + spline.z;
473
474    // Linear
475
476    // let line = LineSegment2 {
477    //     start: line.x,
478    //     end: line.y,
479    // };
480    // let len_sq = line.start.distance_squared(line.end);
481    // let t = ((point - line.start).dot(line.end - line.start) /
482    // len_sq).clamped(0.0, 1.0); let pos = line.start + (line.end - line.start)
483    // * t; return Some((t, pos, pos.distance_squared(point)));
484
485    // Quadratic
486
487    // let curve = QuadraticBezier2 {
488    //     start: line.x,
489    //     ctrl: eval_at(0.5),
490    //     end: line.y,
491    // };
492    // let (t, pos) = curve.binary_search_point_by_steps(point, 16, 0.001);
493    // let t = t.clamped(0.0, 1.0);
494    // let pos = curve.evaluate(t);
495    // return Some((t, pos, pos.distance_squared(point)));
496
497    // Cubic
498
499    // let ctrl_at = |t: f64, end: f64| {
500    //     let a = eval_at(end);
501    //     let b = eval_at(Lerp::lerp(end, t, 0.1));
502    //     let dir = (b - a).normalized();
503    //     let exact = eval_at(t);
504    //     a + dir * exact.distance(a)
505    // };
506    // let curve = CubicBezier2 {
507    //     start: line.x,
508    //     ctrl0: ctrl_at(0.33, 0.0),
509    //     ctrl1: ctrl_at(0.66, 1.0),
510    //     end: line.y,
511    // };
512    // let (t, pos) = curve.binary_search_point_by_steps(point, 12, 0.01);
513    // let t = t.clamped(0.0, 1.0);
514    // let pos = curve.evaluate(t);
515    // return Some((t, pos, pos.distance_squared(point)));
516
517    let a = spline.z.x;
518    let b = spline.y.x;
519    let c = spline.x.x;
520    let d = point.x;
521    let e = spline.z.y;
522    let f = spline.y.y;
523    let g = spline.x.y;
524    let h = point.y;
525    // This is equivalent to solving the following cubic equation (derivation is a
526    // bit annoying):
527    //
528    // A = 2(c^2 + g^2)
529    // B = 3(b * c + g * f)
530    // C = ((a - d) * 2 * c + b^2 + (e - h) * 2 * g + f^2)
531    // D = ((a - d) * b + (e - h) * f)
532    //
533    // Ax³ + Bx² + Cx + D = 0
534    //
535    // Once solved, this yield up to three possible values for t (reflecting minimal
536    // and maximal values).  We should choose the minimal such real value with t
537    // between 0.0 and 1.0.  If we fall outside those bounds, then we are
538    // outside the spline and return None.
539    let a_ = (c * c + g * g) * 2.0;
540    let b_ = (b * c + g * f) * 3.0;
541    let a_d = a - d;
542    let e_h = e - h;
543    let c_ = a_d * c * 2.0 + b * b + e_h * g * 2.0 + f * f;
544    let d_ = a_d * b + e_h * f;
545    let roots = find_roots_cubic(a_, b_, c_, d_);
546    let roots = roots.as_ref();
547
548    let min_root = roots
549        .iter()
550        .copied()
551        .map(|root| {
552            let river_point = spline.x * root * root + spline.y * root + spline.z;
553            if root > 0.0 && root < 1.0 {
554                (root, river_point)
555            } else {
556                let root = root.clamped(0.0, 1.0);
557                let river_point = spline.x * root * root + spline.y * root + spline.z;
558                (root, river_point)
559            }
560        })
561        .map(|(root, river_point)| {
562            let river_distance = river_point.distance_squared(point);
563            (root, river_point, river_distance)
564        })
565        // In the (unlikely?) case that distances are equal, prefer the earliest point along the
566        // river.
567        .min_by(|&(ap, _, a), &(bp, _, b)| {
568            (a, !(0.0..=1.0).contains(&ap), ap)
569                .partial_cmp(&(b, !(0.0..=1.0).contains(&bp), bp))
570                .unwrap()
571        });
572    min_root
573}