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, 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_cave: bool,
144    contains_river: bool,
145    near_water: bool,
146    river_velocity: Vec3<f32>,
147    approx_chunk_terrain_normal: Option<Vec3<f32>>,
148    rockiness: f32,
149    cliff_height: f32,
150    temp: f32,
151    humidity: f32,
152    site: Option<SiteKindMeta>,
153    tracks: Vec<CubicBezier3<f32>>,
154    debug_points: Vec<Vec3<f32>>,
155    debug_lines: Vec<LineSegment3<f32>>,
156    sprite_cfgs: HashMap<Vec3<i32>, SpriteCfg>,
157}
158
159impl TerrainChunkMeta {
160    pub fn new(
161        name: Option<String>,
162        biome: BiomeKind,
163        alt: f32,
164        tree_density: f32,
165        contains_cave: bool,
166        contains_river: bool,
167        near_water: bool,
168        river_velocity: Vec3<f32>,
169        temp: f32,
170        humidity: f32,
171        site: Option<SiteKindMeta>,
172        approx_chunk_terrain_normal: Option<Vec3<f32>>,
173        rockiness: f32,
174        cliff_height: f32,
175    ) -> Self {
176        Self {
177            name,
178            biome,
179            alt,
180            tree_density,
181            contains_cave,
182            contains_river,
183            near_water,
184            river_velocity,
185            temp,
186            humidity,
187            site,
188            tracks: Vec::new(),
189            debug_points: Vec::new(),
190            debug_lines: Vec::new(),
191            sprite_cfgs: HashMap::default(),
192            approx_chunk_terrain_normal,
193            rockiness,
194            cliff_height,
195        }
196    }
197
198    pub fn void() -> Self {
199        Self {
200            name: None,
201            biome: BiomeKind::Void,
202            alt: 0.0,
203            tree_density: 0.0,
204            contains_cave: false,
205            contains_river: false,
206            near_water: false,
207            river_velocity: Vec3::zero(),
208            temp: 0.0,
209            humidity: 0.0,
210            site: None,
211            tracks: Vec::new(),
212            debug_points: Vec::new(),
213            debug_lines: Vec::new(),
214            sprite_cfgs: HashMap::default(),
215            approx_chunk_terrain_normal: None,
216            rockiness: 0.0,
217            cliff_height: 0.0,
218        }
219    }
220
221    pub fn name(&self) -> Option<&str> { self.name.as_deref() }
222
223    pub fn biome(&self) -> BiomeKind { self.biome }
224
225    /// Altitude in blocks
226    pub fn alt(&self) -> f32 { self.alt }
227
228    pub fn tree_density(&self) -> f32 { self.tree_density }
229
230    pub fn contains_cave(&self) -> bool { self.contains_cave }
231
232    pub fn contains_river(&self) -> bool { self.contains_river }
233
234    pub fn near_water(&self) -> bool { self.near_water }
235
236    pub fn river_velocity(&self) -> Vec3<f32> { self.river_velocity }
237
238    pub fn site(&self) -> Option<SiteKindMeta> { self.site }
239
240    /// Temperature from 0 to 1 (possibly -1 to 1)
241    pub fn temp(&self) -> f32 { self.temp }
242
243    pub fn humidity(&self) -> f32 { self.humidity }
244
245    pub fn tracks(&self) -> &[CubicBezier3<f32>] { &self.tracks }
246
247    pub fn add_track(&mut self, bezier: CubicBezier3<f32>) { self.tracks.push(bezier); }
248
249    pub fn debug_points(&self) -> &[Vec3<f32>] { &self.debug_points }
250
251    pub fn add_debug_point(&mut self, point: Vec3<f32>) { self.debug_points.push(point); }
252
253    pub fn debug_lines(&self) -> &[LineSegment3<f32>] { &self.debug_lines }
254
255    pub fn add_debug_line(&mut self, line: LineSegment3<f32>) { self.debug_lines.push(line); }
256
257    pub fn sprite_cfg_at(&self, rpos: Vec3<i32>) -> Option<&SpriteCfg> {
258        self.sprite_cfgs.get(&rpos)
259    }
260
261    pub fn set_sprite_cfg_at(&mut self, rpos: Vec3<i32>, sprite_cfg: SpriteCfg) {
262        self.sprite_cfgs.insert(rpos, sprite_cfg);
263    }
264
265    pub fn approx_chunk_terrain_normal(&self) -> Option<Vec3<f32>> {
266        self.approx_chunk_terrain_normal
267    }
268
269    pub fn rockiness(&self) -> f32 { self.rockiness }
270
271    pub fn cliff_height(&self) -> f32 { self.cliff_height }
272}
273
274// Terrain type aliases
275
276pub type TerrainChunk = chonk::Chonk<Block, TerrainChunkSize, TerrainChunkMeta>;
277pub type TerrainGrid = VolGrid2d<TerrainChunk>;
278
279const TERRAIN_GRID_SEARCH_DIST: i32 = 63;
280
281impl TerrainGrid {
282    /// Find a location suitable for spawning an entity near the given
283    /// position (but in the same chunk).
284    pub fn find_ground(&self, pos: Vec3<i32>) -> Vec3<i32> {
285        self.try_find_ground(pos).unwrap_or(pos)
286    }
287
288    pub fn is_space(&self, pos: Vec3<i32>) -> bool {
289        (0..2).all(|z| {
290            self.get(pos + Vec3::unit_z() * z)
291                .map_or(true, |b| !b.is_solid())
292        })
293    }
294
295    pub fn try_find_space(&self, pos: Vec3<i32>) -> Option<Vec3<i32>> {
296        (0..TERRAIN_GRID_SEARCH_DIST * 2 + 1)
297            .map(|i| if i % 2 == 0 { i } else { -i } / 2)
298            .map(|z_diff| pos + Vec3::unit_z() * z_diff)
299            .find(|pos| self.is_space(*pos))
300    }
301
302    pub fn try_find_ground(&self, pos: Vec3<i32>) -> Option<Vec3<i32>> {
303        (0..TERRAIN_GRID_SEARCH_DIST * 2 + 1)
304            .map(|i| if i % 2 == 0 { i } else { -i } / 2)
305            .map(|z_diff| pos + Vec3::unit_z() * z_diff)
306            .find(|pos| {
307                self.get(pos - Vec3::unit_z()).is_ok_and(|b| b.is_filled()) && self.is_space(*pos)
308            })
309    }
310
311    pub fn get_interpolated<T, F>(&self, pos: Vec2<i32>, mut f: F) -> Option<T>
312    where
313        T: Copy + Default + Add<Output = T> + Mul<f32, Output = T>,
314        F: FnMut(&TerrainChunk) -> T,
315    {
316        let pos = pos.as_::<f64>().wpos_to_cpos();
317
318        let cubic = |a: T, b: T, c: T, d: T, x: f32| -> T {
319            let x2 = x * x;
320
321            // Catmull-Rom splines
322            let co0 = a * -0.5 + b * 1.5 + c * -1.5 + d * 0.5;
323            let co1 = a + b * -2.5 + c * 2.0 + d * -0.5;
324            let co2 = a * -0.5 + c * 0.5;
325            let co3 = b;
326
327            co0 * x2 * x + co1 * x2 + co2 * x + co3
328        };
329
330        let mut x = [T::default(); 4];
331
332        for (x_idx, j) in (-1..3).enumerate() {
333            let y0 = f(self.get_key(pos.map2(Vec2::new(j, -1), |e, q| e.max(0.0) as i32 + q))?);
334            let y1 = f(self.get_key(pos.map2(Vec2::new(j, 0), |e, q| e.max(0.0) as i32 + q))?);
335            let y2 = f(self.get_key(pos.map2(Vec2::new(j, 1), |e, q| e.max(0.0) as i32 + q))?);
336            let y3 = f(self.get_key(pos.map2(Vec2::new(j, 2), |e, q| e.max(0.0) as i32 + q))?);
337
338            x[x_idx] = cubic(y0, y1, y2, y3, pos.y.fract() as f32);
339        }
340
341        Some(cubic(x[0], x[1], x[2], x[3], pos.x.fract() as f32))
342    }
343}
344
345impl TerrainChunk {
346    /// Generate an all-water chunk at a specific sea level.
347    pub fn water(sea_level: i32) -> TerrainChunk {
348        TerrainChunk::new(
349            sea_level,
350            Block::water(SpriteKind::Empty),
351            Block::air(SpriteKind::Empty),
352            TerrainChunkMeta::void(),
353        )
354    }
355
356    /// Find the highest or lowest accessible position within the chunk
357    pub fn find_accessible_pos(&self, spawn_wpos: Vec2<i32>, ascending: bool) -> Vec3<f32> {
358        let min_z = self.get_min_z();
359        let max_z = self.get_max_z();
360
361        let pos = Vec3::new(
362            spawn_wpos.x,
363            spawn_wpos.y,
364            if ascending { min_z } else { max_z },
365        );
366        (0..(max_z - min_z))
367            .map(|z_diff| {
368                if ascending {
369                    pos + Vec3::unit_z() * z_diff
370                } else {
371                    pos - Vec3::unit_z() * z_diff
372                }
373            })
374            .find(|test_pos| {
375                let chunk_relative_xy = test_pos
376                    .xy()
377                    .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.rem_euclid(sz as i32));
378                self.get(
379                    Vec3::new(chunk_relative_xy.x, chunk_relative_xy.y, test_pos.z)
380                        - Vec3::unit_z(),
381                )
382                .is_ok_and(|b| b.is_filled())
383                    && (0..3).all(|z| {
384                        self.get(
385                            Vec3::new(chunk_relative_xy.x, chunk_relative_xy.y, test_pos.z)
386                                + Vec3::unit_z() * z,
387                        )
388                        .map_or(true, |b| !b.is_solid())
389                    })
390            })
391            .unwrap_or(pos)
392            .map(|e| e as f32)
393            + 0.5
394    }
395}
396
397// Terrain helper functions used across multiple crates.
398
399/// Computes the position Vec2 of a SimChunk from an index, where the index was
400/// generated by uniform_noise.
401///
402/// NOTE: Dimensions obey constraints on [map::MapConfig::map_size_lg].
403#[inline(always)]
404pub fn uniform_idx_as_vec2(map_size_lg: MapSizeLg, idx: usize) -> Vec2<i32> {
405    let x_mask = (1 << map_size_lg.vec().x) - 1;
406    Vec2::new((idx & x_mask) as i32, (idx >> map_size_lg.vec().x) as i32)
407}
408
409/// Computes the index of a Vec2 of a SimChunk from a position, where the index
410/// is generated by uniform_noise.  NOTE: Both components of idx should be
411/// in-bounds!
412#[inline(always)]
413pub fn vec2_as_uniform_idx(map_size_lg: MapSizeLg, idx: Vec2<i32>) -> usize {
414    ((idx.y as usize) << map_size_lg.vec().x) | idx.x as usize
415}
416
417// NOTE: want to keep this such that the chunk index is in ascending order!
418pub const NEIGHBOR_DELTA: [(i32, i32); 8] = [
419    (-1, -1),
420    (0, -1),
421    (1, -1),
422    (-1, 0),
423    (1, 0),
424    (-1, 1),
425    (0, 1),
426    (1, 1),
427];
428
429/// Iterate through all cells adjacent to a chunk.
430#[inline(always)]
431pub fn neighbors(map_size_lg: MapSizeLg, posi: usize) -> impl Clone + Iterator<Item = usize> {
432    let pos = uniform_idx_as_vec2(map_size_lg, posi);
433    let world_size = map_size_lg.chunks();
434    NEIGHBOR_DELTA
435        .iter()
436        .map(move |&(x, y)| Vec2::new(pos.x + x, pos.y + y))
437        .filter(move |pos| {
438            pos.x >= 0 && pos.y >= 0 && pos.x < world_size.x as i32 && pos.y < world_size.y as i32
439        })
440        .map(move |pos| vec2_as_uniform_idx(map_size_lg, pos))
441}
442
443pub fn river_spline_coeffs(
444    // _sim: &WorldSim,
445    chunk_pos: Vec2<f64>,
446    spline_derivative: Vec2<f32>,
447    downhill_pos: Vec2<f64>,
448) -> Vec3<Vec2<f64>> {
449    let dxy = downhill_pos - chunk_pos;
450    // Since all splines have been precomputed, we don't have to do that much work
451    // to evaluate the spline.  The spline is just ax^2 + bx + c = 0, where
452    //
453    // a = dxy - chunk.river.spline_derivative
454    // b = chunk.river.spline_derivative
455    // c = chunk_pos
456    let spline_derivative = spline_derivative.map(|e| e as f64);
457    Vec3::new(dxy - spline_derivative, spline_derivative, chunk_pos)
458}
459
460/// Find the nearest point from a quadratic spline to this point (in terms of t,
461/// the "distance along the curve" by which our spline is parameterized).  Note
462/// that if t < 0.0 or t >= 1.0, we probably shouldn't be considered "on the
463/// curve"... hopefully this works out okay and gives us what we want (a
464/// river that extends outwards tangent to a quadratic curve, with width
465/// configured by distance along the line).
466pub fn quadratic_nearest_point(
467    spline: &Vec3<Vec2<f64>>,
468    point: Vec2<f64>,
469    _line: Vec2<Vec2<f64>>, // Used for alternative distance functions below
470) -> Option<(f64, Vec2<f64>, f64)> {
471    //let eval_at = |t: f64| spline.x * t * t + spline.y * t + spline.z;
472
473    // Linear
474
475    // let line = LineSegment2 {
476    //     start: line.x,
477    //     end: line.y,
478    // };
479    // let len_sq = line.start.distance_squared(line.end);
480    // let t = ((point - line.start).dot(line.end - line.start) /
481    // len_sq).clamped(0.0, 1.0); let pos = line.start + (line.end - line.start)
482    // * t; return Some((t, pos, pos.distance_squared(point)));
483
484    // Quadratic
485
486    // let curve = QuadraticBezier2 {
487    //     start: line.x,
488    //     ctrl: eval_at(0.5),
489    //     end: line.y,
490    // };
491    // let (t, pos) = curve.binary_search_point_by_steps(point, 16, 0.001);
492    // let t = t.clamped(0.0, 1.0);
493    // let pos = curve.evaluate(t);
494    // return Some((t, pos, pos.distance_squared(point)));
495
496    // Cubic
497
498    // let ctrl_at = |t: f64, end: f64| {
499    //     let a = eval_at(end);
500    //     let b = eval_at(Lerp::lerp(end, t, 0.1));
501    //     let dir = (b - a).normalized();
502    //     let exact = eval_at(t);
503    //     a + dir * exact.distance(a)
504    // };
505    // let curve = CubicBezier2 {
506    //     start: line.x,
507    //     ctrl0: ctrl_at(0.33, 0.0),
508    //     ctrl1: ctrl_at(0.66, 1.0),
509    //     end: line.y,
510    // };
511    // let (t, pos) = curve.binary_search_point_by_steps(point, 12, 0.01);
512    // let t = t.clamped(0.0, 1.0);
513    // let pos = curve.evaluate(t);
514    // return Some((t, pos, pos.distance_squared(point)));
515
516    let a = spline.z.x;
517    let b = spline.y.x;
518    let c = spline.x.x;
519    let d = point.x;
520    let e = spline.z.y;
521    let f = spline.y.y;
522    let g = spline.x.y;
523    let h = point.y;
524    // This is equivalent to solving the following cubic equation (derivation is a
525    // bit annoying):
526    //
527    // A = 2(c^2 + g^2)
528    // B = 3(b * c + g * f)
529    // C = ((a - d) * 2 * c + b^2 + (e - h) * 2 * g + f^2)
530    // D = ((a - d) * b + (e - h) * f)
531    //
532    // Ax³ + Bx² + Cx + D = 0
533    //
534    // Once solved, this yield up to three possible values for t (reflecting minimal
535    // and maximal values).  We should choose the minimal such real value with t
536    // between 0.0 and 1.0.  If we fall outside those bounds, then we are
537    // outside the spline and return None.
538    let a_ = (c * c + g * g) * 2.0;
539    let b_ = (b * c + g * f) * 3.0;
540    let a_d = a - d;
541    let e_h = e - h;
542    let c_ = a_d * c * 2.0 + b * b + e_h * g * 2.0 + f * f;
543    let d_ = a_d * b + e_h * f;
544    let roots = find_roots_cubic(a_, b_, c_, d_);
545    let roots = roots.as_ref();
546
547    let min_root = roots
548        .iter()
549        .copied()
550        .map(|root| {
551            let river_point = spline.x * root * root + spline.y * root + spline.z;
552            if root > 0.0 && root < 1.0 {
553                (root, river_point)
554            } else {
555                let root = root.clamped(0.0, 1.0);
556                let river_point = spline.x * root * root + spline.y * root + spline.z;
557                (root, river_point)
558            }
559        })
560        .map(|(root, river_point)| {
561            let river_distance = river_point.distance_squared(point);
562            (root, river_point, river_distance)
563        })
564        // In the (unlikely?) case that distances are equal, prefer the earliest point along the
565        // river.
566        .min_by(|&(ap, _, a), &(bp, _, b)| {
567            (a, !(0.0..=1.0).contains(&ap), ap)
568                .partial_cmp(&(b, !(0.0..=1.0).contains(&bp), bp))
569                .unwrap()
570        });
571    min_root
572}