1use std::{
2 collections::HashMap,
3 f32::consts::{FRAC_PI_2, PI},
4};
5
6use bevy::{
7 asset::{AssetPath, RenderAssetUsages, embedded_asset, embedded_path},
8 camera::{primitives::Aabb, visibility::NoAutoAabb},
9 light::NotShadowCaster,
10 mesh::{Indices, PrimitiveTopology},
11 pbr::{ExtendedMaterial, MaterialExtension},
12 prelude::*,
13 render::render_resource::AsBindGroup,
14 shader::ShaderRef,
15};
16
17pub struct ClipmapPlugin;
18
19struct ClipmapPart {
20 handle: Handle<Mesh>,
21 aabb: Aabb,
22}
23
24impl ClipmapPart {
25 fn build(meshes: &mut ResMut<Assets<Mesh>>, builder: MeshBuilder) -> Self {
26 let mut min = Vec3::from_slice(&builder.vertices[0]);
27 let mut max = min;
28 for v in builder.vertices.iter().map(|v| Vec3::from_slice(v)) {
29 min = min.min(v);
30 max = max.max(v);
31 }
32 Self {
33 handle: meshes.add(builder.build()),
34 aabb: Aabb::from_min_max(min, max),
35 }
36 }
37}
38
39#[derive(Component)]
40struct ClipmapParts {
41 square: ClipmapPart,
42 filler: ClipmapPart,
43 center: ClipmapPart,
44 trim: ClipmapPart,
45 stitch: ClipmapPart,
46}
47
48impl Plugin for ClipmapPlugin {
49 fn build(&self, app: &mut App) {
50 embedded_asset!(app, "terrain.wgsl");
51
52 app.add_plugins(MaterialPlugin::<
53 ExtendedMaterial<StandardMaterial, GridMaterial>,
54 >::default())
55 .add_systems(PreUpdate, (init_clipmaps, init_grids))
56 .add_systems(Update, update_grids);
57 }
58}
59
60struct MeshBuilder {
61 unique_vertices: HashMap<(i32, i32), u32>,
62 vertices: Vec<[f32; 3]>,
63 indices: Vec<u32>,
64}
65
66impl MeshBuilder {
67 fn new() -> Self {
68 Self {
69 unique_vertices: HashMap::new(),
70 vertices: vec![],
71 indices: vec![],
72 }
73 }
74
75 fn add_vertex(&mut self, x: i32, y: i32) -> u32 {
76 if let Some(index) = self.unique_vertices.get(&(x, y)) {
77 *index
78 } else {
79 let index = self.vertices.len() as u32;
80 self.vertices.push([x as f32, 0.0, y as f32]);
81 self.unique_vertices.insert((x, y), index);
82 index
83 }
84 }
85
86 fn add_triangle(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, x3: i32, y3: i32) {
87 let p1 = self.add_vertex(x1, y1);
88 let p2 = self.add_vertex(x2, y2);
89 let p3 = self.add_vertex(x3, y3);
90 self.indices.extend([p1, p2, p3]);
91 }
92
93 fn add_square(&mut self, x: i32, y: i32) {
94 let p1 = self.add_vertex(x, y);
95 let p2 = self.add_vertex(x, y + 1);
96 let p3 = self.add_vertex(x + 1, y + 1);
97 let p4 = self.add_vertex(x + 1, y);
98 self.indices.extend([p1, p2, p3]);
99 self.indices.extend([p1, p3, p4]);
100 }
101
102 fn build(self) -> Mesh {
103 Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::all())
104 .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, self.vertices)
105 .with_inserted_indices(Indices::U32(self.indices))
106 }
107}
108
109#[derive(Component)]
112pub struct Clipmap {
113 pub half_width: u32,
116
117 pub levels: u32,
120
121 pub base_scale: f32,
123
124 pub texel_size: f32,
126
127 pub target: Entity,
129
130 pub color: Handle<Image>,
132
133 pub heightmap: Handle<Image>,
135
136 pub horizon: Handle<Image>,
138
139 pub horizon_coeffs: u32,
141
142 pub min: f32,
144 pub max: f32,
145
146 pub wireframe: bool,
148}
149
150#[derive(Component)]
151struct ClipmapGrid {
152 level: u32,
153 trim: Entity,
154}
155
156impl ClipmapGrid {
157 fn scale(&self, base_scale: f32) -> f32 {
158 base_scale * 2u32.pow(self.level) as f32
159 }
160}
161
162fn init_clipmaps(
163 mut commands: Commands,
164 mut meshes: ResMut<Assets<Mesh>>,
165 clipmaps: Query<(Entity, &Clipmap), Added<Clipmap>>,
166) {
167 for (entity, clipmap) in clipmaps {
168 let builder_width = clipmap.half_width as i32 * 2;
169 let filler_width = 2 - clipmap.half_width as i32 % 2;
170 let square_width = (clipmap.half_width as i32 - filler_width) / 2;
171
172 let mut square = MeshBuilder::new();
173 let mut filler = MeshBuilder::new();
174 let mut center = MeshBuilder::new();
175 let mut trim = MeshBuilder::new();
176 let mut stitch = MeshBuilder::new();
177
178 for xy in 0..builder_width.pow(2) {
179 let x = xy % builder_width;
180 let y = xy / builder_width;
181 if x < square_width && y < square_width {
182 square.add_square(x, y);
183 }
184 let range = square_width * 2..square_width * 2 + filler_width;
185 if (range.contains(&x) || range.contains(&y))
186 && x < builder_width - filler_width
187 && y < builder_width - filler_width
188 {
189 center.add_square(x, y);
190 let range = square_width..builder_width - square_width - filler_width;
191 if !range.contains(&x) || !range.contains(&y) {
192 filler.add_square(x, y);
193 }
194 }
195 if x >= builder_width - filler_width || y >= builder_width - filler_width {
196 trim.add_square(x, y);
197 }
198 }
199
200 for x in 0..builder_width / 2 {
201 let x = x * 2;
202 stitch.add_triangle(x, 0, x + 1, 0, x + 2, 0);
203 stitch.add_triangle(x + 2, builder_width, x + 1, builder_width, x, builder_width);
204 stitch.add_triangle(0, x + 2, 0, x + 1, 0, x);
205 stitch.add_triangle(builder_width, x, builder_width, x + 1, builder_width, x + 2);
206 }
207
208 commands.entity(entity).insert((
209 Transform::default(),
210 Visibility::default(),
211 ClipmapParts {
212 square: ClipmapPart::build(&mut meshes, square),
213 filler: ClipmapPart::build(&mut meshes, filler),
214 center: ClipmapPart::build(&mut meshes, center),
215 trim: ClipmapPart::build(&mut meshes, trim),
216 stitch: ClipmapPart::build(&mut meshes, stitch),
217 },
218 ));
219
220 for level in 0..clipmap.levels {
221 commands.entity(entity).with_child(ClipmapGrid {
222 level,
223 trim: Entity::PLACEHOLDER,
224 });
225 }
226 }
227}
228
229fn init_grids(
230 mut commands: Commands,
231 mut materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, GridMaterial>>>,
232 clipmaps: Query<(&Clipmap, &ClipmapParts)>,
233 mut grids: Query<(Entity, &mut ClipmapGrid, &ChildOf), Added<ClipmapGrid>>,
234) {
235 for (entity, mut grid, clipmap) in &mut grids {
236 let (clipmap, parts) = clipmaps.get(clipmap.parent()).unwrap();
237
238 let filler_width = 2 - clipmap.half_width as i32 % 2;
239 let square_width = (clipmap.half_width as i32 - filler_width) / 2;
240
241 commands.entity(entity).insert((
242 Transform::from_scale(Vec3::splat(grid.scale(clipmap.base_scale))),
243 Visibility::default(),
244 ));
245
246 let terrain_material = materials.add(ExtendedMaterial {
247 base: StandardMaterial::default(),
248 extension: GridMaterial {
249 color: clipmap.color.clone(),
250 heightmap: clipmap.heightmap.clone(),
251 horizon: clipmap.horizon.clone(),
252 horizon_coeffs: clipmap.horizon_coeffs,
253 lod: grid.level,
254 texel_size: clipmap.texel_size,
255 minmax: Vec2 {
256 x: clipmap.min,
257 y: clipmap.max,
258 },
259 translation: Vec2::ZERO,
260 wireframe: 0,
261 },
262 });
263
264 let terrain_material_w = materials.add(ExtendedMaterial {
265 base: StandardMaterial::default(),
266 extension: GridMaterial {
267 color: clipmap.color.clone(),
268 heightmap: clipmap.heightmap.clone(),
269 horizon: clipmap.horizon.clone(),
270 horizon_coeffs: clipmap.horizon_coeffs,
271 lod: grid.level,
272 texel_size: clipmap.texel_size,
273 minmax: Vec2 {
274 x: clipmap.min,
275 y: clipmap.max,
276 },
277 translation: Vec2::ZERO,
278 wireframe: 1,
279 },
280 });
281
282 for xy in 0..4 * 4 {
283 let x = xy % 4;
284 let y = xy / 4;
285
286 if grid.level != 0 && (x == 1 || x == 2) && (y == 1 || y == 2) {
287 continue;
288 }
289
290 let offset_x = if x >= 2 { filler_width as f32 } else { 0.0 };
291 let offset_y = if y >= 2 { filler_width as f32 } else { 0.0 };
292
293 commands.entity(entity).with_children(|c| {
294 let mut e = c.spawn((
295 Mesh3d(parts.square.handle.clone()),
296 MeshMaterial3d(terrain_material.clone()),
297 NotShadowCaster,
298 Transform::from_xyz(
299 (x - 2) as f32 * square_width as f32 + offset_x,
300 0.0,
301 (y - 2) as f32 * square_width as f32 + offset_y,
302 ),
303 NoAutoAabb,
304 parts.square.aabb.clone(),
305 ));
306 if clipmap.wireframe {
307 e.with_child((
308 Mesh3d(parts.square.handle.clone()),
309 MeshMaterial3d(terrain_material_w.clone()),
310 NoAutoAabb,
311 parts.square.aabb.clone(),
312 ));
313 }
314 });
315 }
316
317 if grid.level == 0 {
318 commands.entity(entity).with_children(|c| {
319 let mut e = c.spawn((
320 Mesh3d(parts.center.handle.clone()),
321 MeshMaterial3d(terrain_material.clone()),
322 NotShadowCaster,
323 Transform::from_xyz(
324 -2.0 * square_width as f32,
325 0.0,
326 -2.0 * square_width as f32,
327 ),
328 NoAutoAabb,
329 parts.center.aabb,
330 ));
331 if clipmap.wireframe {
332 e.with_child((
333 Mesh3d(parts.center.handle.clone()),
334 MeshMaterial3d(terrain_material_w.clone()),
335 NoAutoAabb,
336 parts.center.aabb,
337 ));
338 }
339 });
340 } else {
341 commands.entity(entity).with_children(|c| {
342 let mut e = c.spawn((
343 Mesh3d(parts.filler.handle.clone()),
344 MeshMaterial3d(terrain_material.clone()),
345 NotShadowCaster,
346 Transform::from_xyz(
347 -2.0 * square_width as f32,
348 0.0,
349 -2.0 * square_width as f32,
350 ),
351 NoAutoAabb,
352 parts.filler.aabb,
353 ));
354 if clipmap.wireframe {
355 e.with_child((
356 Mesh3d(parts.filler.handle.clone()),
357 MeshMaterial3d(terrain_material_w.clone()),
358 NoAutoAabb,
359 parts.filler.aabb,
360 ));
361 }
362 });
363 commands.entity(entity).with_children(|c| {
364 let mut e = c.spawn((
365 Mesh3d(parts.stitch.handle.clone()),
366 MeshMaterial3d(terrain_material.clone()),
367 NotShadowCaster,
368 Transform::from_xyz(-square_width as f32, 0.0, -square_width as f32)
369 .with_scale(Vec3::splat(0.5)),
370 NoAutoAabb,
371 parts.stitch.aabb,
372 ));
373 if clipmap.wireframe {
374 e.with_child((
375 Mesh3d(parts.stitch.handle.clone()),
376 MeshMaterial3d(terrain_material_w.clone()),
377 NoAutoAabb,
378 parts.stitch.aabb,
379 ));
380 }
381 });
382 }
383
384 let mut trim = commands.spawn((
385 Mesh3d(parts.trim.handle.clone()),
386 MeshMaterial3d(terrain_material.clone()),
387 NotShadowCaster,
388 Transform::from_xyz(-2.0 * square_width as f32, 0.0, -2.0 * square_width as f32),
389 NoAutoAabb,
390 parts.trim.aabb,
391 ));
392 if clipmap.wireframe {
393 trim.with_child((
394 Mesh3d(parts.trim.handle.clone()),
395 MeshMaterial3d(terrain_material_w.clone()),
396 NoAutoAabb,
397 parts.trim.aabb,
398 ));
399 }
400 grid.trim = trim.id();
401 commands.entity(entity).add_child(grid.trim);
402 }
403}
404
405fn update_grids(
406 mut transforms: Query<&mut Transform>,
407 mut aabbs: Query<&mut Aabb>,
408 mut terrain_materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, GridMaterial>>>,
409 terrain_material_handles: Query<
410 &MeshMaterial3d<ExtendedMaterial<StandardMaterial, GridMaterial>>,
411 >,
412 clipmaps: Query<&Clipmap>,
413 children: Query<&Children>,
414 grids: Query<(Entity, &ClipmapGrid, &ChildOf), With<Transform>>,
415) {
416 for (entity, grid, clipmap) in grids {
417 let clipmap = clipmaps.get(clipmap.parent()).unwrap();
418 let filler_width = 2 - clipmap.half_width as i32 % 2;
419 let snap_scale = grid.scale(clipmap.base_scale) * filler_width as f32;
420 let target_pos = transforms.get(clipmap.target).unwrap().translation;
421 let snap_factor = (target_pos / snap_scale).floor().as_ivec3().xz();
422 let snap_pos = snap_factor.as_vec2() * snap_scale;
423 transforms.get_mut(entity).unwrap().translation = snap_pos.extend(0.0).xzy();
424
425 let snap_mod2 = ((snap_factor % 2) + 2) % 2;
426 let mut trim_transform = transforms.get_mut(grid.trim).unwrap();
427 trim_transform.translation = {
428 let offset_0 = filler_width as f32 - clipmap.half_width as f32;
429 let offset_1 = clipmap.half_width as f32;
430 Vec3 {
431 x: if snap_mod2.x == 0 { offset_0 } else { offset_1 },
432 y: 0.0,
433 z: if snap_mod2.y == 0 { offset_0 } else { offset_1 },
434 }
435 };
436 trim_transform.rotation = Quat::from_rotation_y(match snap_mod2 {
437 IVec2 { x: 0, y: 0 } => 0.0,
438 IVec2 { x: 0, y: 1 } => FRAC_PI_2,
439 IVec2 { x: 1, y: 0 } => -FRAC_PI_2,
440 IVec2 { x: 1, y: 1 } => PI,
441 _ => unreachable!(),
442 });
443
444 let grid_pos = (snap_pos.extend(0.0).xzy() + trim_transform.translation * snap_scale).xz();
445 let aabb_scale = 2u32.pow(1 + grid.level) as f32;
446 for child in children.iter_descendants(entity) {
447 let Ok(material) = terrain_material_handles.get(child) else {
448 continue;
449 };
450 let Some(material) = terrain_materials.get_mut(material) else {
451 continue;
452 };
453 let Ok(mut aabb) = aabbs.get_mut(child) else {
454 continue;
455 };
456 material.extension.translation = grid_pos;
457 aabb.center.y = (clipmap.max + clipmap.min) / aabb_scale;
458 aabb.half_extents.y = (clipmap.max - clipmap.min) / aabb_scale;
459 }
460 }
461}
462
463#[repr(C)]
464#[derive(Eq, PartialEq, Hash, Copy, Clone)]
465struct WireframeKey {
466 wireframe: bool,
467}
468
469impl From<&GridMaterial> for WireframeKey {
470 fn from(material: &GridMaterial) -> Self {
471 Self {
472 wireframe: material.wireframe != 0,
473 }
474 }
475}
476
477#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
478#[bind_group_data(WireframeKey)]
479struct GridMaterial {
480 #[texture(100)]
481 #[sampler(101)]
482 color: Handle<Image>,
483 #[texture(102)]
484 #[sampler(103)]
485 heightmap: Handle<Image>,
486 #[texture(104, dimension = "2d_array")]
487 #[sampler(105)]
488 horizon: Handle<Image>,
489 #[uniform(106)]
490 horizon_coeffs: u32,
491 #[uniform(107)]
492 lod: u32,
493 #[uniform(108)]
494 texel_size: f32,
495 #[uniform(109)]
496 minmax: Vec2,
497 #[uniform(110)]
498 translation: Vec2,
499 #[uniform(111)]
500 wireframe: u32,
501}
502
503impl MaterialExtension for GridMaterial {
504 fn vertex_shader() -> ShaderRef {
505 ShaderRef::Path(
506 AssetPath::from_path_buf(embedded_path!("terrain.wgsl")).with_source("embedded"),
507 )
508 }
509
510 fn deferred_vertex_shader() -> ShaderRef {
511 ShaderRef::Path(
512 AssetPath::from_path_buf(embedded_path!("terrain.wgsl")).with_source("embedded"),
513 )
514 }
515
516 fn fragment_shader() -> ShaderRef {
517 ShaderRef::Path(
518 AssetPath::from_path_buf(embedded_path!("terrain.wgsl")).with_source("embedded"),
519 )
520 }
521
522 fn deferred_fragment_shader() -> ShaderRef {
523 ShaderRef::Path(
524 AssetPath::from_path_buf(embedded_path!("terrain.wgsl")).with_source("embedded"),
525 )
526 }
527
528 fn specialize(
529 _: &bevy::pbr::MaterialExtensionPipeline,
530 descriptor: &mut bevy::render::render_resource::RenderPipelineDescriptor,
531 _: &bevy::mesh::MeshVertexBufferLayoutRef,
532 key: bevy::pbr::MaterialExtensionKey<Self>,
533 ) -> std::result::Result<(), bevy::render::render_resource::SpecializedMeshPipelineError> {
534 if key.bind_group_data.wireframe {
535 descriptor.primitive.polygon_mode = bevy::render::render_resource::PolygonMode::Line;
536 descriptor.depth_stencil.as_mut().unwrap().bias.slope_scale = 1.0;
537 }
538 Ok(())
539 }
540}