veloren_voxygen/mesh/
terrain.rs

1#![expect(clippy::clone_on_copy)] // TODO: fix after wgpu branch
2
3use crate::{
4    mesh::{
5        MeshGen,
6        greedy::{self, GreedyConfig, GreedyMesh},
7    },
8    render::{AltIndices, FluidVertex, Mesh, TerrainAtlasData, TerrainVertex, Vertex},
9    scene::terrain::{BlocksOfInterest, DEEP_ALT, SHALLOW_ALT},
10};
11use common::{
12    terrain::{Block, TerrainChunk},
13    util::either_with,
14    vol::{ReadVol, RectRasterableVol},
15    volumes::vol_grid_2d::{CachedVolGrid2d, VolGrid2d},
16};
17use common_base::span;
18use std::{collections::VecDeque, fmt::Debug, sync::Arc};
19use tracing::error;
20use vek::*;
21
22#[derive(Clone, Copy, PartialEq)]
23pub enum FaceKind {
24    /// Opaque face that is facing something non-opaque; either
25    /// water (Opaque(true)) or something else (Opaque(false)).
26    Opaque(bool),
27    /// Fluid face that is facing something non-opaque, non-tangible,
28    /// and non-fluid (most likely air).
29    Fluid,
30}
31
32pub const SUNLIGHT: u8 = 24;
33pub const SUNLIGHT_INV: f32 = 1.0 / SUNLIGHT as f32;
34pub const MAX_LIGHT_DIST: i32 = SUNLIGHT as i32;
35
36fn calc_light<V: RectRasterableVol<Vox = Block> + ReadVol + Debug>(
37    is_sunlight: bool,
38    // When above bounds
39    default_light: u8,
40    bounds: Aabb<i32>,
41    vol: &VolGrid2d<V>,
42    lit_blocks: impl Iterator<Item = (Vec3<i32>, u8)>,
43) -> impl Fn(Vec3<i32>) -> f32 + 'static + Send + Sync {
44    span!(_guard, "calc_light");
45    const UNKNOWN: u8 = 255;
46    const OPAQUE: u8 = 254;
47
48    let outer = Aabb {
49        min: bounds.min - Vec3::new(SUNLIGHT as i32, SUNLIGHT as i32, 1),
50        max: bounds.max + Vec3::new(SUNLIGHT as i32, SUNLIGHT as i32, 1),
51    };
52
53    let mut vol_cached = vol.cached();
54
55    let mut light_map = vec![UNKNOWN; outer.size().product() as usize];
56    let lm_idx = {
57        let (w, h, _) = outer.clone().size().into_tuple();
58        move |x, y, z| (w * h * z + h * x + y) as usize
59    };
60    // Light propagation queue
61    let mut prop_que = lit_blocks
62        .map(|(pos, light)| {
63            let rpos = pos - outer.min;
64            light_map[lm_idx(rpos.x, rpos.y, rpos.z)] = light.min(SUNLIGHT); // Brightest light
65            (rpos.x as u8, rpos.y as u8, rpos.z as u16)
66        })
67        .collect::<VecDeque<_>>();
68    // Start sun rays
69    if is_sunlight {
70        for x in 0..outer.size().w {
71            for y in 0..outer.size().h {
72                let mut light = SUNLIGHT as f32;
73                for z in (0..outer.size().d).rev() {
74                    let (min_light, attenuation) = vol_cached
75                        .get(outer.min + Vec3::new(x, y, z))
76                        .map_or((0, 0.0), |b| b.get_max_sunlight());
77
78                    if light > min_light as f32 {
79                        light = (light - attenuation).max(min_light as f32);
80                    }
81
82                    light_map[lm_idx(x, y, z)] = light.floor() as u8;
83
84                    if light <= 0.0 {
85                        break;
86                    } else {
87                        prop_que.push_back((x as u8, y as u8, z as u16));
88                    }
89                }
90            }
91        }
92    }
93
94    // Determines light propagation
95    let propagate = |src: u8,
96                     dest: &mut u8,
97                     pos: Vec3<i32>,
98                     prop_que: &mut VecDeque<_>,
99                     vol: &mut CachedVolGrid2d<V>| {
100        if *dest != OPAQUE {
101            if *dest == UNKNOWN {
102                if vol.get(outer.min + pos).ok().is_some_and(|b| b.is_fluid()) {
103                    *dest = src.saturating_sub(1);
104                    // Can't propagate further
105                    if *dest > 1 {
106                        prop_que.push_back((pos.x as u8, pos.y as u8, pos.z as u16));
107                    }
108                } else {
109                    *dest = OPAQUE;
110                }
111            } else if *dest < src.saturating_sub(1) {
112                *dest = src - 1;
113                // Can't propagate further
114                if *dest > 1 {
115                    prop_que.push_back((pos.x as u8, pos.y as u8, pos.z as u16));
116                }
117            }
118        }
119    };
120
121    // Propagate light
122    while let Some(pos) = prop_que.pop_front() {
123        let pos = Vec3::new(pos.0 as i32, pos.1 as i32, pos.2 as i32);
124        let light = light_map[lm_idx(pos.x, pos.y, pos.z)];
125
126        // Up
127        // Bounds checking
128        if pos.z + 1 < outer.size().d {
129            propagate(
130                light,
131                light_map.get_mut(lm_idx(pos.x, pos.y, pos.z + 1)).unwrap(),
132                Vec3::new(pos.x, pos.y, pos.z + 1),
133                &mut prop_que,
134                &mut vol_cached,
135            )
136        }
137        // Down
138        if pos.z > 0 {
139            propagate(
140                light,
141                light_map.get_mut(lm_idx(pos.x, pos.y, pos.z - 1)).unwrap(),
142                Vec3::new(pos.x, pos.y, pos.z - 1),
143                &mut prop_que,
144                &mut vol_cached,
145            )
146        }
147        // The XY directions
148        if pos.y + 1 < outer.size().h {
149            propagate(
150                light,
151                light_map.get_mut(lm_idx(pos.x, pos.y + 1, pos.z)).unwrap(),
152                Vec3::new(pos.x, pos.y + 1, pos.z),
153                &mut prop_que,
154                &mut vol_cached,
155            )
156        }
157        if pos.y > 0 {
158            propagate(
159                light,
160                light_map.get_mut(lm_idx(pos.x, pos.y - 1, pos.z)).unwrap(),
161                Vec3::new(pos.x, pos.y - 1, pos.z),
162                &mut prop_que,
163                &mut vol_cached,
164            )
165        }
166        if pos.x + 1 < outer.size().w {
167            propagate(
168                light,
169                light_map.get_mut(lm_idx(pos.x + 1, pos.y, pos.z)).unwrap(),
170                Vec3::new(pos.x + 1, pos.y, pos.z),
171                &mut prop_que,
172                &mut vol_cached,
173            )
174        }
175        if pos.x > 0 {
176            propagate(
177                light,
178                light_map.get_mut(lm_idx(pos.x - 1, pos.y, pos.z)).unwrap(),
179                Vec3::new(pos.x - 1, pos.y, pos.z),
180                &mut prop_que,
181                &mut vol_cached,
182            )
183        }
184    }
185
186    let min_bounds = Aabb {
187        min: bounds.min - 1,
188        max: bounds.max + 1,
189    };
190
191    // Minimise light map to reduce duplication. We can now discard light info
192    // for blocks outside of the chunk borders.
193    let mut light_map2 = vec![UNKNOWN; min_bounds.size().product() as usize];
194    let lm_idx2 = {
195        let (w, h, _) = min_bounds.clone().size().into_tuple();
196        move |x, y, z| (w * h * z + h * x + y) as usize
197    };
198    for x in 0..min_bounds.size().w {
199        for y in 0..min_bounds.size().h {
200            for z in 0..min_bounds.size().d {
201                let off = min_bounds.min - outer.min;
202                light_map2[lm_idx2(x, y, z)] = light_map[lm_idx(x + off.x, y + off.y, z + off.z)];
203            }
204        }
205    }
206
207    drop(light_map);
208
209    move |wpos| {
210        let pos = wpos - min_bounds.min;
211        let l = light_map2
212            .get(lm_idx2(pos.x, pos.y, pos.z))
213            .copied()
214            .unwrap_or(default_light);
215
216        if l != OPAQUE && l != UNKNOWN {
217            l as f32 * SUNLIGHT_INV
218        } else {
219            0.0
220        }
221    }
222}
223
224#[expect(clippy::type_complexity)]
225pub fn generate_mesh<'a>(
226    vol: &'a VolGrid2d<TerrainChunk>,
227    (range, max_texture_size, _boi): (Aabb<i32>, Vec2<u16>, &'a BlocksOfInterest),
228) -> MeshGen<
229    TerrainVertex,
230    FluidVertex,
231    TerrainVertex,
232    (
233        Aabb<f32>,
234        TerrainAtlasData,
235        Vec2<u16>,
236        Arc<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>,
237        Arc<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>,
238        AltIndices,
239        (f32, f32),
240    ),
241> {
242    span!(
243        _guard,
244        "generate_mesh",
245        "<&VolGrid2d as Meshable<_, _>>::generate_mesh"
246    );
247
248    // Find blocks that should glow
249    // TODO: Search neighbouring chunks too!
250    // let glow_blocks = boi.lights
251    //     .iter()
252    //     .map(|(pos, glow)| (*pos + range.min.xy(), *glow));
253    /*  DefaultVolIterator::new(vol, range.min - MAX_LIGHT_DIST, range.max + MAX_LIGHT_DIST)
254    .filter_map(|(pos, block)| block.get_glow().map(|glow| (pos, glow))); */
255
256    let mut glow_blocks = Vec::new();
257
258    // TODO: This expensive, use BlocksOfInterest instead
259    let mut volume = vol.cached();
260    for x in -MAX_LIGHT_DIST..range.size().w + MAX_LIGHT_DIST {
261        for y in -MAX_LIGHT_DIST..range.size().h + MAX_LIGHT_DIST {
262            for z in -1..range.size().d + 1 {
263                let wpos = range.min + Vec3::new(x, y, z);
264                volume
265                    .get(wpos)
266                    .ok()
267                    .and_then(|b| b.get_glow())
268                    .map(|glow| glow_blocks.push((wpos, glow)));
269            }
270        }
271    }
272
273    // Calculate chunk lighting (sunlight defaults to 1.0, glow to 0.0)
274    let light = calc_light(true, SUNLIGHT, range, vol, core::iter::empty());
275    let glow = calc_light(false, 0, range, vol, glow_blocks.into_iter());
276
277    let (underground_alt, deep_alt) = vol
278        .get_key(vol.pos_key((range.min + range.max) / 2))
279        .map_or((0.0, 0.0), |c| {
280            (c.meta().alt() - SHALLOW_ALT, c.meta().alt() - DEEP_ALT)
281        });
282
283    let mut opaque_limits = None::<Limits>;
284    let mut fluid_limits = None::<Limits>;
285    let mut air_limits = None::<Limits>;
286    let flat_get = {
287        span!(_guard, "copy to flat array");
288        let (w, h, d) = range.size().into_tuple();
289        // z can range from -1..range.size().d + 1
290        let d = d + 2;
291        let flat = {
292            let mut volume = vol.cached();
293            const AIR: Block = Block::empty();
294            // TODO: Once we can manage it sensibly, consider using something like
295            // Option<Block> instead of just assuming air.
296            let mut flat = vec![AIR; (w * h * d) as usize];
297            let mut i = 0;
298            for x in 0..range.size().w {
299                for y in 0..range.size().h {
300                    for z in -1..range.size().d + 1 {
301                        let wpos = range.min + Vec3::new(x, y, z);
302                        let block = volume
303                            .get(wpos)
304                            .copied()
305                            // TODO: Replace with None or some other more reasonable value,
306                            // since it's not clear this will work properly with liquid.
307                            .unwrap_or(AIR);
308                        if block.is_opaque() {
309                            opaque_limits = opaque_limits
310                                .map(|l| l.including(z))
311                                .or_else(|| Some(Limits::from_value(z)));
312                        } else if block.is_liquid() {
313                            fluid_limits = fluid_limits
314                                .map(|l| l.including(z))
315                                .or_else(|| Some(Limits::from_value(z)));
316                        } else {
317                            // Assume air
318                            air_limits = air_limits
319                                .map(|l| l.including(z))
320                                .or_else(|| Some(Limits::from_value(z)));
321                        };
322                        flat[i] = block;
323                        i += 1;
324                    }
325                }
326            }
327            flat
328        };
329
330        move |Vec3 { x, y, z }| {
331            // z can range from -1..range.size().d + 1
332            let z = z + 1;
333            match flat.get((x * h * d + y * d + z) as usize).copied() {
334                Some(b) => b,
335                None => panic!("x {} y {} z {} d {} h {}", x, y, z, d, h),
336            }
337        }
338    };
339
340    // Constrain iterated area
341    let (z_start, z_end) = match (air_limits, fluid_limits, opaque_limits) {
342        (Some(air), Some(fluid), Some(opaque)) => air.three_way_intersection(fluid, opaque),
343        (Some(air), Some(fluid), None) => air.intersection(fluid),
344        (Some(air), None, Some(opaque)) => air.intersection(opaque),
345        (None, Some(fluid), Some(opaque)) => fluid.intersection(opaque),
346        // No interfaces (Note: if there are multiple fluid types this could change)
347        (Some(_), None, None) | (None, Some(_), None) | (None, None, Some(_)) => None,
348        (None, None, None) => {
349            error!("Impossible unless given an input AABB that has a height of zero");
350            None
351        },
352    }
353    .map_or((0, 0), |limits| {
354        let (start, end) = limits.into_tuple();
355        let start = start.max(0);
356        let end = end.clamp(start, range.size().d - 1);
357        (start, end)
358    });
359
360    let max_size = max_texture_size;
361    assert!(z_end >= z_start);
362    let greedy_size = Vec3::new(range.size().w - 2, range.size().h - 2, z_end - z_start + 1);
363    // NOTE: Terrain sizes are limited to 32 x 32 x 16384 (to fit in 24 bits: 5 + 5
364    // + 14). FIXME: Make this function fallible, since the terrain
365    // information might be dynamically generated which would make this hard
366    // to enforce.
367    assert!(greedy_size.x <= 32 && greedy_size.y <= 32 && greedy_size.z <= 16384);
368    // NOTE: Cast is safe by prior assertion on greedy_size; it fits into a u16,
369    // which always fits into a f32.
370    let max_bounds: Vec3<f32> = greedy_size.as_::<f32>();
371    // NOTE: Cast is safe by prior assertion on greedy_size; it fits into a u16,
372    // which always fits into a usize.
373    let greedy_size = greedy_size.as_::<usize>();
374    let greedy_size_cross = Vec3::new(greedy_size.x - 1, greedy_size.y - 1, greedy_size.z);
375    let draw_delta = Vec3::new(1, 1, z_start);
376
377    let get_light = |_: &mut (), pos: Vec3<i32>| {
378        if flat_get(pos).is_opaque() {
379            0.0
380        } else {
381            light(pos + range.min)
382        }
383    };
384    let get_ao = |_: &mut (), pos: Vec3<i32>| {
385        if flat_get(pos).is_opaque() { 0.0 } else { 1.0 }
386    };
387    let get_glow = |_: &mut (), pos: Vec3<i32>| glow(pos + range.min);
388    let get_color =
389        |_: &mut (), pos: Vec3<i32>| flat_get(pos).get_color().unwrap_or_else(Rgb::zero);
390    let get_kind = |_: &mut (), pos: Vec3<i32>| flat_get(pos).kind() as u8;
391    let get_opacity = |_: &mut (), pos: Vec3<i32>| !flat_get(pos).is_opaque();
392    let should_draw = |_: &mut (), pos: Vec3<i32>, delta: Vec3<i32>, _uv| {
393        should_draw_greedy(pos, delta, &flat_get)
394    };
395    // NOTE: Conversion to f32 is fine since this i32 is actually in bounds for u16.
396    let mesh_delta = Vec3::new(0.0, 0.0, (z_start + range.min.z) as f32);
397    let create_opaque =
398        |atlas_pos, pos, norm, meta| TerrainVertex::new(atlas_pos, pos + mesh_delta, norm, meta);
399    let create_transparent = |_atlas_pos, pos: Vec3<f32>, norm| {
400        // TODO: It *should* be possible to pull most of this code out of this function
401        // and compute it per-chunk. For some reason, this doesn't work! If you,
402        // dear reader, feel like giving it a go then feel free. For now
403        // it's been kept as-is because I'm lazy and water vertices aren't nearly common
404        // enough for this to matter much. If you want to test whether your
405        // change works, look carefully at how waves interact between water
406        // polygons in different chunks. If the join is smooth, you've solved the
407        // problem!
408        let key = vol.pos_key(range.min + pos.as_());
409        let v00 = vol
410            .get_key(key + Vec2::new(0, 0))
411            .map_or(Vec3::zero(), |c| c.meta().river_velocity());
412        let v10 = vol
413            .get_key(key + Vec2::new(1, 0))
414            .map_or(Vec3::zero(), |c| c.meta().river_velocity());
415        let v01 = vol
416            .get_key(key + Vec2::new(0, 1))
417            .map_or(Vec3::zero(), |c| c.meta().river_velocity());
418        let v11 = vol
419            .get_key(key + Vec2::new(1, 1))
420            .map_or(Vec3::zero(), |c| c.meta().river_velocity());
421        let factor =
422            (range.min + pos.as_()).map(|e| e as f32) / TerrainChunk::RECT_SIZE.map(|e| e as f32);
423        let vel = Lerp::lerp(
424            Lerp::lerp(v00, v10, factor.x.rem_euclid(1.0)),
425            Lerp::lerp(v01, v11, factor.x.rem_euclid(1.0)),
426            factor.y.rem_euclid(1.0),
427        );
428        FluidVertex::new(pos + mesh_delta, norm, vel.xy())
429    };
430
431    let mut greedy = GreedyMesh::<TerrainAtlasData, guillotiere::SimpleAtlasAllocator>::new(
432        max_size,
433        greedy::general_config(),
434    );
435    let mut opaque_deep = Vec::new();
436    let mut opaque_shallow = Vec::new();
437    let mut opaque_surface = Vec::new();
438    let mut fluid_mesh = Mesh::new();
439    greedy.push(GreedyConfig {
440        data: (),
441        draw_delta,
442        greedy_size,
443        greedy_size_cross,
444        get_ao,
445        get_light,
446        get_glow,
447        get_opacity,
448        should_draw,
449        push_quad: |atlas_origin, dim, origin, draw_dim, norm, meta: &FaceKind| match meta {
450            FaceKind::Opaque(meta) => {
451                let mut max_z = None;
452                let mut min_z = None;
453                let quad = greedy::create_quad(
454                    atlas_origin,
455                    dim,
456                    origin,
457                    draw_dim,
458                    norm,
459                    meta,
460                    |atlas_pos, pos, norm, &meta| {
461                        max_z = Some(max_z.map_or(pos.z, |z: f32| z.max(pos.z)));
462                        min_z = Some(min_z.map_or(pos.z, |z: f32| z.min(pos.z)));
463                        create_opaque(atlas_pos, pos, norm, meta)
464                    },
465                );
466                let max_alt = mesh_delta.z + max_z.expect("quad had no vertices?");
467                let min_alt = mesh_delta.z + min_z.expect("quad had no vertices?");
468
469                if max_alt < deep_alt {
470                    opaque_deep.push(quad);
471                } else if min_alt > underground_alt {
472                    opaque_surface.push(quad);
473                } else {
474                    opaque_shallow.push(quad);
475                }
476            },
477            FaceKind::Fluid => {
478                fluid_mesh.push_quad(greedy::create_quad(
479                    atlas_origin,
480                    dim,
481                    origin,
482                    draw_dim,
483                    norm,
484                    &(),
485                    |atlas_pos, pos, norm, &_meta| create_transparent(atlas_pos, pos, norm),
486                ));
487            },
488        },
489        make_face_texel: |(col_light, kind): (&mut [u8; 4], &mut u8),
490                          data: &mut (),
491                          pos,
492                          light,
493                          glow,
494                          ao| {
495            *col_light = TerrainVertex::make_col_light(light, glow, get_color(data, pos), ao);
496            *kind = get_kind(data, pos);
497        },
498    });
499
500    let min_bounds = mesh_delta;
501    let bounds = Aabb {
502        min: min_bounds,
503        max: max_bounds + min_bounds,
504    };
505    let (atlas_data, atlas_size) = greedy.finalize();
506
507    let deep_end = opaque_deep.len()
508        * if TerrainVertex::QUADS_INDEX.is_some() {
509            4
510        } else {
511            6
512        };
513    let alt_indices = AltIndices {
514        deep_end,
515        underground_end: deep_end
516            + opaque_shallow.len()
517                * if TerrainVertex::QUADS_INDEX.is_some() {
518                    4
519                } else {
520                    6
521                },
522    };
523    let sun_occluder_z_bounds = (underground_alt.max(bounds.min.z), bounds.max.z);
524
525    (
526        opaque_deep
527            .into_iter()
528            .chain(opaque_shallow)
529            .chain(opaque_surface)
530            .collect(),
531        fluid_mesh,
532        Mesh::new(),
533        (
534            bounds,
535            atlas_data,
536            atlas_size,
537            Arc::new(light),
538            Arc::new(glow),
539            alt_indices,
540            sun_occluder_z_bounds,
541        ),
542    )
543}
544
545/// NOTE: Make sure to reflect any changes to how meshing is performanced in
546/// [scene::terrain::Terrain::skip_remesh].
547pub fn should_draw_greedy(
548    pos: Vec3<i32>,
549    delta: Vec3<i32>,
550    flat_get: impl Fn(Vec3<i32>) -> Block,
551) -> Option<(bool, FaceKind)> {
552    let from = flat_get(pos - delta);
553    let to = flat_get(pos);
554    // Don't use `is_opaque`, because it actually refers to light transmission
555    let from_filled = from.is_filled();
556    if from_filled == to.is_filled() {
557        // Check the interface of liquid and non-tangible non-liquid (e.g. air).
558        let from_liquid = from.is_liquid();
559        if from_liquid == to.is_liquid() || from.is_filled() || to.is_filled() {
560            None
561        } else {
562            // While liquid is not culled, we still try to keep a consistent orientation as
563            // we do for land; if going from liquid to non-liquid,
564            // forwards-facing; otherwise, backwards-facing.
565            Some((from_liquid, FaceKind::Fluid))
566        }
567    } else {
568        // If going from unfilled to filled, backward facing; otherwise, forward
569        // facing.  Also, if either from or to is fluid, set the meta accordingly.
570        Some((
571            from_filled,
572            FaceKind::Opaque(if from_filled {
573                to.is_liquid()
574            } else {
575                from.is_liquid()
576            }),
577        ))
578    }
579}
580
581/// 1D Aabr
582#[derive(Copy, Clone, Debug)]
583struct Limits {
584    min: i32,
585    max: i32,
586}
587
588impl Limits {
589    fn from_value(v: i32) -> Self { Self { min: v, max: v } }
590
591    fn including(mut self, v: i32) -> Self {
592        if v < self.min {
593            self.min = v
594        } else if v > self.max {
595            self.max = v
596        }
597        self
598    }
599
600    fn union(self, other: Self) -> Self {
601        Self {
602            min: self.min.min(other.min),
603            max: self.max.max(other.max),
604        }
605    }
606
607    // Find limits that include the overlap of the two
608    fn intersection(self, other: Self) -> Option<Self> {
609        // Expands intersection by 1 since that fits our use-case
610        // (we need to get blocks on either side of the interface)
611        let min = self.min.max(other.min) - 1;
612        let max = self.max.min(other.max) + 1;
613
614        (min < max).then_some(Self { min, max })
615    }
616
617    // Find limits that include any areas of overlap between two of the three
618    fn three_way_intersection(self, two: Self, three: Self) -> Option<Self> {
619        let intersection = self.intersection(two);
620        let intersection = either_with(self.intersection(three), intersection, Limits::union);
621        either_with(two.intersection(three), intersection, Limits::union)
622    }
623
624    fn into_tuple(self) -> (i32, i32) { (self.min, self.max) }
625}