bevy_clipmap/
lib.rs

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/// The component defining a clipmap.
110/// https://hhoppe.com/gpugcm.pdf
111#[derive(Component)]
112pub struct Clipmap {
113    /// Half width of the grid
114    /// Stored as half because the full width must be even.
115    pub half_width: u32,
116
117    /// Number of LOD levels to generate.
118    /// Each next level covers 2x area of previous one.
119    pub levels: u32,
120
121    /// Base scale of the LOD square in world units.
122    pub base_scale: f32,
123
124    /// Physical size of one texel in meters.
125    pub texel_size: f32,
126
127    /// The entity to follow.
128    pub target: Entity,
129
130    /// Color texture.
131    pub color: Handle<Image>,
132
133    /// Heightmap texture.
134    pub heightmap: Handle<Image>,
135
136    /// FFT-compressed horizon map texture.
137    pub horizon: Handle<Image>,
138
139    /// Number of FFT coefficients.
140    pub horizon_coeffs: u32,
141
142    /// Height bounds.
143    pub min: f32,
144    pub max: f32,
145
146    /// Enable wireframe.
147    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}