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