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
52pub 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 fn is_visible(&self) -> bool {
65 self.in_frustum
72 }
73}
74
75type LightMapFn = Arc<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>;
77
78pub struct TerrainChunkData {
79 load_time: f32,
81 opaque_model: Option<Model<TerrainVertex>>,
82 fluid_model: Option<Model<FluidVertex>>,
83 atlas_alloc: Option<guillotiere::AllocId>,
86 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
109pub const SHALLOW_ALT: f32 = 24.0;
112pub const DEEP_ALT: f32 = 96.0;
115pub const UNDERGROUND_ALT: f32 = (SHALLOW_ALT + DEEP_ALT) * 0.5;
118
119const 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 skip_remesh: bool,
130}
131
132pub 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
145struct MeshWorkerResponse {
148 pos: Vec2<i32>,
149 sprite_instances: [(Vec<SpriteInstance>, AltIndices); SPRITE_LOD_LEVELS],
150 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 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)); let rot = block
193 .sprite_z_rot()
194 .unwrap_or((seed % 17 / 4).min(3) as f32 / 2.0 * std::f32::consts::PI);
199 let mirror = block.sprite_mirror_vec();
200 let variation = match data.variations.len() {
202 1 => 0,
203 2 => (seed as usize % 4) / 3,
204 3 => (seed as usize % 5) / 2,
205 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 let mat = Mat4::identity()
217 .scaled_3d(model_data.scale)
219 .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 for page in model_data.vert_pages.clone() {
228 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
247fn 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 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 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 sprite_instances: {
320 prof_span!("extract sprite_instances");
321 let mut instances = [(); SPRITE_LOD_LEVELS].map(|()| {
322 (
323 Vec::new(), Vec::new(), Vec::new(), )
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 |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 atlas: AtlasAllocator,
407 chunks: HashMap<Vec2<i32>, TerrainChunkData>,
408 shadow_chunks: Vec<(Vec2<i32>, TerrainChunkData)>,
419 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 pub(super) sprite_render_state: Arc<SpriteRenderState>,
437 pub(super) sprite_globals: SpriteGlobalsBindGroup,
438 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 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_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 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 let start_page_num =
538 sprite_mesh.vertices().len() / SPRITE_VERT_PAGE_SIZE as usize;
539 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 let end_page_num = sprite_mesh
549 .vertices()
550 .len()
551 .div_ceil(SPRITE_VERT_PAGE_SIZE as usize);
552 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 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_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 let SpriteWorkerResponse {
608 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 let sprite_verts_buffer = renderer.create_sprite_verts(sprite_mesh);
629
630 Self {
631 state: Arc::new(SpriteRenderState {
632 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 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 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 if let Some(atlas_alloc) = chunk.atlas_alloc {
745 self.atlas.deallocate(atlas_alloc);
746 }
747 }
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 }
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 self.shadow_chunks.push((pos, chunk));
766 }
767
768 if let Some(_todo) = self.mesh_todo.remove(&pos) {
769 }
771 }
772
773 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 fn skip_remesh(old_block: Block, new_block: Block) -> (bool, bool) {
794 let same_mesh =
795 new_block.is_liquid() == old_block.is_liquid() &&
798 new_block.is_opaque() == old_block.is_opaque();
799 let skip_lights = same_mesh &&
800 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 !new_block.has_color() && !old_block.has_color();
807 (skip_color, skip_lights)
808 }
809
810 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 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 for &pos in &scene_data.state.terrain_changes().removed_chunks {
857 self.remove_chunk(pos);
858 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 let mut visible_bounding_box: Option<Aabb<f32>> = None;
873
874 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 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 span!(guard, "Add chunks with modified blocks to mesh todo list");
930 for (&pos, &old_block) in scene_data.state.terrain_changes().modified_blocks.iter() {
932 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 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 let skip_remesh = skip_color && skip_lights;
956
957 let block_effect_radius = crate::mesh::terrain::MAX_LIGHT_DIST;
973
974 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 continue;
988 }
989
990 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 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 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 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 .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 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 let volume = match scene_data.state.terrain().sample(aabr) {
1076 Ok(sample) => sample, Err(VolGrid2dError::NoSuchChunk) => {
1081 continue;
1082 },
1083 _ => panic!("Unhandled edge case"),
1084 };
1085
1086 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 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 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 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<_>>(); for response in incoming_chunks {
1149 match self.mesh_todo.get(&response.pos) {
1150 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 let load_time = self
1164 .chunks
1165 .get(&response.pos)
1166 .map(|chunk| chunk.load_time)
1167 .unwrap_or(current_time as f32);
1168 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 let (new_atlas, new_atlas_textures) =
1180 Self::make_atlas(renderer).expect("Failed to create atlas texture");
1181
1182 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 let atlas_offs = Vec2::new(
1203 allocation.rectangle.min.x as u32,
1204 allocation.rectangle.min.y as u32,
1205 );
1206 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 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 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 Some(_todo) => {},
1266 None => {},
1267 }
1268 }
1269 drop(guard);
1270
1271 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 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 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 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 chunk.can_shadow_point = distance_2 < (128.0 * 128.0);
1325 }
1326 drop(guard);
1327
1328 span!(guard, "Shadow magic");
1329 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 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 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 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 self.chunks.iter_mut()
1403 .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 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 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 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 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 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 .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 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 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 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 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 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 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 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() .for_each(|(model, locals)| {
1755 fluid_drawer.draw(
1756 model,
1757 locals,
1758 )
1759 });
1760 drop(fluid_drawer);
1761 drop(guard);
1762 }
1763}
1764fn 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; 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}