wde_terrain/render/
renderer_gpu.rs

1use std::collections::HashMap;
2
3use bevy::{ecs::system::SystemParamItem, prelude::*};
4use wde_renderer::prelude::*;
5
6use crate::{
7    manager::{ChunkPos, SPLAT_MAP_COUNT, TerrainDirtyTile},
8    prelude::TerrainExtractor,
9    render::{extractor, renderer::TerrainRenderer}
10};
11
12#[derive(Asset, Clone, TypePath, Default)]
13pub struct TerrainTileBgRender {
14    tile_position: ChunkPos,
15    heightmap: Handle<Texture>,
16    splatmaps: Vec<Handle<Texture>>
17}
18impl RenderBinding for TerrainTileBgRender {
19    type Params = ();
20
21    fn describe(
22        &mut self,
23        _params: &SystemParamItem<Self::Params>,
24        builder: &mut RenderBindingBuilder
25    ) {
26        builder.add_texture_view_from_id(Some(self.heightmap.id()));
27        builder.add_texture_sampler_from_id(Some(self.heightmap.id()));
28        for i in 0..SPLAT_MAP_COUNT / 4 {
29            if i as usize >= self.splatmaps.len() {
30                continue;
31            }
32            builder.add_texture_view_from_id(Some(self.splatmaps[i as usize].id()));
33            builder.add_texture_sampler_from_id(Some(self.splatmaps[i as usize].id()));
34        }
35    }
36
37    fn label(&self) -> &str {
38        Box::leak(
39            format!(
40                "terrain-tile-{}-{}-render",
41                self.tile_position.x, self.tile_position.y
42            )
43            .into_boxed_str()
44        )
45    }
46}
47
48#[derive(Asset, Clone, TypePath, Default)]
49pub struct TerrainTileBgCompute {
50    tile_position: ChunkPos,
51    heightmap: Handle<Texture>,
52    splatmaps: Vec<Handle<Texture>>
53}
54impl RenderBinding for TerrainTileBgCompute {
55    type Params = ();
56
57    fn describe(
58        &mut self,
59        _params: &SystemParamItem<Self::Params>,
60        builder: &mut RenderBindingBuilder
61    ) {
62        builder.add_storage_texture_from_id(Some(self.heightmap.id()));
63        for i in 0..SPLAT_MAP_COUNT / 4 {
64            if i as usize >= self.splatmaps.len() {
65                continue;
66            }
67            builder.add_storage_texture_from_id(Some(self.splatmaps[i as usize].id()));
68        }
69    }
70
71    fn label(&self) -> &str {
72        Box::leak(
73            format!(
74                "terrain-tile-{}-{}-compute",
75                self.tile_position.x, self.tile_position.y
76            )
77            .into_boxed_str()
78        )
79    }
80}
81
82/// Same as `TerrainRenderTile`, but with the texture handles replaced by their corresponding asset IDs. Only present on the GPU side.
83#[derive(Default, Clone)]
84pub struct TerrainRenderTileGPU {
85    /// The position of the tile in world space (x, z)
86    pub position: ChunkPos,
87
88    /// The heightmap and splat maps for this tile
89    pub heightmap: Handle<Texture>,
90    pub splatmaps: Vec<Handle<Texture>>,
91
92    // The associated render and compute bind groups
93    pub render_bind_group: Option<Handle<TerrainTileBgRender>>,
94    pub compute_bind_group: Option<Handle<TerrainTileBgCompute>>
95}
96
97/// Holds the tiles used for rendering. Note that all tiles are not necessarily rendered.
98#[derive(Resource, Default)]
99pub struct TerrainRendererGPU {
100    /// True if the renderer has been initialized (i.e., the bind group of each tiles have been created and is ready for rendering)
101    pub ready: bool,
102    /// Mapping of the tile position (x, z) to its tile index in the tiles vector
103    pub pos_to_tile: HashMap<ChunkPos, usize>,
104    /// A list of terrain render tiles that make up the entire terrain.
105    pub tiles: Vec<TerrainRenderTileGPU>,
106    // List of tile maps that are dirty and need to be re-processed
107    pub dirty: Vec<Option<TerrainDirtyTile>>
108}
109impl TerrainRendererGPU {
110    // Extract the dirty tiles from the main world and move them to the GPU renderer resource.
111    pub fn extract_dirty(
112        main_terrain_extractor: &TerrainExtractor,
113        render_terrain_extractor: &mut TerrainExtractor,
114        terrain_renderer_query: Query<&TerrainRenderer>,
115        gpu_terrain_renderer: &mut TerrainRendererGPU
116    ) {
117        // Run extractor if needed
118        extractor::extract_dirty(main_terrain_extractor, render_terrain_extractor);
119
120        // Get the terrain renderer resource and the GPU terrain tiles resource
121        let terrain_renderer = match terrain_renderer_query.iter().next() {
122            Some(terrain) => terrain,
123            None => return
124        };
125
126        // If it is the first time the terrain is ready, extract the tiles data from the main world and move them to the GPU renderer resource
127        if gpu_terrain_renderer.tiles.is_empty() {
128            gpu_terrain_renderer.tiles = terrain_renderer
129                .tiles
130                .iter()
131                .map(|tile| TerrainRenderTileGPU {
132                    position: tile.position,
133                    heightmap: tile.heightmap.clone(),
134                    splatmaps: tile.splatmaps.to_vec(),
135                    render_bind_group: None,
136                    compute_bind_group: None
137                })
138                .collect();
139            gpu_terrain_renderer.pos_to_tile = terrain_renderer.pos_to_tile.clone();
140        }
141
142        // Extract the dirty tiles
143        for i in 0..terrain_renderer.dirty.len() {
144            if let Some(dirty_tile) = &terrain_renderer.dirty[i] {
145                gpu_terrain_renderer.dirty.push(Some(dirty_tile.clone()));
146            }
147        }
148    }
149
150    // Upload the dirty tiles to the GPU by updating the corresponding textures with the new data, and clear the dirty list.
151    pub fn upload_dirty(
152        mut gpu_terrain_renderer: ResMut<TerrainRendererGPU>,
153        textures: Res<RenderAssets<GpuTexture>>,
154        render_instance: Res<RenderInstance>
155    ) {
156        let render_instance = render_instance.0.read().unwrap();
157
158        // Process the dirty tiles by updating the corresponding textures with the new data
159        for i in 0..gpu_terrain_renderer.dirty.len() {
160            if let Some(dirty_tile) = gpu_terrain_renderer.dirty[i].take() {
161                let (pos, map_type, splat_map_index, tile_data) = dirty_tile;
162                let tile_index = match gpu_terrain_renderer.pos_to_tile.get(&pos) {
163                    Some(index) => *index,
164                    None => continue
165                };
166                let tile = &mut gpu_terrain_renderer.tiles[tile_index];
167                match map_type {
168                    0 => {
169                        // Heightmap
170                        let heightmap = match textures.get(&tile.heightmap) {
171                            Some(texture) => texture,
172                            None => continue
173                        };
174                        heightmap.texture.copy_from_buffer(
175                            &render_instance,
176                            heightmap.texture.format,
177                            bytemuck::cast_slice(&tile_data)
178                        );
179                    }
180                    1 => {
181                        // Splatmap
182                        let splatmap = match textures.get(&tile.splatmaps[splat_map_index as usize])
183                        {
184                            Some(texture) => texture,
185                            None => continue
186                        };
187                        splatmap.texture.copy_from_buffer(
188                            &render_instance,
189                            splatmap.texture.format,
190                            bytemuck::cast_slice(&tile_data)
191                        );
192                    }
193                    _ => unreachable!()
194                };
195            }
196        }
197    }
198
199    // Create the bind groups and layouts for tiles that are not ready
200    pub fn prepare_bind_groups(
201        asset_server: Res<AssetServer>,
202        mut gpu_terrain_renderer: ResMut<TerrainRendererGPU>
203    ) {
204        // Check if all tiles gpu data is read
205        if gpu_terrain_renderer.ready {
206            return;
207        }
208
209        // Create the bind groups for the dirty tiles
210        for tile in &mut gpu_terrain_renderer.tiles {
211            // Check if already processed
212            if tile.render_bind_group.is_some() {
213                continue;
214            }
215
216            // Create the bind group layout for the render tile
217            tile.render_bind_group = Some(asset_server.add(TerrainTileBgRender {
218                tile_position: tile.position,
219                heightmap: tile.heightmap.clone(),
220                splatmaps: tile.splatmaps.to_vec()
221            }));
222
223            // Create the bind group layout for the compute tile
224            tile.compute_bind_group = Some(asset_server.add(TerrainTileBgCompute {
225                tile_position: tile.position,
226                heightmap: tile.heightmap.clone(),
227                splatmaps: tile.splatmaps.to_vec()
228            }));
229        }
230
231        // Check if all tiles are ready
232        gpu_terrain_renderer.ready = gpu_terrain_renderer
233            .tiles
234            .iter()
235            .all(|tile| tile.render_bind_group.is_some());
236    }
237}