veloren_voxygen/scene/terrain/
mod.rs

1mod sprite;
2mod watcher;
3
4pub use self::watcher::{BlocksOfInterest, FireplaceType, Interaction};
5use sprite::{FilteredSpriteData, SpriteData, SpriteModelData, SpriteSpec};
6
7use crate::{
8    mesh::{
9        greedy::{GreedyMesh, SpriteAtlasAllocator},
10        segment::generate_mesh_base_vol_sprite,
11        terrain::{SUNLIGHT_INV, generate_mesh},
12    },
13    render::{
14        AltIndices, CullingMode, FigureSpriteAtlasData, FirstPassDrawer, FluidVertex, GlobalModel,
15        Instances, LodData, Mesh, Model, RenderError, Renderer, SPRITE_VERT_PAGE_SIZE,
16        SpriteDrawer, SpriteGlobalsBindGroup, SpriteInstance, SpriteVertex, SpriteVerts,
17        TerrainAtlasData, TerrainLocals, TerrainShadowDrawer, TerrainVertex,
18        pipelines::{self, AtlasData, AtlasTextures},
19    },
20    scene::terrain::sprite::SpriteModelConfig,
21};
22
23use super::{
24    RAIN_THRESHOLD, SceneData,
25    camera::{self, Camera},
26    math,
27};
28use common::{
29    assets::{AssetExt, DotVox},
30    figure::Segment,
31    spiral::Spiral2d,
32    terrain::{Block, SpriteKind, TerrainChunk},
33    vol::{BaseVol, ReadVol, RectRasterableVol, SampleVol},
34    volumes::vol_grid_2d::{VolGrid2d, VolGrid2dError},
35};
36use common_base::{prof_span, span};
37use core::{f32, fmt::Debug, marker::PhantomData, time::Duration};
38use crossbeam_channel as channel;
39use guillotiere::AtlasAllocator;
40use hashbrown::HashMap;
41use std::sync::{
42    Arc,
43    atomic::{AtomicU64, Ordering},
44};
45use tracing::warn;
46use treeculler::{AABB, BVol, Frustum};
47use vek::*;
48
49const SPRITE_SCALE: Vec3<f32> = Vec3::new(1.0 / 11.0, 1.0 / 11.0, 1.0 / 11.0);
50pub const SPRITE_LOD_LEVELS: usize = 5;
51
52// For rain occlusion we only need to render the closest chunks.
53/// How many chunks are maximally rendered for rain occlusion.
54pub const RAIN_OCCLUSION_CHUNKS: usize = 25;
55
56#[derive(Clone, Copy, Debug)]
57struct Visibility {
58    in_range: bool,
59    in_frustum: bool,
60}
61
62impl Visibility {
63    /// Should the chunk actually get rendered?
64    fn is_visible(&self) -> bool {
65        // Currently, we don't take into account in_range to allow all chunks to do
66        // pop-in. This isn't really a problem because we no longer have VD mist
67        // or anything like that. Also, we don't load chunks outside of the VD
68        // anyway so this literally just controls which chunks get actually
69        // rendered.
70        /* self.in_range && */
71        self.in_frustum
72    }
73}
74
75/// Type of closure used for light mapping.
76type LightMapFn = Arc<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>;
77
78pub struct TerrainChunkData {
79    // GPU data
80    load_time: f32,
81    opaque_model: Option<Model<TerrainVertex>>,
82    fluid_model: Option<Model<FluidVertex>>,
83    /// If this is `None`, this texture is not allocated in the current atlas,
84    /// and therefore there is no need to free its allocation.
85    atlas_alloc: Option<guillotiere::AllocId>,
86    /// The actual backing texture for this chunk.  Use this for rendering
87    /// purposes.  The texture is reference-counted, so it will be
88    /// automatically freed when no chunks are left that need it (though
89    /// shadow chunks will still keep it alive; we could deal with this by
90    /// making this an `Option`, but it probably isn't worth it since they
91    /// shouldn't be that much more nonlocal than regular chunks).
92    atlas_textures: Arc<AtlasTextures<pipelines::terrain::Locals, TerrainAtlasData>>,
93    light_map: LightMapFn,
94    glow_map: LightMapFn,
95    sprite_instances: [(Instances<SpriteInstance>, AltIndices); SPRITE_LOD_LEVELS],
96    locals: pipelines::terrain::BoundLocals,
97    pub blocks_of_interest: BlocksOfInterest,
98
99    visible: Visibility,
100    can_shadow_point: bool,
101    can_shadow_sun: bool,
102    z_bounds: (f32, f32),
103    sun_occluder_z_bounds: (f32, f32),
104    frustum_last_plane_index: u8,
105
106    alt_indices: AltIndices,
107}
108
109/// The depth at which the intermediate zone between underground and surface
110/// begins
111pub const SHALLOW_ALT: f32 = 24.0;
112/// The depth at which the intermediate zone between underground and surface
113/// ends
114pub const DEEP_ALT: f32 = 96.0;
115/// The depth below the surface altitude at which the camera switches from
116/// displaying surface elements to underground elements
117pub const UNDERGROUND_ALT: f32 = (SHALLOW_ALT + DEEP_ALT) * 0.5;
118
119// The distance (in chunks) within which all levels of the chunks will be drawn
120// to minimise cull-related popping.
121const NEVER_CULL_DIST: i32 = 3;
122
123#[derive(Copy, Clone)]
124struct ChunkMeshState {
125    pos: Vec2<i32>,
126    started_tick: u64,
127    is_worker_active: bool,
128    // If this is set, we skip the actual meshing part of the update.
129    skip_remesh: bool,
130}
131
132/// Just the mesh part of a mesh worker response.
133pub struct MeshWorkerResponseMesh {
134    z_bounds: (f32, f32),
135    sun_occluder_z_bounds: (f32, f32),
136    opaque_mesh: Mesh<TerrainVertex>,
137    fluid_mesh: Mesh<FluidVertex>,
138    atlas_texture_data: TerrainAtlasData,
139    atlas_size: Vec2<u16>,
140    light_map: LightMapFn,
141    glow_map: LightMapFn,
142    alt_indices: AltIndices,
143}
144
145/// A type produced by mesh worker threads corresponding to the position and
146/// mesh of a chunk.
147struct MeshWorkerResponse {
148    pos: Vec2<i32>,
149    sprite_instances: [(Vec<SpriteInstance>, AltIndices); SPRITE_LOD_LEVELS],
150    /// If None, this update was requested without meshing.
151    mesh: Option<MeshWorkerResponseMesh>,
152    started_tick: u64,
153    blocks_of_interest: BlocksOfInterest,
154}
155
156pub(super) fn get_sprite_instances<'a, I: 'a>(
157    lod_levels: &'a mut [I; SPRITE_LOD_LEVELS],
158    set_instance: impl Fn(&mut I, SpriteInstance, Vec3<i32>),
159    blocks: impl Iterator<Item = (Vec3<f32>, Block)>,
160    mut to_wpos: impl FnMut(Vec3<f32>) -> Vec3<i32>,
161    mut light_map: impl FnMut(Vec3<i32>) -> f32,
162    mut glow_normal_at_wpos: impl FnMut(Vec3<i32>) -> (Vec3<f32>, f32),
163    sprite_data: &HashMap<SpriteKind, FilteredSpriteData>,
164    missing_sprite_placeholder: &SpriteData,
165) {
166    prof_span!("extract sprite_instances");
167    for (rel_pos, block) in blocks {
168        let Some(sprite) = block.get_sprite() else {
169            continue;
170        };
171        // Short-circuit and skip hashmap interaction since this is every fluid block
172        // (including air)
173        if matches!(sprite, SpriteKind::Empty) {
174            continue;
175        }
176
177        let data = sprite_data
178            .get(&sprite)
179            .and_then(|filtered| filtered.for_block(&block))
180            .unwrap_or(missing_sprite_placeholder);
181
182        if data.variations.is_empty() {
183            continue;
184        }
185
186        let wpos = to_wpos(rel_pos);
187        let seed = (wpos.x as u64)
188            .wrapping_mul(3)
189            .wrapping_add((wpos.y as u64).wrapping_mul(7))
190            .wrapping_add((wpos.x as u64).wrapping_mul(wpos.y as u64)); // Awful PRNG
191
192        let rot = block
193            .sprite_z_rot()
194            // % 4 is non uniform, So we take % 17 and divide by four, giving the chances:
195            // 0..=2: 4/17
196            // 3: 5/17
197            // Then multiply by π/2 rad to get axis aligned rotations.
198            .unwrap_or((seed % 17 / 4).min(3) as f32 / 2.0 * std::f32::consts::PI);
199        let mirror = block.sprite_mirror_vec();
200        // try to make the variation more uniform as the PRNG is highly unfair
201        let variation = match data.variations.len() {
202            1 => 0,
203            2 => (seed as usize % 4) / 3,
204            3 => (seed as usize % 5) / 2,
205            // for four use a different seed than for ori to not have them match always
206            4 => ((seed.wrapping_add(wpos.x as u64)) as usize % 7).div_ceil(2),
207            _ => seed as usize % data.variations.len(),
208        };
209        let variant = &data.variations[variation];
210
211        let light = light_map(wpos);
212        let glow = glow_normal_at_wpos(wpos);
213
214        for (lod_level, model_data) in lod_levels.iter_mut().zip(variant) {
215            // TODO: worth precomputing the constant parts of this?
216            let mat = Mat4::identity()
217                // Scaling for different LOD resolutions
218                .scaled_3d(model_data.scale)
219                // Offset
220                .translated_3d(model_data.offset)
221                .scaled_3d(SPRITE_SCALE * mirror)
222                .rotated_z(rot)
223                .translated_3d(
224                    rel_pos + Vec3::new(0.5, 0.5, 0.0)
225                );
226            // Add an instance for each page in the sprite model
227            for page in model_data.vert_pages.clone() {
228                // TODO: could be more efficient to create once outside this loop and clone
229                // while modifying vert_page?
230                let instance = SpriteInstance::new(
231                    mat,
232                    data.wind_sway,
233                    model_data.scale.z,
234                    rel_pos.as_(),
235                    light,
236                    glow,
237                    page,
238                    sprite.is_door(),
239                    mirror.map(|e| (e < 0.0) as u32).sum() % 2 == 1,
240                );
241                set_instance(lod_level, instance, wpos);
242            }
243        }
244    }
245}
246
247/// Function executed by worker threads dedicated to chunk meshing.
248///
249/// skip_remesh is either None (do the full remesh, including recomputing the
250/// light map), or Some((light_map, glow_map)).
251fn mesh_worker(
252    pos: Vec2<i32>,
253    z_bounds: (f32, f32),
254    skip_remesh: Option<(LightMapFn, LightMapFn)>,
255    started_tick: u64,
256    volume: <VolGrid2d<TerrainChunk> as SampleVol<Aabr<i32>>>::Sample,
257    max_texture_size: u16,
258    chunk: Arc<TerrainChunk>,
259    range: Aabb<i32>,
260    sprite_render_state: &SpriteRenderState,
261) -> MeshWorkerResponse {
262    span!(_guard, "mesh_worker");
263    let blocks_of_interest = BlocksOfInterest::from_blocks(
264        chunk.iter_changed().map(|(pos, block)| (pos, *block)),
265        chunk.meta().river_velocity(),
266        chunk.meta().temp(),
267        chunk.meta().humidity(),
268        &*chunk,
269    );
270
271    let mesh;
272    let (light_map, glow_map) = if let Some((light_map, glow_map)) = &skip_remesh {
273        mesh = None;
274        (&**light_map, glow_map)
275    } else {
276        let (
277            opaque_mesh,
278            fluid_mesh,
279            _shadow_mesh,
280            (
281                bounds,
282                atlas_texture_data,
283                atlas_size,
284                light_map,
285                glow_map,
286                alt_indices,
287                sun_occluder_z_bounds,
288            ),
289        ) = generate_mesh(
290            &volume,
291            (
292                range,
293                Vec2::new(max_texture_size, max_texture_size),
294                &blocks_of_interest,
295            ),
296        );
297        mesh = Some(MeshWorkerResponseMesh {
298            // TODO: Take sprite bounds into account somehow?
299            z_bounds: (bounds.min.z, bounds.max.z),
300            sun_occluder_z_bounds,
301            opaque_mesh,
302            fluid_mesh,
303            atlas_texture_data,
304            atlas_size,
305            light_map,
306            glow_map,
307            alt_indices,
308        });
309        // Pointer juggling so borrows work out.
310        let mesh = mesh.as_ref().unwrap();
311        (&*mesh.light_map, &mesh.glow_map)
312    };
313    let to_wpos = |rel_pos: Vec3<f32>| {
314        Vec3::from(pos * TerrainChunk::RECT_SIZE.map(|e: u32| e as i32)) + rel_pos.as_()
315    };
316    MeshWorkerResponse {
317        pos,
318        // Extract sprite locations from volume
319        sprite_instances: {
320            prof_span!("extract sprite_instances");
321            let mut instances = [(); SPRITE_LOD_LEVELS].map(|()| {
322                (
323                    Vec::new(), // Deep
324                    Vec::new(), // Shallow
325                    Vec::new(), // Surface
326                )
327            });
328
329            let (underground_alt, deep_alt) = volume
330                .get_key(volume.pos_key((range.min + range.max) / 2))
331                .map_or((0.0, 0.0), |c| {
332                    (c.meta().alt() - SHALLOW_ALT, c.meta().alt() - DEEP_ALT)
333                });
334
335            get_sprite_instances(
336                &mut instances,
337                |(deep_level, shallow_level, surface_level), instance, wpos| {
338                    if (wpos.z as f32) < deep_alt {
339                        deep_level.push(instance);
340                    } else if wpos.z as f32 > underground_alt {
341                        surface_level.push(instance);
342                    } else {
343                        shallow_level.push(instance);
344                    }
345                },
346                (0..TerrainChunk::RECT_SIZE.x as i32)
347                    .flat_map(|x| {
348                        (0..TerrainChunk::RECT_SIZE.y as i32).flat_map(move |y| {
349                            (z_bounds.0 as i32..z_bounds.1 as i32)
350                                .map(move |z| Vec3::new(x, y, z).as_())
351                        })
352                    })
353                    .filter_map(|rel_pos| Some((rel_pos, *volume.get(to_wpos(rel_pos)).ok()?))),
354                to_wpos,
355                light_map,
356                |wpos| {
357                    glow_normal_at_wpos_inner(
358                        // TODO: It is bad and incorrect that we can't fetch the BoIs of chunks
359                        // in the local neighbourhood, it can make sprite lighting incorrect on
360                        // chunk borders!!!
361                        |key| Some(&blocks_of_interest).filter(|_| key == pos),
362                        |key| Some(glow_map).filter(|_| key == pos),
363                        wpos.map(|e| e as f32 + 0.5),
364                    )
365                },
366                &sprite_render_state.sprite_data,
367                &sprite_render_state.missing_sprite_placeholder,
368            );
369
370            instances.map(|(deep_level, shallow_level, surface_level)| {
371                let deep_end = deep_level.len();
372                let alt_indices = AltIndices {
373                    deep_end,
374                    underground_end: deep_end + shallow_level.len(),
375                };
376                (
377                    deep_level
378                        .into_iter()
379                        .chain(shallow_level)
380                        .chain(surface_level)
381                        .collect(),
382                    alt_indices,
383                )
384            })
385        },
386        mesh,
387        blocks_of_interest,
388        started_tick,
389    }
390}
391
392pub struct Terrain<V: RectRasterableVol = TerrainChunk> {
393    /// This is always the *current* atlas into which data is being allocated.
394    /// Once an atlas is too full to allocate the next texture, we always
395    /// allocate a fresh texture and start allocating into that.  Trying to
396    /// keep more than one texture available for allocation doesn't seem
397    /// worth it, because our allocation patterns are heavily spatial (so all
398    /// data allocated around the same time should have a very similar lifetime,
399    /// even in pathological cases).  As a result, fragmentation effects
400    /// should be minimal.
401    ///
402    /// TODO: Consider "moving GC" style allocation to deal with spatial
403    /// fragmentation effects due to odd texture sizes, which in some cases
404    /// might significantly reduce the number of textures we need for
405    /// particularly difficult locations.
406    atlas: AtlasAllocator,
407    chunks: HashMap<Vec2<i32>, TerrainChunkData>,
408    /// Temporary storage for dead chunks that might still be shadowing chunks
409    /// in view.  We wait until either the chunk definitely cannot be
410    /// shadowing anything the player can see, the chunk comes back into
411    /// view, or for daylight to end, before removing it (whichever comes
412    /// first).
413    ///
414    /// Note that these chunks are not complete; for example, they are missing
415    /// texture data (they still currently hold onto a reference to their
416    /// backing texture, but it generally can't be trusted for rendering
417    /// purposes).
418    shadow_chunks: Vec<(Vec2<i32>, TerrainChunkData)>,
419    /* /// Secondary index into the terrain chunk table, used to sort through chunks by z index from
420    /// the top down.
421    z_index_down: BTreeSet<Vec3<i32>>,
422    /// Secondary index into the terrain chunk table, used to sort through chunks by z index from
423    /// the bottom up.
424    z_index_up: BTreeSet<Vec3<i32>>, */
425    // The mpsc sender and receiver used for talking to meshing worker threads.
426    // We keep the sender component for no reason other than to clone it and send it to new
427    // workers.
428    mesh_send_tmp: channel::Sender<MeshWorkerResponse>,
429    mesh_recv: channel::Receiver<MeshWorkerResponse>,
430    mesh_todo: HashMap<Vec2<i32>, ChunkMeshState>,
431    mesh_todos_active: Arc<AtomicU64>,
432    mesh_recv_overflow: f32,
433
434    // GPU data
435    // Maps sprite kind + variant to data detailing how to render it
436    pub(super) sprite_render_state: Arc<SpriteRenderState>,
437    pub(super) sprite_globals: SpriteGlobalsBindGroup,
438    /// As stated previously, this is always the very latest texture into which
439    /// we allocate.  Code cannot assume that this is the assigned texture
440    /// for any particular chunk; look at the `texture` field in
441    /// `TerrainChunkData` for that.
442    atlas_textures: Arc<AtlasTextures<pipelines::terrain::Locals, TerrainAtlasData>>,
443
444    phantom: PhantomData<V>,
445}
446
447impl TerrainChunkData {
448    pub fn can_shadow_sun(&self) -> bool { self.visible.is_visible() || self.can_shadow_sun }
449}
450
451pub(crate) struct SpriteRenderState {
452    // TODO: This could be an `AssetHandle<SpriteSpec>`, to get hot-reloading. However, this would
453    // need to regenerate `sprite_data` and `sprite_atlas_textures`, and re-run
454    // `get_sprite_instances` for any meshed chunks.
455    //pub sprite_config: Arc<SpriteSpec>,
456
457    // Maps sprite kind + variant to data detailing how to render it
458    pub(super) sprite_data: HashMap<SpriteKind, FilteredSpriteData>,
459    pub(super) missing_sprite_placeholder: SpriteData,
460    pub(super) sprite_atlas_textures:
461        AtlasTextures<pipelines::sprite::Locals, FigureSpriteAtlasData>,
462}
463
464#[derive(Clone)]
465pub struct SpriteRenderContext {
466    pub(super) state: Arc<SpriteRenderState>,
467    pub(super) sprite_verts_buffer: Arc<SpriteVerts>,
468}
469
470pub type SpriteRenderContextLazy = Box<dyn FnMut(&mut Renderer) -> SpriteRenderContext>;
471
472impl SpriteRenderContext {
473    pub fn new(renderer: &mut Renderer) -> SpriteRenderContextLazy {
474        let max_texture_size = renderer.max_texture_size();
475
476        struct SpriteWorkerResponse {
477            //sprite_config: Arc<SpriteSpec>,
478            sprite_data: HashMap<SpriteKind, FilteredSpriteData>,
479            missing_sprite_placeholder: SpriteData,
480            sprite_atlas_texture_data: FigureSpriteAtlasData,
481            sprite_atlas_size: Vec2<u16>,
482            sprite_mesh: Mesh<SpriteVertex>,
483        }
484
485        let join_handle = std::thread::spawn(move || {
486            prof_span!("mesh all sprites");
487            // Load all the sprite config data.
488            let sprite_config =
489                Arc::<SpriteSpec>::load_expect("voxygen.voxel.sprite_manifest").cloned();
490
491            let max_size = Vec2::from(u16::try_from(max_texture_size).unwrap_or(u16::MAX));
492            let mut greedy = GreedyMesh::<FigureSpriteAtlasData, SpriteAtlasAllocator>::new(
493                max_size,
494                crate::mesh::greedy::sprite_config(),
495            );
496            let mut sprite_mesh = Mesh::new();
497
498            let mut config_to_data = |sprite_model_config: &_| {
499                let SpriteModelConfig {
500                    model,
501                    offset,
502                    lod_axes,
503                    custom_indices,
504                } = sprite_model_config;
505                let scaled = [1.0, 0.8, 0.6, 0.4, 0.2];
506                let offset = Vec3::from(*offset);
507                let lod_axes = Vec3::from(*lod_axes);
508                let model = DotVox::load_expect(model);
509                let zero = Vec3::zero();
510                let model = &model.read().0;
511                let model_size = if let Some(model) = model.models.first() {
512                    let dot_vox::Size { x, y, z } = model.size;
513                    Vec3::new(x, y, z)
514                } else {
515                    zero
516                };
517                let max_model_size = Vec3::new(31.0, 31.0, 63.0);
518                let model_scale = max_model_size.map2(model_size, |max_sz: f32, cur_sz| {
519                    let scale = max_sz / max_sz.max(cur_sz as f32);
520                    if scale < 1.0 && (cur_sz as f32 * scale).ceil() > max_sz {
521                        scale - 0.001
522                    } else {
523                        scale
524                    }
525                });
526                prof_span!(guard, "mesh sprite");
527                let lod_sprite_data = scaled.map(|lod_scale_orig| {
528                    let lod_scale = model_scale
529                        * if lod_scale_orig == 1.0 {
530                            Vec3::broadcast(1.0)
531                        } else {
532                            lod_axes * lod_scale_orig
533                                + lod_axes.map(|e| if e == 0.0 { 1.0 } else { 0.0 })
534                        };
535
536                    // Get starting page count of opaque mesh
537                    let start_page_num =
538                        sprite_mesh.vertices().len() / SPRITE_VERT_PAGE_SIZE as usize;
539                    // Mesh generation exclusively acts using side effects; it
540                    // has no interesting return value, but updates the mesh.
541                    generate_mesh_base_vol_sprite(
542                        Segment::from_vox_model_index(model, 0, Some(custom_indices))
543                            .scaled_by(lod_scale),
544                        (&mut greedy, &mut sprite_mesh, false),
545                        offset.map(|e: f32| e.floor()) * lod_scale,
546                    );
547                    // Get the number of pages after the model was meshed
548                    let end_page_num = sprite_mesh
549                        .vertices()
550                        .len()
551                        .div_ceil(SPRITE_VERT_PAGE_SIZE as usize);
552                    // Fill the current last page up with degenerate verts
553                    sprite_mesh.vertices_mut_vec().resize_with(
554                        end_page_num * SPRITE_VERT_PAGE_SIZE as usize,
555                        SpriteVertex::default,
556                    );
557
558                    let sprite_scale = Vec3::one() / lod_scale;
559
560                    SpriteModelData {
561                        vert_pages: start_page_num as u32..end_page_num as u32,
562                        scale: sprite_scale,
563                        offset: offset.map(|e| e.rem_euclid(1.0)),
564                    }
565                });
566                drop(guard);
567
568                lod_sprite_data
569            };
570
571            let sprite_data = sprite_config.map_to_data(&mut config_to_data);
572
573            // TODO: test appearance of this
574            let missing_sprite_placeholder = SpriteData {
575                variations: vec![config_to_data(&SpriteModelConfig {
576                    model: "voxygen.voxel.not_found".into(),
577                    offset: (-5.5, -5.5, 0.0),
578                    lod_axes: (1.0, 1.0, 1.0),
579                    custom_indices: HashMap::default(),
580                })]
581                .into(),
582                wind_sway: 1.0,
583            };
584
585            let (sprite_atlas_texture_data, sprite_atlas_size) = {
586                prof_span!("finalize");
587                greedy.finalize()
588            };
589
590            SpriteWorkerResponse {
591                //sprite_config,
592                sprite_data,
593                missing_sprite_placeholder,
594                sprite_atlas_texture_data,
595                sprite_atlas_size,
596                sprite_mesh,
597            }
598        });
599
600        let init = core::cell::OnceCell::new();
601        let mut join_handle = Some(join_handle);
602        let mut closure = move |renderer: &mut Renderer| {
603            // The second unwrap can only fail if the sprite meshing thread panics, which
604            // implies that our sprite assets either were not found or did not
605            // satisfy the size requirements for meshing, both of which are
606            // considered invariant violations.
607            let SpriteWorkerResponse {
608                //sprite_config,
609                sprite_data,
610                missing_sprite_placeholder,
611                sprite_atlas_texture_data,
612                sprite_atlas_size,
613                sprite_mesh,
614            } = join_handle
615                .take()
616                .expect(
617                    "Closure should only be called once (in a `OnceCell::get_or_init`) in the \
618                     absence of caught panics!",
619                )
620                .join()
621                .unwrap();
622
623            let [sprite_col_lights] =
624                sprite_atlas_texture_data.create_textures(renderer, sprite_atlas_size);
625            let sprite_atlas_textures = renderer.sprite_bind_atlas_textures(sprite_col_lights);
626
627            // Write sprite model to a 1D texture
628            let sprite_verts_buffer = renderer.create_sprite_verts(sprite_mesh);
629
630            Self {
631                state: Arc::new(SpriteRenderState {
632                    // TODO: these are all Arcs, would it makes sense to factor out the Arc?
633                    //sprite_config: Arc::clone(&sprite_config),
634                    sprite_data,
635                    missing_sprite_placeholder,
636                    sprite_atlas_textures,
637                }),
638                sprite_verts_buffer: Arc::new(sprite_verts_buffer),
639            }
640        };
641        Box::new(move |renderer| init.get_or_init(|| closure(renderer)).clone())
642    }
643}
644
645impl<V: RectRasterableVol> Terrain<V> {
646    pub fn new(
647        renderer: &mut Renderer,
648        global_model: &GlobalModel,
649        lod_data: &LodData,
650        sprite_render_context: SpriteRenderContext,
651    ) -> Self {
652        // Create a new mpsc (Multiple Produced, Single Consumer) pair for communicating
653        // with worker threads that are meshing chunks.
654        let (send, recv) = channel::unbounded();
655
656        let (atlas, atlas_textures) =
657            Self::make_atlas(renderer).expect("Failed to create atlas texture");
658
659        Self {
660            atlas,
661            chunks: HashMap::default(),
662            shadow_chunks: Vec::default(),
663            mesh_send_tmp: send,
664            mesh_recv: recv,
665            mesh_todo: HashMap::default(),
666            mesh_todos_active: Arc::new(AtomicU64::new(0)),
667            mesh_recv_overflow: 0.0,
668            sprite_render_state: sprite_render_context.state,
669            sprite_globals: renderer.bind_sprite_globals(
670                global_model,
671                lod_data,
672                &sprite_render_context.sprite_verts_buffer,
673            ),
674            atlas_textures: Arc::new(atlas_textures),
675            phantom: PhantomData,
676        }
677    }
678
679    fn make_atlas(
680        renderer: &mut Renderer,
681    ) -> Result<
682        (
683            AtlasAllocator,
684            AtlasTextures<pipelines::terrain::Locals, TerrainAtlasData>,
685        ),
686        RenderError,
687    > {
688        span!(_guard, "make_atlas", "Terrain::make_atlas");
689        let max_texture_size = renderer.max_texture_size();
690        let atlas_size = guillotiere::Size::new(max_texture_size as i32, max_texture_size as i32);
691        let atlas = AtlasAllocator::with_options(atlas_size, &guillotiere::AllocatorOptions {
692            // TODO: Verify some good empirical constants.
693            small_size_threshold: 128,
694            large_size_threshold: 1024,
695            ..guillotiere::AllocatorOptions::default()
696        });
697        let [col_lights, kinds] = [wgpu::TextureFormat::Rgba8Unorm, wgpu::TextureFormat::R8Uint]
698            .map(|fmt| {
699                renderer.create_texture_raw(
700                    &wgpu::TextureDescriptor {
701                        label: Some("Terrain atlas texture"),
702                        size: wgpu::Extent3d {
703                            width: max_texture_size,
704                            height: max_texture_size,
705                            depth_or_array_layers: 1,
706                        },
707                        mip_level_count: 1,
708                        sample_count: 1,
709                        dimension: wgpu::TextureDimension::D2,
710                        format: fmt,
711                        usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
712                        view_formats: &[],
713                    },
714                    &wgpu::TextureViewDescriptor {
715                        label: Some("Terrain atlas texture view"),
716                        format: Some(fmt),
717                        dimension: Some(wgpu::TextureViewDimension::D2),
718                        usage: None,
719                        aspect: wgpu::TextureAspect::All,
720                        base_mip_level: 0,
721                        mip_level_count: None,
722                        base_array_layer: 0,
723                        array_layer_count: None,
724                    },
725                    &wgpu::SamplerDescriptor {
726                        label: Some("Terrain atlas sampler"),
727                        address_mode_u: wgpu::AddressMode::ClampToEdge,
728                        address_mode_v: wgpu::AddressMode::ClampToEdge,
729                        address_mode_w: wgpu::AddressMode::ClampToEdge,
730                        mag_filter: wgpu::FilterMode::Nearest,
731                        min_filter: wgpu::FilterMode::Nearest,
732                        mipmap_filter: wgpu::FilterMode::Nearest,
733                        ..Default::default()
734                    },
735                )
736            });
737        let textures = renderer.terrain_bind_atlas_textures(col_lights, kinds);
738        Ok((atlas, textures))
739    }
740
741    fn remove_chunk_meta(&mut self, _pos: Vec2<i32>, chunk: &TerrainChunkData) {
742        // No need to free the allocation if the chunk is not allocated in the current
743        // atlas, since we don't bother tracking it at that point.
744        if let Some(atlas_alloc) = chunk.atlas_alloc {
745            self.atlas.deallocate(atlas_alloc);
746        }
747        /* let (zmin, zmax) = chunk.z_bounds;
748        self.z_index_up.remove(Vec3::from(zmin, pos.x, pos.y));
749        self.z_index_down.remove(Vec3::from(zmax, pos.x, pos.y)); */
750    }
751
752    fn insert_chunk(&mut self, pos: Vec2<i32>, chunk: TerrainChunkData) {
753        if let Some(old) = self.chunks.insert(pos, chunk) {
754            self.remove_chunk_meta(pos, &old);
755        }
756        /* let (zmin, zmax) = chunk.z_bounds;
757        self.z_index_up.insert(Vec3::from(zmin, pos.x, pos.y));
758        self.z_index_down.insert(Vec3::from(zmax, pos.x, pos.y)); */
759    }
760
761    fn remove_chunk(&mut self, pos: Vec2<i32>) {
762        if let Some(chunk) = self.chunks.remove(&pos) {
763            self.remove_chunk_meta(pos, &chunk);
764            // Temporarily remember dead chunks for shadowing purposes.
765            self.shadow_chunks.push((pos, chunk));
766        }
767
768        if let Some(_todo) = self.mesh_todo.remove(&pos) {
769            //Do nothing on todo mesh removal.
770        }
771    }
772
773    /// Find the light level (sunlight) at the given world position.
774    pub fn light_at_wpos(&self, wpos: Vec3<i32>) -> f32 {
775        let chunk_pos = Vec2::from(wpos).map2(TerrainChunk::RECT_SIZE, |e: i32, sz| {
776            e.div_euclid(sz as i32)
777        });
778        self.chunks
779            .get(&chunk_pos)
780            .map(|c| (c.light_map)(wpos))
781            .unwrap_or(1.0)
782    }
783
784    /// Determine whether a given block change actually require remeshing.
785    ///
786    /// Returns (skip_color, skip_lights) where
787    ///
788    /// skip_color means no textures were recolored (i.e. this was a sprite only
789    /// change).
790    ///
791    /// skip_lights means no remeshing or relighting was required
792    /// (i.e. the block opacity / lighting info / block kind didn't change).
793    fn skip_remesh(old_block: Block, new_block: Block) -> (bool, bool) {
794        let same_mesh =
795            // Both blocks are of the same opacity and same liquidity (since these are what we use
796            // to determine mesh boundaries).
797            new_block.is_liquid() == old_block.is_liquid() &&
798            new_block.is_opaque() == old_block.is_opaque();
799        let skip_lights = same_mesh &&
800            // Block glow and sunlight handling are the same (so we don't have to redo
801            // lighting).
802            new_block.get_glow() == old_block.get_glow() &&
803            new_block.get_max_sunlight() == old_block.get_max_sunlight();
804        let skip_color = same_mesh &&
805            // Both blocks are uncolored
806            !new_block.has_color() && !old_block.has_color();
807        (skip_color, skip_lights)
808    }
809
810    /// Find the glow level (light from lamps) at the given world position.
811    pub fn glow_at_wpos(&self, wpos: Vec3<i32>) -> f32 {
812        glow_at_wpos_inner(
813            |chunk_pos| self.chunks.get(&chunk_pos).map(|c| &c.glow_map),
814            wpos,
815        )
816    }
817
818    pub fn glow_normal_at_wpos(&self, wpos: Vec3<f32>) -> (Vec3<f32>, f32) {
819        glow_normal_at_wpos_inner(
820            |chunk_pos| self.chunks.get(&chunk_pos).map(|c| &c.blocks_of_interest),
821            |chunk_pos| self.chunks.get(&chunk_pos).map(|c| &c.glow_map),
822            wpos,
823        )
824    }
825
826    /// Maintain terrain data. To be called once per tick.
827    ///
828    /// The returned visible bounding volumes take into account the current
829    /// camera position (i.e: when underground, surface structures will be
830    /// culled from the volume).
831    pub fn maintain(
832        &mut self,
833        renderer: &mut Renderer,
834        scene_data: &SceneData,
835        focus_pos: Vec3<f32>,
836        loaded_distance: f32,
837        camera: &Camera,
838    ) -> (
839        Aabb<f32>,
840        Vec<math::Vec3<f32>>,
841        math::Aabr<f32>,
842        Vec<math::Vec3<f32>>,
843        math::Aabr<f32>,
844    ) {
845        let camera::Dependents {
846            view_mat,
847            proj_mat_treeculler,
848            cam_pos,
849            ..
850        } = camera.dependents();
851
852        // Remove any models for chunks that have been recently removed.
853        // Note: Does this before adding to todo list just in case removed chunks were
854        // replaced with new chunks (although this would probably be recorded as
855        // modified chunks)
856        for &pos in &scene_data.state.terrain_changes().removed_chunks {
857            self.remove_chunk(pos);
858            // Remove neighbors from meshing todo
859            for i in -1..2 {
860                for j in -1..2 {
861                    if i != 0 || j != 0 {
862                        self.mesh_todo.remove(&(pos + Vec2::new(i, j)));
863                    }
864                }
865            }
866        }
867
868        span!(_guard, "maintain", "Terrain::maintain");
869        let current_tick = scene_data.tick;
870        let current_time = scene_data.state.get_time();
871        // The visible bounding box of all chunks, not including culled regions
872        let mut visible_bounding_box: Option<Aabb<f32>> = None;
873
874        // Add any recently created or changed chunks to the list of chunks to be
875        // meshed.
876        span!(guard, "Add new/modified chunks to mesh todo list");
877        for (modified, pos) in scene_data
878            .state
879            .terrain_changes()
880            .modified_chunks
881            .iter()
882            .map(|c| (true, c))
883            .chain(
884                scene_data
885                    .state
886                    .terrain_changes()
887                    .new_chunks
888                    .iter()
889                    .map(|c| (false, c)),
890            )
891        {
892            // TODO: ANOTHER PROBLEM HERE!
893            // What happens if the block on the edge of a chunk gets modified? We need to
894            // spawn a mesh worker to remesh its neighbour(s) too since their
895            // ambient occlusion and face elision information changes too!
896            for i in -1..2 {
897                for j in -1..2 {
898                    let pos = pos + Vec2::new(i, j);
899
900                    if !(self.chunks.contains_key(&pos) || self.mesh_todo.contains_key(&pos))
901                        || modified
902                    {
903                        let mut neighbours = true;
904                        for i in -1..2 {
905                            for j in -1..2 {
906                                neighbours &= scene_data
907                                    .state
908                                    .terrain()
909                                    .contains_key_real(pos + Vec2::new(i, j));
910                            }
911                        }
912
913                        if neighbours {
914                            self.mesh_todo.insert(pos, ChunkMeshState {
915                                pos,
916                                started_tick: current_tick,
917                                is_worker_active: false,
918                                skip_remesh: false,
919                            });
920                        }
921                    }
922                }
923            }
924        }
925        drop(guard);
926
927        // Add the chunks belonging to recently changed blocks to the list of chunks to
928        // be meshed
929        span!(guard, "Add chunks with modified blocks to mesh todo list");
930        // TODO: would be useful if modified blocks were grouped by chunk
931        for (&pos, &old_block) in scene_data.state.terrain_changes().modified_blocks.iter() {
932            // terrain_changes() are both set and applied during the same tick on the
933            // client, so the current state is the new state and modified_blocks
934            // stores the old state.
935            let new_block = scene_data.state.get_block(pos);
936
937            let (skip_color, skip_lights) = if let Some(new_block) = new_block {
938                Self::skip_remesh(old_block, new_block)
939            } else {
940                // The block coordinates of a modified block should be in bounds, since they are
941                // only retained if setting the block was successful during the state tick in
942                // client.  So this is definitely a bug, but we can recover safely by just
943                // conservatively doing a full remesh in this case, rather than crashing the
944                // game.
945                warn!(
946                    "Invariant violation: pos={:?} should be a valid block position.  This is a \
947                     bug; please contact the developers if you see this error message!",
948                    pos
949                );
950                (false, false)
951            };
952
953            // Currently, we can only skip remeshing if both lights and
954            // colors don't need to be reworked.
955            let skip_remesh = skip_color && skip_lights;
956
957            // TODO: Be cleverer about this to avoid remeshing all neighbours. There are a
958            // few things that can create an 'effect at a distance'. These are
959            // as follows:
960            // - A glowing block is added or removed, thereby causing a lighting
961            //   recalculation proportional to its glow radius.
962            // - An opaque block that was blocking sunlight from entering a cavity is
963            //   removed (or added) thereby
964            // changing the way that sunlight propagates into the cavity.
965            //
966            // We can and should be cleverer about this, but it's non-trivial. For now, we
967            // don't remesh if only a block color changed or a sprite was
968            // altered in a way that doesn't affect its glow, but we make no
969            // attempt to do smarter cavity checking (to see if altering the
970            // block changed the sunlight neighbors could get).
971            // let block_effect_radius = block.get_glow().unwrap_or(0).max(1);
972            let block_effect_radius = crate::mesh::terrain::MAX_LIGHT_DIST;
973
974            // Handle block changes on chunk borders
975            // Remesh all neighbours because we have complex lighting now
976            // TODO: if lighting is on the server this can be updated to only remesh when
977            // lighting changes in that neighbouring chunk or if the block
978            // change was on the border
979            for x in -1..2 {
980                for y in -1..2 {
981                    let neighbour_pos = pos + Vec3::new(x, y, 0) * block_effect_radius;
982                    let neighbour_chunk_pos = scene_data.state.terrain().pos_key(neighbour_pos);
983
984                    if skip_lights && !(x == 0 && y == 0) {
985                        // We don't need to remesh neighboring chunks if this block change doesn't
986                        // require relighting.
987                        continue;
988                    }
989
990                    // Only remesh if this chunk has all its neighbors
991                    let mut neighbours = true;
992                    for i in -1..2 {
993                        for j in -1..2 {
994                            neighbours &= scene_data
995                                .state
996                                .terrain()
997                                .contains_key_real(neighbour_chunk_pos + Vec2::new(i, j));
998                        }
999                    }
1000                    if neighbours {
1001                        let todo =
1002                            self.mesh_todo
1003                                .entry(neighbour_chunk_pos)
1004                                .or_insert(ChunkMeshState {
1005                                    pos: neighbour_chunk_pos,
1006                                    started_tick: current_tick,
1007                                    is_worker_active: false,
1008                                    skip_remesh,
1009                                });
1010
1011                        // Make sure not to skip remeshing a chunk if it already had to be
1012                        // fully meshed for other reasons.  Even if the mesh is currently active
1013                        // (so relighting would be redundant), we currently have to remesh
1014                        // everything unless the previous mesh was also able to skip remeshing,
1015                        // since otherwise the active remesh is computing new lighting values
1016                        // that we don't have yet.
1017                        todo.skip_remesh &= skip_remesh;
1018                        todo.is_worker_active = false;
1019                        todo.started_tick = current_tick;
1020                    }
1021                }
1022            }
1023        }
1024        drop(guard);
1025
1026        // Limit ourselves to u16::MAX even if larger textures are supported.
1027        let max_texture_size = renderer.max_texture_size();
1028        let meshing_cores = match num_cpus::get() as u64 {
1029            n if n < 4 => 1,
1030            n if n < 8 => n - 3,
1031            n => n - 4,
1032        };
1033
1034        span!(guard, "Queue meshing from todo list");
1035        let mesh_focus_pos = focus_pos.map(|e| e.trunc()).xy().as_::<i64>();
1036        //TODO: this is actually no loop, it just runs for a single entry because of
1037        // the `min_by_key`. Evaluate actually looping here
1038        while let Some((todo, chunk)) = self
1039            .mesh_todo
1040            .values_mut()
1041            .filter(|todo| !todo.is_worker_active)
1042            .min_by_key(|todo| ((todo.pos.as_::<i64>() * TerrainChunk::RECT_SIZE.as_::<i64>()).distance_squared(mesh_focus_pos), todo.started_tick))
1043            // Find a reference to the actual `TerrainChunk` we're meshing
1044            .and_then(|todo| {
1045                let pos = todo.pos;
1046                Some((todo, scene_data.state
1047                    .terrain()
1048                    .get_key_arc(pos)
1049                    .cloned()
1050                    .or_else(|| {
1051                        warn!("Invariant violation: a chunk whose neighbors have not been fetched was found in the todo list,
1052                              which could halt meshing entirely.");
1053                        None
1054                    })?))
1055            })
1056        {
1057            if self.mesh_todos_active.load(Ordering::Relaxed) > meshing_cores {
1058                break;
1059            }
1060
1061            // like ambient occlusion and edge elision, we also need the borders
1062            // of the chunk's neighbours too (hence the `- 1` and `+ 1`).
1063            let aabr = Aabr {
1064                min: todo
1065                    .pos
1066                    .map2(VolGrid2d::<V>::chunk_size(), |e, sz| e * sz as i32 - 1),
1067                max: todo.pos.map2(VolGrid2d::<V>::chunk_size(), |e, sz| {
1068                    (e + 1) * sz as i32 + 1
1069                }),
1070            };
1071
1072            // Copy out the chunk data we need to perform the meshing. We do this by taking
1073            // a sample of the terrain that includes both the chunk we want and
1074            // its neighbours.
1075            let volume = match scene_data.state.terrain().sample(aabr) {
1076                Ok(sample) => sample, /* TODO: Ensure that all of the chunk's neighbours still
1077                                        * exist to avoid buggy shadow borders */
1078                // Either this chunk or its neighbours doesn't yet exist, so we keep it in the
1079                // queue to be processed at a later date when we have its neighbours.
1080                Err(VolGrid2dError::NoSuchChunk) => {
1081                    continue;
1082                },
1083                _ => panic!("Unhandled edge case"),
1084            };
1085
1086            // The region to actually mesh
1087            let min_z = volume
1088                .iter()
1089                .fold(i32::MAX, |min, (_, chunk)| chunk.get_min_z().min(min));
1090            let max_z = volume
1091                .iter()
1092                .fold(i32::MIN, |max, (_, chunk)| chunk.get_max_z().max(max));
1093
1094            let aabb = Aabb {
1095                min: Vec3::from(aabr.min) + Vec3::unit_z() * (min_z - 2),
1096                max: Vec3::from(aabr.max) + Vec3::unit_z() * (max_z + 2),
1097            };
1098
1099            // Clone various things so that they can be moved into the thread.
1100            let send = self.mesh_send_tmp.clone();
1101            let pos = todo.pos;
1102
1103            let chunks = &self.chunks;
1104            let skip_remesh = todo
1105                .skip_remesh
1106                .then_some(())
1107                .and_then(|_| chunks.get(&pos))
1108                .map(|chunk| (Arc::clone(&chunk.light_map), Arc::clone(&chunk.glow_map)));
1109
1110            // Queue the worker thread.
1111            let started_tick = todo.started_tick;
1112            let sprite_render_state = Arc::clone(&self.sprite_render_state);
1113            let cnt = Arc::clone(&self.mesh_todos_active);
1114            cnt.fetch_add(1, Ordering::Relaxed);
1115            scene_data
1116                .state
1117                .slow_job_pool()
1118                .spawn("TERRAIN_MESHING", move || {
1119                    let _ = send.send(mesh_worker(
1120                        pos,
1121                        (min_z as f32, max_z as f32),
1122                        skip_remesh,
1123                        started_tick,
1124                        volume,
1125                        max_texture_size as u16,
1126                        chunk,
1127                        aabb,
1128                        &sprite_render_state,
1129                    ));
1130                    cnt.fetch_sub(1, Ordering::Relaxed);
1131                });
1132            todo.is_worker_active = true;
1133        }
1134        drop(guard);
1135
1136        // Receive a chunk mesh from a worker thread and upload it to the GPU, then
1137        // store it. Vary the rate at which we pull items out to correlate with the
1138        // framerate, preventing tail latency.
1139        span!(guard, "Get/upload meshed chunk");
1140        const CHUNKS_PER_SECOND: f32 = 240.0;
1141        let recv_count =
1142            scene_data.state.get_delta_time() * CHUNKS_PER_SECOND + self.mesh_recv_overflow;
1143        self.mesh_recv_overflow = recv_count.fract();
1144        let incoming_chunks =
1145            std::iter::from_fn(|| self.mesh_recv.recv_timeout(Duration::new(0, 0)).ok())
1146                .take(recv_count.floor() as usize)
1147                .collect::<Vec<_>>(); // Avoid ownership issue
1148        for response in incoming_chunks {
1149            match self.mesh_todo.get(&response.pos) {
1150                // It's the mesh we want, insert the newly finished model into the terrain model
1151                // data structure (convert the mesh to a model first of course).
1152                Some(todo) if response.started_tick <= todo.started_tick => {
1153                    let started_tick = todo.started_tick;
1154
1155                    let sprite_instances =
1156                        response.sprite_instances.map(|(instances, alt_indices)| {
1157                            (renderer.create_instances(&instances), alt_indices)
1158                        });
1159
1160                    if let Some(mesh) = response.mesh {
1161                        // Full update, insert the whole chunk.
1162
1163                        let load_time = self
1164                            .chunks
1165                            .get(&response.pos)
1166                            .map(|chunk| chunk.load_time)
1167                            .unwrap_or(current_time as f32);
1168                        // TODO: Allocate new atlas on allocation failure.
1169                        let atlas = &mut self.atlas;
1170                        let chunks = &mut self.chunks;
1171                        let atlas_textures = &mut self.atlas_textures;
1172                        let alloc_size = guillotiere::Size::new(
1173                            i32::from(mesh.atlas_size.x),
1174                            i32::from(mesh.atlas_size.y),
1175                        );
1176
1177                        let allocation = atlas.allocate(alloc_size).unwrap_or_else(|| {
1178                            // Atlas allocation failure: try allocating a new texture and atlas.
1179                            let (new_atlas, new_atlas_textures) =
1180                                Self::make_atlas(renderer).expect("Failed to create atlas texture");
1181
1182                            // We reset the atlas and clear allocations from existing chunks,
1183                            // even though we haven't yet
1184                            // checked whether the new allocation can fit in
1185                            // the texture.  This is reasonable because we don't have a fallback
1186                            // if a single chunk can't fit in an empty atlas of maximum size.
1187                            //
1188                            // TODO: Consider attempting defragmentation first rather than just
1189                            // always moving everything into the new chunk.
1190                            chunks.iter_mut().for_each(|(_, chunk)| {
1191                                chunk.atlas_alloc = None;
1192                            });
1193                            *atlas = new_atlas;
1194                            *atlas_textures = Arc::new(new_atlas_textures);
1195
1196                            atlas
1197                                .allocate(alloc_size)
1198                                .expect("Chunk data does not fit in a texture of maximum size.")
1199                        });
1200
1201                        // NOTE: Cast is safe since the origin was a u16.
1202                        let atlas_offs = Vec2::new(
1203                            allocation.rectangle.min.x as u32,
1204                            allocation.rectangle.min.y as u32,
1205                        );
1206                        // Update col_lights texture
1207                        renderer.update_texture(
1208                            &atlas_textures.textures[0],
1209                            atlas_offs.into_array(),
1210                            mesh.atlas_size.as_().into_array(),
1211                            &mesh.atlas_texture_data.col_lights,
1212                        );
1213                        // Update kinds texture
1214                        renderer.update_texture(
1215                            &atlas_textures.textures[1],
1216                            atlas_offs.into_array(),
1217                            mesh.atlas_size.as_().into_array(),
1218                            &mesh.atlas_texture_data.kinds,
1219                        );
1220
1221                        self.insert_chunk(response.pos, TerrainChunkData {
1222                            load_time,
1223                            opaque_model: renderer.create_model(&mesh.opaque_mesh),
1224                            fluid_model: renderer.create_model(&mesh.fluid_mesh),
1225                            atlas_alloc: Some(allocation.id),
1226                            atlas_textures: Arc::clone(&self.atlas_textures),
1227                            light_map: mesh.light_map,
1228                            glow_map: mesh.glow_map,
1229                            sprite_instances,
1230                            locals: renderer.create_terrain_bound_locals(&[TerrainLocals::new(
1231                                Vec3::from(
1232                                    response.pos.map2(VolGrid2d::<V>::chunk_size(), |e, sz| {
1233                                        e as f32 * sz as f32
1234                                    }),
1235                                ),
1236                                Quaternion::identity(),
1237                                atlas_offs,
1238                                load_time,
1239                            )]),
1240                            visible: Visibility {
1241                                in_range: false,
1242                                in_frustum: false,
1243                            },
1244                            can_shadow_point: false,
1245                            can_shadow_sun: false,
1246                            blocks_of_interest: response.blocks_of_interest,
1247                            z_bounds: mesh.z_bounds,
1248                            sun_occluder_z_bounds: mesh.sun_occluder_z_bounds,
1249                            frustum_last_plane_index: 0,
1250                            alt_indices: mesh.alt_indices,
1251                        });
1252                    } else if let Some(chunk) = self.chunks.get_mut(&response.pos) {
1253                        // There was an update that didn't require a remesh (probably related to
1254                        // non-glowing sprites) so we just update those.
1255                        chunk.sprite_instances = sprite_instances;
1256                        chunk.blocks_of_interest = response.blocks_of_interest;
1257                    }
1258
1259                    if response.started_tick == started_tick {
1260                        self.mesh_todo.remove(&response.pos);
1261                    }
1262                },
1263                // Chunk must have been removed, or it was spawned on an old tick. Drop the mesh
1264                // since it's either out of date or no longer needed.
1265                Some(_todo) => {},
1266                None => {},
1267            }
1268        }
1269        drop(guard);
1270
1271        // Construct view frustum
1272        span!(guard, "Construct view frustum");
1273        let focus_off = focus_pos.map(|e| e.trunc());
1274        let frustum = Frustum::from_modelview_projection(
1275            (proj_mat_treeculler * view_mat * Mat4::translation_3d(-focus_off)).into_col_arrays(),
1276        );
1277        drop(guard);
1278
1279        // Update chunk visibility
1280        span!(guard, "Update chunk visibility");
1281        let chunk_sz = V::RECT_SIZE.x as f32;
1282        for (pos, chunk) in &mut self.chunks {
1283            let chunk_pos = pos.as_::<f32>() * chunk_sz;
1284
1285            chunk.can_shadow_sun = false;
1286
1287            // Limit focus_pos to chunk bounds and ensure the chunk is within the fog
1288            // boundary
1289            let nearest_in_chunk = Vec2::from(focus_pos).clamped(chunk_pos, chunk_pos + chunk_sz);
1290            let distance_2 = Vec2::<f32>::from(focus_pos).distance_squared(nearest_in_chunk);
1291            let in_range = distance_2 < loaded_distance.powi(2);
1292
1293            chunk.visible.in_range = in_range;
1294
1295            // Ensure the chunk is within the view frustum
1296            let chunk_min = [chunk_pos.x, chunk_pos.y, chunk.z_bounds.0];
1297            let chunk_max = [
1298                chunk_pos.x + chunk_sz,
1299                chunk_pos.y + chunk_sz,
1300                chunk.sun_occluder_z_bounds.1,
1301            ];
1302
1303            let (in_frustum, last_plane_index) = AABB::new(chunk_min, chunk_max)
1304                .coherent_test_against_frustum(&frustum, chunk.frustum_last_plane_index);
1305
1306            chunk.frustum_last_plane_index = last_plane_index;
1307            chunk.visible.in_frustum = in_frustum;
1308            let chunk_area = Aabr {
1309                min: chunk_pos,
1310                max: chunk_pos + chunk_sz,
1311            };
1312
1313            if in_frustum {
1314                let visible_box = Aabb {
1315                    min: chunk_area.min.with_z(chunk.sun_occluder_z_bounds.0),
1316                    max: chunk_area.max.with_z(chunk.sun_occluder_z_bounds.1),
1317                };
1318                visible_bounding_box = visible_bounding_box
1319                    .map(|e| e.union(visible_box))
1320                    .or(Some(visible_box));
1321            }
1322            // FIXME: Hack that only works when only the lantern casts point shadows
1323            // (and hardcodes the shadow distance).  Should ideally exist per-light, too.
1324            chunk.can_shadow_point = distance_2 < (128.0 * 128.0);
1325        }
1326        drop(guard);
1327
1328        span!(guard, "Shadow magic");
1329        // PSRs: potential shadow receivers
1330        let visible_bounding_box = visible_bounding_box.unwrap_or(Aabb {
1331            min: focus_pos - 2.0,
1332            max: focus_pos + 2.0,
1333        });
1334        let inv_proj_view =
1335            math::Mat4::from_col_arrays((proj_mat_treeculler * view_mat).into_col_arrays())
1336                .as_::<f64>()
1337                .inverted();
1338
1339        // PSCs: Potential shadow casters
1340        let ray_direction = scene_data.get_sun_dir();
1341        let collides_with_aabr = |a: math::Aabb<f32>, b: math::Aabr<f32>| {
1342            let min = math::Vec4::new(a.min.x, a.min.y, b.min.x, b.min.y);
1343            let max = math::Vec4::new(b.max.x, b.max.y, a.max.x, a.max.y);
1344            #[cfg(feature = "simd")]
1345            return min.partial_cmple_simd(max).reduce_and();
1346            #[cfg(not(feature = "simd"))]
1347            return min.partial_cmple(&max).reduce_and();
1348        };
1349
1350        let (visible_light_volume, visible_psr_bounds) = if ray_direction.z < 0.0
1351            && renderer.pipeline_modes().shadow.is_map()
1352        {
1353            let visible_bounding_box = math::Aabb::<f32> {
1354                min: (visible_bounding_box.min - focus_off),
1355                max: (visible_bounding_box.max - focus_off),
1356            };
1357            let visible_bounds_fine = visible_bounding_box.as_::<f64>();
1358            // NOTE: We use proj_mat_treeculler here because
1359            // calc_focused_light_volume_points makes the assumption that the
1360            // near plane lies before the far plane.
1361            let visible_light_volume = math::calc_focused_light_volume_points(
1362                inv_proj_view,
1363                ray_direction.as_::<f64>(),
1364                visible_bounds_fine,
1365                1e-6,
1366            )
1367            .map(|v| v.as_::<f32>())
1368            .collect::<Vec<_>>();
1369
1370            let up: math::Vec3<f32> = { math::Vec3::unit_y() };
1371            let ray_mat = math::Mat4::look_at_rh(cam_pos, cam_pos + ray_direction, up);
1372            let visible_bounds = math::Aabr::from(math::fit_psr(
1373                ray_mat,
1374                visible_light_volume.iter().copied(),
1375                |p| p,
1376            ));
1377            let ray_mat = ray_mat * math::Mat4::translation_3d(-focus_off);
1378
1379            let can_shadow_sun = |pos: Vec2<i32>, chunk: &TerrainChunkData| {
1380                let chunk_pos = pos.as_::<f32>() * chunk_sz;
1381
1382                // Ensure the chunk is within the PSR set.
1383                let chunk_box = math::Aabb {
1384                    min: math::Vec3::new(chunk_pos.x, chunk_pos.y, chunk.z_bounds.0),
1385                    max: math::Vec3::new(
1386                        chunk_pos.x + chunk_sz,
1387                        chunk_pos.y + chunk_sz,
1388                        chunk.z_bounds.1,
1389                    ),
1390                };
1391
1392                let chunk_from_light = math::fit_psr(
1393                    ray_mat,
1394                    math::aabb_to_points(chunk_box).iter().copied(),
1395                    |p| p,
1396                );
1397                collides_with_aabr(chunk_from_light, visible_bounds)
1398            };
1399
1400            // Handle potential shadow casters (chunks that aren't visible, but are still in
1401            // range) to see if they could cast shadows.
1402            self.chunks.iter_mut()
1403                // NOTE: We deliberately avoid doing this computation for chunks we already know
1404                // are visible, since by definition they'll always intersect the visible view
1405                // frustum.
1406                .filter(|chunk| !chunk.1.visible.in_frustum)
1407                .for_each(|(&pos, chunk)| {
1408                    chunk.can_shadow_sun = can_shadow_sun(pos, chunk);
1409                });
1410
1411            // Handle dead chunks that we kept around only to make sure shadows don't blink
1412            // out when a chunk disappears.
1413            //
1414            // If the sun can currently cast shadows, we retain only those shadow chunks
1415            // that both: 1. have not been replaced by a real chunk instance,
1416            // and 2. are currently potential shadow casters (as witnessed by
1417            // `can_shadow_sun` returning true).
1418            //
1419            // NOTE: Please make sure this runs *after* any code that could insert a chunk!
1420            // Otherwise we may end up with multiple instances of the chunk trying to cast
1421            // shadows at the same time.
1422            let chunks = &self.chunks;
1423            self.shadow_chunks
1424                .retain(|(pos, chunk)| !chunks.contains_key(pos) && can_shadow_sun(*pos, chunk));
1425
1426            (visible_light_volume, visible_bounds)
1427        } else {
1428            // There's no daylight or no shadows, so there's no reason to keep any
1429            // shadow chunks around.
1430            self.shadow_chunks.clear();
1431            (Vec::new(), math::Aabr {
1432                min: math::Vec2::zero(),
1433                max: math::Vec2::zero(),
1434            })
1435        };
1436        drop(guard);
1437        span!(guard, "Rain occlusion magic");
1438        // Check if there is rain near the camera
1439        let max_weather = scene_data
1440            .state
1441            .max_weather_near(focus_off.xy() + cam_pos.xy());
1442        let (visible_occlusion_volume, visible_por_bounds) = if max_weather.rain > RAIN_THRESHOLD {
1443            let visible_bounding_box = math::Aabb::<f32> {
1444                min: (visible_bounding_box.min - focus_off),
1445                max: (visible_bounding_box.max - focus_off),
1446            };
1447            let visible_bounds_fine = math::Aabb {
1448                min: visible_bounding_box.min.as_::<f64>(),
1449                max: visible_bounding_box.max.as_::<f64>(),
1450            };
1451            let weather = scene_data.client.weather_at_player();
1452            let ray_direction = weather.rain_vel().normalized();
1453
1454            // NOTE: We use proj_mat_treeculler here because
1455            // calc_focused_light_volume_points makes the assumption that the
1456            // near plane lies before the far plane.
1457            let visible_volume = math::calc_focused_light_volume_points(
1458                inv_proj_view,
1459                ray_direction.as_::<f64>(),
1460                visible_bounds_fine,
1461                1e-6,
1462            )
1463            .map(|v| v.as_::<f32>())
1464            .collect::<Vec<_>>();
1465            let ray_mat =
1466                math::Mat4::look_at_rh(cam_pos, cam_pos + ray_direction, math::Vec3::unit_y());
1467            let visible_bounds = math::Aabr::from(math::fit_psr(
1468                ray_mat,
1469                visible_volume.iter().copied(),
1470                |p| p,
1471            ));
1472
1473            (visible_volume, visible_bounds)
1474        } else {
1475            (Vec::new(), math::Aabr::default())
1476        };
1477
1478        drop(guard);
1479        (
1480            visible_bounding_box,
1481            visible_light_volume,
1482            visible_psr_bounds,
1483            visible_occlusion_volume,
1484            visible_por_bounds,
1485        )
1486    }
1487
1488    pub fn get(&self, chunk_key: Vec2<i32>) -> Option<&TerrainChunkData> {
1489        self.chunks.get(&chunk_key)
1490    }
1491
1492    pub fn chunk_count(&self) -> usize { self.chunks.len() }
1493
1494    pub fn visible_chunk_count(&self) -> usize {
1495        self.chunks
1496            .iter()
1497            .filter(|(_, c)| c.visible.is_visible())
1498            .count()
1499    }
1500
1501    pub fn shadow_chunk_count(&self) -> usize { self.shadow_chunks.len() }
1502
1503    pub fn render_shadows<'a>(
1504        &'a self,
1505        drawer: &mut TerrainShadowDrawer<'_, 'a>,
1506        focus_pos: Vec3<f32>,
1507        culling_mode: CullingMode,
1508    ) {
1509        span!(_guard, "render_shadows", "Terrain::render_shadows");
1510        let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
1511            (e as i32).div_euclid(sz as i32)
1512        });
1513
1514        let chunk_iter = Spiral2d::new()
1515            .filter_map(|rpos| {
1516                let pos = focus_chunk + rpos;
1517                self.chunks.get(&pos)
1518            })
1519            .take(self.chunks.len());
1520
1521        // Directed shadows
1522        //
1523        // NOTE: We also render shadows for dead chunks that were found to still be
1524        // potential shadow casters, to avoid shadows suddenly disappearing at
1525        // very steep sun angles (e.g. sunrise / sunset).
1526        chunk_iter
1527            .filter(|chunk| chunk.can_shadow_sun())
1528            .chain(self.shadow_chunks.iter().map(|(_, chunk)| chunk))
1529            .filter_map(|chunk| {
1530                Some((
1531                    chunk.opaque_model.as_ref()?,
1532                    &chunk.locals,
1533                    &chunk.alt_indices,
1534                ))
1535            })
1536            .for_each(|(model, locals, alt_indices)| {
1537                drawer.draw(model, locals, alt_indices, culling_mode)
1538            });
1539    }
1540
1541    pub fn render_rain_occlusion<'a>(
1542        &'a self,
1543        drawer: &mut TerrainShadowDrawer<'_, 'a>,
1544        focus_pos: Vec3<f32>,
1545    ) {
1546        span!(_guard, "render_occlusion", "Terrain::render_occlusion");
1547        let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
1548            (e as i32).div_euclid(sz as i32)
1549        });
1550        let chunk_iter = Spiral2d::new()
1551            .filter_map(|rpos| {
1552                let pos = focus_chunk + rpos;
1553                self.chunks.get(&pos)
1554            })
1555            .take(self.chunks.len().min(RAIN_OCCLUSION_CHUNKS));
1556
1557        chunk_iter
1558            // Find a way to keep this?
1559            // .filter(|chunk| chunk.can_shadow_sun())
1560            .filter_map(|chunk| Some((
1561                chunk
1562                    .opaque_model
1563                    .as_ref()?,
1564                &chunk.locals,
1565                &chunk.alt_indices,
1566            )))
1567            .for_each(|(model, locals, alt_indices)| drawer.draw(model, locals, alt_indices, CullingMode::None));
1568    }
1569
1570    pub fn chunks_for_point_shadows(
1571        &self,
1572        focus_pos: Vec3<f32>,
1573    ) -> impl Clone
1574    + Iterator<
1575        Item = (
1576            &Model<pipelines::terrain::Vertex>,
1577            &pipelines::terrain::BoundLocals,
1578        ),
1579    > {
1580        let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
1581            (e as i32).div_euclid(sz as i32)
1582        });
1583
1584        let chunk_iter = Spiral2d::new()
1585            .filter_map(move |rpos| {
1586                let pos = focus_chunk + rpos;
1587                self.chunks.get(&pos)
1588            })
1589            .take(self.chunks.len());
1590
1591        // Point shadows
1592        //
1593        // NOTE: We don't bother retaining chunks unless they cast sun shadows, so we
1594        // don't use `shadow_chunks` here.
1595        chunk_iter
1596            .filter(|chunk| chunk.can_shadow_point)
1597            .filter_map(|chunk| {
1598                chunk
1599                    .opaque_model
1600                    .as_ref()
1601                    .map(|model| (model, &chunk.locals))
1602            })
1603    }
1604
1605    pub fn render<'a>(
1606        &'a self,
1607        drawer: &mut FirstPassDrawer<'a>,
1608        focus_pos: Vec3<f32>,
1609        culling_mode: CullingMode,
1610    ) {
1611        span!(_guard, "render", "Terrain::render");
1612        let mut drawer = drawer.draw_terrain();
1613
1614        let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
1615            (e as i32).div_euclid(sz as i32)
1616        });
1617
1618        Spiral2d::new()
1619            .filter_map(|rpos| {
1620                let pos = focus_chunk + rpos;
1621                Some((rpos, self.chunks.get(&pos)?))
1622            })
1623            .take(self.chunks.len())
1624            .filter(|(_, chunk)| chunk.visible.is_visible())
1625            .filter_map(|(rpos, chunk)| {
1626                Some((
1627                    rpos,
1628                    chunk.opaque_model.as_ref()?,
1629                    &chunk.atlas_textures,
1630                    &chunk.locals,
1631                    &chunk.alt_indices,
1632                ))
1633            })
1634            .for_each(|(rpos, model, atlas_textures, locals, alt_indices)| {
1635                // Always draw all of close chunks to avoid terrain 'popping'
1636                let culling_mode = if rpos.magnitude_squared() < NEVER_CULL_DIST.pow(2) {
1637                    CullingMode::None
1638                } else {
1639                    culling_mode
1640                };
1641                drawer.draw(model, atlas_textures, locals, alt_indices, culling_mode)
1642            });
1643    }
1644
1645    pub fn render_sprites<'a>(
1646        &'a self,
1647        sprite_drawer: &mut SpriteDrawer<'_, 'a>,
1648        focus_pos: Vec3<f32>,
1649        cam_pos: Vec3<f32>,
1650        sprite_render_distance: f32,
1651        culling_mode: CullingMode,
1652    ) {
1653        span!(_guard, "render_sprites", "Terrain::render_sprites");
1654
1655        let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
1656            (e as i32).div_euclid(sz as i32)
1657        });
1658
1659        // Avoid switching textures
1660        let chunk_iter = Spiral2d::new()
1661            .filter_map(|rpos| {
1662                let pos = focus_chunk + rpos;
1663                Some((rpos, pos, self.chunks.get(&pos)?))
1664            })
1665            .take(self.chunks.len());
1666
1667        let chunk_size = V::RECT_SIZE.map(|e| e as f32);
1668
1669        let sprite_low_detail_distance = sprite_render_distance * 0.75;
1670        let sprite_mid_detail_distance = sprite_render_distance * 0.5;
1671        let sprite_hid_detail_distance = sprite_render_distance * 0.35;
1672        let sprite_high_detail_distance = sprite_render_distance * 0.15;
1673
1674        chunk_iter
1675            .clone()
1676            .filter(|(_, _, c)| c.visible.is_visible())
1677            .for_each(|(rpos, pos, chunk)| {
1678                // Skip chunk if it has no sprites
1679                if chunk.sprite_instances[0].0.count() == 0 {
1680                    return;
1681                }
1682
1683                let chunk_center = pos.map2(chunk_size, |e, sz| (e as f32 + 0.5) * sz);
1684                let focus_dist_sqrd = Vec2::from(focus_pos).distance_squared(chunk_center);
1685                let dist_sqrd = Aabr {
1686                    min: chunk_center - chunk_size * 0.5,
1687                    max: chunk_center + chunk_size * 0.5,
1688                }
1689                .projected_point(cam_pos.xy())
1690                .distance_squared(cam_pos.xy());
1691
1692                if focus_dist_sqrd < sprite_render_distance.powi(2) {
1693                    let lod_level = if dist_sqrd < sprite_high_detail_distance.powi(2) {
1694                        0
1695                    } else if dist_sqrd < sprite_hid_detail_distance.powi(2) {
1696                        1
1697                    } else if dist_sqrd < sprite_mid_detail_distance.powi(2) {
1698                        2
1699                    } else if dist_sqrd < sprite_low_detail_distance.powi(2) {
1700                        3
1701                    } else {
1702                        4
1703                    };
1704
1705                    // Always draw all of close chunks to avoid terrain 'popping'
1706                    let culling_mode = if rpos.magnitude_squared() < NEVER_CULL_DIST.pow(2) {
1707                        CullingMode::None
1708                    } else {
1709                        culling_mode
1710                    };
1711
1712                    sprite_drawer.draw(
1713                        &chunk.locals,
1714                        &chunk.sprite_instances[lod_level].0,
1715                        &chunk.sprite_instances[lod_level].1,
1716                        culling_mode,
1717                    );
1718                }
1719            });
1720    }
1721
1722    pub fn render_translucent<'a>(
1723        &'a self,
1724        drawer: &mut FirstPassDrawer<'a>,
1725        focus_pos: Vec3<f32>,
1726    ) {
1727        span!(_guard, "render_translucent", "Terrain::render_translucent");
1728        let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
1729            (e as i32).div_euclid(sz as i32)
1730        });
1731
1732        // Avoid switching textures
1733        let chunk_iter = Spiral2d::new()
1734            .filter_map(|rpos| {
1735                let pos = focus_chunk + rpos;
1736                self.chunks.get(&pos).map(|c| (pos, c))
1737            })
1738            .take(self.chunks.len());
1739
1740        // Translucent
1741        span!(guard, "Fluid chunks");
1742        let mut fluid_drawer = drawer.draw_fluid();
1743        chunk_iter
1744            .filter(|(_, chunk)| chunk.visible.is_visible())
1745            .filter_map(|(_, chunk)| {
1746                chunk
1747                    .fluid_model
1748                    .as_ref()
1749                    .map(|model| (model, &chunk.locals))
1750            })
1751            .collect::<Vec<_>>()
1752            .into_iter()
1753            .rev() // Render back-to-front
1754            .for_each(|(model, locals)| {
1755                fluid_drawer.draw(
1756                    model,
1757                    locals,
1758                )
1759            });
1760        drop(fluid_drawer);
1761        drop(guard);
1762    }
1763}
1764/// Find the glow level (light from lamps) at the given world position.
1765fn glow_at_wpos_inner<'a>(
1766    chunk_glow_map: impl Fn(Vec2<i32>) -> Option<&'a LightMapFn>,
1767    wpos: Vec3<i32>,
1768) -> f32 {
1769    let chunk_pos = Vec2::from(wpos).map2(TerrainChunk::RECT_SIZE, |e: i32, sz| {
1770        e.div_euclid(sz as i32)
1771    });
1772    chunk_glow_map(chunk_pos).map_or(0.0, |gmap| gmap(wpos))
1773}
1774
1775fn glow_normal_at_wpos_inner<'a>(
1776    chunk_boi: impl Fn(Vec2<i32>) -> Option<&'a BlocksOfInterest>,
1777    chunk_glow_map: impl Fn(Vec2<i32>) -> Option<&'a LightMapFn>,
1778    wpos: Vec3<f32>,
1779) -> (Vec3<f32>, f32) {
1780    const AMBIANCE: f32 = 0.15; // 0-1, the proportion of light that should illuminate the rear of an object
1781    let chunk_key = wpos.xy().map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
1782        (e.floor() as i32).div_euclid(sz as i32)
1783    });
1784
1785    let lights = Spiral2d::new().take(9).flat_map(|rpos| {
1786        let chunk_pos = chunk_key + rpos;
1787        chunk_boi(chunk_pos)
1788            .into_iter()
1789            .flat_map(|c| c.lights.iter())
1790            .filter_map(move |(light_pos, level)| {
1791                let chunk_wpos =
1792                    Vec3::<i32>::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
1793                let light_wpos = (chunk_wpos + *light_pos).map(|e| e as f32 + 0.5);
1794                let diff = light_wpos - wpos;
1795                let effect = ((*level as f32 - diff.map(|e| e.abs()).sum()).max(0.0)
1796                    * SUNLIGHT_INV)
1797                    .powf(0.5);
1798                if effect > 0.0 {
1799                    Some((effect, diff))
1800                } else {
1801                    None
1802                }
1803            })
1804    });
1805    let (bias, total) = lights.clone().fold(
1806        (Vec3::broadcast(0.0), 0.0),
1807        |(bias, total), (effect, diff)| {
1808            (
1809                bias + diff.try_normalized().unwrap_or_else(Vec3::zero) * effect,
1810                total + effect,
1811            )
1812        },
1813    );
1814    let bias = bias * (1.0 - AMBIANCE) / total.max(0.001);
1815
1816    let bias_norm = bias.try_normalized().unwrap_or_else(Vec3::zero);
1817    let (weight, total) = lights.fold((0.0, 0.0), |(weight, total), (effect, diff)| {
1818        (
1819            weight
1820                + diff
1821                    .try_normalized()
1822                    .unwrap_or_else(Vec3::zero)
1823                    .dot(bias_norm)
1824                    * effect,
1825            total + effect,
1826        )
1827    });
1828    let weight = weight / total.max(0.001);
1829
1830    (
1831        bias * weight,
1832        glow_at_wpos_inner(chunk_glow_map, wpos.map(|e| e.floor() as i32)),
1833    )
1834}