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        println!("{filler_width} {square_width} {builder_width}");
153
154        let mut square = MeshBuilder::new();
155        let mut filler = MeshBuilder::new();
156        let mut center = MeshBuilder::new();
157        let mut trim = MeshBuilder::new();
158        let mut stitch = MeshBuilder::new();
159
160        for xy in 0..builder_width.pow(2) {
161            let x = xy % builder_width;
162            let y = xy / builder_width;
163            if x < square_width && y < square_width {
164                square.add_square(x, y);
165            }
166            let range = square_width * 2..square_width * 2 + filler_width;
167            if (range.contains(&x) || range.contains(&y))
168                && x < builder_width - filler_width
169                && y < builder_width - filler_width
170            {
171                center.add_square(x, y);
172                let range = square_width..builder_width - square_width - filler_width;
173                if !range.contains(&x) || !range.contains(&y) {
174                    filler.add_square(x, y);
175                }
176            }
177            if x >= builder_width - filler_width || y >= builder_width - filler_width {
178                trim.add_square(x, y);
179            }
180        }
181
182        for x in 0..builder_width / 2 {
183            let x = x * 2;
184            stitch.add_triangle(x, 0, x + 1, 0, x + 2, 0);
185            stitch.add_triangle(x + 2, builder_width, x + 1, builder_width, x, builder_width);
186            stitch.add_triangle(0, x + 2, 0, x + 1, 0, x);
187            stitch.add_triangle(builder_width, x, builder_width, x + 1, builder_width, x + 2);
188        }
189
190        commands.entity(entity).insert((
191            Transform::default(),
192            Visibility::default(),
193            Handles {
194                square: meshes.add(square.build()),
195                filler: meshes.add(filler.build()),
196                center: meshes.add(center.build()),
197                trim: meshes.add(trim.build()),
198                stitch: meshes.add(stitch.build()),
199            },
200        ));
201
202        for level in 0..clipmap.levels {
203            commands.entity(entity).with_child(ClipmapGrid {
204                level,
205                trim: Entity::PLACEHOLDER,
206            });
207        }
208    }
209}
210
211fn init_grids(
212    mut commands: Commands,
213    mut materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, GridMaterial>>>,
214    clipmaps: Query<(&Clipmap, &Handles)>,
215    mut grids: Query<(Entity, &mut ClipmapGrid, &ChildOf), Added<ClipmapGrid>>,
216) {
217    for (entity, mut grid, clipmap) in &mut grids {
218        let (clipmap, handles) = clipmaps.get(clipmap.parent()).unwrap();
219
220        let filler_width = 2 - clipmap.half_width as i32 % 2;
221        let square_width = (clipmap.half_width as i32 - filler_width) / 2;
222
223        commands.entity(entity).insert((
224            Transform::from_scale(Vec3::splat(grid.scale(clipmap.base_scale))),
225            Visibility::default(),
226        ));
227
228        let terrain_material = materials.add(ExtendedMaterial {
229            base: StandardMaterial::default(),
230            extension: GridMaterial {
231                color: clipmap.color.clone(),
232                heightmap: clipmap.heightmap.clone(),
233                horizon: clipmap.horizon.clone(),
234                horizon_coeffs: clipmap.horizon_coeffs,
235                lod: grid.level,
236                texel_size: clipmap.texel_size,
237                minmax: Vec2 {
238                    x: clipmap.min,
239                    y: clipmap.max,
240                },
241                translation: Vec2::ZERO,
242                wireframe: 0,
243            },
244        });
245
246        let terrain_material_w = 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: 1,
261            },
262        });
263
264        for xy in 0..4 * 4 {
265            let x = xy % 4;
266            let y = xy / 4;
267
268            if grid.level != 0 && (x == 1 || x == 2) && (y == 1 || y == 2) {
269                continue;
270            }
271
272            let offset_x = if x >= 2 { filler_width as f32 } else { 0.0 };
273            let offset_y = if y >= 2 { filler_width as f32 } else { 0.0 };
274
275            commands.entity(entity).with_children(|c| {
276                let mut e = c.spawn((
277                    Mesh3d(handles.square.clone()),
278                    MeshMaterial3d(terrain_material.clone()),
279                    NotShadowCaster,
280                    Transform::from_xyz(
281                        (x - 2) as f32 * square_width as f32 + offset_x,
282                        0.0,
283                        (y - 2) as f32 * square_width as f32 + offset_y,
284                    ),
285                ));
286                if clipmap.wireframe {
287                    e.with_child((
288                        Mesh3d(handles.square.clone()),
289                        MeshMaterial3d(terrain_material_w.clone()),
290                    ));
291                }
292            });
293        }
294
295        if grid.level == 0 {
296            commands.entity(entity).with_children(|c| {
297                let mut e = c.spawn((
298                    Mesh3d(handles.center.clone()),
299                    MeshMaterial3d(terrain_material.clone()),
300                    NotShadowCaster,
301                    Transform::from_xyz(
302                        -2.0 * square_width as f32,
303                        0.0,
304                        -2.0 * square_width as f32,
305                    ),
306                ));
307                if clipmap.wireframe {
308                    e.with_child((
309                        Mesh3d(handles.center.clone()),
310                        MeshMaterial3d(terrain_material_w.clone()),
311                    ));
312                }
313            });
314        } else {
315            commands.entity(entity).with_children(|c| {
316                let mut e = c.spawn((
317                    Mesh3d(handles.filler.clone()),
318                    MeshMaterial3d(terrain_material.clone()),
319                    NotShadowCaster,
320                    Transform::from_xyz(
321                        -2.0 * square_width as f32,
322                        0.0,
323                        -2.0 * square_width as f32,
324                    ),
325                ));
326                if clipmap.wireframe {
327                    e.with_child((
328                        Mesh3d(handles.filler.clone()),
329                        MeshMaterial3d(terrain_material_w.clone()),
330                    ));
331                }
332            });
333            commands.entity(entity).with_children(|c| {
334                let mut e = c.spawn((
335                    Mesh3d(handles.stitch.clone()),
336                    MeshMaterial3d(terrain_material.clone()),
337                    NotShadowCaster,
338                    Transform::from_xyz(-square_width as f32, 0.0, -square_width as f32)
339                        .with_scale(Vec3::splat(0.5)),
340                ));
341                if clipmap.wireframe {
342                    e.with_child((
343                        Mesh3d(handles.stitch.clone()),
344                        MeshMaterial3d(terrain_material_w.clone()),
345                    ));
346                }
347            });
348        }
349
350        let mut trim = commands.spawn((
351            Mesh3d(handles.trim.clone()),
352            MeshMaterial3d(terrain_material.clone()),
353            NotShadowCaster,
354            Transform::from_xyz(-2.0 * square_width as f32, 0.0, -2.0 * square_width as f32),
355        ));
356        if clipmap.wireframe {
357            trim.with_child((
358                Mesh3d(handles.trim.clone()),
359                MeshMaterial3d(terrain_material_w.clone()),
360            ));
361        }
362        grid.trim = trim.id();
363        commands.entity(entity).add_child(grid.trim);
364    }
365}
366
367fn update_grids(
368    mut transforms: Query<&mut Transform>,
369    mut aabbs: Query<&mut Aabb>,
370    mut terrain_materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, GridMaterial>>>,
371    terrain_material_handles: Query<
372        &MeshMaterial3d<ExtendedMaterial<StandardMaterial, GridMaterial>>,
373    >,
374    clipmaps: Query<&Clipmap>,
375    children: Query<&Children>,
376    grids: Query<(Entity, &ClipmapGrid, &ChildOf), With<Transform>>,
377) {
378    for (entity, grid, clipmap) in grids {
379        let clipmap = clipmaps.get(clipmap.parent()).unwrap();
380        let filler_width = 2 - clipmap.half_width as i32 % 2;
381        let scale = grid.scale(clipmap.base_scale) * filler_width as f32;
382        let target_pos = transforms.get(clipmap.target).unwrap().translation;
383        let snap_factor = (target_pos / scale).floor().as_ivec3();
384        let snap_pos = snap_factor.as_vec3() * scale;
385        transforms.get_mut(entity).unwrap().translation = snap_pos;
386
387        let snap_mod2 = ((snap_factor.xz() % 2) + 2) % 2;
388        let mut trim_transform = transforms.get_mut(grid.trim).unwrap();
389        trim_transform.translation = {
390            let offset_0 = filler_width as f32 - clipmap.half_width as f32;
391            let offset_1 = clipmap.half_width as f32;
392            Vec3 {
393                x: if snap_mod2.x == 0 { offset_0 } else { offset_1 },
394                y: 0.0,
395                z: if snap_mod2.y == 0 { offset_0 } else { offset_1 },
396            }
397        };
398        trim_transform.rotation = Quat::from_rotation_y(match snap_mod2 {
399            IVec2 { x: 0, y: 0 } => 0.0,
400            IVec2 { x: 0, y: 1 } => FRAC_PI_2,
401            IVec2 { x: 1, y: 0 } => -FRAC_PI_2,
402            IVec2 { x: 1, y: 1 } => PI,
403            _ => unreachable!(),
404        });
405
406        let grid_pos = (snap_pos + trim_transform.translation * scale).xz();
407        for child in children.iter_descendants(entity) {
408            let Ok(material) = terrain_material_handles.get(child) else {
409                continue;
410            };
411            let Some(material) = terrain_materials.get_mut(material) else {
412                continue;
413            };
414            let Ok(mut aabb) = aabbs.get_mut(child) else {
415                continue;
416            };
417            material.extension.translation = grid_pos;
418            aabb.center.y = clipmap.min + (clipmap.max - clipmap.min) / 2.0;
419            aabb.half_extents.y = (clipmap.max - clipmap.min) / 2.0;
420        }
421    }
422}
423
424#[repr(C)]
425#[derive(Eq, PartialEq, Hash, Copy, Clone)]
426struct WireframeKey {
427    wireframe: bool,
428}
429
430impl From<&GridMaterial> for WireframeKey {
431    fn from(material: &GridMaterial) -> Self {
432        Self {
433            wireframe: material.wireframe != 0,
434        }
435    }
436}
437
438#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
439#[bind_group_data(WireframeKey)]
440struct GridMaterial {
441    #[texture(100)]
442    #[sampler(101)]
443    color: Handle<Image>,
444    #[texture(102)]
445    #[sampler(103)]
446    heightmap: Handle<Image>,
447    #[texture(104, dimension = "2d_array")]
448    #[sampler(105)]
449    horizon: Handle<Image>,
450    #[uniform(106)]
451    horizon_coeffs: u32,
452    #[uniform(107)]
453    lod: u32,
454    #[uniform(108)]
455    texel_size: f32,
456    #[uniform(109)]
457    minmax: Vec2,
458    #[uniform(110)]
459    translation: Vec2,
460    #[uniform(111)]
461    wireframe: u32,
462}
463
464impl MaterialExtension for GridMaterial {
465    fn vertex_shader() -> ShaderRef {
466        ShaderRef::Path(
467            AssetPath::from_path_buf(embedded_path!("terrain.wgsl")).with_source("embedded"),
468        )
469    }
470
471    fn deferred_vertex_shader() -> ShaderRef {
472        ShaderRef::Path(
473            AssetPath::from_path_buf(embedded_path!("terrain.wgsl")).with_source("embedded"),
474        )
475    }
476
477    fn fragment_shader() -> ShaderRef {
478        ShaderRef::Path(
479            AssetPath::from_path_buf(embedded_path!("terrain.wgsl")).with_source("embedded"),
480        )
481    }
482
483    fn deferred_fragment_shader() -> ShaderRef {
484        ShaderRef::Path(
485            AssetPath::from_path_buf(embedded_path!("terrain.wgsl")).with_source("embedded"),
486        )
487    }
488
489    fn specialize(
490        _: &bevy::pbr::MaterialExtensionPipeline,
491        descriptor: &mut bevy::render::render_resource::RenderPipelineDescriptor,
492        _: &bevy::mesh::MeshVertexBufferLayoutRef,
493        key: bevy::pbr::MaterialExtensionKey<Self>,
494    ) -> std::result::Result<(), bevy::render::render_resource::SpecializedMeshPipelineError> {
495        if key.bind_group_data.wireframe {
496            descriptor.primitive.polygon_mode = bevy::render::render_resource::PolygonMode::Line;
497            descriptor.depth_stencil.as_mut().unwrap().bias.slope_scale = 1.0;
498        }
499        Ok(())
500    }
501}