1use std::collections::HashMap;
2
3use bevy::{prelude::*, window::PrimaryWindow};
4use wde_camera::prelude::*;
5use wde_renderer::prelude::*;
6
7use crate::{
8 manager::{
9 CHUNK_COUNT, CHUNK_MAP_SIZE, CHUNK_SIZE, ChunkPos, SPLAT_MAPS, Terrain, TerrainDirtyTile
10 },
11 render::dependencies::terrain_buffer::TerrainTileDescription
12};
13
14pub const TERRAIN_LOD_COUNT: usize = 5;
16const LOD_BASE_DISTANCE: f32 = 48.0;
19
20#[derive(Clone, Debug, Default)]
22pub struct TerrainDrawList {
23 pub tiles: Vec<TerrainTileDescription>,
24 pub lod_ranges: [std::ops::Range<u32>; TERRAIN_LOD_COUNT]
26}
27
28#[derive(Component, Reflect)]
32#[reflect(Component)]
33pub struct TerrainRenderer {
34 pub pos_to_layer: HashMap<ChunkPos, u32>,
36 #[reflect(ignore)]
38 pub heightmap_array: Option<Handle<Texture>>,
39 #[reflect(ignore)]
41 pub splatmap_arrays: Vec<Handle<Texture>>,
42 #[reflect(ignore)]
44 pub dirty: Vec<TerrainDirtyTile>,
45 #[reflect(ignore)]
47 pub draw_list: TerrainDrawList,
48 #[reflect(ignore)]
50 bounds: HashMap<ChunkPos, (f32, f32)>
51}
52
53impl TerrainRenderer {
54 pub fn new(asset_server: &AssetServer) -> Self {
56 let layer_count = CHUNK_COUNT * CHUNK_COUNT;
57 let usages = TextureUsages::COPY_SRC
58 | TextureUsages::COPY_DST
59 | TextureUsages::TEXTURE_BINDING
60 | TextureUsages::STORAGE_BINDING;
61
62 let heightmap_array = asset_server.add(Texture {
63 label: "terrain_heightmap_array".to_string(),
64 size: (CHUNK_MAP_SIZE, CHUNK_MAP_SIZE),
65 format: TextureFormat::R32Float,
66 usages,
67 layer_count,
68 filterable: false,
69 ..Default::default()
70 });
71 let splatmap_arrays = (0..SPLAT_MAPS)
72 .map(|k| {
73 asset_server.add(Texture {
74 label: format!("terrain_splatmap_array_{k}"),
75 size: (CHUNK_MAP_SIZE, CHUNK_MAP_SIZE),
76 format: TextureFormat::Rgba8Unorm,
77 usages,
78 layer_count,
79 ..Default::default()
80 })
81 })
82 .collect();
83
84 let mut pos_to_layer = HashMap::new();
86 let mut layer = 0u32;
87 for i in 0..CHUNK_COUNT {
88 for j in 0..CHUNK_COUNT {
89 let px = i as i32 - (CHUNK_COUNT as i32) / 2;
90 let pz = j as i32 - (CHUNK_COUNT as i32) / 2;
91 pos_to_layer.insert(IVec2::new(px, pz), layer);
92 layer += 1;
93 }
94 }
95
96 Self {
97 pos_to_layer,
98 heightmap_array: Some(heightmap_array),
99 splatmap_arrays,
100 dirty: Vec::new(),
101 draw_list: TerrainDrawList::default(),
102 bounds: HashMap::new()
103 }
104 }
105
106 pub fn extract_dirty(
108 mut renderer: Query<&mut TerrainRenderer>,
109 mut terrain: Query<&mut Terrain>
110 ) {
111 let (Ok(mut renderer), Ok(mut terrain)) = (renderer.single_mut(), terrain.single_mut())
112 else {
113 return;
114 };
115 renderer.dirty.clear();
116 renderer.dirty.append(&mut terrain.dirty_render);
117
118 let renderer = renderer.as_mut();
120 for (pos, map_type, _, _) in &renderer.dirty {
121 if *map_type == 0
122 && let Some(heights) = terrain.heights.get(pos)
123 {
124 let bounds = heights
125 .iter()
126 .fold((f32::MAX, f32::MIN), |(lo, hi), v| (lo.min(*v), hi.max(*v)));
127 renderer.bounds.insert(*pos, bounds);
128 }
129 }
130 }
131
132 pub fn update_draw_list(
135 mut renderer: Query<&mut TerrainRenderer>,
136 camera: Query<(&GlobalTransform, &CameraView), With<ActiveCamera>>,
137 window: Query<&Window, With<PrimaryWindow>>
138 ) {
139 let (Ok(mut renderer), Ok((camera_transform, view))) =
140 (renderer.single_mut(), camera.single())
141 else {
142 return;
143 };
144 let aspect = window
145 .single()
146 .map_or(1.0, |w| w.width() / w.height().max(1.0));
147 let view_proj = Mat4::perspective_rh(view.fov.to_radians(), aspect, view.znear, view.zfar)
148 * camera_transform.to_matrix().inverse();
149 let eye = camera_transform.translation();
150
151 let renderer = renderer.as_mut();
152 let half = CHUNK_SIZE / 2.0;
153 let lod_of = |pos: ChunkPos| -> Option<u32> {
154 let (lo, hi) = *renderer.bounds.get(&pos)?;
155 let center = pos.as_vec2() * CHUNK_SIZE;
156 let min = Vec3::new(center.x - half, lo, center.y - half);
157 let max = Vec3::new(center.x + half, hi, center.y + half);
158 let distance = eye.clamp(min, max).distance(eye);
159 let lod = (distance / LOD_BASE_DISTANCE).max(1.0).log2().floor() as u32 + 1;
160 Some(match distance < LOD_BASE_DISTANCE {
161 true => 0,
162 false => lod.min(TERRAIN_LOD_COUNT as u32 - 1)
163 })
164 };
165 let visible = |pos: ChunkPos| -> bool {
166 let Some(&(lo, hi)) = renderer.bounds.get(&pos) else {
167 return false;
168 };
169 let center = pos.as_vec2() * CHUNK_SIZE;
170 let corners = (0..8).map(|c| {
171 let x = center.x + if c & 1 == 0 { -half } else { half };
172 let z = center.y + if c & 2 == 0 { -half } else { half };
173 let y = if c & 4 == 0 { lo } else { hi };
174 view_proj * Vec4::new(x, y, z, 1.0)
175 });
176 let corners: Vec<Vec4> = corners.collect();
178 let outside = |test: &dyn Fn(&Vec4) -> bool| corners.iter().all(test);
179 !(outside(&|p| p.x < -p.w)
180 || outside(&|p| p.x > p.w)
181 || outside(&|p| p.y < -p.w)
182 || outside(&|p| p.y > p.w)
183 || outside(&|p| p.z < 0.0)
184 || outside(&|p| p.z > p.w))
185 };
186
187 let lods: HashMap<ChunkPos, u32> = renderer
188 .pos_to_layer
189 .keys()
190 .filter_map(|pos| Some((*pos, lod_of(*pos)?)))
191 .collect();
192 let mut tiles: Vec<TerrainTileDescription> = renderer
193 .pos_to_layer
194 .iter()
195 .filter(|(pos, _)| visible(**pos))
196 .filter_map(|(pos, layer)| {
197 let lod = *lods.get(pos)?;
198 let neighbor = |offset: IVec2| *lods.get(&(*pos + offset)).unwrap_or(&lod) as f32;
200 Some(TerrainTileDescription {
201 pos: [pos.x as f32, pos.y as f32],
202 lod: lod as f32,
203 tile_layer: *layer,
204 neighbor_lods: [
205 neighbor(IVec2::NEG_X),
206 neighbor(IVec2::X),
207 neighbor(IVec2::NEG_Y),
208 neighbor(IVec2::Y)
209 ]
210 })
211 })
212 .collect();
213 tiles.sort_by_key(|t| t.lod as u32);
214
215 let mut lod_ranges: [std::ops::Range<u32>; TERRAIN_LOD_COUNT] = Default::default();
216 for (lod, range) in lod_ranges.iter_mut().enumerate() {
217 let start = tiles.partition_point(|t| (t.lod as usize) < lod) as u32;
218 let end = tiles.partition_point(|t| (t.lod as usize) <= lod) as u32;
219 *range = start..end;
220 }
221 renderer.draw_list = TerrainDrawList { tiles, lod_ranges };
222 }
223}