1#![allow(clippy::many_single_char_names)]
21use crate::gpu::batch::{
67 build_circle_batch, build_fill_batch, build_fill_extrusion_batch, build_fill_pattern_batch,
68 build_heatmap_batch, build_hillshade_batches, build_line_batch, build_line_pattern_batch,
69 build_placeholder_batches, build_symbol_batch, build_terrain_batches, build_tile_batches,
70 build_vector_batch, find_terrain_texture_actual, CircleBatchEntry, FillBatchEntry,
71 FillExtrusionBatchEntry, FillPatternBatchEntry, HeatmapBatchEntry, HillshadeBatch,
72 LineBatchEntry, LinePatternBatchEntry, SymbolBatchEntry, TerrainBatch, TilePageBatches,
73 VectorBatchEntry,
74};
75use crate::gpu::column_vertex::{ColumnInstanceData, ColumnVertex};
76use crate::gpu::depth::create_depth_texture;
77use crate::gpu::grid_extrusion_vertex::GridExtrusionVertex;
78use crate::gpu::grid_scalar_vertex::GridScalarVertex;
79use crate::gpu::image_overlay_vertex::ImageOverlayVertex;
80use crate::gpu::model_vertex::ModelVertex;
81use crate::gpu::terrain_buffers::TerrainInteractionBuffers;
82use crate::gpu::terrain_grid_vertex::TerrainGridVertex;
83use crate::gpu::tile_atlas::TileAtlas;
84use crate::painter::{PainterPass, PainterPlan};
85use crate::pipeline::circle_pipeline::CirclePipeline;
86use crate::pipeline::column_pipeline::ColumnPipeline;
87use crate::pipeline::fill_extrusion_pipeline::FillExtrusionPipeline;
88use crate::pipeline::fill_pattern_pipeline::FillPatternPipeline;
89use crate::pipeline::fill_pipeline::FillPipeline;
90use crate::pipeline::grid_extrusion_pipeline::GridExtrusionPipeline;
91use crate::pipeline::grid_scalar_pipeline::GridScalarPipeline;
92use crate::pipeline::heatmap_colormap_pipeline::HeatmapColormapPipeline;
93use crate::pipeline::heatmap_pipeline::HeatmapPipeline;
94use crate::pipeline::hillshade_pipeline::HillshadePipeline;
95use crate::pipeline::image_overlay_pipeline::ImageOverlayPipeline;
96use crate::pipeline::line_pattern_pipeline::LinePatternPipeline;
97use crate::pipeline::line_pipeline::LinePipeline;
98use crate::pipeline::model_pipeline::ModelPipeline;
99use crate::pipeline::sky_pipeline::{SkyPipeline, SkyUniform};
100use crate::pipeline::symbol_pipeline::SymbolPipeline;
101use crate::pipeline::terrain_data_pipeline::TerrainDataPipeline;
102use crate::pipeline::terrain_pipeline::TerrainPipeline;
103use crate::pipeline::tile_pipeline::TilePipeline;
104use crate::pipeline::uniforms::ViewProjUniform;
105use crate::pipeline::vector_pipeline::VectorPipeline;
106use glam::{DVec3, Mat4};
107use rustial_engine as rustial_math;
108use rustial_engine::TileId;
109use rustial_engine::{
110 materialize_terrain_mesh, DecodedImage, LayerId, MapState, ModelInstance, TerrainMeshData,
111 TileData, VectorMeshData, VectorRenderMode, VisibleTile, VisualizationOverlay,
112};
113use std::sync::Arc;
114use wgpu::util::DeviceExt;
115
116#[repr(C)]
117#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
118struct TerrainTileUniform {
119 geo_bounds: [f32; 4],
120 scene_origin: [f32; 4],
121 elev_params: [f32; 4],
122 elev_region: [f32; 4],
123}
124
125struct SharedTerrainGridMesh {
126 vertex_buffer: wgpu::Buffer,
127 index_buffer: wgpu::Buffer,
128 index_count: u32,
129}
130
131struct CachedHeightTexture {
132 generation: u64,
133 view: wgpu::TextureView,
134}
135
136#[repr(C)]
137#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
138struct GridScalarUniform {
139 origin_counts: [f32; 4],
140 grid_params: [f32; 4],
141 scene_origin: [f32; 4],
142 value_params: [f32; 4],
143 base_altitude: [f32; 4],
144}
145
146struct SharedColumnMesh {
147 vertex_buffer: wgpu::Buffer,
148 index_buffer: wgpu::Buffer,
149 index_count: u32,
150}
151
152struct CachedGridScalarOverlay {
153 vertex_buffer: wgpu::Buffer,
154 index_buffer: wgpu::Buffer,
155 index_count: u32,
156 vertex_count: usize,
157 #[allow(dead_code)]
158 uniform_buffer: wgpu::Buffer,
159 bind_group: wgpu::BindGroup,
160 #[allow(dead_code)]
161 scalar_texture: wgpu::Texture,
162 #[allow(dead_code)]
163 ramp_texture: wgpu::Texture,
164 generation: u64,
165 value_generation: u64,
166 ramp_fingerprint: u64,
167 grid_fingerprint: u64,
168 terrain_fingerprint: u64,
169 projection: rustial_engine::CameraProjection,
170 origin_key: [i64; 3],
171}
172
173struct CachedGridExtrusionOverlay {
174 vertex_buffer: wgpu::Buffer,
175 index_buffer: wgpu::Buffer,
176 index_count: u32,
177 vertex_count: usize,
178 generation: u64,
179 value_generation: u64,
180 origin_key: [i64; 3],
181 grid_fingerprint: u64,
182 params_fingerprint: u64,
183 ramp_fingerprint: u64,
184 terrain_fingerprint: u64,
185}
186
187struct CachedColumnOverlay {
188 instance_buffer: wgpu::Buffer,
189 instance_count: u32,
190 generation: u64,
191 origin_key: [i64; 3],
192 columns_fingerprint: u64,
193 ramp_fingerprint: u64,
194 instance_data: Vec<ColumnInstanceData>,
195}
196
197struct CachedPointCloudOverlay {
198 instance_buffer: wgpu::Buffer,
199 instance_count: u32,
200 generation: u64,
201 origin_key: [i64; 3],
202 points_fingerprint: u64,
203 ramp_fingerprint: u64,
204 instance_data: Vec<ColumnInstanceData>,
205}
206
207#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
209pub struct VisualizationPerfStats {
210 pub grid_scalar_rebuilds: u32,
212 pub grid_scalar_value_updates: u32,
214 pub grid_extrusion_rebuilds: u32,
216 pub grid_extrusion_value_updates: u32,
218 pub column_rebuilds: u32,
220 pub column_partial_writes: u32,
222 pub column_partial_write_ranges: u32,
224 pub point_cloud_rebuilds: u32,
226 pub point_cloud_partial_writes: u32,
228 pub point_cloud_partial_write_ranges: u32,
230}
231
232struct CachedTerrainTileBind {
239 #[allow(dead_code)]
240 uniform_buffer: wgpu::Buffer,
241 bind_group: wgpu::BindGroup,
242 origin_key: [i64; 3],
244 generation: u64,
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
250struct TerrainTileBindKey {
251 tile: TileId,
252 pipeline: TerrainPipelineKind,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
258enum TerrainPipelineKind {
259 Terrain,
260 TerrainData,
261 Hillshade,
262}
263
264struct TerrainDataDirtyState {
270 dirty: bool,
272 last_vp: [f32; 16],
275 last_terrain_fingerprint: u64,
277}
278
279impl Default for TerrainDataDirtyState {
280 fn default() -> Self {
281 Self {
282 dirty: true,
283 last_vp: [0.0; 16],
284 last_terrain_fingerprint: 0,
285 }
286 }
287}
288
289impl TerrainDataDirtyState {
290 fn needs_update(&self, vp: &glam::DMat4, terrain_meshes: &[TerrainMeshData]) -> bool {
292 if self.dirty {
293 return true;
294 }
295 let vp_f32 = vp.to_cols_array().map(|v| v as f32);
296 if vp_f32 != self.last_vp {
297 return true;
298 }
299 let fp = Self::terrain_fingerprint(terrain_meshes);
300 fp != self.last_terrain_fingerprint
301 }
302
303 fn mark_clean(&mut self, vp: &glam::DMat4, terrain_meshes: &[TerrainMeshData]) {
305 self.dirty = false;
306 self.last_vp = vp.to_cols_array().map(|v| v as f32);
307 self.last_terrain_fingerprint = Self::terrain_fingerprint(terrain_meshes);
308 }
309
310 fn terrain_fingerprint(terrain_meshes: &[TerrainMeshData]) -> u64 {
311 let mut h: u64 = terrain_meshes.len() as u64;
312 for mesh in terrain_meshes {
313 h = h
314 .wrapping_mul(31)
315 .wrapping_add(mesh.tile.zoom as u64)
316 .wrapping_mul(31)
317 .wrapping_add(mesh.tile.x as u64)
318 .wrapping_mul(31)
319 .wrapping_add(mesh.tile.y as u64)
320 .wrapping_mul(31)
321 .wrapping_add(mesh.generation);
322 }
323 h
324 }
325}
326
327#[allow(clippy::single_range_in_vec_init)]
328fn diff_column_instance_ranges(
329 old: &[ColumnInstanceData],
330 new: &[ColumnInstanceData],
331) -> Vec<std::ops::Range<usize>> {
332 if old.len() != new.len() {
333 return if new.is_empty() {
334 Vec::new()
335 } else {
336 vec![(0..new.len())]
337 };
338 }
339
340 let mut ranges = Vec::new();
341 let mut current_start: Option<usize> = None;
342
343 for (index, (old_item, new_item)) in old.iter().zip(new.iter()).enumerate() {
344 if old_item != new_item {
345 if current_start.is_none() {
346 current_start = Some(index);
347 }
348 } else if let Some(start) = current_start.take() {
349 ranges.push(start..index);
350 }
351 }
352
353 if let Some(start) = current_start {
354 ranges.push(start..new.len());
355 }
356
357 ranges
358}
359
360#[derive(Debug, Clone, PartialEq)]
364struct TileBatchCacheKey {
365 tiles: Vec<(TileId, TileId, u32)>,
372 origin: [i64; 3],
374 projection: rustial_engine::CameraProjection,
376}
377
378impl TileBatchCacheKey {
379 fn new(
380 visible_tiles: &[VisibleTile],
381 camera_origin: DVec3,
382 projection: rustial_engine::CameraProjection,
383 ) -> Self {
384 let tiles: Vec<(TileId, TileId, u32)> = visible_tiles
385 .iter()
386 .map(|vt| (vt.target, vt.actual, vt.fade_opacity.to_bits()))
387 .collect();
388 let origin = [
389 (camera_origin.x * 100.0) as i64,
390 (camera_origin.y * 100.0) as i64,
391 (camera_origin.z * 100.0) as i64,
392 ];
393 Self {
394 tiles,
395 origin,
396 projection,
397 }
398 }
399}
400
401#[derive(Debug, Clone, PartialEq)]
404struct VectorBatchCacheKey {
405 layers: Vec<(usize, usize)>,
407 origin: [i64; 3],
409}
410
411impl VectorBatchCacheKey {
412 fn new(vector_meshes: &[VectorMeshData], camera_origin: DVec3) -> Self {
413 let layers: Vec<(usize, usize)> = vector_meshes
414 .iter()
415 .map(|m| (m.positions.len(), m.indices.len()))
416 .collect();
417 let origin = [
418 (camera_origin.x * 100.0) as i64,
419 (camera_origin.y * 100.0) as i64,
420 (camera_origin.z * 100.0) as i64,
421 ];
422 Self { layers, origin }
423 }
424}
425
426pub struct RenderParams<'a> {
435 pub state: &'a MapState,
437 pub device: &'a wgpu::Device,
439 pub queue: &'a wgpu::Queue,
441 pub color_view: &'a wgpu::TextureView,
443 pub visible_tiles: &'a [VisibleTile],
445 pub vector_meshes: &'a [VectorMeshData],
447 pub model_instances: &'a [ModelInstance],
449 pub clear_color: [f32; 4],
453}
454
455pub struct WgpuMapRenderer {
489 tile_pipeline: TilePipeline,
491 terrain_pipeline: TerrainPipeline,
492 terrain_data_pipeline: TerrainDataPipeline,
493 hillshade_pipeline: HillshadePipeline,
494 grid_scalar_pipeline: GridScalarPipeline,
495 grid_extrusion_pipeline: GridExtrusionPipeline,
496 column_pipeline: ColumnPipeline,
497 vector_pipeline: VectorPipeline,
498 fill_pipeline: FillPipeline,
499 fill_pattern_pipeline: FillPatternPipeline,
500 fill_extrusion_pipeline: FillExtrusionPipeline,
501 line_pipeline: LinePipeline,
502 line_pattern_pipeline: LinePatternPipeline,
503 circle_pipeline: CirclePipeline,
504 heatmap_pipeline: HeatmapPipeline,
505 heatmap_colormap_pipeline: HeatmapColormapPipeline,
507 symbol_pipeline: SymbolPipeline,
508 model_pipeline: ModelPipeline,
509 image_overlay_pipeline: ImageOverlayPipeline,
510 sky_pipeline: SkyPipeline,
512
513 uniform_buffer: wgpu::Buffer,
518 uniform_bind_group: wgpu::BindGroup,
520 terrain_uniform_bind_group: wgpu::BindGroup,
522 terrain_data_uniform_bind_group: wgpu::BindGroup,
524 hillshade_uniform_bind_group: wgpu::BindGroup,
526 grid_scalar_uniform_bind_group: wgpu::BindGroup,
528 grid_extrusion_uniform_bind_group: wgpu::BindGroup,
530 column_uniform_bind_group: wgpu::BindGroup,
532 vector_uniform_bind_group: wgpu::BindGroup,
534 fill_extrusion_uniform_bind_group: wgpu::BindGroup,
536 model_uniform_bind_group: wgpu::BindGroup,
538 line_uniform_bind_group: wgpu::BindGroup,
540 circle_uniform_bind_group: wgpu::BindGroup,
542 heatmap_uniform_bind_group: wgpu::BindGroup,
544 heatmap_colormap_uniform_bind_group: wgpu::BindGroup,
546 heatmap_accum_texture: wgpu::Texture,
548 heatmap_accum_view: wgpu::TextureView,
550 _heatmap_ramp_texture: wgpu::Texture,
552 heatmap_ramp_view: wgpu::TextureView,
554 heatmap_colormap_textures_bind_group: wgpu::BindGroup,
556 symbol_uniform_bind_group: wgpu::BindGroup,
558 image_overlay_uniform_bind_group: wgpu::BindGroup,
560 sky_uniform_buffer: wgpu::Buffer,
562 sky_uniform_bind_group: wgpu::BindGroup,
564
565 shadow_fill_extrusion_pipeline: crate::pipeline::shadow_pipeline::ShadowFillExtrusionPipeline,
568 shadow_model_pipeline: crate::pipeline::shadow_pipeline::ShadowModelPipeline,
570 _shadow_map_textures: Vec<wgpu::Texture>,
572 shadow_map_views: Vec<wgpu::TextureView>,
574 _shadow_comparison_sampler: wgpu::Sampler,
576 shadow_params_buffer: wgpu::Buffer,
578 shadow_depth_uniform_buffers: Vec<wgpu::Buffer>,
580 shadow_depth_bind_groups: Vec<wgpu::BindGroup>,
582 shadow_receiver_bind_group: wgpu::BindGroup,
584
585 sampler: wgpu::Sampler,
589 grid_scalar_ramp_sampler: wgpu::Sampler,
591 fill_pattern_sampler: wgpu::Sampler,
593
594 depth_view: wgpu::TextureView,
597 width: u32,
599 height: u32,
601 terrain_interaction_buffers: TerrainInteractionBuffers,
603
604 tile_atlas: TileAtlas,
607 hillshade_atlas: TileAtlas,
609 page_bind_groups: Vec<wgpu::BindGroup>,
613 page_terrain_bind_groups: Vec<wgpu::BindGroup>,
615 page_hillshade_bind_groups: Vec<wgpu::BindGroup>,
617
618 model_mesh_cache: std::collections::HashMap<ModelMeshKey, CachedModelMesh>,
622 shared_terrain_grids: std::collections::HashMap<u16, SharedTerrainGridMesh>,
624 height_texture_cache: std::collections::HashMap<TileId, CachedHeightTexture>,
626 shared_column_mesh: Option<SharedColumnMesh>,
628 grid_scalar_overlay_cache: std::collections::HashMap<LayerId, CachedGridScalarOverlay>,
630 grid_extrusion_overlay_cache: std::collections::HashMap<LayerId, CachedGridExtrusionOverlay>,
632 column_overlay_cache: std::collections::HashMap<LayerId, CachedColumnOverlay>,
634 point_cloud_overlay_cache: std::collections::HashMap<LayerId, CachedPointCloudOverlay>,
636
637 cached_tile_batches: Vec<TilePageBatches>,
640 tile_batch_cache_key: Option<TileBatchCacheKey>,
642 cached_vector_batches: Vec<Option<VectorBatchEntry>>,
644 vector_batch_cache_key: Option<VectorBatchCacheKey>,
646 cached_fill_extrusion_batches: Vec<Option<FillExtrusionBatchEntry>>,
648 cached_fill_batches: Vec<Option<FillBatchEntry>>,
650 cached_fill_pattern_batches: Vec<Option<FillPatternBatchEntry>>,
652 cached_line_batches: Vec<Option<LineBatchEntry>>,
654 cached_line_pattern_batches: Vec<Option<LinePatternBatchEntry>>,
656 cached_circle_batches: Vec<Option<CircleBatchEntry>>,
658 cached_heatmap_batches: Vec<Option<HeatmapBatchEntry>>,
660 cached_symbol_batch: Option<SymbolBatchEntry>,
662 symbol_atlas_texture: Option<(wgpu::Texture, wgpu::TextureView)>,
664 symbol_atlas_bind_group: Option<wgpu::BindGroup>,
666 symbol_glyph_atlas: rustial_engine::symbols::GlyphAtlas,
668 symbol_glyph_provider: Box<dyn rustial_engine::symbols::GlyphProvider>,
670
671 terrain_tile_bind_cache: std::collections::HashMap<TerrainTileBindKey, CachedTerrainTileBind>,
674
675 terrain_data_dirty: TerrainDataDirtyState,
679
680 cached_model_transforms: Option<CachedModelTransforms>,
683 cached_placeholder_batch: Option<VectorBatchEntry>,
685 cached_image_overlay_batches: Vec<CachedImageOverlayBatch>,
687 visualization_perf_stats: VisualizationPerfStats,
689}
690
691#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
707struct ModelMeshKey {
708 pos_len: usize,
709 idx_len: usize,
710 fingerprint: u64,
711}
712
713impl ModelMeshKey {
714 fn from_mesh(mesh: &rustial_engine::ModelMesh) -> Self {
715 let mut fingerprint: u64 = mesh.positions.len() as u64;
716 if let Some(first) = mesh.positions.first() {
717 fingerprint = fingerprint
718 .wrapping_mul(31)
719 .wrapping_add(first[0].to_bits() as u64)
720 .wrapping_mul(31)
721 .wrapping_add(first[1].to_bits() as u64)
722 .wrapping_mul(31)
723 .wrapping_add(first[2].to_bits() as u64);
724 }
725 if let Some(&first_idx) = mesh.indices.first() {
726 fingerprint = fingerprint.wrapping_mul(31).wrapping_add(first_idx as u64);
727 }
728 Self {
729 pos_len: mesh.positions.len(),
730 idx_len: mesh.indices.len(),
731 fingerprint,
732 }
733 }
734}
735
736struct CachedModelMesh {
738 vertex_buffer: wgpu::Buffer,
739 index_buffer: wgpu::Buffer,
740 index_count: u32,
741}
742
743struct CachedModelTransforms {
748 #[allow(dead_code)]
749 buffer: wgpu::Buffer,
750 bind_group: wgpu::BindGroup,
751 stride: usize,
753 instance_count: usize,
755 fingerprint: u64,
758}
759
760struct CachedImageOverlayBatch {
762 vertex_buffer: wgpu::Buffer,
763 index_buffer: wgpu::Buffer,
764 texture: wgpu::Texture,
765 #[allow(dead_code)]
766 texture_view: wgpu::TextureView,
767 texture_bind_group: wgpu::BindGroup,
768 layer_id: rustial_engine::LayerId,
770 tex_dimensions: (u32, u32),
772 data_arc_ptr: usize,
774}
775
776impl WgpuMapRenderer {
781 pub fn new(
791 device: &wgpu::Device,
792 _queue: &wgpu::Queue,
793 format: wgpu::TextureFormat,
794 width: u32,
795 height: u32,
796 ) -> Self {
797 let tile_pipeline = TilePipeline::new(device, format);
798 let terrain_pipeline =
799 TerrainPipeline::new(device, format, &tile_pipeline.uniform_bind_group_layout);
800 let terrain_data_pipeline = TerrainDataPipeline::new(device);
801 let hillshade_pipeline = HillshadePipeline::new(device, format);
802 let grid_scalar_pipeline = GridScalarPipeline::new(device, format);
803 let grid_extrusion_pipeline = GridExtrusionPipeline::new(device, format);
804 let column_pipeline = ColumnPipeline::new(device, format);
805 let vector_pipeline = VectorPipeline::new(device, format);
806 let fill_pipeline = FillPipeline::new(device, format);
807 let fill_pattern_pipeline = FillPatternPipeline::new(device, format);
808 let fill_extrusion_pipeline = FillExtrusionPipeline::new(device, format);
809 let line_pipeline = LinePipeline::new(device, format);
810 let line_pattern_pipeline = LinePatternPipeline::new(device, format);
811 let circle_pipeline = CirclePipeline::new(device, format);
812 let heatmap_pipeline = HeatmapPipeline::new(device);
813 let heatmap_colormap_pipeline = HeatmapColormapPipeline::new(device, format);
814 let symbol_pipeline = SymbolPipeline::new(device, format);
815 let model_pipeline = ModelPipeline::new(device, format);
816 let image_overlay_pipeline = ImageOverlayPipeline::new(device, format);
817 let sky_pipeline = SkyPipeline::new(device, format);
818
819 let uniform_data = ViewProjUniform::from_dmat4(&glam::DMat4::IDENTITY);
821 let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
822 label: Some("rustial_uniform_buf"),
823 contents: bytemuck::bytes_of(&uniform_data),
824 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
825 });
826
827 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
832 label: Some("rustial_uniform_bg"),
833 layout: &tile_pipeline.uniform_bind_group_layout,
834 entries: &[wgpu::BindGroupEntry {
835 binding: 0,
836 resource: uniform_buffer.as_entire_binding(),
837 }],
838 });
839
840 let column_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
841 label: Some("rustial_column_uniform_bg"),
842 layout: &column_pipeline.uniform_bind_group_layout,
843 entries: &[wgpu::BindGroupEntry {
844 binding: 0,
845 resource: uniform_buffer.as_entire_binding(),
846 }],
847 });
848
849 let grid_extrusion_uniform_bind_group =
850 device.create_bind_group(&wgpu::BindGroupDescriptor {
851 label: Some("rustial_grid_extrusion_uniform_bg"),
852 layout: &grid_extrusion_pipeline.uniform_bind_group_layout,
853 entries: &[wgpu::BindGroupEntry {
854 binding: 0,
855 resource: uniform_buffer.as_entire_binding(),
856 }],
857 });
858
859 let terrain_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
860 label: Some("rustial_terrain_uniform_bg"),
861 layout: &terrain_pipeline.uniform_bind_group_layout,
862 entries: &[wgpu::BindGroupEntry {
863 binding: 0,
864 resource: uniform_buffer.as_entire_binding(),
865 }],
866 });
867
868 let hillshade_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
869 label: Some("rustial_hillshade_uniform_bg"),
870 layout: &hillshade_pipeline.uniform_bind_group_layout,
871 entries: &[wgpu::BindGroupEntry {
872 binding: 0,
873 resource: uniform_buffer.as_entire_binding(),
874 }],
875 });
876
877 let grid_scalar_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
878 label: Some("rustial_grid_scalar_uniform_bg"),
879 layout: &grid_scalar_pipeline.uniform_bind_group_layout,
880 entries: &[wgpu::BindGroupEntry {
881 binding: 0,
882 resource: uniform_buffer.as_entire_binding(),
883 }],
884 });
885
886 let vector_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
887 label: Some("rustial_vector_uniform_bg"),
888 layout: &vector_pipeline.uniform_bind_group_layout,
889 entries: &[wgpu::BindGroupEntry {
890 binding: 0,
891 resource: uniform_buffer.as_entire_binding(),
892 }],
893 });
894
895 let fill_extrusion_uniform_bind_group =
896 device.create_bind_group(&wgpu::BindGroupDescriptor {
897 label: Some("rustial_fill_extrusion_uniform_bg"),
898 layout: &fill_extrusion_pipeline.uniform_bind_group_layout,
899 entries: &[wgpu::BindGroupEntry {
900 binding: 0,
901 resource: uniform_buffer.as_entire_binding(),
902 }],
903 });
904
905 let model_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
906 label: Some("rustial_model_uniform_bg"),
907 layout: &model_pipeline.uniform_bind_group_layout,
908 entries: &[wgpu::BindGroupEntry {
909 binding: 0,
910 resource: uniform_buffer.as_entire_binding(),
911 }],
912 });
913
914 let line_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
915 label: Some("rustial_line_uniform_bg"),
916 layout: &line_pipeline.uniform_bind_group_layout,
917 entries: &[wgpu::BindGroupEntry {
918 binding: 0,
919 resource: uniform_buffer.as_entire_binding(),
920 }],
921 });
922
923 let circle_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
924 label: Some("rustial_circle_uniform_bg"),
925 layout: &circle_pipeline.uniform_bind_group_layout,
926 entries: &[wgpu::BindGroupEntry {
927 binding: 0,
928 resource: uniform_buffer.as_entire_binding(),
929 }],
930 });
931
932 let heatmap_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
933 label: Some("rustial_heatmap_uniform_bg"),
934 layout: &heatmap_pipeline.uniform_bind_group_layout,
935 entries: &[wgpu::BindGroupEntry {
936 binding: 0,
937 resource: uniform_buffer.as_entire_binding(),
938 }],
939 });
940
941 let heatmap_colormap_uniform_bind_group =
942 device.create_bind_group(&wgpu::BindGroupDescriptor {
943 label: Some("rustial_heatmap_colormap_uniform_bg"),
944 layout: &heatmap_colormap_pipeline.uniform_bind_group_layout,
945 entries: &[wgpu::BindGroupEntry {
946 binding: 0,
947 resource: uniform_buffer.as_entire_binding(),
948 }],
949 });
950
951 let symbol_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
952 label: Some("rustial_symbol_uniform_bg"),
953 layout: &symbol_pipeline.uniform_bind_group_layout,
954 entries: &[wgpu::BindGroupEntry {
955 binding: 0,
956 resource: uniform_buffer.as_entire_binding(),
957 }],
958 });
959
960 let image_overlay_uniform_bind_group =
961 device.create_bind_group(&wgpu::BindGroupDescriptor {
962 label: Some("rustial_image_overlay_uniform_bg"),
963 layout: &image_overlay_pipeline.uniform_bind_group_layout,
964 entries: &[wgpu::BindGroupEntry {
965 binding: 0,
966 resource: uniform_buffer.as_entire_binding(),
967 }],
968 });
969
970 let sky_uniform_data = SkyUniform::default();
972 let sky_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
973 label: Some("rustial_sky_uniform_buf"),
974 contents: bytemuck::bytes_of(&sky_uniform_data),
975 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
976 });
977 let sky_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
978 label: Some("rustial_sky_uniform_bg"),
979 layout: &sky_pipeline.uniform_bind_group_layout,
980 entries: &[wgpu::BindGroupEntry {
981 binding: 0,
982 resource: sky_uniform_buffer.as_entire_binding(),
983 }],
984 });
985
986 let terrain_data_uniform_bind_group =
987 device.create_bind_group(&wgpu::BindGroupDescriptor {
988 label: Some("rustial_terrain_data_uniform_bg"),
989 layout: &terrain_data_pipeline.uniform_bind_group_layout,
990 entries: &[wgpu::BindGroupEntry {
991 binding: 0,
992 resource: uniform_buffer.as_entire_binding(),
993 }],
994 });
995
996 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
997 label: Some("rustial_sampler"),
998 address_mode_u: wgpu::AddressMode::ClampToEdge,
999 address_mode_v: wgpu::AddressMode::ClampToEdge,
1000 mag_filter: wgpu::FilterMode::Linear,
1001 min_filter: wgpu::FilterMode::Linear,
1002 mipmap_filter: wgpu::FilterMode::Linear,
1003 anisotropy_clamp: 16,
1004 ..Default::default()
1005 });
1006
1007 let grid_scalar_ramp_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1008 label: Some("rustial_grid_scalar_ramp_sampler"),
1009 address_mode_u: wgpu::AddressMode::ClampToEdge,
1010 address_mode_v: wgpu::AddressMode::ClampToEdge,
1011 mag_filter: wgpu::FilterMode::Linear,
1012 min_filter: wgpu::FilterMode::Linear,
1013 mipmap_filter: wgpu::FilterMode::Nearest,
1014 ..Default::default()
1015 });
1016
1017 let fill_pattern_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1018 label: Some("rustial_fill_pattern_sampler"),
1019 address_mode_u: wgpu::AddressMode::Repeat,
1020 address_mode_v: wgpu::AddressMode::Repeat,
1021 mag_filter: wgpu::FilterMode::Linear,
1022 min_filter: wgpu::FilterMode::Linear,
1023 mipmap_filter: wgpu::FilterMode::Linear,
1024 ..Default::default()
1025 });
1026
1027 let w = width.max(1);
1029 let h = height.max(1);
1030 let depth_view = create_depth_texture(device, w, h);
1031 let terrain_interaction_buffers = TerrainInteractionBuffers::new(device, w, h);
1032
1033 let (heatmap_accum_texture, heatmap_accum_view) =
1035 create_heatmap_accum_texture(device, w, h);
1036 let heatmap_ramp_texture = create_default_heatmap_ramp_texture(device, _queue);
1037 let heatmap_ramp_view =
1038 heatmap_ramp_texture.create_view(&wgpu::TextureViewDescriptor::default());
1039 let heatmap_colormap_textures_bind_group = create_heatmap_colormap_bind_group(
1040 device,
1041 &heatmap_colormap_pipeline.textures_bind_group_layout,
1042 &heatmap_accum_view,
1043 &heatmap_ramp_view,
1044 &sampler,
1045 );
1046
1047 let shadow_fill_extrusion_pipeline =
1049 crate::pipeline::shadow_pipeline::ShadowFillExtrusionPipeline::new(device);
1050 let shadow_model_pipeline =
1051 crate::pipeline::shadow_pipeline::ShadowModelPipeline::new(device);
1052
1053 let shadow_map_resolution = 2048u32;
1054 let (shadow_map_textures, shadow_map_views) =
1055 create_shadow_map_textures(device, shadow_map_resolution, 2);
1056
1057 let shadow_comparison_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1058 label: Some("rustial_shadow_comparison_sampler"),
1059 compare: Some(wgpu::CompareFunction::LessEqual),
1060 mag_filter: wgpu::FilterMode::Linear,
1061 min_filter: wgpu::FilterMode::Linear,
1062 ..Default::default()
1063 });
1064
1065 let shadow_params_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1066 label: Some("rustial_shadow_params_buffer"),
1067 size: 160, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1069 mapped_at_creation: false,
1070 });
1071
1072 let shadow_depth_uniform_buffers: Vec<wgpu::Buffer> = (0..2)
1073 .map(|i| {
1074 device.create_buffer(&wgpu::BufferDescriptor {
1075 label: Some(&format!("rustial_shadow_depth_uniform_{i}")),
1076 size: 64, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1078 mapped_at_creation: false,
1079 })
1080 })
1081 .collect();
1082
1083 let shadow_depth_bind_groups: Vec<wgpu::BindGroup> = shadow_depth_uniform_buffers
1084 .iter()
1085 .enumerate()
1086 .map(|(i, buf)| {
1087 device.create_bind_group(&wgpu::BindGroupDescriptor {
1088 label: Some(&format!("rustial_shadow_depth_bg_{i}")),
1089 layout: &shadow_fill_extrusion_pipeline.uniform_bind_group_layout,
1090 entries: &[wgpu::BindGroupEntry {
1091 binding: 0,
1092 resource: buf.as_entire_binding(),
1093 }],
1094 })
1095 })
1096 .collect();
1097
1098 let shadow_receiver_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1099 label: Some("rustial_shadow_receiver_bg"),
1100 layout: &fill_extrusion_pipeline.shadow_bind_group_layout,
1101 entries: &[
1102 wgpu::BindGroupEntry {
1103 binding: 0,
1104 resource: shadow_params_buffer.as_entire_binding(),
1105 },
1106 wgpu::BindGroupEntry {
1107 binding: 1,
1108 resource: wgpu::BindingResource::TextureView(&shadow_map_views[0]),
1109 },
1110 wgpu::BindGroupEntry {
1111 binding: 2,
1112 resource: wgpu::BindingResource::TextureView(&shadow_map_views[1]),
1113 },
1114 wgpu::BindGroupEntry {
1115 binding: 3,
1116 resource: wgpu::BindingResource::Sampler(&shadow_comparison_sampler),
1117 },
1118 ],
1119 });
1120
1121 Self {
1122 tile_pipeline,
1123 terrain_pipeline,
1124 terrain_data_pipeline,
1125 hillshade_pipeline,
1126 grid_scalar_pipeline,
1127 grid_extrusion_pipeline,
1128 column_pipeline,
1129 vector_pipeline,
1130 fill_pipeline,
1131 fill_pattern_pipeline,
1132 fill_extrusion_pipeline,
1133 line_pipeline,
1134 line_pattern_pipeline,
1135 circle_pipeline,
1136 heatmap_pipeline,
1137 heatmap_colormap_pipeline,
1138 symbol_pipeline,
1139 model_pipeline,
1140 image_overlay_pipeline,
1141 sky_pipeline,
1142 uniform_buffer,
1143 uniform_bind_group,
1144 terrain_uniform_bind_group,
1145 terrain_data_uniform_bind_group,
1146 hillshade_uniform_bind_group,
1147 grid_scalar_uniform_bind_group,
1148 grid_extrusion_uniform_bind_group,
1149 column_uniform_bind_group,
1150 vector_uniform_bind_group,
1151 fill_extrusion_uniform_bind_group,
1152 model_uniform_bind_group,
1153 line_uniform_bind_group,
1154 circle_uniform_bind_group,
1155 heatmap_uniform_bind_group,
1156 heatmap_colormap_uniform_bind_group,
1157 heatmap_accum_texture,
1158 heatmap_accum_view,
1159 _heatmap_ramp_texture: heatmap_ramp_texture,
1160 heatmap_ramp_view,
1161 heatmap_colormap_textures_bind_group,
1162 symbol_uniform_bind_group,
1163 image_overlay_uniform_bind_group,
1164 sky_uniform_buffer,
1165 sky_uniform_bind_group,
1166 shadow_fill_extrusion_pipeline,
1167 shadow_model_pipeline,
1168 _shadow_map_textures: shadow_map_textures,
1169 shadow_map_views,
1170 _shadow_comparison_sampler: shadow_comparison_sampler,
1171 shadow_params_buffer,
1172 shadow_depth_uniform_buffers,
1173 shadow_depth_bind_groups,
1174 shadow_receiver_bind_group,
1175 sampler,
1176 grid_scalar_ramp_sampler,
1177 fill_pattern_sampler,
1178 depth_view,
1179 width: w,
1180 height: h,
1181 terrain_interaction_buffers,
1182 tile_atlas: TileAtlas::new(),
1183 hillshade_atlas: TileAtlas::new(),
1184 page_bind_groups: Vec::new(),
1185 page_terrain_bind_groups: Vec::new(),
1186 page_hillshade_bind_groups: Vec::new(),
1187 model_mesh_cache: std::collections::HashMap::new(),
1188 shared_terrain_grids: std::collections::HashMap::new(),
1189 height_texture_cache: std::collections::HashMap::new(),
1190 shared_column_mesh: None,
1191 grid_scalar_overlay_cache: std::collections::HashMap::new(),
1192 grid_extrusion_overlay_cache: std::collections::HashMap::new(),
1193 column_overlay_cache: std::collections::HashMap::new(),
1194 point_cloud_overlay_cache: std::collections::HashMap::new(),
1195 cached_tile_batches: Vec::new(),
1196 tile_batch_cache_key: None,
1197 cached_vector_batches: Vec::new(),
1198 vector_batch_cache_key: None,
1199 cached_fill_extrusion_batches: Vec::new(),
1200 cached_fill_batches: Vec::new(),
1201 cached_fill_pattern_batches: Vec::new(),
1202 cached_line_batches: Vec::new(),
1203 cached_line_pattern_batches: Vec::new(),
1204 cached_circle_batches: Vec::new(),
1205 cached_heatmap_batches: Vec::new(),
1206 cached_symbol_batch: None,
1207 symbol_atlas_texture: None,
1208 symbol_atlas_bind_group: None,
1209 symbol_glyph_atlas: rustial_engine::symbols::GlyphAtlas::new(),
1210 symbol_glyph_provider: Box::new(rustial_engine::symbols::ProceduralGlyphProvider::new()),
1211 terrain_tile_bind_cache: std::collections::HashMap::new(),
1212 terrain_data_dirty: TerrainDataDirtyState::default(),
1213 cached_model_transforms: None,
1214 cached_placeholder_batch: None,
1215 cached_image_overlay_batches: Vec::new(),
1216 visualization_perf_stats: VisualizationPerfStats::default(),
1217 }
1218 }
1219
1220 pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
1227 self.width = width.max(1);
1228 self.height = height.max(1);
1229 self.depth_view = create_depth_texture(device, self.width, self.height);
1230 self.terrain_interaction_buffers
1231 .resize(device, self.width, self.height);
1232 self.terrain_data_dirty.dirty = true;
1233
1234 let (tex, view) = create_heatmap_accum_texture(device, self.width, self.height);
1237 self.heatmap_accum_texture = tex;
1238 self.heatmap_accum_view = view;
1239 self.heatmap_colormap_textures_bind_group = create_heatmap_colormap_bind_group(
1240 device,
1241 &self.heatmap_colormap_pipeline.textures_bind_group_layout,
1242 &self.heatmap_accum_view,
1243 &self.heatmap_ramp_view,
1244 &self.sampler,
1245 );
1246 }
1247
1248 pub fn set_glyph_provider(
1256 &mut self,
1257 provider: Box<dyn rustial_engine::symbols::GlyphProvider>,
1258 ) {
1259 self.symbol_glyph_provider = provider;
1260 }
1261
1262 pub fn upload_tile(&mut self, device: &wgpu::Device, tile_id: TileId, image: &DecodedImage) {
1272 if self.tile_atlas.contains(&tile_id) {
1273 return;
1274 }
1275
1276 if let Err(err) = image.validate_rgba8() {
1277 log::warn!(
1278 "wgpu upload_tile: skipping invalid tile {:?}: {}",
1279 tile_id,
1280 err
1281 );
1282 return;
1283 }
1284
1285 self.tile_atlas.insert(device, tile_id, image);
1286 self.tile_batch_cache_key = None;
1287
1288 self.rebuild_page_bind_groups(device);
1290 }
1291
1292 pub fn upload_hillshade(
1294 &mut self,
1295 device: &wgpu::Device,
1296 tile_id: TileId,
1297 image: &DecodedImage,
1298 ) {
1299 if self.hillshade_atlas.contains(&tile_id) {
1300 return;
1301 }
1302 if let Err(err) = image.validate_rgba8() {
1303 log::warn!(
1304 "wgpu upload_hillshade: skipping invalid tile {:?}: {}",
1305 tile_id,
1306 err
1307 );
1308 return;
1309 }
1310 self.hillshade_atlas.insert(device, tile_id, image);
1311 self.rebuild_page_bind_groups(device);
1312 }
1313
1314 pub fn flush_atlas_uploads(&mut self, queue: &wgpu::Queue) {
1322 self.tile_atlas.flush_uploads(queue);
1323 self.hillshade_atlas.flush_uploads(queue);
1324 }
1325
1326 fn get_or_create_shared_column_mesh(&mut self, device: &wgpu::Device) -> &SharedColumnMesh {
1327 if self.shared_column_mesh.is_none() {
1328 let (vertices, indices) = build_unit_column_mesh();
1329 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1330 label: Some("column_unit_box_vb"),
1331 contents: bytemuck::cast_slice(&vertices),
1332 usage: wgpu::BufferUsages::VERTEX,
1333 });
1334 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1335 label: Some("column_unit_box_ib"),
1336 contents: bytemuck::cast_slice(&indices),
1337 usage: wgpu::BufferUsages::INDEX,
1338 });
1339 self.shared_column_mesh = Some(SharedColumnMesh {
1340 vertex_buffer,
1341 index_buffer,
1342 index_count: indices.len() as u32,
1343 });
1344 }
1345 self.shared_column_mesh.as_ref().expect("column mesh")
1346 }
1347
1348 fn get_or_create_grid_scalar_overlay(
1349 &mut self,
1350 device: &wgpu::Device,
1351 queue: &wgpu::Queue,
1352 overlay: &VisualizationOverlay,
1353 state: &MapState,
1354 scene_origin: DVec3,
1355 terrain_fingerprint: u64,
1356 ) -> Option<()> {
1357 let VisualizationOverlay::GridScalar {
1358 layer_id,
1359 grid,
1360 field,
1361 ramp,
1362 } = overlay
1363 else {
1364 return None;
1365 };
1366
1367 let origin_key = [
1368 (scene_origin.x * 100.0) as i64,
1369 (scene_origin.y * 100.0) as i64,
1370 (scene_origin.z * 100.0) as i64,
1371 ];
1372 let ramp_fingerprint = grid_scalar_ramp_fingerprint(ramp);
1373 let grid_fingerprint = grid_extrusion_grid_fingerprint(grid);
1374 let projection = state.camera().projection();
1375 let (vertices, indices) = build_grid_scalar_geometry(grid, state, scene_origin);
1376
1377 let recreate = if let Some(cached) = self.grid_scalar_overlay_cache.get(layer_id) {
1378 cached.generation != field.generation
1379 || cached.ramp_fingerprint != ramp_fingerprint
1380 || cached.grid_fingerprint != grid_fingerprint
1381 || cached.projection != projection
1382 || cached.index_count as usize != indices.len()
1383 || cached.vertex_count != vertices.len()
1384 } else {
1385 true
1386 };
1387
1388 if recreate {
1389 self.visualization_perf_stats.grid_scalar_rebuilds += 1;
1390 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1391 label: Some(&format!("grid_scalar_vb_{layer_id}")),
1392 contents: bytemuck::cast_slice(&vertices),
1393 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1394 });
1395 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1396 label: Some(&format!("grid_scalar_ib_{layer_id}")),
1397 contents: bytemuck::cast_slice(&indices),
1398 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1399 });
1400 let scalar_texture = create_grid_scalar_texture(device, queue, field);
1401 let scalar_view = scalar_texture.create_view(&wgpu::TextureViewDescriptor::default());
1402 let ramp_texture = create_grid_scalar_ramp_texture(device, queue, ramp);
1403 let ramp_view = ramp_texture.create_view(&wgpu::TextureViewDescriptor::default());
1404 let uniform = build_grid_scalar_uniform(grid, field, state, scene_origin, 1.0);
1405 let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1406 label: Some(&format!("grid_scalar_uniform_{layer_id}")),
1407 contents: bytemuck::bytes_of(&uniform),
1408 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1409 });
1410 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1411 label: Some(&format!("grid_scalar_bg_{layer_id}")),
1412 layout: &self.grid_scalar_pipeline.overlay_bind_group_layout,
1413 entries: &[
1414 wgpu::BindGroupEntry {
1415 binding: 0,
1416 resource: uniform_buffer.as_entire_binding(),
1417 },
1418 wgpu::BindGroupEntry {
1419 binding: 1,
1420 resource: wgpu::BindingResource::TextureView(&scalar_view),
1421 },
1422 wgpu::BindGroupEntry {
1423 binding: 2,
1424 resource: wgpu::BindingResource::TextureView(&ramp_view),
1425 },
1426 wgpu::BindGroupEntry {
1427 binding: 3,
1428 resource: wgpu::BindingResource::Sampler(&self.grid_scalar_ramp_sampler),
1429 },
1430 ],
1431 });
1432 self.grid_scalar_overlay_cache.insert(
1433 *layer_id,
1434 CachedGridScalarOverlay {
1435 vertex_buffer,
1436 index_buffer,
1437 index_count: indices.len() as u32,
1438 vertex_count: vertices.len(),
1439 uniform_buffer,
1440 bind_group,
1441 scalar_texture,
1442 ramp_texture,
1443 generation: field.generation,
1444 value_generation: field.value_generation,
1445 ramp_fingerprint,
1446 grid_fingerprint,
1447 terrain_fingerprint,
1448 projection,
1449 origin_key,
1450 },
1451 );
1452 return Some(());
1453 }
1454
1455 if let Some(cached) = self.grid_scalar_overlay_cache.get_mut(layer_id) {
1456 let uniform = build_grid_scalar_uniform(grid, field, state, scene_origin, 1.0);
1457 if cached.value_generation != field.value_generation {
1458 self.visualization_perf_stats.grid_scalar_value_updates += 1;
1459 write_grid_scalar_texture(queue, &cached.scalar_texture, field);
1460 cached.value_generation = field.value_generation;
1461 queue.write_buffer(&cached.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
1462 }
1463 if cached.origin_key != origin_key || cached.terrain_fingerprint != terrain_fingerprint
1464 {
1465 queue.write_buffer(
1466 &cached.vertex_buffer,
1467 0,
1468 bytemuck::cast_slice::<GridScalarVertex, u8>(&vertices),
1469 );
1470 queue.write_buffer(&cached.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
1471 cached.origin_key = origin_key;
1472 cached.terrain_fingerprint = terrain_fingerprint;
1473 }
1474 }
1475
1476 Some(())
1477 }
1478
1479 fn get_or_create_point_cloud_overlay(
1480 &mut self,
1481 device: &wgpu::Device,
1482 queue: &wgpu::Queue,
1483 overlay: &VisualizationOverlay,
1484 state: &MapState,
1485 scene_origin: DVec3,
1486 ) -> Option<()> {
1487 let VisualizationOverlay::Points {
1488 layer_id,
1489 points,
1490 ramp,
1491 } = overlay
1492 else {
1493 return None;
1494 };
1495
1496 let origin_key = [
1497 (scene_origin.x * 100.0) as i64,
1498 (scene_origin.y * 100.0) as i64,
1499 (scene_origin.z * 100.0) as i64,
1500 ];
1501 let points_fingerprint = point_set_fingerprint(points);
1502 let ramp_fingerprint = grid_scalar_ramp_fingerprint(ramp);
1503 let instances = build_point_instances(points, ramp, state, scene_origin);
1504
1505 let needs_rebuild = if let Some(cached) = self.point_cloud_overlay_cache.get(layer_id) {
1506 cached.generation != points.generation
1507 || cached.ramp_fingerprint != ramp_fingerprint
1508 || cached.instance_count as usize != instances.len()
1509 } else {
1510 true
1511 };
1512
1513 if needs_rebuild {
1514 self.visualization_perf_stats.point_cloud_rebuilds += 1;
1515 let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1516 label: Some(&format!("point_cloud_instances_{layer_id}")),
1517 contents: bytemuck::cast_slice(&instances),
1518 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1519 });
1520 self.point_cloud_overlay_cache.insert(
1521 *layer_id,
1522 CachedPointCloudOverlay {
1523 instance_buffer,
1524 instance_count: instances.len() as u32,
1525 generation: points.generation,
1526 origin_key,
1527 points_fingerprint,
1528 ramp_fingerprint,
1529 instance_data: instances,
1530 },
1531 );
1532 return Some(());
1533 }
1534
1535 if let Some(cached) = self.point_cloud_overlay_cache.get_mut(layer_id) {
1536 let ranges = diff_column_instance_ranges(&cached.instance_data, &instances);
1537 if !ranges.is_empty() {
1538 self.visualization_perf_stats.point_cloud_partial_writes += 1;
1539 self.visualization_perf_stats
1540 .point_cloud_partial_write_ranges += ranges.len() as u32;
1541 }
1542 for range in ranges {
1543 let start = range.start;
1544 let end = range.end;
1545 let byte_offset =
1546 (start * std::mem::size_of::<ColumnInstanceData>()) as wgpu::BufferAddress;
1547 queue.write_buffer(
1548 &cached.instance_buffer,
1549 byte_offset,
1550 bytemuck::cast_slice::<ColumnInstanceData, u8>(&instances[start..end]),
1551 );
1552 }
1553 cached.instance_data = instances;
1554 cached.origin_key = origin_key;
1555 cached.points_fingerprint = points_fingerprint;
1556 }
1557
1558 Some(())
1559 }
1560
1561 fn get_or_create_grid_extrusion_overlay(
1562 &mut self,
1563 device: &wgpu::Device,
1564 queue: &wgpu::Queue,
1565 overlay: &VisualizationOverlay,
1566 state: &MapState,
1567 scene_origin: DVec3,
1568 terrain_fingerprint: u64,
1569 ) -> Option<()> {
1570 let VisualizationOverlay::GridExtrusion {
1571 layer_id,
1572 grid,
1573 field,
1574 ramp,
1575 params,
1576 } = overlay
1577 else {
1578 return None;
1579 };
1580
1581 let origin_key = [
1582 (scene_origin.x * 100.0) as i64,
1583 (scene_origin.y * 100.0) as i64,
1584 (scene_origin.z * 100.0) as i64,
1585 ];
1586 let grid_fingerprint = grid_extrusion_grid_fingerprint(grid);
1587 let params_fingerprint = grid_extrusion_params_fingerprint(params);
1588 let ramp_fingerprint = grid_scalar_ramp_fingerprint(ramp);
1589
1590 let (vertices, indices) =
1591 build_grid_extrusion_geometry(grid, field, ramp, params, state, scene_origin);
1592
1593 let needs_rebuild = if let Some(cached) = self.grid_extrusion_overlay_cache.get(layer_id) {
1594 cached.generation != field.generation
1595 || cached.grid_fingerprint != grid_fingerprint
1596 || cached.params_fingerprint != params_fingerprint
1597 || cached.ramp_fingerprint != ramp_fingerprint
1598 || cached.terrain_fingerprint != terrain_fingerprint
1599 || cached.index_count as usize != indices.len()
1600 || cached.vertex_count != vertices.len()
1601 } else {
1602 true
1603 };
1604
1605 if needs_rebuild {
1606 self.visualization_perf_stats.grid_extrusion_rebuilds += 1;
1607 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1608 label: Some(&format!("grid_extrusion_vb_{layer_id}")),
1609 contents: bytemuck::cast_slice(&vertices),
1610 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1611 });
1612 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1613 label: Some(&format!("grid_extrusion_ib_{layer_id}")),
1614 contents: bytemuck::cast_slice(&indices),
1615 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1616 });
1617
1618 self.grid_extrusion_overlay_cache.insert(
1619 *layer_id,
1620 CachedGridExtrusionOverlay {
1621 vertex_buffer,
1622 index_buffer,
1623 index_count: indices.len() as u32,
1624 vertex_count: vertices.len(),
1625 generation: field.generation,
1626 value_generation: field.value_generation,
1627 origin_key,
1628 grid_fingerprint,
1629 params_fingerprint,
1630 ramp_fingerprint,
1631 terrain_fingerprint,
1632 },
1633 );
1634 return Some(());
1635 }
1636
1637 if let Some(cached) = self.grid_extrusion_overlay_cache.get_mut(layer_id) {
1638 if cached.value_generation != field.value_generation || cached.origin_key != origin_key
1639 {
1640 self.visualization_perf_stats.grid_extrusion_value_updates += 1;
1641 let vertex_bytes = bytemuck::cast_slice::<GridExtrusionVertex, u8>(&vertices);
1642 queue.write_buffer(&cached.vertex_buffer, 0, vertex_bytes);
1643 }
1644 cached.value_generation = field.value_generation;
1645 cached.origin_key = origin_key;
1646 cached.terrain_fingerprint = terrain_fingerprint;
1647 }
1648 Some(())
1649 }
1650
1651 fn get_or_create_column_overlay(
1652 &mut self,
1653 device: &wgpu::Device,
1654 queue: &wgpu::Queue,
1655 overlay: &VisualizationOverlay,
1656 state: &MapState,
1657 scene_origin: DVec3,
1658 ) -> Option<()> {
1659 let VisualizationOverlay::Columns {
1660 layer_id,
1661 columns,
1662 ramp,
1663 } = overlay
1664 else {
1665 return None;
1666 };
1667
1668 let origin_key = [
1669 (scene_origin.x * 100.0) as i64,
1670 (scene_origin.y * 100.0) as i64,
1671 (scene_origin.z * 100.0) as i64,
1672 ];
1673 let columns_fingerprint = column_set_fingerprint(columns);
1674 let ramp_fingerprint = grid_scalar_ramp_fingerprint(ramp);
1675 let instances = build_column_instances(columns, ramp, state, scene_origin);
1676
1677 let needs_rebuild = if let Some(cached) = self.column_overlay_cache.get(layer_id) {
1678 cached.generation != columns.generation
1679 || cached.ramp_fingerprint != ramp_fingerprint
1680 || cached.instance_count as usize != instances.len()
1681 } else {
1682 true
1683 };
1684
1685 if needs_rebuild {
1686 self.visualization_perf_stats.column_rebuilds += 1;
1687 let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1688 label: Some(&format!("column_instances_{layer_id}")),
1689 contents: bytemuck::cast_slice(&instances),
1690 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1691 });
1692 self.column_overlay_cache.insert(
1693 *layer_id,
1694 CachedColumnOverlay {
1695 instance_buffer,
1696 instance_count: instances.len() as u32,
1697 generation: columns.generation,
1698 origin_key,
1699 columns_fingerprint,
1700 ramp_fingerprint,
1701 instance_data: instances,
1702 },
1703 );
1704 return Some(());
1705 }
1706
1707 if let Some(cached) = self.column_overlay_cache.get_mut(layer_id) {
1708 let ranges = diff_column_instance_ranges(&cached.instance_data, &instances);
1709 if !ranges.is_empty() {
1710 self.visualization_perf_stats.column_partial_writes += 1;
1711 self.visualization_perf_stats.column_partial_write_ranges += ranges.len() as u32;
1712 }
1713 for range in ranges {
1714 let start = range.start;
1715 let end = range.end;
1716 let byte_offset =
1717 (start * std::mem::size_of::<ColumnInstanceData>()) as wgpu::BufferAddress;
1718 queue.write_buffer(
1719 &cached.instance_buffer,
1720 byte_offset,
1721 bytemuck::cast_slice::<ColumnInstanceData, u8>(&instances[start..end]),
1722 );
1723 }
1724 cached.instance_data = instances;
1725 cached.origin_key = origin_key;
1726 cached.columns_fingerprint = columns_fingerprint;
1727 }
1728
1729 Some(())
1730 }
1731
1732 pub fn render(
1739 &mut self,
1740 state: &MapState,
1741 device: &wgpu::Device,
1742 queue: &wgpu::Queue,
1743 color_view: &wgpu::TextureView,
1744 visible_tiles: &[VisibleTile],
1745 ) {
1746 let clear_color = state.computed_fog().clear_color;
1747 self.render_full(&RenderParams {
1748 state,
1749 device,
1750 queue,
1751 color_view,
1752 visible_tiles,
1753 vector_meshes: &[],
1754 model_instances: &[],
1755 clear_color,
1756 });
1757 }
1758
1759 pub fn render_full(&mut self, params: &RenderParams<'_>) {
1775 self.visualization_perf_stats = VisualizationPerfStats::default();
1776 let scene_camera_origin = params.state.scene_world_origin();
1778 let frame = params.state.frame_output();
1779 let visualization = &frame.visualization;
1780 let view = params.state.camera().view_matrix(DVec3::ZERO);
1781 let proj = params.state.camera().projection_matrix();
1782 let vp = proj * view;
1783
1784 let cam = params.state.camera();
1787 let eye = cam.eye_offset();
1788 let fog = params.state.computed_fog();
1789 let clear_color = fog.clear_color;
1790
1791 let mut uniform = ViewProjUniform::from_dmat4(&vp);
1792 uniform.fog_color = fog.fog_color;
1793 uniform.eye_pos = [eye.x as f32, eye.y as f32, eye.z as f32, 0.0];
1794 uniform.fog_params = [fog.fog_start, fog.fog_end, fog.fog_density, 0.0];
1795 if let Some(hillshade) = params.state.hillshade() {
1796 uniform.hillshade_highlight = hillshade.highlight_color;
1797 uniform.hillshade_shadow = hillshade.shadow_color;
1798 uniform.hillshade_accent = hillshade.accent_color;
1799 uniform.hillshade_light = [
1800 hillshade.illumination_direction,
1801 hillshade.illumination_altitude,
1802 hillshade.exaggeration,
1803 hillshade.opacity,
1804 ];
1805 }
1806
1807 let lighting = params.state.computed_lighting();
1809 uniform.ambient_color = [
1810 lighting.ambient_color[0],
1811 lighting.ambient_color[1],
1812 lighting.ambient_color[2],
1813 lighting.lighting_enabled,
1814 ];
1815 uniform.directional_dir = [
1816 lighting.directional_dir[0],
1817 lighting.directional_dir[1],
1818 lighting.directional_dir[2],
1819 0.0,
1820 ];
1821 uniform.directional_color = [
1822 lighting.directional_color[0],
1823 lighting.directional_color[1],
1824 lighting.directional_color[2],
1825 0.0,
1826 ];
1827
1828 params
1829 .queue
1830 .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
1831
1832 {
1834 let sky = params.state.computed_sky();
1835 let inv_vp = vp.inverse();
1836 let inv_vp_f32 = inv_vp.as_mat4();
1837 let sky_uniform = SkyUniform {
1838 inv_view_proj: inv_vp_f32.to_cols_array_2d(),
1839 sun_dir: [
1840 sky.sun_direction[0],
1841 sky.sun_direction[1],
1842 sky.sun_direction[2],
1843 sky.sun_intensity,
1844 ],
1845 rayleigh_tint: [
1846 sky.rayleigh_color[0],
1847 sky.rayleigh_color[1],
1848 sky.rayleigh_color[2],
1849 sky.sky_enabled,
1850 ],
1851 mie_tint: [sky.mie_color[0], sky.mie_color[1], sky.mie_color[2], 1.0],
1852 };
1853 params.queue.write_buffer(
1854 &self.sky_uniform_buffer,
1855 0,
1856 bytemuck::bytes_of(&sky_uniform),
1857 );
1858 }
1859
1860 {
1862 let shadow = params.state.computed_shadow();
1863 let shadow_uniform = ShadowParamsUniform {
1864 light_matrix_0: shadow.light_matrices[0],
1865 light_matrix_1: shadow.light_matrices[1],
1866 shadow_config: [
1867 shadow.intensity,
1868 shadow.texel_size,
1869 shadow.normal_offset,
1870 shadow.cascade_split,
1871 ],
1872 shadow_dir: [
1873 lighting.directional_dir[0],
1874 lighting.directional_dir[1],
1875 lighting.directional_dir[2],
1876 if shadow.enabled { 1.0 } else { 0.0 },
1877 ],
1878 };
1879 params.queue.write_buffer(
1880 &self.shadow_params_buffer,
1881 0,
1882 bytemuck::bytes_of(&shadow_uniform),
1883 );
1884 }
1885
1886 for vt in params.visible_tiles {
1888 if let Some(TileData::Raster(ref img)) = vt.data {
1889 self.upload_tile(params.device, vt.actual, img);
1890 }
1891 }
1892 for raster in params.state.hillshade_rasters() {
1893 self.upload_hillshade(params.device, raster.tile, &raster.image);
1894 }
1895
1896 self.flush_atlas_uploads(params.queue);
1898
1899 for vt in params.visible_tiles {
1901 self.tile_atlas.mark_used(&vt.actual);
1902 }
1903 let terrain_meshes = params.state.terrain_meshes();
1904 for mesh in terrain_meshes {
1905 if let Some(actual_tile) = find_terrain_texture_actual(mesh.tile, params.visible_tiles)
1906 {
1907 self.tile_atlas.mark_used(&actual_tile);
1908 }
1909 }
1910 for raster in params.state.hillshade_rasters() {
1911 self.hillshade_atlas.mark_used(&raster.tile);
1912 }
1913
1914 let use_shared_terrain = !terrain_meshes.is_empty()
1915 && matches!(
1916 params.state.camera().projection(),
1917 rustial_engine::CameraProjection::WebMercator
1918 | rustial_engine::CameraProjection::Equirectangular
1919 )
1920 && terrain_meshes
1921 .iter()
1922 .all(|mesh| mesh.elevation_texture.is_some());
1923
1924 let materialized_terrain_meshes: Vec<TerrainMeshData> = if use_shared_terrain {
1925 Vec::new()
1926 } else {
1927 terrain_meshes
1928 .iter()
1929 .map(|mesh| {
1930 materialize_terrain_mesh(
1931 mesh,
1932 params.state.camera().projection(),
1933 rustial_engine::skirt_height(
1934 mesh.tile.zoom,
1935 mesh.vertical_exaggeration as f64,
1936 ),
1937 )
1938 })
1939 .collect()
1940 };
1941
1942 if !params.model_instances.is_empty() {
1944 self.cache_model_meshes(params.device, params.model_instances);
1945 self.cache_model_transforms(
1946 params.device,
1947 params.model_instances,
1948 scene_camera_origin,
1949 params.state,
1950 );
1951 } else {
1952 self.cached_model_transforms = None;
1953 }
1954
1955 let tile_batch_key = TileBatchCacheKey::new(
1957 params.visible_tiles,
1958 scene_camera_origin,
1959 params.state.camera().projection(),
1960 );
1961 if self.tile_batch_cache_key.as_ref() != Some(&tile_batch_key) {
1962 self.cached_tile_batches = build_tile_batches(
1963 params.device,
1964 params.visible_tiles,
1965 &self.tile_atlas,
1966 scene_camera_origin,
1967 params.state.camera().projection(),
1968 );
1969 self.tile_batch_cache_key = Some(tile_batch_key);
1970 }
1971
1972 let terrain_batches = if !use_shared_terrain && !materialized_terrain_meshes.is_empty() {
1973 build_terrain_batches(
1974 params.device,
1975 &materialized_terrain_meshes,
1976 &self.tile_atlas,
1977 scene_camera_origin,
1978 params.visible_tiles,
1979 )
1980 } else {
1981 Vec::new()
1982 };
1983
1984 let hillshade_batches = if !materialized_terrain_meshes.is_empty()
1985 && !params.state.hillshade_rasters().is_empty()
1986 {
1987 build_hillshade_batches(
1988 params.device,
1989 &materialized_terrain_meshes,
1990 params.state.hillshade_rasters(),
1991 &self.hillshade_atlas,
1992 scene_camera_origin,
1993 )
1994 } else {
1995 Vec::new()
1996 };
1997
1998 let vector_batch_key = VectorBatchCacheKey::new(params.vector_meshes, scene_camera_origin);
1999 if self.vector_batch_cache_key.as_ref() != Some(&vector_batch_key) {
2000 self.cached_vector_batches = params
2001 .vector_meshes
2002 .iter()
2003 .filter(|mesh| mesh.render_mode == VectorRenderMode::Generic)
2004 .map(|mesh| build_vector_batch(params.device, mesh, scene_camera_origin))
2005 .collect();
2006 self.cached_fill_batches = params
2007 .vector_meshes
2008 .iter()
2009 .filter(|mesh| {
2010 mesh.render_mode == VectorRenderMode::Fill && mesh.fill_pattern.is_none()
2011 })
2012 .map(|mesh| {
2013 build_fill_batch(
2014 params.device,
2015 mesh,
2016 scene_camera_origin,
2017 &self.uniform_buffer,
2018 &self.fill_pipeline.uniform_bind_group_layout,
2019 )
2020 })
2021 .collect();
2022 self.cached_fill_pattern_batches = params
2023 .vector_meshes
2024 .iter()
2025 .filter(|mesh| {
2026 mesh.render_mode == VectorRenderMode::Fill && mesh.fill_pattern.is_some()
2027 })
2028 .map(|mesh| {
2029 build_fill_pattern_batch(
2030 params.device,
2031 params.queue,
2032 mesh,
2033 scene_camera_origin,
2034 &self.uniform_buffer,
2035 &self.fill_pattern_pipeline.uniform_bind_group_layout,
2036 &self.fill_pattern_pipeline.texture_bind_group_layout,
2037 &self.fill_pattern_sampler,
2038 )
2039 })
2040 .collect();
2041 self.cached_fill_extrusion_batches = params
2042 .vector_meshes
2043 .iter()
2044 .filter(|mesh| mesh.render_mode == VectorRenderMode::FillExtrusion)
2045 .map(|mesh| build_fill_extrusion_batch(params.device, mesh, scene_camera_origin))
2046 .collect();
2047 self.cached_line_batches = params
2048 .vector_meshes
2049 .iter()
2050 .filter(|mesh| {
2051 mesh.render_mode == VectorRenderMode::Line && mesh.line_pattern.is_none()
2052 })
2053 .map(|mesh| build_line_batch(params.device, mesh, scene_camera_origin))
2054 .collect();
2055 self.cached_line_pattern_batches = params
2056 .vector_meshes
2057 .iter()
2058 .filter(|mesh| {
2059 mesh.render_mode == VectorRenderMode::Line && mesh.line_pattern.is_some()
2060 })
2061 .map(|mesh| {
2062 build_line_pattern_batch(
2063 params.device,
2064 params.queue,
2065 mesh,
2066 scene_camera_origin,
2067 &self.uniform_buffer,
2068 &self.line_pattern_pipeline.uniform_bind_group_layout,
2069 &self.line_pattern_pipeline.texture_bind_group_layout,
2070 &self.fill_pattern_sampler,
2071 )
2072 })
2073 .collect();
2074 self.cached_circle_batches = params
2075 .vector_meshes
2076 .iter()
2077 .filter(|mesh| mesh.render_mode == VectorRenderMode::Circle)
2078 .map(|mesh| build_circle_batch(params.device, mesh, scene_camera_origin))
2079 .collect();
2080 self.cached_heatmap_batches = params
2081 .vector_meshes
2082 .iter()
2083 .filter(|mesh| mesh.render_mode == VectorRenderMode::Heatmap)
2084 .map(|mesh| build_heatmap_batch(params.device, mesh, scene_camera_origin))
2085 .collect();
2086 self.vector_batch_cache_key = Some(vector_batch_key);
2087 }
2088
2089 {
2091 let symbols = &frame.symbols;
2092 if !symbols.is_empty() {
2093 self.symbol_glyph_atlas = rustial_engine::symbols::GlyphAtlas::new();
2095 for symbol in symbols.iter() {
2096 if symbol.visible && symbol.opacity > 0.0 {
2097 if let Some(text) = &symbol.text {
2098 self.symbol_glyph_atlas
2099 .request_text(&symbol.font_stack, text);
2100 }
2101 }
2102 }
2103 self.symbol_glyph_atlas
2105 .load_requested(&*self.symbol_glyph_provider);
2106
2107 let dims = self.symbol_glyph_atlas.dimensions();
2108 if dims[0] > 0 && dims[1] > 0 {
2109 let tex = params.device.create_texture(&wgpu::TextureDescriptor {
2111 label: Some("symbol_atlas_tex"),
2112 size: wgpu::Extent3d {
2113 width: dims[0] as u32,
2114 height: dims[1] as u32,
2115 depth_or_array_layers: 1,
2116 },
2117 mip_level_count: 1,
2118 sample_count: 1,
2119 dimension: wgpu::TextureDimension::D2,
2120 format: wgpu::TextureFormat::R8Unorm,
2121 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2122 view_formats: &[],
2123 });
2124 params.queue.write_texture(
2125 wgpu::TexelCopyTextureInfo {
2126 texture: &tex,
2127 mip_level: 0,
2128 origin: wgpu::Origin3d::ZERO,
2129 aspect: wgpu::TextureAspect::All,
2130 },
2131 self.symbol_glyph_atlas.alpha(),
2132 wgpu::TexelCopyBufferLayout {
2133 offset: 0,
2134 bytes_per_row: Some(dims[0] as u32),
2135 rows_per_image: Some(dims[1] as u32),
2136 },
2137 wgpu::Extent3d {
2138 width: dims[0] as u32,
2139 height: dims[1] as u32,
2140 depth_or_array_layers: 1,
2141 },
2142 );
2143 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
2144 let atlas_bg = params.device.create_bind_group(&wgpu::BindGroupDescriptor {
2145 label: Some("symbol_atlas_bg"),
2146 layout: &self.symbol_pipeline.atlas_bind_group_layout,
2147 entries: &[
2148 wgpu::BindGroupEntry {
2149 binding: 0,
2150 resource: wgpu::BindingResource::TextureView(&view),
2151 },
2152 wgpu::BindGroupEntry {
2153 binding: 1,
2154 resource: wgpu::BindingResource::Sampler(&self.sampler),
2155 },
2156 ],
2157 });
2158 self.symbol_atlas_texture = Some((tex, view));
2159 self.symbol_atlas_bind_group = Some(atlas_bg);
2160 }
2161
2162 let mut laid_out_symbols: Vec<rustial_engine::symbols::PlacedSymbol> =
2164 symbols.to_vec();
2165 rustial_engine::symbols::layout_symbol_glyphs(
2166 &mut laid_out_symbols,
2167 &self.symbol_glyph_atlas,
2168 );
2169
2170 self.cached_symbol_batch = build_symbol_batch(
2172 params.device,
2173 &laid_out_symbols,
2174 &self.symbol_glyph_atlas,
2175 scene_camera_origin,
2176 self.symbol_glyph_atlas.render_em_px(),
2177 );
2178 } else {
2179 self.cached_symbol_batch = None;
2180 self.symbol_atlas_bind_group = None;
2181 self.symbol_atlas_texture = None;
2182 }
2183 }
2184
2185 self.cached_placeholder_batch = build_placeholder_batches(
2187 params.device,
2188 &frame.placeholders,
2189 params.state.placeholder_style(),
2190 scene_camera_origin,
2191 );
2192
2193 self.build_image_overlay_batches(
2195 params.device,
2196 params.queue,
2197 &frame.image_overlays,
2198 scene_camera_origin,
2199 );
2200
2201 if use_shared_terrain {
2203 for mesh in terrain_meshes {
2204 self.get_or_create_shared_grid(params.device, mesh.grid_resolution);
2205 let scene_origin = scene_camera_origin;
2206 self.get_or_create_terrain_tile_bind(
2208 params.device,
2209 params.queue,
2210 mesh,
2211 params.state,
2212 scene_origin,
2213 TerrainPipelineKind::Terrain,
2214 );
2215 self.get_or_create_terrain_tile_bind(
2216 params.device,
2217 params.queue,
2218 mesh,
2219 params.state,
2220 scene_origin,
2221 TerrainPipelineKind::TerrainData,
2222 );
2223 self.get_or_create_terrain_tile_bind(
2224 params.device,
2225 params.queue,
2226 mesh,
2227 params.state,
2228 scene_origin,
2229 TerrainPipelineKind::Hillshade,
2230 );
2231 }
2232 }
2233
2234 let grid_scalar_overlays: Vec<_> = visualization
2235 .iter()
2236 .filter(|overlay| matches!(overlay, VisualizationOverlay::GridScalar { .. }))
2237 .collect();
2238 let visible_grid_scalar_overlays: Vec<_> = grid_scalar_overlays
2239 .iter()
2240 .copied()
2241 .filter(|overlay| {
2242 visualization_overlay_intersects_scene_viewport(overlay, params.state)
2243 })
2244 .collect();
2245 let grid_extrusion_overlays: Vec<_> = visualization
2246 .iter()
2247 .filter(|overlay| matches!(overlay, VisualizationOverlay::GridExtrusion { .. }))
2248 .collect();
2249 let visible_grid_extrusion_overlays: Vec<_> = grid_extrusion_overlays
2250 .iter()
2251 .copied()
2252 .filter(|overlay| {
2253 visualization_overlay_intersects_scene_viewport(overlay, params.state)
2254 })
2255 .collect();
2256 let column_overlays: Vec<_> = visualization
2257 .iter()
2258 .filter(|overlay| matches!(overlay, VisualizationOverlay::Columns { .. }))
2259 .collect();
2260 let visible_column_overlays: Vec<_> = column_overlays
2261 .iter()
2262 .copied()
2263 .filter(|overlay| {
2264 visualization_overlay_intersects_scene_viewport(overlay, params.state)
2265 })
2266 .collect();
2267 let point_cloud_overlays: Vec<_> = visualization
2268 .iter()
2269 .filter(|overlay| matches!(overlay, VisualizationOverlay::Points { .. }))
2270 .collect();
2271 let visible_point_cloud_overlays: Vec<_> = point_cloud_overlays
2272 .iter()
2273 .copied()
2274 .filter(|overlay| {
2275 visualization_overlay_intersects_scene_viewport(overlay, params.state)
2276 })
2277 .collect();
2278 let terrain_fingerprint = TerrainDataDirtyState::terrain_fingerprint(terrain_meshes);
2279 if !visible_grid_scalar_overlays.is_empty() {
2280 for overlay in &visible_grid_scalar_overlays {
2281 self.get_or_create_grid_scalar_overlay(
2282 params.device,
2283 params.queue,
2284 overlay,
2285 params.state,
2286 scene_camera_origin,
2287 terrain_fingerprint,
2288 );
2289 }
2290 }
2291 if !visible_column_overlays.is_empty() {
2292 self.get_or_create_shared_column_mesh(params.device);
2293 for overlay in &visible_column_overlays {
2294 self.get_or_create_column_overlay(
2295 params.device,
2296 params.queue,
2297 overlay,
2298 params.state,
2299 scene_camera_origin,
2300 );
2301 }
2302 }
2303 if !visible_point_cloud_overlays.is_empty() {
2304 self.get_or_create_shared_column_mesh(params.device);
2305 for overlay in &visible_point_cloud_overlays {
2306 self.get_or_create_point_cloud_overlay(
2307 params.device,
2308 params.queue,
2309 overlay,
2310 params.state,
2311 scene_camera_origin,
2312 );
2313 }
2314 }
2315 if !visible_grid_extrusion_overlays.is_empty() {
2316 for overlay in &visible_grid_extrusion_overlays {
2317 self.get_or_create_grid_extrusion_overlay(
2318 params.device,
2319 params.queue,
2320 overlay,
2321 params.state,
2322 scene_camera_origin,
2323 terrain_fingerprint,
2324 );
2325 }
2326 }
2327
2328 let mut encoder = params
2330 .device
2331 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2332 label: Some("rustial_encoder"),
2333 });
2334
2335 let has_heatmap = !self.cached_heatmap_batches.is_empty()
2336 && self.cached_heatmap_batches.iter().any(|b| b.is_some());
2337
2338 let painter_plan = PainterPlan::with_shadows(
2339 !terrain_meshes.is_empty(),
2340 !params.state.hillshade_rasters().is_empty(),
2341 has_heatmap,
2342 params.state.computed_shadow().enabled,
2343 );
2344
2345 for painter_pass in painter_plan.iter() {
2346 match painter_pass {
2347 PainterPass::SkyAtmosphere => {
2348 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2349 label: Some("rustial_pass_sky_atmosphere"),
2350 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2351 view: params.color_view,
2352 resolve_target: None,
2353 ops: wgpu::Operations {
2354 load: wgpu::LoadOp::Clear(wgpu::Color {
2355 r: clear_color[0] as f64,
2356 g: clear_color[1] as f64,
2357 b: clear_color[2] as f64,
2358 a: clear_color[3] as f64,
2359 }),
2360 store: wgpu::StoreOp::Store,
2361 },
2362 })],
2363 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2364 view: &self.depth_view,
2365 depth_ops: Some(wgpu::Operations {
2366 load: wgpu::LoadOp::Clear(1.0),
2367 store: wgpu::StoreOp::Store,
2368 }),
2369 stencil_ops: None,
2370 }),
2371 ..Default::default()
2372 });
2373
2374 if params.state.computed_sky().sky_enabled > 0.5 {
2377 pass.set_pipeline(&self.sky_pipeline.pipeline);
2378 pass.set_bind_group(0, &self.sky_uniform_bind_group, &[]);
2379 pass.draw(0..3, 0..1);
2380 }
2381 }
2382 PainterPass::TerrainData => {
2383 if !self.terrain_data_dirty.needs_update(&vp, terrain_meshes) {
2389 continue;
2390 }
2391 {
2392 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2393 label: Some("rustial_pass_terrain_data"),
2394 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2395 view: self.terrain_interaction_buffers.coord_view(),
2396 resolve_target: None,
2397 ops: wgpu::Operations {
2398 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2399 store: wgpu::StoreOp::Store,
2400 },
2401 })],
2402 depth_stencil_attachment: Some(
2403 wgpu::RenderPassDepthStencilAttachment {
2404 view: self.terrain_interaction_buffers.depth_view(),
2405 depth_ops: Some(wgpu::Operations {
2406 load: wgpu::LoadOp::Clear(1.0),
2407 store: wgpu::StoreOp::Store,
2408 }),
2409 stencil_ops: None,
2410 },
2411 ),
2412 ..Default::default()
2413 });
2414 if use_shared_terrain {
2415 self.render_shared_terrain_data_tiles(
2416 &mut pass,
2417 params.state,
2418 terrain_meshes,
2419 );
2420 } else {
2421 self.render_terrain_data_batches(&mut pass, &terrain_batches);
2422 }
2423 }
2424 self.terrain_data_dirty.mark_clean(&vp, terrain_meshes);
2425 }
2426 PainterPass::ShadowDepth => {
2427 let shadow = params.state.computed_shadow();
2428 let cascade_count = (shadow.cascade_count as usize)
2429 .min(self.shadow_map_views.len())
2430 .min(self.shadow_depth_bind_groups.len());
2431
2432 for cascade_idx in 0..cascade_count {
2434 let light_vp_bytes: &[u8] =
2435 bytemuck::cast_slice(&shadow.light_matrices[cascade_idx]);
2436 params.queue.write_buffer(
2437 &self.shadow_depth_uniform_buffers[cascade_idx],
2438 0,
2439 light_vp_bytes,
2440 );
2441 }
2442
2443 for cascade_idx in 0..cascade_count {
2444 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2446 label: Some("rustial_pass_shadow_depth"),
2447 color_attachments: &[],
2448 depth_stencil_attachment: Some(
2449 wgpu::RenderPassDepthStencilAttachment {
2450 view: &self.shadow_map_views[cascade_idx],
2451 depth_ops: Some(wgpu::Operations {
2452 load: wgpu::LoadOp::Clear(1.0),
2453 store: wgpu::StoreOp::Store,
2454 }),
2455 stencil_ops: None,
2456 },
2457 ),
2458 ..Default::default()
2459 });
2460
2461 if self
2463 .cached_fill_extrusion_batches
2464 .iter()
2465 .any(|b| b.is_some())
2466 {
2467 pass.set_pipeline(&self.shadow_fill_extrusion_pipeline.pipeline);
2468 pass.set_bind_group(
2469 0,
2470 &self.shadow_depth_bind_groups[cascade_idx],
2471 &[],
2472 );
2473 for batch in self.cached_fill_extrusion_batches.iter().flatten() {
2474 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2475 pass.set_index_buffer(
2476 batch.index_buffer.slice(..),
2477 wgpu::IndexFormat::Uint32,
2478 );
2479 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2480 }
2481 }
2482
2483 if let Some(ref cached) = self.cached_model_transforms {
2485 if cached.instance_count > 0 {
2486 pass.set_pipeline(&self.shadow_model_pipeline.pipeline);
2487 pass.set_bind_group(
2488 0,
2489 &self.shadow_depth_bind_groups[cascade_idx],
2490 &[],
2491 );
2492 for i in 0..cached.instance_count {
2493 let dyn_offset = (i * cached.stride) as u32;
2494 if let Some(instance) = params.model_instances.get(i) {
2495 let mesh_key = ModelMeshKey::from_mesh(&instance.mesh);
2496 if let Some(cached_mesh) =
2497 self.model_mesh_cache.get(&mesh_key)
2498 {
2499 pass.set_bind_group(
2500 1,
2501 &cached.bind_group,
2502 &[dyn_offset],
2503 );
2504 pass.set_vertex_buffer(
2505 0,
2506 cached_mesh.vertex_buffer.slice(..),
2507 );
2508 pass.set_index_buffer(
2509 cached_mesh.index_buffer.slice(..),
2510 wgpu::IndexFormat::Uint32,
2511 );
2512 pass.draw_indexed(0..cached_mesh.index_count, 0, 0..1);
2513 }
2514 }
2515 }
2516 }
2517 }
2518 }
2519 }
2520 PainterPass::OpaqueScene => {
2521 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2522 label: Some("rustial_pass_opaque"),
2523 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2524 view: params.color_view,
2525 resolve_target: None,
2526 ops: wgpu::Operations {
2527 load: wgpu::LoadOp::Load,
2528 store: wgpu::StoreOp::Store,
2529 },
2530 })],
2531 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2532 view: &self.depth_view,
2533 depth_ops: Some(wgpu::Operations {
2534 load: wgpu::LoadOp::Load,
2535 store: wgpu::StoreOp::Store,
2536 }),
2537 stencil_ops: None,
2538 }),
2539 ..Default::default()
2540 });
2541
2542 if let Some(ref ph_batch) = self.cached_placeholder_batch {
2545 pass.set_pipeline(&self.vector_pipeline.pipeline);
2546 pass.set_bind_group(0, &self.vector_uniform_bind_group, &[]);
2547 pass.set_vertex_buffer(0, ph_batch.vertex_buffer.slice(..));
2548 pass.set_index_buffer(
2549 ph_batch.index_buffer.slice(..),
2550 wgpu::IndexFormat::Uint32,
2551 );
2552 pass.draw_indexed(0..ph_batch.index_count, 0, 0..1);
2553 }
2554
2555 if use_shared_terrain {
2556 self.render_shared_terrain_tiles(
2557 &mut pass,
2558 params.state,
2559 terrain_meshes,
2560 params.visible_tiles,
2561 );
2562 } else if !terrain_batches.is_empty() {
2563 self.render_terrain_batches(&mut pass, &terrain_batches);
2564 } else {
2565 self.render_tile_batches(&mut pass, &self.cached_tile_batches);
2566 }
2567
2568 if !visible_grid_scalar_overlays.is_empty() {
2569 self.render_grid_scalar_overlays(&mut pass, &visible_grid_scalar_overlays);
2570 }
2571 if !visible_grid_extrusion_overlays.is_empty() {
2572 self.render_grid_extrusion_overlays(
2573 &mut pass,
2574 &visible_grid_extrusion_overlays,
2575 );
2576 }
2577 if !visible_column_overlays.is_empty() {
2578 self.render_column_overlays(&mut pass, &visible_column_overlays);
2579 }
2580 if !visible_point_cloud_overlays.is_empty() {
2581 self.render_point_cloud_overlays(&mut pass, &visible_point_cloud_overlays);
2582 }
2583
2584 self.render_vector_batches(&mut pass, &self.cached_vector_batches);
2585 self.render_fill_batches(&mut pass, &self.cached_fill_batches);
2586 self.render_fill_pattern_batches(&mut pass, &self.cached_fill_pattern_batches);
2587 self.render_fill_extrusion_batches(
2588 &mut pass,
2589 &self.cached_fill_extrusion_batches,
2590 );
2591 self.render_line_batches(&mut pass, &self.cached_line_batches);
2592 self.render_line_pattern_batches(&mut pass, &self.cached_line_pattern_batches);
2593 self.render_circle_batches(&mut pass, &self.cached_circle_batches);
2594 self.render_image_overlay_batches(&mut pass);
2598 self.render_symbol_batch(&mut pass);
2599
2600 if !params.model_instances.is_empty() {
2601 self.render_models(&mut pass, params.model_instances, params.device);
2602 }
2603 }
2604 PainterPass::HeatmapAccumulation => {
2605 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2608 label: Some("rustial_pass_heatmap_accum"),
2609 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2610 view: &self.heatmap_accum_view,
2611 resolve_target: None,
2612 ops: wgpu::Operations {
2613 load: wgpu::LoadOp::Clear(wgpu::Color {
2614 r: 0.0,
2615 g: 0.0,
2616 b: 0.0,
2617 a: 0.0,
2618 }),
2619 store: wgpu::StoreOp::Store,
2620 },
2621 })],
2622 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2623 view: &self.depth_view,
2624 depth_ops: Some(wgpu::Operations {
2625 load: wgpu::LoadOp::Load,
2626 store: wgpu::StoreOp::Store,
2627 }),
2628 stencil_ops: None,
2629 }),
2630 ..Default::default()
2631 });
2632 self.render_heatmap_batches(&mut pass, &self.cached_heatmap_batches);
2633 }
2634 PainterPass::HeatmapColormap => {
2635 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2639 label: Some("rustial_pass_heatmap_colormap"),
2640 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2641 view: params.color_view,
2642 resolve_target: None,
2643 ops: wgpu::Operations {
2644 load: wgpu::LoadOp::Load,
2645 store: wgpu::StoreOp::Store,
2646 },
2647 })],
2648 depth_stencil_attachment: None,
2649 ..Default::default()
2650 });
2651 pass.set_pipeline(&self.heatmap_colormap_pipeline.pipeline);
2652 pass.set_bind_group(0, &self.heatmap_colormap_uniform_bind_group, &[]);
2653 pass.set_bind_group(1, &self.heatmap_colormap_textures_bind_group, &[]);
2654 pass.draw(0..3, 0..1); }
2656 PainterPass::HillshadeOverlay => {
2657 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2658 label: Some("rustial_pass_hillshade_overlay"),
2659 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2660 view: params.color_view,
2661 resolve_target: None,
2662 ops: wgpu::Operations {
2663 load: wgpu::LoadOp::Load,
2664 store: wgpu::StoreOp::Store,
2665 },
2666 })],
2667 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2668 view: &self.depth_view,
2669 depth_ops: Some(wgpu::Operations {
2670 load: wgpu::LoadOp::Load,
2671 store: wgpu::StoreOp::Store,
2672 }),
2673 stencil_ops: None,
2674 }),
2675 ..Default::default()
2676 });
2677
2678 if use_shared_terrain {
2679 self.render_shared_hillshade_tiles(&mut pass, params.state, terrain_meshes);
2680 } else {
2681 self.render_hillshade_batches(&mut pass, &hillshade_batches);
2682 }
2683 }
2684 }
2685 }
2686
2687 params.queue.submit(std::iter::once(encoder.finish()));
2689
2690 self.prune_height_texture_cache(terrain_meshes);
2691 self.prune_terrain_tile_bind_cache(terrain_meshes);
2692 self.prune_grid_scalar_overlay_cache(&grid_scalar_overlays);
2693 self.prune_grid_extrusion_overlay_cache(&grid_extrusion_overlays);
2694 self.prune_column_overlay_cache(&column_overlays);
2695 self.prune_point_cloud_overlay_cache(&point_cloud_overlays);
2696
2697 let tile_count_before = self.tile_atlas.len();
2699 self.tile_atlas.end_frame();
2700 if self.tile_atlas.len() != tile_count_before {
2701 self.tile_batch_cache_key = None;
2702 }
2703 self.hillshade_atlas.end_frame();
2704 }
2705
2706 fn render_tile_batches<'a>(
2710 &'a self,
2711 pass: &mut wgpu::RenderPass<'a>,
2712 batches: &'a [TilePageBatches],
2713 ) {
2714 pass.set_bind_group(0, &self.uniform_bind_group, &[]);
2715
2716 for (page_idx, batch) in batches.iter().enumerate() {
2717 let bg = match self.page_bind_groups.get(page_idx) {
2718 Some(bg) => bg,
2719 None => continue,
2720 };
2721
2722 if let Some(batch) = batch.opaque.as_ref() {
2723 pass.set_pipeline(&self.tile_pipeline.pipeline);
2724 pass.set_bind_group(1, bg, &[]);
2725 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2726 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2727 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2728 }
2729
2730 if let Some(batch) = batch.translucent.as_ref() {
2731 pass.set_pipeline(&self.tile_pipeline.translucent_pipeline);
2732 pass.set_bind_group(1, bg, &[]);
2733 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2734 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2735 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2736 }
2737 }
2738 }
2739
2740 fn render_grid_scalar_overlays<'a>(
2741 &'a self,
2742 pass: &mut wgpu::RenderPass<'a>,
2743 overlays: &[&'a VisualizationOverlay],
2744 ) {
2745 pass.set_pipeline(&self.grid_scalar_pipeline.pipeline);
2746 pass.set_bind_group(0, &self.grid_scalar_uniform_bind_group, &[]);
2747
2748 for overlay in overlays {
2749 let VisualizationOverlay::GridScalar { layer_id, .. } = overlay else {
2750 continue;
2751 };
2752 let Some(cached) = self.grid_scalar_overlay_cache.get(layer_id) else {
2753 continue;
2754 };
2755 pass.set_bind_group(1, &cached.bind_group, &[]);
2756 pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
2757 pass.set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2758 pass.draw_indexed(0..cached.index_count, 0, 0..1);
2759 }
2760 }
2761
2762 fn render_grid_extrusion_overlays<'a>(
2763 &'a self,
2764 pass: &mut wgpu::RenderPass<'a>,
2765 overlays: &[&'a VisualizationOverlay],
2766 ) {
2767 pass.set_pipeline(&self.grid_extrusion_pipeline.pipeline);
2768 pass.set_bind_group(0, &self.grid_extrusion_uniform_bind_group, &[]);
2769
2770 for overlay in overlays {
2771 let VisualizationOverlay::GridExtrusion { layer_id, .. } = overlay else {
2772 continue;
2773 };
2774 let Some(cached) = self.grid_extrusion_overlay_cache.get(layer_id) else {
2775 continue;
2776 };
2777 pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
2778 pass.set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2779 pass.draw_indexed(0..cached.index_count, 0, 0..1);
2780 }
2781 }
2782
2783 fn render_column_overlays<'a>(
2784 &'a self,
2785 pass: &mut wgpu::RenderPass<'a>,
2786 overlays: &[&'a VisualizationOverlay],
2787 ) {
2788 let Some(mesh) = self.shared_column_mesh.as_ref() else {
2789 return;
2790 };
2791
2792 pass.set_pipeline(&self.column_pipeline.pipeline);
2793 pass.set_bind_group(0, &self.column_uniform_bind_group, &[]);
2794
2795 for overlay in overlays {
2796 let VisualizationOverlay::Columns { layer_id, .. } = overlay else {
2797 continue;
2798 };
2799 let Some(cached) = self.column_overlay_cache.get(layer_id) else {
2800 continue;
2801 };
2802 if cached.instance_count == 0 {
2803 continue;
2804 }
2805 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2806 pass.set_vertex_buffer(1, cached.instance_buffer.slice(..));
2807 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2808 pass.draw_indexed(0..mesh.index_count, 0, 0..cached.instance_count);
2809 }
2810 }
2811
2812 fn render_point_cloud_overlays<'a>(
2813 &'a self,
2814 pass: &mut wgpu::RenderPass<'a>,
2815 overlays: &[&'a VisualizationOverlay],
2816 ) {
2817 let Some(mesh) = self.shared_column_mesh.as_ref() else {
2818 return;
2819 };
2820
2821 pass.set_pipeline(&self.column_pipeline.pipeline);
2822 pass.set_bind_group(0, &self.column_uniform_bind_group, &[]);
2823
2824 for overlay in overlays {
2825 let VisualizationOverlay::Points { layer_id, .. } = overlay else {
2826 continue;
2827 };
2828 let Some(cached) = self.point_cloud_overlay_cache.get(layer_id) else {
2829 continue;
2830 };
2831 if cached.instance_count == 0 {
2832 continue;
2833 }
2834 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2835 pass.set_vertex_buffer(1, cached.instance_buffer.slice(..));
2836 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2837 pass.draw_indexed(0..mesh.index_count, 0, 0..cached.instance_count);
2838 }
2839 }
2840
2841 fn render_terrain_batches<'a>(
2845 &'a self,
2846 pass: &mut wgpu::RenderPass<'a>,
2847 batches: &'a [Option<TerrainBatch>],
2848 ) {
2849 pass.set_pipeline(&self.terrain_pipeline.pipeline);
2850 pass.set_bind_group(0, &self.terrain_uniform_bind_group, &[]);
2851
2852 for (page_idx, batch) in batches.iter().enumerate() {
2853 let batch = match batch {
2854 Some(b) => b,
2855 None => continue,
2856 };
2857 let bg = match self.page_terrain_bind_groups.get(page_idx) {
2858 Some(bg) => bg,
2859 None => continue,
2860 };
2861
2862 pass.set_bind_group(1, bg, &[]);
2863 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2864 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2865 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2866 }
2867 }
2868
2869 fn render_terrain_data_batches<'a>(
2872 &'a self,
2873 pass: &mut wgpu::RenderPass<'a>,
2874 batches: &'a [Option<TerrainBatch>],
2875 ) {
2876 pass.set_pipeline(&self.terrain_data_pipeline.pipeline);
2877 pass.set_bind_group(0, &self.terrain_data_uniform_bind_group, &[]);
2878
2879 for batch in batches.iter().flatten() {
2880 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2881 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2882 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2883 }
2884 }
2885
2886 fn render_hillshade_batches<'a>(
2890 &'a self,
2891 pass: &mut wgpu::RenderPass<'a>,
2892 batches: &'a [Option<HillshadeBatch>],
2893 ) {
2894 pass.set_pipeline(&self.hillshade_pipeline.pipeline);
2895 pass.set_bind_group(0, &self.hillshade_uniform_bind_group, &[]);
2896
2897 for (page_idx, batch) in batches.iter().enumerate() {
2898 let batch = match batch {
2899 Some(b) => b,
2900 None => continue,
2901 };
2902 let bg = match self.page_hillshade_bind_groups.get(page_idx) {
2903 Some(bg) => bg,
2904 None => continue,
2905 };
2906
2907 pass.set_bind_group(1, bg, &[]);
2908 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2909 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2910 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2911 }
2912 }
2913
2914 fn render_vector_batches<'a>(
2919 &'a self,
2920 pass: &mut wgpu::RenderPass<'a>,
2921 batches: &'a [Option<VectorBatchEntry>],
2922 ) {
2923 let mut pipeline_set = false;
2924
2925 for batch in batches.iter().flatten() {
2926 if !pipeline_set {
2927 pass.set_pipeline(&self.vector_pipeline.pipeline);
2928 pass.set_bind_group(0, &self.vector_uniform_bind_group, &[]);
2929 pipeline_set = true;
2930 }
2931
2932 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2933 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2934 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2935 }
2936 }
2937
2938 fn render_fill_batches<'a>(
2942 &'a self,
2943 pass: &mut wgpu::RenderPass<'a>,
2944 batches: &'a [Option<FillBatchEntry>],
2945 ) {
2946 for batch in batches.iter().flatten() {
2947 pass.set_pipeline(&self.fill_pipeline.pipeline);
2948 pass.set_bind_group(0, &batch.bind_group, &[]);
2950 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2951 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2952 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2953 }
2954 }
2955
2956 fn render_fill_pattern_batches<'a>(
2960 &'a self,
2961 pass: &mut wgpu::RenderPass<'a>,
2962 batches: &'a [Option<FillPatternBatchEntry>],
2963 ) {
2964 for batch in batches.iter().flatten() {
2965 pass.set_pipeline(&self.fill_pattern_pipeline.pipeline);
2966 pass.set_bind_group(0, &batch.uniform_bind_group, &[]);
2967 pass.set_bind_group(1, &batch.texture_bind_group, &[]);
2968 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2969 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2970 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2971 }
2972 }
2973
2974 fn render_fill_extrusion_batches<'a>(
2978 &'a self,
2979 pass: &mut wgpu::RenderPass<'a>,
2980 batches: &'a [Option<FillExtrusionBatchEntry>],
2981 ) {
2982 let mut pipeline_set = false;
2983
2984 for batch in batches.iter().flatten() {
2985 if !pipeline_set {
2986 pass.set_pipeline(&self.fill_extrusion_pipeline.pipeline);
2987 pass.set_bind_group(0, &self.fill_extrusion_uniform_bind_group, &[]);
2988 pass.set_bind_group(1, &self.shadow_receiver_bind_group, &[]);
2989 pipeline_set = true;
2990 }
2991
2992 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
2993 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2994 pass.draw_indexed(0..batch.index_count, 0, 0..1);
2995 }
2996 }
2997
2998 fn render_line_batches<'a>(
2999 &'a self,
3000 pass: &mut wgpu::RenderPass<'a>,
3001 batches: &'a [Option<LineBatchEntry>],
3002 ) {
3003 let mut pipeline_set = false;
3004
3005 for batch in batches.iter().flatten() {
3006 if !pipeline_set {
3007 pass.set_pipeline(&self.line_pipeline.pipeline);
3008 pass.set_bind_group(0, &self.line_uniform_bind_group, &[]);
3009 pipeline_set = true;
3010 }
3011
3012 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
3013 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3014 pass.draw_indexed(0..batch.index_count, 0, 0..1);
3015 }
3016 }
3017
3018 fn render_line_pattern_batches<'a>(
3019 &'a self,
3020 pass: &mut wgpu::RenderPass<'a>,
3021 batches: &'a [Option<LinePatternBatchEntry>],
3022 ) {
3023 for batch in batches.iter().flatten() {
3024 pass.set_pipeline(&self.line_pattern_pipeline.pipeline);
3025 pass.set_bind_group(0, &batch.uniform_bind_group, &[]);
3026 pass.set_bind_group(1, &batch.texture_bind_group, &[]);
3027 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
3028 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3029 pass.draw_indexed(0..batch.index_count, 0, 0..1);
3030 }
3031 }
3032
3033 fn render_circle_batches<'a>(
3034 &'a self,
3035 pass: &mut wgpu::RenderPass<'a>,
3036 batches: &'a [Option<CircleBatchEntry>],
3037 ) {
3038 let mut pipeline_set = false;
3039
3040 for batch in batches.iter().flatten() {
3041 if !pipeline_set {
3042 pass.set_pipeline(&self.circle_pipeline.pipeline);
3043 pass.set_bind_group(0, &self.circle_uniform_bind_group, &[]);
3044 pipeline_set = true;
3045 }
3046
3047 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
3048 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3049 pass.draw_indexed(0..batch.index_count, 0, 0..1);
3050 }
3051 }
3052
3053 fn render_heatmap_batches<'a>(
3054 &'a self,
3055 pass: &mut wgpu::RenderPass<'a>,
3056 batches: &'a [Option<HeatmapBatchEntry>],
3057 ) {
3058 let mut pipeline_set = false;
3059
3060 for batch in batches.iter().flatten() {
3061 if !pipeline_set {
3062 pass.set_pipeline(&self.heatmap_pipeline.pipeline);
3063 pass.set_bind_group(0, &self.heatmap_uniform_bind_group, &[]);
3064 pipeline_set = true;
3065 }
3066
3067 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
3068 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3069 pass.draw_indexed(0..batch.index_count, 0, 0..1);
3070 }
3071 }
3072
3073 fn build_image_overlay_batches(
3080 &mut self,
3081 device: &wgpu::Device,
3082 queue: &wgpu::Queue,
3083 overlays: &[rustial_engine::layers::ImageOverlayData],
3084 camera_origin: glam::DVec3,
3085 ) {
3086 let mut old_cache: Vec<CachedImageOverlayBatch> =
3088 std::mem::take(&mut self.cached_image_overlay_batches);
3089
3090 for overlay in overlays {
3091 if overlay.width == 0 || overlay.height == 0 || overlay.data.is_empty() {
3092 continue;
3093 }
3094
3095 let data_arc_ptr = Arc::as_ptr(&overlay.data) as usize;
3096
3097 let cached_idx = old_cache
3099 .iter()
3100 .position(|c| c.layer_id == overlay.layer_id);
3101
3102 let uvs = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
3104 let vertices: Vec<ImageOverlayVertex> = overlay
3105 .corners
3106 .iter()
3107 .zip(uvs.iter())
3108 .map(|(corner, uv)| {
3109 let rel = [
3110 (corner[0] - camera_origin.x) as f32,
3111 (corner[1] - camera_origin.y) as f32,
3112 (corner[2] - camera_origin.z) as f32,
3113 ];
3114 ImageOverlayVertex {
3115 position: rel,
3116 uv: *uv,
3117 opacity: overlay.opacity,
3118 }
3119 })
3120 .collect();
3121 let indices: Vec<u32> = vec![0, 1, 2, 0, 2, 3];
3122
3123 if let Some(idx) = cached_idx {
3124 let mut cached = old_cache.remove(idx);
3125
3126 cached.vertex_buffer =
3128 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3129 label: Some("image_overlay_vb"),
3130 contents: bytemuck::cast_slice(&vertices),
3131 usage: wgpu::BufferUsages::VERTEX,
3132 });
3133 cached.index_buffer =
3134 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3135 label: Some("image_overlay_ib"),
3136 contents: bytemuck::cast_slice(&indices),
3137 usage: wgpu::BufferUsages::INDEX,
3138 });
3139
3140 if cached.data_arc_ptr != data_arc_ptr {
3142 if cached.tex_dimensions == (overlay.width, overlay.height) {
3143 queue.write_texture(
3145 wgpu::TexelCopyTextureInfo {
3146 texture: &cached.texture,
3147 mip_level: 0,
3148 origin: wgpu::Origin3d::ZERO,
3149 aspect: wgpu::TextureAspect::All,
3150 },
3151 &overlay.data,
3152 wgpu::TexelCopyBufferLayout {
3153 offset: 0,
3154 bytes_per_row: Some(overlay.width * 4),
3155 rows_per_image: Some(overlay.height),
3156 },
3157 wgpu::Extent3d {
3158 width: overlay.width,
3159 height: overlay.height,
3160 depth_or_array_layers: 1,
3161 },
3162 );
3163 } else {
3164 let (texture, texture_view, texture_bind_group) =
3166 self.create_overlay_texture(device, queue, overlay);
3167 cached.texture = texture;
3168 cached.texture_view = texture_view;
3169 cached.texture_bind_group = texture_bind_group;
3170 cached.tex_dimensions = (overlay.width, overlay.height);
3171 }
3172 cached.data_arc_ptr = data_arc_ptr;
3173 }
3174
3175 self.cached_image_overlay_batches.push(cached);
3176 } else {
3177 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3179 label: Some("image_overlay_vb"),
3180 contents: bytemuck::cast_slice(&vertices),
3181 usage: wgpu::BufferUsages::VERTEX,
3182 });
3183 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3184 label: Some("image_overlay_ib"),
3185 contents: bytemuck::cast_slice(&indices),
3186 usage: wgpu::BufferUsages::INDEX,
3187 });
3188 let (texture, texture_view, texture_bind_group) =
3189 self.create_overlay_texture(device, queue, overlay);
3190
3191 self.cached_image_overlay_batches
3192 .push(CachedImageOverlayBatch {
3193 vertex_buffer,
3194 index_buffer,
3195 texture,
3196 texture_view,
3197 texture_bind_group,
3198 layer_id: overlay.layer_id,
3199 tex_dimensions: (overlay.width, overlay.height),
3200 data_arc_ptr,
3201 });
3202 }
3203 }
3204 }
3206
3207 fn create_overlay_texture(
3209 &self,
3210 device: &wgpu::Device,
3211 queue: &wgpu::Queue,
3212 overlay: &rustial_engine::layers::ImageOverlayData,
3213 ) -> (wgpu::Texture, wgpu::TextureView, wgpu::BindGroup) {
3214 let texture = device.create_texture(&wgpu::TextureDescriptor {
3215 label: Some("image_overlay_tex"),
3216 size: wgpu::Extent3d {
3217 width: overlay.width,
3218 height: overlay.height,
3219 depth_or_array_layers: 1,
3220 },
3221 mip_level_count: 1,
3222 sample_count: 1,
3223 dimension: wgpu::TextureDimension::D2,
3224 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3225 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3226 view_formats: &[],
3227 });
3228 queue.write_texture(
3229 wgpu::TexelCopyTextureInfo {
3230 texture: &texture,
3231 mip_level: 0,
3232 origin: wgpu::Origin3d::ZERO,
3233 aspect: wgpu::TextureAspect::All,
3234 },
3235 &overlay.data,
3236 wgpu::TexelCopyBufferLayout {
3237 offset: 0,
3238 bytes_per_row: Some(overlay.width * 4),
3239 rows_per_image: Some(overlay.height),
3240 },
3241 wgpu::Extent3d {
3242 width: overlay.width,
3243 height: overlay.height,
3244 depth_or_array_layers: 1,
3245 },
3246 );
3247 let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3248 let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3249 label: Some("image_overlay_tex_bg"),
3250 layout: &self.image_overlay_pipeline.texture_bind_group_layout,
3251 entries: &[
3252 wgpu::BindGroupEntry {
3253 binding: 0,
3254 resource: wgpu::BindingResource::TextureView(&texture_view),
3255 },
3256 wgpu::BindGroupEntry {
3257 binding: 1,
3258 resource: wgpu::BindingResource::Sampler(&self.sampler),
3259 },
3260 ],
3261 });
3262 (texture, texture_view, texture_bind_group)
3263 }
3264
3265 fn render_image_overlay_batches<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
3267 if self.cached_image_overlay_batches.is_empty() {
3268 return;
3269 }
3270 pass.set_pipeline(&self.image_overlay_pipeline.pipeline);
3271 pass.set_bind_group(0, &self.image_overlay_uniform_bind_group, &[]);
3272 for batch in &self.cached_image_overlay_batches {
3273 pass.set_bind_group(1, &batch.texture_bind_group, &[]);
3274 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
3275 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3276 pass.draw_indexed(0..6, 0, 0..1);
3277 }
3278 }
3279
3280 fn render_symbol_batch<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
3281 let batch = match &self.cached_symbol_batch {
3282 Some(b) => b,
3283 None => return,
3284 };
3285 let atlas_bg = match &self.symbol_atlas_bind_group {
3286 Some(bg) => bg,
3287 None => return,
3288 };
3289
3290 pass.set_pipeline(&self.symbol_pipeline.pipeline);
3291 pass.set_bind_group(0, &self.symbol_uniform_bind_group, &[]);
3292 pass.set_bind_group(1, atlas_bg, &[]);
3293 pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
3294 pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3295 pass.draw_indexed(0..batch.index_count, 0, 0..1);
3296 }
3297
3298 fn render_models<'a>(
3299 &'a self,
3300 pass: &mut wgpu::RenderPass<'a>,
3301 model_instances: &[ModelInstance],
3302 device: &wgpu::Device,
3303 ) {
3304 if model_instances.is_empty() {
3305 return;
3306 }
3307
3308 let cached = match &self.cached_model_transforms {
3309 Some(c) if c.instance_count == model_instances.len() => c,
3310 _ => return,
3311 };
3312
3313 pass.set_pipeline(&self.model_pipeline.pipeline);
3314 pass.set_bind_group(0, &self.model_uniform_bind_group, &[]);
3315 pass.set_bind_group(2, &self.shadow_receiver_bind_group, &[]);
3316
3317 for (i, instance) in model_instances.iter().enumerate() {
3318 let dyn_offset = (i * cached.stride) as u32;
3319 let mesh_key = ModelMeshKey::from_mesh(&instance.mesh);
3320 let cached_mesh = self.model_mesh_cache.get(&mesh_key);
3321
3322 if let Some(cached_mesh) = cached_mesh {
3323 pass.set_bind_group(1, &cached.bind_group, &[dyn_offset]);
3324 pass.set_vertex_buffer(0, cached_mesh.vertex_buffer.slice(..));
3325 pass.set_index_buffer(
3326 cached_mesh.index_buffer.slice(..),
3327 wgpu::IndexFormat::Uint32,
3328 );
3329 pass.draw_indexed(0..cached_mesh.index_count, 0, 0..1);
3330 } else {
3331 let vertices = build_model_vertices(&instance.mesh);
3332 let vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3333 label: Some("model_inline_vb"),
3334 contents: bytemuck::cast_slice(&vertices),
3335 usage: wgpu::BufferUsages::VERTEX,
3336 });
3337 let ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3338 label: Some("model_inline_ib"),
3339 contents: bytemuck::cast_slice(&instance.mesh.indices),
3340 usage: wgpu::BufferUsages::INDEX,
3341 });
3342 let index_count = instance.mesh.indices.len() as u32;
3343 pass.set_bind_group(1, &cached.bind_group, &[dyn_offset]);
3344 pass.set_vertex_buffer(0, vb.slice(..));
3345 pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
3346 pass.draw_indexed(0..index_count, 0, 0..1);
3347 }
3348 }
3349 }
3350
3351 pub fn cache_model_meshes(&mut self, device: &wgpu::Device, model_instances: &[ModelInstance]) {
3357 for instance in model_instances {
3358 let key = ModelMeshKey::from_mesh(&instance.mesh);
3359 if self.model_mesh_cache.contains_key(&key) {
3360 continue;
3361 }
3362
3363 let vertices = build_model_vertices(&instance.mesh);
3364 let vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3365 label: Some("cached_model_vb"),
3366 contents: bytemuck::cast_slice(&vertices),
3367 usage: wgpu::BufferUsages::VERTEX,
3368 });
3369 let ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3370 label: Some("cached_model_ib"),
3371 contents: bytemuck::cast_slice(&instance.mesh.indices),
3372 usage: wgpu::BufferUsages::INDEX,
3373 });
3374
3375 self.model_mesh_cache.insert(
3376 key,
3377 CachedModelMesh {
3378 vertex_buffer: vb,
3379 index_buffer: ib,
3380 index_count: instance.mesh.indices.len() as u32,
3381 },
3382 );
3383 }
3384 }
3385
3386 fn cache_model_transforms(
3395 &mut self,
3396 device: &wgpu::Device,
3397 model_instances: &[ModelInstance],
3398 camera_origin: DVec3,
3399 state: &MapState,
3400 ) {
3401 let min_align = device.limits().min_uniform_buffer_offset_alignment as usize;
3402 let stride = 64_usize.div_ceil(min_align) * min_align;
3403
3404 let mut fp: u64 = model_instances.len() as u64;
3406 let origin_key = [
3407 (camera_origin.x * 100.0) as i64,
3408 (camera_origin.y * 100.0) as i64,
3409 (camera_origin.z * 100.0) as i64,
3410 ];
3411 fp = fp
3412 .wrapping_mul(31)
3413 .wrapping_add(origin_key[0] as u64)
3414 .wrapping_mul(31)
3415 .wrapping_add(origin_key[1] as u64)
3416 .wrapping_mul(31)
3417 .wrapping_add(origin_key[2] as u64);
3418 for instance in model_instances {
3419 fp = fp
3420 .wrapping_mul(31)
3421 .wrapping_add(instance.position.lat.to_bits())
3422 .wrapping_mul(31)
3423 .wrapping_add(instance.position.lon.to_bits())
3424 .wrapping_mul(31)
3425 .wrapping_add(instance.scale.to_bits())
3426 .wrapping_mul(31)
3427 .wrapping_add(instance.heading.to_bits());
3428 }
3429
3430 if let Some(ref cached) = self.cached_model_transforms {
3431 if cached.fingerprint == fp && cached.instance_count == model_instances.len() {
3432 return;
3433 }
3434 }
3435
3436 let mut transform_bytes = vec![0u8; stride * model_instances.len()];
3437 for (i, instance) in model_instances.iter().enumerate() {
3438 let terrain_elev = state.elevation_at(&instance.position);
3439 let altitude = instance.resolve_altitude(terrain_elev);
3440
3441 let world_pos = state.camera().projection().project(&instance.position);
3442 let rel_x = (world_pos.position.x - camera_origin.x) as f32;
3443 let rel_y = (world_pos.position.y - camera_origin.y) as f32;
3444 let rel_z = (altitude - camera_origin.z) as f32;
3445
3446 let scale = instance.scale as f32;
3447 let heading = instance.heading as f32;
3448 let pitch = instance.pitch as f32;
3449 let roll = instance.roll as f32;
3450
3451 let rotation = glam::Quat::from_rotation_z(heading)
3452 * glam::Quat::from_rotation_x(pitch)
3453 * glam::Quat::from_rotation_y(roll);
3454 let transform = Mat4::from_translation(glam::Vec3::new(rel_x, rel_y, rel_z))
3455 * Mat4::from_quat(rotation)
3456 * Mat4::from_scale(glam::Vec3::splat(scale));
3457
3458 let mat = transform.to_cols_array_2d();
3459 let mat_bytes = bytemuck::cast_slice(&mat);
3460 let offset = i * stride;
3461 transform_bytes[offset..offset + 64].copy_from_slice(mat_bytes);
3462 }
3463
3464 let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3465 label: Some("model_transforms_cached_buf"),
3466 contents: &transform_bytes,
3467 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3468 });
3469
3470 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3471 label: Some("model_transforms_cached_bg"),
3472 layout: &self.model_pipeline.model_bind_group_layout,
3473 entries: &[wgpu::BindGroupEntry {
3474 binding: 0,
3475 resource: buffer.as_entire_binding(),
3476 }],
3477 });
3478
3479 self.cached_model_transforms = Some(CachedModelTransforms {
3480 buffer,
3481 bind_group,
3482 stride,
3483 instance_count: model_instances.len(),
3484 fingerprint: fp,
3485 })
3486 }
3487
3488 fn rebuild_page_bind_groups(&mut self, device: &wgpu::Device) {
3492 let tile_pages = self.tile_atlas.page_count();
3493 while self.page_bind_groups.len() < tile_pages {
3494 let idx = self.page_bind_groups.len();
3495 let view = &self.tile_atlas.pages[idx].view;
3496 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
3497 label: Some(&format!("rustial_tile_page_bg_{idx}")),
3498 layout: &self.tile_pipeline.texture_bind_group_layout,
3499 entries: &[
3500 wgpu::BindGroupEntry {
3501 binding: 0,
3502 resource: wgpu::BindingResource::TextureView(view),
3503 },
3504 wgpu::BindGroupEntry {
3505 binding: 1,
3506 resource: wgpu::BindingResource::Sampler(&self.sampler),
3507 },
3508 ],
3509 });
3510 self.page_bind_groups.push(bg);
3511
3512 let terrain_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
3513 label: Some(&format!("rustial_terrain_page_bg_{idx}")),
3514 layout: &self.terrain_pipeline.texture_bind_group_layout,
3515 entries: &[
3516 wgpu::BindGroupEntry {
3517 binding: 0,
3518 resource: wgpu::BindingResource::TextureView(view),
3519 },
3520 wgpu::BindGroupEntry {
3521 binding: 1,
3522 resource: wgpu::BindingResource::Sampler(&self.sampler),
3523 },
3524 ],
3525 });
3526 self.page_terrain_bind_groups.push(terrain_bg);
3527 }
3528
3529 let hillshade_pages = self.hillshade_atlas.page_count();
3530 while self.page_hillshade_bind_groups.len() < hillshade_pages {
3531 let idx = self.page_hillshade_bind_groups.len();
3532 let view = &self.hillshade_atlas.pages[idx].view;
3533 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
3534 label: Some(&format!("rustial_hillshade_page_bg_{idx}")),
3535 layout: &self.hillshade_pipeline.texture_bind_group_layout,
3536 entries: &[
3537 wgpu::BindGroupEntry {
3538 binding: 0,
3539 resource: wgpu::BindingResource::TextureView(view),
3540 },
3541 wgpu::BindGroupEntry {
3542 binding: 1,
3543 resource: wgpu::BindingResource::Sampler(&self.sampler),
3544 },
3545 ],
3546 });
3547 self.page_hillshade_bind_groups.push(bg);
3548 }
3549 }
3550
3551 fn get_or_create_shared_grid(
3554 &mut self,
3555 device: &wgpu::Device,
3556 resolution: u16,
3557 ) -> &SharedTerrainGridMesh {
3558 self.shared_terrain_grids
3559 .entry(resolution)
3560 .or_insert_with(|| {
3561 let (vertices, indices) = build_shared_terrain_grid(resolution as usize);
3562 let vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3563 label: Some(&format!("terrain_grid_vb_{resolution}")),
3564 contents: bytemuck::cast_slice(&vertices),
3565 usage: wgpu::BufferUsages::VERTEX,
3566 });
3567 let ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3568 label: Some(&format!("terrain_grid_ib_{resolution}")),
3569 contents: bytemuck::cast_slice(&indices),
3570 usage: wgpu::BufferUsages::INDEX,
3571 });
3572 SharedTerrainGridMesh {
3573 vertex_buffer: vb,
3574 index_buffer: ib,
3575 index_count: indices.len() as u32,
3576 }
3577 });
3578 #[allow(clippy::unwrap_used)] self.shared_terrain_grids.get(&resolution).unwrap()
3580 }
3581
3582 fn get_or_create_terrain_tile_bind(
3583 &mut self,
3584 device: &wgpu::Device,
3585 queue: &wgpu::Queue,
3586 mesh: &TerrainMeshData,
3587 state: &MapState,
3588 scene_origin: DVec3,
3589 pipeline_kind: TerrainPipelineKind,
3590 ) -> Option<()> {
3591 let elevation = mesh.elevation_texture.as_ref()?;
3592 let key = TerrainTileBindKey {
3593 tile: mesh.tile,
3594 pipeline: pipeline_kind,
3595 };
3596 let origin_key = [
3597 (scene_origin.x * 100.0) as i64,
3598 (scene_origin.y * 100.0) as i64,
3599 (scene_origin.z * 100.0) as i64,
3600 ];
3601
3602 if let Some(cached) = self.terrain_tile_bind_cache.get(&key) {
3604 if cached.origin_key == origin_key && cached.generation == mesh.generation {
3605 return Some(());
3606 }
3607 }
3608
3609 let tile = mesh.tile;
3611 let gen = mesh.generation;
3612 let needs_height = self
3613 .height_texture_cache
3614 .get(&tile)
3615 .is_none_or(|c| c.generation != gen);
3616 if needs_height {
3617 let size = wgpu::Extent3d {
3618 width: elevation.width.max(1),
3619 height: elevation.height.max(1),
3620 depth_or_array_layers: 1,
3621 };
3622 let texture = device.create_texture(&wgpu::TextureDescriptor {
3623 label: Some(&format!("height_tex_{:?}", tile)),
3624 size,
3625 mip_level_count: 1,
3626 sample_count: 1,
3627 dimension: wgpu::TextureDimension::D2,
3628 format: wgpu::TextureFormat::R32Float,
3629 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3630 view_formats: &[],
3631 });
3632 queue.write_texture(
3633 wgpu::TexelCopyTextureInfo {
3634 texture: &texture,
3635 mip_level: 0,
3636 origin: wgpu::Origin3d::ZERO,
3637 aspect: wgpu::TextureAspect::All,
3638 },
3639 bytemuck::cast_slice(&elevation.data),
3640 wgpu::TexelCopyBufferLayout {
3641 offset: 0,
3642 bytes_per_row: Some(elevation.width.max(1) * 4),
3643 rows_per_image: None,
3644 },
3645 size,
3646 );
3647 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3648 self.height_texture_cache.insert(
3649 tile,
3650 CachedHeightTexture {
3651 generation: gen,
3652 view,
3653 },
3654 );
3655 }
3656
3657 let tile_uniform = build_terrain_tile_uniform(mesh, elevation, state, scene_origin);
3659
3660 let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3661 label: Some(&format!(
3662 "terrain_tile_uniform_{:?}_{:?}",
3663 mesh.tile, pipeline_kind
3664 )),
3665 contents: bytemuck::bytes_of(&tile_uniform),
3666 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3667 });
3668
3669 let layout = match pipeline_kind {
3670 TerrainPipelineKind::Terrain => &self.terrain_pipeline.tile_bind_group_layout,
3671 TerrainPipelineKind::TerrainData => &self.terrain_data_pipeline.tile_bind_group_layout,
3672 TerrainPipelineKind::Hillshade => &self.hillshade_pipeline.tile_bind_group_layout,
3673 };
3674
3675 let height_view_ref = &self.height_texture_cache.get(&tile)?.view;
3677 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3678 label: Some(&format!(
3679 "terrain_tile_bg_{:?}_{:?}",
3680 mesh.tile, pipeline_kind
3681 )),
3682 layout,
3683 entries: &[
3684 wgpu::BindGroupEntry {
3685 binding: 0,
3686 resource: uniform_buffer.as_entire_binding(),
3687 },
3688 wgpu::BindGroupEntry {
3689 binding: 1,
3690 resource: wgpu::BindingResource::TextureView(height_view_ref),
3691 },
3692 ],
3693 });
3694
3695 self.terrain_tile_bind_cache.insert(
3696 key,
3697 CachedTerrainTileBind {
3698 uniform_buffer,
3699 bind_group,
3700 origin_key,
3701 generation: mesh.generation,
3702 },
3703 );
3704 Some(())
3705 }
3706
3707 fn render_shared_terrain_tiles<'a>(
3708 &'a self,
3709 pass: &mut wgpu::RenderPass<'a>,
3710 _state: &MapState,
3711 terrain_meshes: &[TerrainMeshData],
3712 visible_tiles: &[VisibleTile],
3713 ) {
3714 pass.set_pipeline(&self.terrain_pipeline.pipeline);
3715 pass.set_bind_group(0, &self.terrain_uniform_bind_group, &[]);
3716
3717 for mesh in terrain_meshes {
3718 let grid = match self.shared_terrain_grids.get(&mesh.grid_resolution) {
3719 Some(g) => g,
3720 None => continue,
3721 };
3722
3723 if let Some(actual) = find_terrain_texture_actual(mesh.tile, visible_tiles) {
3724 if let Some(region) = self.tile_atlas.get(&actual) {
3725 if let Some(bg) = self.page_terrain_bind_groups.get(region.page) {
3726 pass.set_bind_group(1, bg, &[]);
3727 }
3728 }
3729 }
3730
3731 let key = TerrainTileBindKey {
3732 tile: mesh.tile,
3733 pipeline: TerrainPipelineKind::Terrain,
3734 };
3735 if let Some(cached) = self.terrain_tile_bind_cache.get(&key) {
3736 pass.set_bind_group(2, &cached.bind_group, &[]);
3737 pass.set_vertex_buffer(0, grid.vertex_buffer.slice(..));
3738 pass.set_index_buffer(grid.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3739 pass.draw_indexed(0..grid.index_count, 0, 0..1);
3740 }
3741 }
3742 }
3743
3744 fn render_shared_terrain_data_tiles<'a>(
3745 &'a self,
3746 pass: &mut wgpu::RenderPass<'a>,
3747 _state: &MapState,
3748 terrain_meshes: &[TerrainMeshData],
3749 ) {
3750 pass.set_pipeline(&self.terrain_data_pipeline.pipeline);
3751 pass.set_bind_group(0, &self.terrain_data_uniform_bind_group, &[]);
3752
3753 for mesh in terrain_meshes {
3754 let grid = match self.shared_terrain_grids.get(&mesh.grid_resolution) {
3755 Some(g) => g,
3756 None => continue,
3757 };
3758
3759 let key = TerrainTileBindKey {
3760 tile: mesh.tile,
3761 pipeline: TerrainPipelineKind::TerrainData,
3762 };
3763 if let Some(cached) = self.terrain_tile_bind_cache.get(&key) {
3764 pass.set_bind_group(1, &cached.bind_group, &[]);
3765 pass.set_vertex_buffer(0, grid.vertex_buffer.slice(..));
3766 pass.set_index_buffer(grid.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3767 pass.draw_indexed(0..grid.index_count, 0, 0..1);
3768 }
3769 }
3770 }
3771
3772 fn render_shared_hillshade_tiles<'a>(
3773 &'a self,
3774 pass: &mut wgpu::RenderPass<'a>,
3775 _state: &MapState,
3776 terrain_meshes: &[TerrainMeshData],
3777 ) {
3778 pass.set_pipeline(&self.hillshade_pipeline.pipeline);
3779 pass.set_bind_group(0, &self.hillshade_uniform_bind_group, &[]);
3780
3781 for mesh in terrain_meshes {
3782 let grid = match self.shared_terrain_grids.get(&mesh.grid_resolution) {
3783 Some(g) => g,
3784 None => continue,
3785 };
3786
3787 if let Some(region) = self.hillshade_atlas.get(&mesh.tile) {
3788 if let Some(bg) = self.page_hillshade_bind_groups.get(region.page) {
3789 pass.set_bind_group(1, bg, &[]);
3790 }
3791 }
3792
3793 let key = TerrainTileBindKey {
3794 tile: mesh.tile,
3795 pipeline: TerrainPipelineKind::Hillshade,
3796 };
3797 if let Some(cached) = self.terrain_tile_bind_cache.get(&key) {
3798 pass.set_bind_group(2, &cached.bind_group, &[]);
3799 pass.set_vertex_buffer(0, grid.vertex_buffer.slice(..));
3800 pass.set_index_buffer(grid.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3801 pass.draw_indexed(0..grid.index_count, 0, 0..1);
3802 }
3803 }
3804 }
3805
3806 fn prune_height_texture_cache(&mut self, terrain_meshes: &[TerrainMeshData]) {
3809 let live: std::collections::HashSet<TileId> =
3810 terrain_meshes.iter().map(|m| m.tile).collect();
3811 self.height_texture_cache
3812 .retain(|tile, _| live.contains(tile));
3813 }
3814
3815 fn prune_terrain_tile_bind_cache(&mut self, terrain_meshes: &[TerrainMeshData]) {
3816 let live: std::collections::HashSet<TileId> =
3817 terrain_meshes.iter().map(|m| m.tile).collect();
3818 self.terrain_tile_bind_cache
3819 .retain(|key, _| live.contains(&key.tile));
3820 }
3821
3822 fn prune_grid_scalar_overlay_cache(&mut self, overlays: &[&VisualizationOverlay]) {
3823 let live: std::collections::HashSet<LayerId> = overlays
3824 .iter()
3825 .filter_map(|overlay| match overlay {
3826 VisualizationOverlay::GridScalar { layer_id, .. } => Some(*layer_id),
3827 _ => None,
3828 })
3829 .collect();
3830 self.grid_scalar_overlay_cache
3831 .retain(|layer_id, _| live.contains(layer_id));
3832 }
3833
3834 fn prune_grid_extrusion_overlay_cache(&mut self, overlays: &[&VisualizationOverlay]) {
3835 let live: std::collections::HashSet<LayerId> = overlays
3836 .iter()
3837 .filter_map(|overlay| match overlay {
3838 VisualizationOverlay::GridExtrusion { layer_id, .. } => Some(*layer_id),
3839 _ => None,
3840 })
3841 .collect();
3842 self.grid_extrusion_overlay_cache
3843 .retain(|layer_id, _| live.contains(layer_id));
3844 }
3845
3846 fn prune_column_overlay_cache(&mut self, overlays: &[&VisualizationOverlay]) {
3847 let live: std::collections::HashSet<LayerId> = overlays
3848 .iter()
3849 .filter_map(|overlay| match overlay {
3850 VisualizationOverlay::Columns { layer_id, .. } => Some(*layer_id),
3851 _ => None,
3852 })
3853 .collect();
3854 self.column_overlay_cache
3855 .retain(|layer_id, _| live.contains(layer_id));
3856 }
3857
3858 fn prune_point_cloud_overlay_cache(&mut self, overlays: &[&VisualizationOverlay]) {
3859 let live: std::collections::HashSet<LayerId> = overlays
3860 .iter()
3861 .filter_map(|overlay| match overlay {
3862 VisualizationOverlay::Points { layer_id, .. } => Some(*layer_id),
3863 _ => None,
3864 })
3865 .collect();
3866 self.point_cloud_overlay_cache
3867 .retain(|layer_id, _| live.contains(layer_id));
3868 }
3869
3870 pub fn render_to_buffer(
3894 &mut self,
3895 state: &MapState,
3896 device: &wgpu::Device,
3897 queue: &wgpu::Queue,
3898 visible_tiles: &[VisibleTile],
3899 vector_meshes: &[VectorMeshData],
3900 model_instances: &[ModelInstance],
3901 ) -> Option<Vec<u8>> {
3902 let format = wgpu::TextureFormat::Rgba8UnormSrgb;
3903
3904 let color_tex = device.create_texture(&wgpu::TextureDescriptor {
3905 label: Some("rustial_render_to_buffer_color"),
3906 size: wgpu::Extent3d {
3907 width: self.width,
3908 height: self.height,
3909 depth_or_array_layers: 1,
3910 },
3911 mip_level_count: 1,
3912 sample_count: 1,
3913 dimension: wgpu::TextureDimension::D2,
3914 format,
3915 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3916 view_formats: &[],
3917 });
3918 let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
3919
3920 let clear_color = state.computed_fog().clear_color;
3921
3922 self.render_full(&RenderParams {
3923 state,
3924 device,
3925 queue,
3926 color_view: &color_view,
3927 visible_tiles,
3928 vector_meshes,
3929 model_instances,
3930 clear_color,
3931 });
3932
3933 let bytes_per_row = self.width * 4;
3934 let buffer_size = (bytes_per_row * self.height) as wgpu::BufferAddress;
3935 let readback = device.create_buffer(&wgpu::BufferDescriptor {
3936 label: Some("rustial_render_to_buffer_readback"),
3937 size: buffer_size,
3938 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
3939 mapped_at_creation: false,
3940 });
3941
3942 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
3943 label: Some("rustial_render_to_buffer_encoder"),
3944 });
3945 encoder.copy_texture_to_buffer(
3946 wgpu::TexelCopyTextureInfo {
3947 texture: &color_tex,
3948 mip_level: 0,
3949 origin: wgpu::Origin3d::ZERO,
3950 aspect: wgpu::TextureAspect::All,
3951 },
3952 wgpu::TexelCopyBufferInfo {
3953 buffer: &readback,
3954 layout: wgpu::TexelCopyBufferLayout {
3955 offset: 0,
3956 bytes_per_row: Some(bytes_per_row),
3957 rows_per_image: Some(self.height),
3958 },
3959 },
3960 wgpu::Extent3d {
3961 width: self.width,
3962 height: self.height,
3963 depth_or_array_layers: 1,
3964 },
3965 );
3966 queue.submit(std::iter::once(encoder.finish()));
3967
3968 let slice = readback.slice(..);
3969 let (tx, rx) = std::sync::mpsc::channel();
3970 slice.map_async(wgpu::MapMode::Read, move |res| {
3971 let _ = tx.send(res);
3972 });
3973 let _ = device.poll(wgpu::PollType::wait());
3974 rx.recv().ok()?.ok()?;
3975
3976 let data = slice.get_mapped_range().to_vec();
3977 readback.unmap();
3978 Some(data)
3979 }
3980
3981 pub fn width(&self) -> u32 {
3983 self.width
3984 }
3985
3986 pub fn height(&self) -> u32 {
3988 self.height
3989 }
3990
3991 pub fn visualization_perf_stats(&self) -> VisualizationPerfStats {
3993 self.visualization_perf_stats
3994 }
3995
3996 pub fn tile_atlas_diagnostics(&self) -> crate::gpu::tile_atlas::AtlasDiagnostics {
4002 self.tile_atlas.diagnostics()
4003 }
4004
4005 pub fn hillshade_atlas_diagnostics(&self) -> crate::gpu::tile_atlas::AtlasDiagnostics {
4007 self.hillshade_atlas.diagnostics()
4008 }
4009}
4010
4011fn build_grid_scalar_geometry(
4016 grid: &rustial_engine::GeoGrid,
4017 state: &MapState,
4018 scene_origin: DVec3,
4019) -> (Vec<GridScalarVertex>, Vec<u32>) {
4020 let rows = grid.rows.max(1);
4021 let cols = grid.cols.max(1);
4022 let mut vertices = Vec::with_capacity((rows + 1) * (cols + 1));
4023 let mut indices = Vec::with_capacity(rows * cols * 6);
4024
4025 for row in 0..=rows {
4026 for col in 0..=cols {
4027 let u = col as f32 / cols as f32;
4028 let v = row as f32 / rows as f32;
4029 let coord = grid_corner_coord(grid, row, col, state);
4030 let projected = state.camera().projection().project(&coord);
4031 vertices.push(GridScalarVertex {
4032 position: [
4033 (projected.position.x - scene_origin.x) as f32,
4034 (projected.position.y - scene_origin.y) as f32,
4035 (projected.position.z - scene_origin.z + 0.05) as f32,
4036 ],
4037 uv: [u, v],
4038 });
4039 }
4040 }
4041
4042 for row in 0..rows {
4043 for col in 0..cols {
4044 let tl = (row * (cols + 1) + col) as u32;
4045 let tr = tl + 1;
4046 let bl = ((row + 1) * (cols + 1) + col) as u32;
4047 let br = bl + 1;
4048 indices.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
4049 }
4050 }
4051
4052 (vertices, indices)
4053}
4054
4055fn grid_corner_coord(
4056 grid: &rustial_engine::GeoGrid,
4057 row: usize,
4058 col: usize,
4059 state: &MapState,
4060) -> rustial_math::GeoCoord {
4061 let dx = col as f64 * grid.cell_width;
4062 let dy = row as f64 * grid.cell_height;
4063 let (sin_r, cos_r) = grid.rotation.sin_cos();
4064 let rx = dx * cos_r - dy * sin_r;
4065 let ry = dx * sin_r + dy * cos_r;
4066 let coord = offset_geo_coord(&grid.origin, rx, ry);
4067 let altitude = resolve_grid_surface_altitude(grid, &coord, state);
4068 rustial_math::GeoCoord::new(coord.lat, coord.lon, altitude)
4069}
4070
4071fn create_grid_scalar_texture(
4072 device: &wgpu::Device,
4073 queue: &wgpu::Queue,
4074 field: &rustial_engine::ScalarField2D,
4075) -> wgpu::Texture {
4076 let size = wgpu::Extent3d {
4077 width: field.cols.max(1) as u32,
4078 height: field.rows.max(1) as u32,
4079 depth_or_array_layers: 1,
4080 };
4081 let texture = device.create_texture(&wgpu::TextureDescriptor {
4082 label: Some("grid_scalar_field_texture"),
4083 size,
4084 mip_level_count: 1,
4085 sample_count: 1,
4086 dimension: wgpu::TextureDimension::D2,
4087 format: wgpu::TextureFormat::R32Float,
4088 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4089 view_formats: &[],
4090 });
4091 write_grid_scalar_texture(queue, &texture, field);
4092 texture
4093}
4094
4095fn write_grid_scalar_texture(
4096 queue: &wgpu::Queue,
4097 texture: &wgpu::Texture,
4098 field: &rustial_engine::ScalarField2D,
4099) {
4100 let size = wgpu::Extent3d {
4101 width: field.cols.max(1) as u32,
4102 height: field.rows.max(1) as u32,
4103 depth_or_array_layers: 1,
4104 };
4105 queue.write_texture(
4106 wgpu::TexelCopyTextureInfo {
4107 texture,
4108 mip_level: 0,
4109 origin: wgpu::Origin3d::ZERO,
4110 aspect: wgpu::TextureAspect::All,
4111 },
4112 bytemuck::cast_slice(&field.data),
4113 wgpu::TexelCopyBufferLayout {
4114 offset: 0,
4115 bytes_per_row: Some(field.cols.max(1) as u32 * 4),
4116 rows_per_image: Some(field.rows.max(1) as u32),
4117 },
4118 size,
4119 );
4120}
4121
4122fn create_grid_scalar_ramp_texture(
4123 device: &wgpu::Device,
4124 queue: &wgpu::Queue,
4125 ramp: &rustial_engine::ColorRamp,
4126) -> wgpu::Texture {
4127 let width = 256u32;
4128 let data = ramp.as_texture_data(width);
4129 let size = wgpu::Extent3d {
4130 width,
4131 height: 1,
4132 depth_or_array_layers: 1,
4133 };
4134 let texture = device.create_texture(&wgpu::TextureDescriptor {
4135 label: Some("grid_scalar_ramp_texture"),
4136 size,
4137 mip_level_count: 1,
4138 sample_count: 1,
4139 dimension: wgpu::TextureDimension::D2,
4140 format: wgpu::TextureFormat::Rgba8UnormSrgb,
4141 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4142 view_formats: &[],
4143 });
4144 queue.write_texture(
4145 wgpu::TexelCopyTextureInfo {
4146 texture: &texture,
4147 mip_level: 0,
4148 origin: wgpu::Origin3d::ZERO,
4149 aspect: wgpu::TextureAspect::All,
4150 },
4151 &data,
4152 wgpu::TexelCopyBufferLayout {
4153 offset: 0,
4154 bytes_per_row: Some(width * 4),
4155 rows_per_image: Some(1),
4156 },
4157 size,
4158 );
4159 texture
4160}
4161
4162fn create_heatmap_accum_texture(
4168 device: &wgpu::Device,
4169 width: u32,
4170 height: u32,
4171) -> (wgpu::Texture, wgpu::TextureView) {
4172 let texture = device.create_texture(&wgpu::TextureDescriptor {
4173 label: Some("heatmap_accum_texture"),
4174 size: wgpu::Extent3d {
4175 width,
4176 height,
4177 depth_or_array_layers: 1,
4178 },
4179 mip_level_count: 1,
4180 sample_count: 1,
4181 dimension: wgpu::TextureDimension::D2,
4182 format: wgpu::TextureFormat::R16Float,
4183 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
4184 view_formats: &[],
4185 });
4186 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
4187 (texture, view)
4188}
4189
4190#[allow(clippy::needless_range_loop)]
4195fn create_default_heatmap_ramp_texture(
4196 device: &wgpu::Device,
4197 queue: &wgpu::Queue,
4198) -> wgpu::Texture {
4199 const WIDTH: u32 = 256;
4200 let stops: &[(f32, [u8; 4])] = &[
4201 (0.00, [0, 0, 0, 0]),
4202 (0.10, [65, 105, 225, 255]),
4203 (0.30, [0, 255, 255, 255]),
4204 (0.50, [0, 255, 0, 255]),
4205 (0.70, [255, 255, 0, 255]),
4206 (1.00, [255, 0, 0, 255]),
4207 ];
4208
4209 let mut data = vec![0u8; WIDTH as usize * 4];
4210 for i in 0..WIDTH as usize {
4211 let t = i as f32 / (WIDTH - 1) as f32;
4212 let mut lo = 0;
4214 for s in 1..stops.len() {
4215 if stops[s].0 >= t {
4216 lo = s - 1;
4217 break;
4218 }
4219 }
4220 let hi = (lo + 1).min(stops.len() - 1);
4221 let range = stops[hi].0 - stops[lo].0;
4222 let frac = if range > 0.0 {
4223 (t - stops[lo].0) / range
4224 } else {
4225 0.0
4226 };
4227 for c in 0..4 {
4228 let a = stops[lo].1[c] as f32;
4229 let b = stops[hi].1[c] as f32;
4230 data[i * 4 + c] = (a + (b - a) * frac).round() as u8;
4231 }
4232 }
4233
4234 let size = wgpu::Extent3d {
4235 width: WIDTH,
4236 height: 1,
4237 depth_or_array_layers: 1,
4238 };
4239 let texture = device.create_texture(&wgpu::TextureDescriptor {
4240 label: Some("heatmap_ramp_texture"),
4241 size,
4242 mip_level_count: 1,
4243 sample_count: 1,
4244 dimension: wgpu::TextureDimension::D2,
4245 format: wgpu::TextureFormat::Rgba8Unorm,
4246 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4247 view_formats: &[],
4248 });
4249 queue.write_texture(
4250 wgpu::TexelCopyTextureInfo {
4251 texture: &texture,
4252 mip_level: 0,
4253 origin: wgpu::Origin3d::ZERO,
4254 aspect: wgpu::TextureAspect::All,
4255 },
4256 &data,
4257 wgpu::TexelCopyBufferLayout {
4258 offset: 0,
4259 bytes_per_row: Some(WIDTH * 4),
4260 rows_per_image: Some(1),
4261 },
4262 size,
4263 );
4264 texture
4265}
4266
4267fn create_heatmap_colormap_bind_group(
4269 device: &wgpu::Device,
4270 layout: &wgpu::BindGroupLayout,
4271 accum_view: &wgpu::TextureView,
4272 ramp_view: &wgpu::TextureView,
4273 sampler: &wgpu::Sampler,
4274) -> wgpu::BindGroup {
4275 device.create_bind_group(&wgpu::BindGroupDescriptor {
4276 label: Some("heatmap_colormap_textures_bg"),
4277 layout,
4278 entries: &[
4279 wgpu::BindGroupEntry {
4280 binding: 0,
4281 resource: wgpu::BindingResource::TextureView(accum_view),
4282 },
4283 wgpu::BindGroupEntry {
4284 binding: 1,
4285 resource: wgpu::BindingResource::TextureView(ramp_view),
4286 },
4287 wgpu::BindGroupEntry {
4288 binding: 2,
4289 resource: wgpu::BindingResource::Sampler(sampler),
4290 },
4291 ],
4292 })
4293}
4294
4295fn build_grid_scalar_uniform(
4296 grid: &rustial_engine::GeoGrid,
4297 field: &rustial_engine::ScalarField2D,
4298 state: &MapState,
4299 scene_origin: DVec3,
4300 opacity: f32,
4301) -> GridScalarUniform {
4302 let projection_kind = match state.camera().projection() {
4303 rustial_engine::CameraProjection::WebMercator => 0.0,
4304 rustial_engine::CameraProjection::Equirectangular => 1.0,
4305 _ => 0.0,
4306 };
4307 let base_altitude = match grid.altitude_mode {
4308 rustial_engine::AltitudeMode::ClampToGround => 0.0,
4309 rustial_engine::AltitudeMode::RelativeToGround => grid.origin.alt as f32,
4310 rustial_engine::AltitudeMode::Absolute => grid.origin.alt as f32,
4311 };
4312 GridScalarUniform {
4313 origin_counts: [
4314 grid.origin.lat as f32,
4315 grid.origin.lon as f32,
4316 grid.rows as f32,
4317 grid.cols as f32,
4318 ],
4319 grid_params: [
4320 grid.cell_width as f32,
4321 grid.cell_height as f32,
4322 grid.rotation as f32,
4323 opacity,
4324 ],
4325 scene_origin: [
4326 scene_origin.x as f32,
4327 scene_origin.y as f32,
4328 scene_origin.z as f32,
4329 projection_kind,
4330 ],
4331 value_params: [
4332 field.min,
4333 field.max,
4334 field.nan_value.unwrap_or(0.0),
4335 if field.nan_value.is_some() { 1.0 } else { 0.0 },
4336 ],
4337 base_altitude: [base_altitude, 0.0, 0.0, 0.0],
4338 }
4339}
4340
4341fn grid_scalar_ramp_fingerprint(ramp: &rustial_engine::ColorRamp) -> u64 {
4342 let mut h = ramp.stops.len() as u64;
4343 for stop in &ramp.stops {
4344 h = h
4345 .wrapping_mul(31)
4346 .wrapping_add(stop.value.to_bits() as u64)
4347 .wrapping_mul(31)
4348 .wrapping_add(stop.color[0].to_bits() as u64)
4349 .wrapping_mul(31)
4350 .wrapping_add(stop.color[1].to_bits() as u64)
4351 .wrapping_mul(31)
4352 .wrapping_add(stop.color[2].to_bits() as u64)
4353 .wrapping_mul(31)
4354 .wrapping_add(stop.color[3].to_bits() as u64);
4355 }
4356 h
4357}
4358
4359fn grid_extrusion_params_fingerprint(params: &rustial_engine::ExtrusionParams) -> u64 {
4360 (params.height_scale.to_bits())
4361 .wrapping_mul(31)
4362 .wrapping_add(params.base_meters.to_bits())
4363}
4364
4365fn grid_extrusion_grid_fingerprint(grid: &rustial_engine::GeoGrid) -> u64 {
4366 let mut h = 17u64;
4367 h = h.wrapping_mul(31).wrapping_add(grid.origin.lat.to_bits());
4368 h = h.wrapping_mul(31).wrapping_add(grid.origin.lon.to_bits());
4369 h = h.wrapping_mul(31).wrapping_add(grid.origin.alt.to_bits());
4370 h = h.wrapping_mul(31).wrapping_add(grid.rows as u64);
4371 h = h.wrapping_mul(31).wrapping_add(grid.cols as u64);
4372 h = h.wrapping_mul(31).wrapping_add(grid.cell_width.to_bits());
4373 h = h.wrapping_mul(31).wrapping_add(grid.cell_height.to_bits());
4374 h = h.wrapping_mul(31).wrapping_add(grid.rotation.to_bits());
4375 h = h.wrapping_mul(31).wrapping_add(match grid.altitude_mode {
4376 rustial_engine::AltitudeMode::ClampToGround => 0,
4377 rustial_engine::AltitudeMode::RelativeToGround => 1,
4378 rustial_engine::AltitudeMode::Absolute => 2,
4379 });
4380 h
4381}
4382
4383fn build_grid_extrusion_geometry(
4384 grid: &rustial_engine::GeoGrid,
4385 field: &rustial_engine::ScalarField2D,
4386 ramp: &rustial_engine::ColorRamp,
4387 params: &rustial_engine::ExtrusionParams,
4388 state: &MapState,
4389 scene_origin: DVec3,
4390) -> (Vec<GridExtrusionVertex>, Vec<u32>) {
4391 let mut vertices = Vec::new();
4392 let mut indices = Vec::new();
4393
4394 for row in 0..grid.rows {
4395 for col in 0..grid.cols {
4396 let Some(value) = field.sample(row, col) else {
4397 continue;
4398 };
4399
4400 let t = field.normalized(row, col).unwrap_or(0.5);
4401 let color = ramp.evaluate(t);
4402 let corners = grid_cell_corners_world(grid, row, col, state, scene_origin, params);
4403 append_extruded_cell_geometry(
4404 &mut vertices,
4405 &mut indices,
4406 corners,
4407 value * params.height_scale as f32,
4408 color,
4409 );
4410 }
4411 }
4412
4413 (vertices, indices)
4414}
4415
4416fn append_extruded_cell_geometry(
4417 vertices: &mut Vec<GridExtrusionVertex>,
4418 indices: &mut Vec<u32>,
4419 corners: [[f32; 3]; 4],
4420 extrusion_height: f32,
4421 color: [f32; 4],
4422) {
4423 let [nw, ne, sw, se] = corners;
4424 let top = [
4425 [nw[0], nw[1], nw[2] + extrusion_height],
4426 [ne[0], ne[1], ne[2] + extrusion_height],
4427 [sw[0], sw[1], sw[2] + extrusion_height],
4428 [se[0], se[1], se[2] + extrusion_height],
4429 ];
4430 let base = [nw, ne, sw, se];
4431
4432 append_quad(
4433 vertices,
4434 indices,
4435 top[0],
4436 top[1],
4437 top[2],
4438 top[3],
4439 [0.0, 0.0, 1.0],
4440 color,
4441 );
4442 append_quad(
4443 vertices,
4444 indices,
4445 base[0],
4446 base[1],
4447 top[0],
4448 top[1],
4449 [0.0, -1.0, 0.0],
4450 color,
4451 );
4452 append_quad(
4453 vertices,
4454 indices,
4455 top[2],
4456 top[3],
4457 base[2],
4458 base[3],
4459 [0.0, 1.0, 0.0],
4460 color,
4461 );
4462 append_quad(
4463 vertices,
4464 indices,
4465 base[0],
4466 top[0],
4467 base[2],
4468 top[2],
4469 [-1.0, 0.0, 0.0],
4470 color,
4471 );
4472 append_quad(
4473 vertices,
4474 indices,
4475 top[1],
4476 base[1],
4477 top[3],
4478 base[3],
4479 [1.0, 0.0, 0.0],
4480 color,
4481 );
4482}
4483
4484#[allow(clippy::too_many_arguments)]
4485fn append_quad(
4486 vertices: &mut Vec<GridExtrusionVertex>,
4487 indices: &mut Vec<u32>,
4488 a: [f32; 3],
4489 b: [f32; 3],
4490 c: [f32; 3],
4491 d: [f32; 3],
4492 normal: [f32; 3],
4493 color: [f32; 4],
4494) {
4495 let base_index = vertices.len() as u32;
4496 vertices.extend_from_slice(&[
4497 GridExtrusionVertex {
4498 position: a,
4499 normal,
4500 color,
4501 },
4502 GridExtrusionVertex {
4503 position: b,
4504 normal,
4505 color,
4506 },
4507 GridExtrusionVertex {
4508 position: c,
4509 normal,
4510 color,
4511 },
4512 GridExtrusionVertex {
4513 position: d,
4514 normal,
4515 color,
4516 },
4517 ]);
4518 indices.extend_from_slice(&[
4519 base_index,
4520 base_index + 2,
4521 base_index + 1,
4522 base_index + 1,
4523 base_index + 2,
4524 base_index + 3,
4525 ]);
4526}
4527
4528fn grid_cell_corners_world(
4529 grid: &rustial_engine::GeoGrid,
4530 row: usize,
4531 col: usize,
4532 state: &MapState,
4533 scene_origin: DVec3,
4534 params: &rustial_engine::ExtrusionParams,
4535) -> [[f32; 3]; 4] {
4536 let nw = project_grid_offset(
4537 grid,
4538 col as f64 * grid.cell_width,
4539 row as f64 * grid.cell_height,
4540 state,
4541 scene_origin,
4542 params,
4543 );
4544 let ne = project_grid_offset(
4545 grid,
4546 (col + 1) as f64 * grid.cell_width,
4547 row as f64 * grid.cell_height,
4548 state,
4549 scene_origin,
4550 params,
4551 );
4552 let sw = project_grid_offset(
4553 grid,
4554 col as f64 * grid.cell_width,
4555 (row + 1) as f64 * grid.cell_height,
4556 state,
4557 scene_origin,
4558 params,
4559 );
4560 let se = project_grid_offset(
4561 grid,
4562 (col + 1) as f64 * grid.cell_width,
4563 (row + 1) as f64 * grid.cell_height,
4564 state,
4565 scene_origin,
4566 params,
4567 );
4568 [nw, ne, sw, se]
4569}
4570
4571fn project_grid_offset(
4572 grid: &rustial_engine::GeoGrid,
4573 dx: f64,
4574 dy: f64,
4575 state: &MapState,
4576 scene_origin: DVec3,
4577 params: &rustial_engine::ExtrusionParams,
4578) -> [f32; 3] {
4579 let (sin_r, cos_r) = grid.rotation.sin_cos();
4580 let rx = dx * cos_r - dy * sin_r;
4581 let ry = dx * sin_r + dy * cos_r;
4582 let coord = offset_geo_coord(&grid.origin, rx, ry);
4583 let altitude = resolve_grid_base_altitude(grid, &coord, state, params) as f64;
4584 let elevated_coord = rustial_math::GeoCoord::new(coord.lat, coord.lon, altitude);
4585 let projected = state.camera().projection().project(&elevated_coord);
4586 [
4587 (projected.position.x - scene_origin.x) as f32,
4588 (projected.position.y - scene_origin.y) as f32,
4589 (projected.position.z - scene_origin.z) as f32,
4590 ]
4591}
4592
4593fn offset_geo_coord(
4594 origin: &rustial_math::GeoCoord,
4595 dx_meters: f64,
4596 dy_meters: f64,
4597) -> rustial_math::GeoCoord {
4598 const METERS_PER_DEG_LAT: f64 = 111_320.0;
4599 let lat = origin.lat - dy_meters / METERS_PER_DEG_LAT;
4600 let cos_lat = origin.lat.to_radians().cos().max(1e-10);
4601 let lon = origin.lon + dx_meters / (METERS_PER_DEG_LAT * cos_lat);
4602 rustial_math::GeoCoord::new(lat, lon, origin.alt)
4603}
4604
4605fn resolve_grid_base_altitude(
4606 grid: &rustial_engine::GeoGrid,
4607 coord: &rustial_math::GeoCoord,
4608 state: &MapState,
4609 params: &rustial_engine::ExtrusionParams,
4610) -> f32 {
4611 let terrain = state.elevation_at(coord).unwrap_or(0.0);
4612 match grid.altitude_mode {
4613 rustial_engine::AltitudeMode::ClampToGround => (terrain + params.base_meters) as f32,
4614 rustial_engine::AltitudeMode::RelativeToGround => {
4615 (terrain + grid.origin.alt + params.base_meters) as f32
4616 }
4617 rustial_engine::AltitudeMode::Absolute => (grid.origin.alt + params.base_meters) as f32,
4618 }
4619}
4620
4621fn resolve_grid_surface_altitude(
4622 grid: &rustial_engine::GeoGrid,
4623 coord: &rustial_math::GeoCoord,
4624 state: &MapState,
4625) -> f64 {
4626 let terrain = state.elevation_at(coord).unwrap_or(0.0);
4627 match grid.altitude_mode {
4628 rustial_engine::AltitudeMode::ClampToGround => terrain,
4629 rustial_engine::AltitudeMode::RelativeToGround => terrain + grid.origin.alt,
4630 rustial_engine::AltitudeMode::Absolute => grid.origin.alt,
4631 }
4632}
4633
4634fn build_unit_column_mesh() -> (Vec<ColumnVertex>, Vec<u32>) {
4635 let vertices = vec![
4636 ColumnVertex {
4638 position: [-0.5, -0.5, 1.0],
4639 normal: [0.0, 0.0, 1.0],
4640 },
4641 ColumnVertex {
4642 position: [0.5, -0.5, 1.0],
4643 normal: [0.0, 0.0, 1.0],
4644 },
4645 ColumnVertex {
4646 position: [-0.5, 0.5, 1.0],
4647 normal: [0.0, 0.0, 1.0],
4648 },
4649 ColumnVertex {
4650 position: [0.5, 0.5, 1.0],
4651 normal: [0.0, 0.0, 1.0],
4652 },
4653 ColumnVertex {
4655 position: [-0.5, -0.5, 0.0],
4656 normal: [0.0, 0.0, -1.0],
4657 },
4658 ColumnVertex {
4659 position: [0.5, -0.5, 0.0],
4660 normal: [0.0, 0.0, -1.0],
4661 },
4662 ColumnVertex {
4663 position: [-0.5, 0.5, 0.0],
4664 normal: [0.0, 0.0, -1.0],
4665 },
4666 ColumnVertex {
4667 position: [0.5, 0.5, 0.0],
4668 normal: [0.0, 0.0, -1.0],
4669 },
4670 ColumnVertex {
4672 position: [-0.5, -0.5, 0.0],
4673 normal: [0.0, -1.0, 0.0],
4674 },
4675 ColumnVertex {
4676 position: [0.5, -0.5, 0.0],
4677 normal: [0.0, -1.0, 0.0],
4678 },
4679 ColumnVertex {
4680 position: [-0.5, -0.5, 1.0],
4681 normal: [0.0, -1.0, 0.0],
4682 },
4683 ColumnVertex {
4684 position: [0.5, -0.5, 1.0],
4685 normal: [0.0, -1.0, 0.0],
4686 },
4687 ColumnVertex {
4689 position: [-0.5, 0.5, 1.0],
4690 normal: [0.0, 1.0, 0.0],
4691 },
4692 ColumnVertex {
4693 position: [0.5, 0.5, 1.0],
4694 normal: [0.0, 1.0, 0.0],
4695 },
4696 ColumnVertex {
4697 position: [-0.5, 0.5, 0.0],
4698 normal: [0.0, 1.0, 0.0],
4699 },
4700 ColumnVertex {
4701 position: [0.5, 0.5, 0.0],
4702 normal: [0.0, 1.0, 0.0],
4703 },
4704 ColumnVertex {
4706 position: [-0.5, -0.5, 0.0],
4707 normal: [-1.0, 0.0, 0.0],
4708 },
4709 ColumnVertex {
4710 position: [-0.5, -0.5, 1.0],
4711 normal: [-1.0, 0.0, 0.0],
4712 },
4713 ColumnVertex {
4714 position: [-0.5, 0.5, 0.0],
4715 normal: [-1.0, 0.0, 0.0],
4716 },
4717 ColumnVertex {
4718 position: [-0.5, 0.5, 1.0],
4719 normal: [-1.0, 0.0, 0.0],
4720 },
4721 ColumnVertex {
4723 position: [0.5, -0.5, 1.0],
4724 normal: [1.0, 0.0, 0.0],
4725 },
4726 ColumnVertex {
4727 position: [0.5, -0.5, 0.0],
4728 normal: [1.0, 0.0, 0.0],
4729 },
4730 ColumnVertex {
4731 position: [0.5, 0.5, 1.0],
4732 normal: [1.0, 0.0, 0.0],
4733 },
4734 ColumnVertex {
4735 position: [0.5, 0.5, 0.0],
4736 normal: [1.0, 0.0, 0.0],
4737 },
4738 ];
4739 let indices = vec![
4740 0, 2, 1, 1, 2, 3, 4, 5, 6, 5, 7, 6, 8, 10, 9, 9, 10, 11, 12, 14, 13, 13, 14, 15, 16, 18,
4741 17, 17, 18, 19, 20, 22, 21, 21, 22, 23,
4742 ];
4743 (vertices, indices)
4744}
4745
4746fn build_column_instances(
4747 columns: &rustial_engine::ColumnInstanceSet,
4748 ramp: &rustial_engine::ColorRamp,
4749 state: &MapState,
4750 scene_origin: DVec3,
4751) -> Vec<ColumnInstanceData> {
4752 let (min_height, max_height) = column_height_range(columns);
4753 columns
4754 .columns
4755 .iter()
4756 .map(|column| {
4757 let projected = state.camera().projection().project(&column.position);
4758 let base_z = resolve_column_base_altitude(column, state);
4759 let normalized = if (max_height - min_height).abs() < f64::EPSILON {
4760 0.5
4761 } else {
4762 ((column.height - min_height) / (max_height - min_height)).clamp(0.0, 1.0)
4763 } as f32;
4764 let color = column.color.unwrap_or_else(|| ramp.evaluate(normalized));
4765 ColumnInstanceData {
4766 base_position: [
4767 (projected.position.x - scene_origin.x) as f32,
4768 (projected.position.y - scene_origin.y) as f32,
4769 (base_z - scene_origin.z) as f32,
4770 ],
4771 dimensions: [column.width as f32, column.height as f32, 0.0, 0.0],
4772 color,
4773 }
4774 })
4775 .collect()
4776}
4777
4778fn build_point_instances(
4779 points: &rustial_engine::PointInstanceSet,
4780 ramp: &rustial_engine::ColorRamp,
4781 state: &MapState,
4782 scene_origin: DVec3,
4783) -> Vec<ColumnInstanceData> {
4784 points
4785 .points
4786 .iter()
4787 .map(|point| {
4788 let projected = state.camera().projection().project(&point.position);
4789 let center_z = resolve_point_altitude(point, state);
4790 let diameter = (point.radius * 2.0) as f32;
4791 let color = point
4792 .color
4793 .unwrap_or_else(|| ramp.evaluate(point.intensity.clamp(0.0, 1.0)));
4794 ColumnInstanceData {
4795 base_position: [
4796 (projected.position.x - scene_origin.x) as f32,
4797 (projected.position.y - scene_origin.y) as f32,
4798 (center_z - scene_origin.z - point.radius) as f32,
4799 ],
4800 dimensions: [diameter, diameter, 0.0, 0.0],
4801 color,
4802 }
4803 })
4804 .collect()
4805}
4806
4807fn column_height_range(columns: &rustial_engine::ColumnInstanceSet) -> (f64, f64) {
4808 let mut min_height = f64::INFINITY;
4809 let mut max_height = f64::NEG_INFINITY;
4810 for column in &columns.columns {
4811 min_height = min_height.min(column.height);
4812 max_height = max_height.max(column.height);
4813 }
4814 if min_height.is_infinite() || max_height.is_infinite() {
4815 (0.0, 0.0)
4816 } else {
4817 (min_height, max_height)
4818 }
4819}
4820
4821fn resolve_point_altitude(point: &rustial_engine::PointInstance, state: &MapState) -> f64 {
4822 let terrain = state.elevation_at(&point.position).unwrap_or(0.0);
4823 match point.altitude_mode {
4824 rustial_engine::AltitudeMode::ClampToGround => terrain,
4825 rustial_engine::AltitudeMode::RelativeToGround => terrain + point.position.alt,
4826 rustial_engine::AltitudeMode::Absolute => point.position.alt,
4827 }
4828}
4829
4830fn resolve_column_base_altitude(column: &rustial_engine::ColumnInstance, state: &MapState) -> f64 {
4831 let terrain = state.elevation_at(&column.position).unwrap_or(0.0);
4832 match column.altitude_mode {
4833 rustial_engine::AltitudeMode::ClampToGround => terrain + column.base,
4834 rustial_engine::AltitudeMode::RelativeToGround => {
4835 terrain + column.position.alt + column.base
4836 }
4837 rustial_engine::AltitudeMode::Absolute => column.position.alt + column.base,
4838 }
4839}
4840
4841fn column_set_fingerprint(columns: &rustial_engine::ColumnInstanceSet) -> u64 {
4842 let mut h = columns.columns.len() as u64;
4843 for column in &columns.columns {
4844 h = h
4845 .wrapping_mul(31)
4846 .wrapping_add(column.position.lat.to_bits());
4847 h = h
4848 .wrapping_mul(31)
4849 .wrapping_add(column.position.lon.to_bits());
4850 h = h
4851 .wrapping_mul(31)
4852 .wrapping_add(column.position.alt.to_bits());
4853 h = h.wrapping_mul(31).wrapping_add(column.height.to_bits());
4854 h = h.wrapping_mul(31).wrapping_add(column.base.to_bits());
4855 h = h.wrapping_mul(31).wrapping_add(column.width.to_bits());
4856 h = h.wrapping_mul(31).wrapping_add(column.pick_id);
4857 h = h.wrapping_mul(31).wrapping_add(match column.altitude_mode {
4858 rustial_engine::AltitudeMode::ClampToGround => 0,
4859 rustial_engine::AltitudeMode::RelativeToGround => 1,
4860 rustial_engine::AltitudeMode::Absolute => 2,
4861 });
4862 if let Some(color) = column.color {
4863 h = h.wrapping_mul(31).wrapping_add(color[0].to_bits() as u64);
4864 h = h.wrapping_mul(31).wrapping_add(color[1].to_bits() as u64);
4865 h = h.wrapping_mul(31).wrapping_add(color[2].to_bits() as u64);
4866 h = h.wrapping_mul(31).wrapping_add(color[3].to_bits() as u64);
4867 }
4868 }
4869 h
4870}
4871
4872fn point_set_fingerprint(points: &rustial_engine::PointInstanceSet) -> u64 {
4873 let mut h = points.points.len() as u64;
4874 for point in &points.points {
4875 h = h
4876 .wrapping_mul(31)
4877 .wrapping_add(point.position.lat.to_bits());
4878 h = h
4879 .wrapping_mul(31)
4880 .wrapping_add(point.position.lon.to_bits());
4881 h = h
4882 .wrapping_mul(31)
4883 .wrapping_add(point.position.alt.to_bits());
4884 h = h.wrapping_mul(31).wrapping_add(point.radius.to_bits());
4885 h = h
4886 .wrapping_mul(31)
4887 .wrapping_add(point.intensity.to_bits() as u64);
4888 h = h.wrapping_mul(31).wrapping_add(point.pick_id);
4889 h = h.wrapping_mul(31).wrapping_add(match point.altitude_mode {
4890 rustial_engine::AltitudeMode::ClampToGround => 0,
4891 rustial_engine::AltitudeMode::RelativeToGround => 1,
4892 rustial_engine::AltitudeMode::Absolute => 2,
4893 });
4894 if let Some(color) = point.color {
4895 h = h.wrapping_mul(31).wrapping_add(color[0].to_bits() as u64);
4896 h = h.wrapping_mul(31).wrapping_add(color[1].to_bits() as u64);
4897 h = h.wrapping_mul(31).wrapping_add(color[2].to_bits() as u64);
4898 h = h.wrapping_mul(31).wrapping_add(color[3].to_bits() as u64);
4899 }
4900 }
4901 h
4902}
4903
4904fn visualization_overlay_intersects_scene_viewport(
4905 overlay: &VisualizationOverlay,
4906 state: &MapState,
4907) -> bool {
4908 let scene_origin = state.scene_world_origin();
4909 let Some(bounds) = visualization_overlay_world_bounds(overlay, state, scene_origin) else {
4910 return false;
4911 };
4912 bounds.intersects(state.scene_viewport_bounds())
4913}
4914
4915fn visualization_overlay_world_bounds(
4916 overlay: &VisualizationOverlay,
4917 state: &MapState,
4918 scene_origin: DVec3,
4919) -> Option<rustial_math::WorldBounds> {
4920 match overlay {
4921 VisualizationOverlay::GridScalar { grid, .. }
4922 | VisualizationOverlay::GridExtrusion { grid, .. } => {
4923 Some(grid_world_bounds(grid, state, scene_origin))
4924 }
4925 VisualizationOverlay::Columns { columns, .. } => {
4926 column_world_bounds(columns, state, scene_origin)
4927 }
4928 VisualizationOverlay::Points { points, .. } => {
4929 point_world_bounds(points, state, scene_origin)
4930 }
4931 }
4932}
4933
4934fn grid_world_bounds(
4935 grid: &rustial_engine::GeoGrid,
4936 state: &MapState,
4937 scene_origin: DVec3,
4938) -> rustial_math::WorldBounds {
4939 let corners = [
4940 grid_corner_coord(grid, 0, 0, state),
4941 grid_corner_coord(grid, 0, grid.cols, state),
4942 grid_corner_coord(grid, grid.rows, 0, state),
4943 grid_corner_coord(grid, grid.rows, grid.cols, state),
4944 ];
4945 let projected: Vec<_> = corners
4946 .iter()
4947 .map(|coord| state.camera().projection().project(coord))
4948 .collect();
4949 let mut bounds = rustial_math::WorldBounds::new(
4950 rustial_math::WorldCoord::new(
4951 projected[0].position.x - scene_origin.x,
4952 projected[0].position.y - scene_origin.y,
4953 projected[0].position.z - scene_origin.z,
4954 ),
4955 rustial_math::WorldCoord::new(
4956 projected[0].position.x - scene_origin.x,
4957 projected[0].position.y - scene_origin.y,
4958 projected[0].position.z - scene_origin.z,
4959 ),
4960 );
4961 for projected in projected.into_iter().skip(1) {
4962 bounds.extend_point(&rustial_math::WorldCoord::new(
4963 projected.position.x - scene_origin.x,
4964 projected.position.y - scene_origin.y,
4965 projected.position.z - scene_origin.z,
4966 ));
4967 }
4968 bounds
4969}
4970
4971fn point_world_bounds(
4972 points: &rustial_engine::PointInstanceSet,
4973 state: &MapState,
4974 scene_origin: DVec3,
4975) -> Option<rustial_math::WorldBounds> {
4976 let mut bounds: Option<rustial_math::WorldBounds> = None;
4977 for point in &points.points {
4978 let projected = state.camera().projection().project(&point.position);
4979 let radius = point.radius;
4980 let center_z = resolve_point_altitude(point, state) - scene_origin.z;
4981 let point_bounds = rustial_math::WorldBounds::new(
4982 rustial_math::WorldCoord::new(
4983 projected.position.x - scene_origin.x - radius,
4984 projected.position.y - scene_origin.y - radius,
4985 center_z - radius,
4986 ),
4987 rustial_math::WorldCoord::new(
4988 projected.position.x - scene_origin.x + radius,
4989 projected.position.y - scene_origin.y + radius,
4990 center_z + radius,
4991 ),
4992 );
4993 if let Some(existing) = bounds.as_mut() {
4994 existing.extend(&point_bounds);
4995 } else {
4996 bounds = Some(point_bounds);
4997 }
4998 }
4999 bounds
5000}
5001
5002fn column_world_bounds(
5003 columns: &rustial_engine::ColumnInstanceSet,
5004 state: &MapState,
5005 scene_origin: DVec3,
5006) -> Option<rustial_math::WorldBounds> {
5007 let mut bounds: Option<rustial_math::WorldBounds> = None;
5008 for column in &columns.columns {
5009 let projected = state.camera().projection().project(&column.position);
5010 let base_z = resolve_column_base_altitude(column, state) - scene_origin.z;
5011 let half_width = column.width * 0.5;
5012 let column_bounds = rustial_math::WorldBounds::new(
5013 rustial_math::WorldCoord::new(
5014 projected.position.x - scene_origin.x - half_width,
5015 projected.position.y - scene_origin.y - half_width,
5016 base_z,
5017 ),
5018 rustial_math::WorldCoord::new(
5019 projected.position.x - scene_origin.x + half_width,
5020 projected.position.y - scene_origin.y + half_width,
5021 base_z + column.height,
5022 ),
5023 );
5024 if let Some(existing) = bounds.as_mut() {
5025 existing.extend(&column_bounds);
5026 } else {
5027 bounds = Some(column_bounds);
5028 }
5029 }
5030 bounds
5031}
5032
5033fn build_shared_terrain_grid(resolution: usize) -> (Vec<TerrainGridVertex>, Vec<u32>) {
5034 let res = resolution.max(2);
5035 let mut vertices = Vec::with_capacity(res * res);
5036 let mut indices = Vec::with_capacity((res - 1) * (res - 1) * 6);
5037
5038 for row in 0..res {
5039 for col in 0..res {
5040 let u = col as f32 / (res - 1) as f32;
5041 let v = row as f32 / (res - 1) as f32;
5042 vertices.push(TerrainGridVertex {
5043 uv: [u, v],
5044 skirt: 0.0,
5045 });
5046 }
5047 }
5048
5049 for row in 0..(res - 1) {
5050 for col in 0..(res - 1) {
5051 let tl = (row * res + col) as u32;
5052 let tr = tl + 1;
5053 let bl = ((row + 1) * res + col) as u32;
5054 let br = bl + 1;
5055 indices.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
5056 }
5057 }
5058
5059 let edges: [Vec<usize>; 4] = [
5060 (0..res).collect(),
5061 ((res - 1) * res..res * res).collect(),
5062 (0..res).map(|r| r * res).collect(),
5063 (0..res).map(|r| r * res + res - 1).collect(),
5064 ];
5065
5066 for edge in &edges {
5067 for i in 0..edge.len() - 1 {
5068 let a = edge[i] as u32;
5069 let b = edge[i + 1] as u32;
5070 let uv_a = vertices[edge[i]].uv;
5071 let uv_b = vertices[edge[i + 1]].uv;
5072 let base_a = vertices.len() as u32;
5073 let base_b = base_a + 1;
5074 vertices.push(TerrainGridVertex {
5075 uv: uv_a,
5076 skirt: 1.0,
5077 });
5078 vertices.push(TerrainGridVertex {
5079 uv: uv_b,
5080 skirt: 1.0,
5081 });
5082 indices.extend_from_slice(&[a, base_a, b, b, base_a, base_b]);
5083 }
5084 }
5085
5086 (vertices, indices)
5087}
5088
5089fn build_terrain_tile_uniform(
5090 mesh: &TerrainMeshData,
5091 elevation: &rustial_engine::TerrainElevationTexture,
5092 state: &MapState,
5093 scene_origin: DVec3,
5094) -> TerrainTileUniform {
5095 let nw = rustial_math::tile_to_geo(&mesh.tile);
5096 let se = rustial_math::tile_xy_to_geo(
5097 mesh.tile.zoom,
5098 mesh.tile.x as f64 + 1.0,
5099 mesh.tile.y as f64 + 1.0,
5100 );
5101 let projection_kind = match state.camera().projection() {
5102 rustial_engine::CameraProjection::WebMercator => 0.0,
5103 rustial_engine::CameraProjection::Equirectangular => 1.0,
5104 _ => 0.0,
5105 };
5106 let skirt =
5107 rustial_engine::skirt_height(mesh.tile.zoom, mesh.vertical_exaggeration as f64) as f32;
5108 let skirt_base = (elevation.min_elev * mesh.vertical_exaggeration - skirt).max(-skirt * 3.0);
5109 let elev_region = if mesh.tile != mesh.elevation_source_tile {
5110 rustial_engine::elevation_region_in_texture_space(
5111 mesh.elevation_region,
5112 elevation.width,
5113 elevation.height,
5114 )
5115 } else {
5116 mesh.elevation_region
5117 };
5118 TerrainTileUniform {
5119 geo_bounds: [nw.lat as f32, nw.lon as f32, se.lat as f32, se.lon as f32],
5120 scene_origin: [
5121 scene_origin.x as f32,
5122 scene_origin.y as f32,
5123 scene_origin.z as f32,
5124 projection_kind,
5125 ],
5126 elev_params: [
5127 mesh.vertical_exaggeration,
5128 skirt_base,
5129 elevation.min_elev,
5130 elevation.max_elev,
5131 ],
5132 elev_region: [
5133 elev_region.u_min,
5134 elev_region.v_min,
5135 elev_region.u_max,
5136 elev_region.v_max,
5137 ],
5138 }
5139}
5140
5141fn build_model_vertices(mesh: &rustial_engine::ModelMesh) -> Vec<ModelVertex> {
5142 debug_assert_eq!(mesh.positions.len(), mesh.normals.len());
5143 debug_assert_eq!(mesh.positions.len(), mesh.uvs.len());
5144
5145 mesh.positions
5146 .iter()
5147 .zip(mesh.normals.iter())
5148 .zip(mesh.uvs.iter())
5149 .map(|((pos, normal), uv)| ModelVertex {
5150 position: *pos,
5151 normal: *normal,
5152 uv: *uv,
5153 })
5154 .collect()
5155}
5156
5157#[cfg(test)]
5158mod tests {
5159 use super::*;
5160 use rustial_engine::{
5161 ColorRamp, ColorStop, ColumnInstance, ColumnInstanceSet, GeoCoord, GeoGrid,
5162 VisualizationOverlay,
5163 };
5164
5165 fn visible_tile_with_fade(fade_opacity: f32) -> VisibleTile {
5166 let id = TileId::new(3, 4, 2);
5167 VisibleTile {
5168 target: id,
5169 actual: id,
5170 data: None,
5171 fade_opacity,
5172 }
5173 }
5174
5175 fn test_ramp() -> ColorRamp {
5176 ColorRamp::new(vec![
5177 ColorStop {
5178 value: 0.0,
5179 color: [0.0, 0.0, 1.0, 0.5],
5180 },
5181 ColorStop {
5182 value: 1.0,
5183 color: [1.0, 0.0, 0.0, 0.8],
5184 },
5185 ])
5186 }
5187
5188 #[test]
5189 fn tile_batch_cache_key_changes_when_fade_opacity_changes() {
5190 let a = [visible_tile_with_fade(0.25)];
5191 let b = [visible_tile_with_fade(0.75)];
5192
5193 let key_a = TileBatchCacheKey::new(
5194 &a,
5195 DVec3::ZERO,
5196 rustial_engine::CameraProjection::WebMercator,
5197 );
5198 let key_b = TileBatchCacheKey::new(
5199 &b,
5200 DVec3::ZERO,
5201 rustial_engine::CameraProjection::WebMercator,
5202 );
5203
5204 assert_ne!(
5205 key_a, key_b,
5206 "tile batch cache key must include fade-sensitive inputs"
5207 );
5208 }
5209
5210 #[test]
5211 fn tile_batch_cache_key_stays_equal_when_fade_opacity_matches() {
5212 let a = [visible_tile_with_fade(1.0)];
5213 let b = [visible_tile_with_fade(1.0)];
5214
5215 let key_a = TileBatchCacheKey::new(
5216 &a,
5217 DVec3::ZERO,
5218 rustial_engine::CameraProjection::WebMercator,
5219 );
5220 let key_b = TileBatchCacheKey::new(
5221 &b,
5222 DVec3::ZERO,
5223 rustial_engine::CameraProjection::WebMercator,
5224 );
5225
5226 assert_eq!(key_a, key_b);
5227 }
5228
5229 #[test]
5230 fn diff_column_instance_ranges_tracks_contiguous_changes() {
5231 let old = vec![
5232 ColumnInstanceData {
5233 base_position: [0.0, 0.0, 0.0],
5234 dimensions: [1.0, 2.0, 0.0, 0.0],
5235 color: [1.0, 0.0, 0.0, 1.0],
5236 },
5237 ColumnInstanceData {
5238 base_position: [1.0, 0.0, 0.0],
5239 dimensions: [1.0, 2.0, 0.0, 0.0],
5240 color: [0.0, 1.0, 0.0, 1.0],
5241 },
5242 ColumnInstanceData {
5243 base_position: [2.0, 0.0, 0.0],
5244 dimensions: [1.0, 2.0, 0.0, 0.0],
5245 color: [0.0, 0.0, 1.0, 1.0],
5246 },
5247 ColumnInstanceData {
5248 base_position: [3.0, 0.0, 0.0],
5249 dimensions: [1.0, 2.0, 0.0, 0.0],
5250 color: [1.0, 1.0, 0.0, 1.0],
5251 },
5252 ];
5253 let mut new = old.clone();
5254 new[1].dimensions[1] = 4.0;
5255 new[2].color = [1.0, 0.0, 1.0, 1.0];
5256
5257 assert_eq!(diff_column_instance_ranges(&old, &new), vec![1..3]);
5258 }
5259
5260 #[test]
5261 fn diff_column_instance_ranges_splits_disjoint_changes() {
5262 let old = vec![
5263 ColumnInstanceData {
5264 base_position: [0.0, 0.0, 0.0],
5265 dimensions: [1.0, 2.0, 0.0, 0.0],
5266 color: [1.0, 0.0, 0.0, 1.0],
5267 },
5268 ColumnInstanceData {
5269 base_position: [1.0, 0.0, 0.0],
5270 dimensions: [1.0, 2.0, 0.0, 0.0],
5271 color: [0.0, 1.0, 0.0, 1.0],
5272 },
5273 ColumnInstanceData {
5274 base_position: [2.0, 0.0, 0.0],
5275 dimensions: [1.0, 2.0, 0.0, 0.0],
5276 color: [0.0, 0.0, 1.0, 1.0],
5277 },
5278 ];
5279 let mut new = old.clone();
5280 new[0].dimensions[0] = 3.0;
5281 new[2].base_position[2] = 5.0;
5282
5283 assert_eq!(diff_column_instance_ranges(&old, &new), vec![0..1, 2..3]);
5284 }
5285
5286 #[test]
5287 fn visualization_overlay_visibility_rejects_far_grid() {
5288 let mut state = MapState::new();
5289 state.set_viewport(1280, 720);
5290 state.set_camera_target(GeoCoord::from_lat_lon(0.0, 0.0));
5291 state.set_camera_distance(1_000.0);
5292 state.update_camera(1.0 / 60.0);
5293
5294 let overlay = VisualizationOverlay::GridScalar {
5295 layer_id: LayerId::next(),
5296 grid: GeoGrid::new(GeoCoord::from_lat_lon(70.0, 120.0), 2, 2, 50.0, 50.0),
5297 field: rustial_engine::ScalarField2D::from_data(2, 2, vec![1.0; 4]),
5298 ramp: test_ramp(),
5299 };
5300
5301 assert!(!visualization_overlay_intersects_scene_viewport(
5302 &overlay, &state
5303 ));
5304 }
5305
5306 #[test]
5307 fn visualization_overlay_visibility_accepts_near_columns() {
5308 let mut state = MapState::new();
5309 state.set_viewport(1280, 720);
5310 state.set_camera_target(GeoCoord::from_lat_lon(0.0, 0.0));
5311 state.set_camera_distance(1_000.0);
5312 state.update_camera(1.0 / 60.0);
5313
5314 let overlay = VisualizationOverlay::Columns {
5315 layer_id: LayerId::next(),
5316 columns: ColumnInstanceSet::new(vec![ColumnInstance::new(
5317 GeoCoord::from_lat_lon(0.0, 0.0),
5318 10.0,
5319 5.0,
5320 )]),
5321 ramp: test_ramp(),
5322 };
5323
5324 assert!(visualization_overlay_intersects_scene_viewport(
5325 &overlay, &state
5326 ));
5327 }
5328}
5329
5330fn create_shadow_map_textures(
5337 device: &wgpu::Device,
5338 resolution: u32,
5339 cascade_count: u32,
5340) -> (Vec<wgpu::Texture>, Vec<wgpu::TextureView>) {
5341 let mut textures = Vec::with_capacity(cascade_count as usize);
5342 let mut views = Vec::with_capacity(cascade_count as usize);
5343 for i in 0..cascade_count {
5344 let tex = device.create_texture(&wgpu::TextureDescriptor {
5345 label: Some(&format!("shadow_cascade_{i}")),
5346 size: wgpu::Extent3d {
5347 width: resolution,
5348 height: resolution,
5349 depth_or_array_layers: 1,
5350 },
5351 mip_level_count: 1,
5352 sample_count: 1,
5353 dimension: wgpu::TextureDimension::D2,
5354 format: wgpu::TextureFormat::Depth32Float,
5355 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
5356 view_formats: &[],
5357 });
5358 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
5359 textures.push(tex);
5360 views.push(view);
5361 }
5362 (textures, views)
5363}
5364
5365#[repr(C)]
5369#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
5370struct ShadowParamsUniform {
5371 light_matrix_0: [[f32; 4]; 4],
5373 light_matrix_1: [[f32; 4]; 4],
5375 shadow_config: [f32; 4],
5377 shadow_dir: [f32; 4],
5379}