#![allow(clippy::many_single_char_names)]
use crate::gpu::batch::{
build_circle_batch, build_fill_batch, build_fill_extrusion_batch,
build_fill_pattern_batch, build_heatmap_batch, build_hillshade_batches,
build_line_batch, build_line_pattern_batch, build_placeholder_batches,
build_symbol_batch, build_terrain_batches, build_tile_batches,
build_vector_batch, find_terrain_texture_actual, CircleBatchEntry,
FillBatchEntry, FillExtrusionBatchEntry, FillPatternBatchEntry,
HeatmapBatchEntry, HillshadeBatch, LineBatchEntry,
LinePatternBatchEntry, SymbolBatchEntry, TerrainBatch, TilePageBatches,
VectorBatchEntry,
};
use crate::gpu::depth::create_depth_texture;
use crate::gpu::column_vertex::{ColumnInstanceData, ColumnVertex};
use crate::gpu::grid_extrusion_vertex::GridExtrusionVertex;
use crate::gpu::grid_scalar_vertex::GridScalarVertex;
use crate::gpu::image_overlay_vertex::ImageOverlayVertex;
use crate::gpu::model_vertex::ModelVertex;
use crate::gpu::terrain_buffers::TerrainInteractionBuffers;
use crate::gpu::terrain_grid_vertex::TerrainGridVertex;
use crate::gpu::tile_atlas::TileAtlas;
use crate::painter::{PainterPass, PainterPlan};
use crate::pipeline::circle_pipeline::CirclePipeline;
use crate::pipeline::column_pipeline::ColumnPipeline;
use crate::pipeline::fill_extrusion_pipeline::FillExtrusionPipeline;
use crate::pipeline::fill_pattern_pipeline::FillPatternPipeline;
use crate::pipeline::fill_pipeline::FillPipeline;
use crate::pipeline::grid_scalar_pipeline::GridScalarPipeline;
use crate::pipeline::grid_extrusion_pipeline::GridExtrusionPipeline;
use crate::pipeline::heatmap_colormap_pipeline::HeatmapColormapPipeline;
use crate::pipeline::heatmap_pipeline::HeatmapPipeline;
use crate::pipeline::hillshade_pipeline::HillshadePipeline;
use crate::pipeline::image_overlay_pipeline::ImageOverlayPipeline;
use crate::pipeline::line_pipeline::LinePipeline;
use crate::pipeline::line_pattern_pipeline::LinePatternPipeline;
use crate::pipeline::model_pipeline::ModelPipeline;
use crate::pipeline::symbol_pipeline::SymbolPipeline;
use crate::pipeline::terrain_data_pipeline::TerrainDataPipeline;
use crate::pipeline::terrain_pipeline::TerrainPipeline;
use crate::pipeline::tile_pipeline::TilePipeline;
use crate::pipeline::uniforms::ViewProjUniform;
use crate::pipeline::vector_pipeline::VectorPipeline;
use glam::{DVec3, Mat4};
use rustial_engine::{
materialize_terrain_mesh, DecodedImage, LayerId, MapState, ModelInstance, TerrainMeshData,
TileData, VectorMeshData, VectorRenderMode, VisibleTile, VisualizationOverlay,
};
use rustial_engine as rustial_math;
use rustial_engine::TileId;
use std::sync::Arc;
use wgpu::util::DeviceExt;
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct TerrainTileUniform {
geo_bounds: [f32; 4],
scene_origin: [f32; 4],
elev_params: [f32; 4],
elev_region: [f32; 4],
}
struct SharedTerrainGridMesh {
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
index_count: u32,
}
struct CachedHeightTexture {
generation: u64,
view: wgpu::TextureView,
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct GridScalarUniform {
origin_counts: [f32; 4],
grid_params: [f32; 4],
scene_origin: [f32; 4],
value_params: [f32; 4],
base_altitude: [f32; 4],
}
struct SharedColumnMesh {
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
index_count: u32,
}
struct CachedGridScalarOverlay {
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
index_count: u32,
vertex_count: usize,
#[allow(dead_code)]
uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
#[allow(dead_code)]
scalar_texture: wgpu::Texture,
#[allow(dead_code)]
ramp_texture: wgpu::Texture,
generation: u64,
value_generation: u64,
ramp_fingerprint: u64,
grid_fingerprint: u64,
terrain_fingerprint: u64,
projection: rustial_engine::CameraProjection,
origin_key: [i64; 3],
}
struct CachedGridExtrusionOverlay {
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
index_count: u32,
vertex_count: usize,
generation: u64,
value_generation: u64,
origin_key: [i64; 3],
grid_fingerprint: u64,
params_fingerprint: u64,
ramp_fingerprint: u64,
terrain_fingerprint: u64,
}
struct CachedColumnOverlay {
instance_buffer: wgpu::Buffer,
instance_count: u32,
generation: u64,
origin_key: [i64; 3],
columns_fingerprint: u64,
ramp_fingerprint: u64,
instance_data: Vec<ColumnInstanceData>,
}
struct CachedPointCloudOverlay {
instance_buffer: wgpu::Buffer,
instance_count: u32,
generation: u64,
origin_key: [i64; 3],
points_fingerprint: u64,
ramp_fingerprint: u64,
instance_data: Vec<ColumnInstanceData>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct VisualizationPerfStats {
pub grid_scalar_rebuilds: u32,
pub grid_scalar_value_updates: u32,
pub grid_extrusion_rebuilds: u32,
pub grid_extrusion_value_updates: u32,
pub column_rebuilds: u32,
pub column_partial_writes: u32,
pub column_partial_write_ranges: u32,
pub point_cloud_rebuilds: u32,
pub point_cloud_partial_writes: u32,
pub point_cloud_partial_write_ranges: u32,
}
struct CachedTerrainTileBind {
#[allow(dead_code)]
uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
origin_key: [i64; 3],
generation: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct TerrainTileBindKey {
tile: TileId,
pipeline: TerrainPipelineKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum TerrainPipelineKind {
Terrain,
TerrainData,
Hillshade,
}
struct TerrainDataDirtyState {
dirty: bool,
last_vp: [f32; 16],
last_terrain_fingerprint: u64,
}
impl Default for TerrainDataDirtyState {
fn default() -> Self {
Self {
dirty: true,
last_vp: [0.0; 16],
last_terrain_fingerprint: 0,
}
}
}
impl TerrainDataDirtyState {
fn needs_update(
&self,
vp: &glam::DMat4,
terrain_meshes: &[TerrainMeshData],
) -> bool {
if self.dirty {
return true;
}
let vp_f32 = vp.to_cols_array().map(|v| v as f32);
if vp_f32 != self.last_vp {
return true;
}
let fp = Self::terrain_fingerprint(terrain_meshes);
fp != self.last_terrain_fingerprint
}
fn mark_clean(
&mut self,
vp: &glam::DMat4,
terrain_meshes: &[TerrainMeshData],
) {
self.dirty = false;
self.last_vp = vp.to_cols_array().map(|v| v as f32);
self.last_terrain_fingerprint = Self::terrain_fingerprint(terrain_meshes);
}
fn terrain_fingerprint(terrain_meshes: &[TerrainMeshData]) -> u64 {
let mut h: u64 = terrain_meshes.len() as u64;
for mesh in terrain_meshes {
h = h
.wrapping_mul(31)
.wrapping_add(mesh.tile.zoom as u64)
.wrapping_mul(31)
.wrapping_add(mesh.tile.x as u64)
.wrapping_mul(31)
.wrapping_add(mesh.tile.y as u64)
.wrapping_mul(31)
.wrapping_add(mesh.generation);
}
h
}
}
fn diff_column_instance_ranges(
old: &[ColumnInstanceData],
new: &[ColumnInstanceData],
) -> Vec<std::ops::Range<usize>> {
if old.len() != new.len() {
return if new.is_empty() { Vec::new() } else { vec![0..new.len()] };
}
let mut ranges = Vec::new();
let mut current_start: Option<usize> = None;
for (index, (old_item, new_item)) in old.iter().zip(new.iter()).enumerate() {
if old_item != new_item {
if current_start.is_none() {
current_start = Some(index);
}
} else if let Some(start) = current_start.take() {
ranges.push(start..index);
}
}
if let Some(start) = current_start {
ranges.push(start..new.len());
}
ranges
}
#[derive(Debug, Clone, PartialEq)]
struct TileBatchCacheKey {
tiles: Vec<(TileId, TileId, u32)>,
origin: [i64; 3],
projection: rustial_engine::CameraProjection,
}
impl TileBatchCacheKey {
fn new(
visible_tiles: &[VisibleTile],
camera_origin: DVec3,
projection: rustial_engine::CameraProjection,
) -> Self {
let tiles: Vec<(TileId, TileId, u32)> = visible_tiles
.iter()
.map(|vt| (vt.target, vt.actual, vt.fade_opacity.to_bits()))
.collect();
let origin = [
(camera_origin.x * 100.0) as i64,
(camera_origin.y * 100.0) as i64,
(camera_origin.z * 100.0) as i64,
];
Self { tiles, origin, projection }
}
}
#[derive(Debug, Clone, PartialEq)]
struct VectorBatchCacheKey {
layers: Vec<(usize, usize)>,
origin: [i64; 3],
}
impl VectorBatchCacheKey {
fn new(vector_meshes: &[VectorMeshData], camera_origin: DVec3) -> Self {
let layers: Vec<(usize, usize)> = vector_meshes
.iter()
.map(|m| (m.positions.len(), m.indices.len()))
.collect();
let origin = [
(camera_origin.x * 100.0) as i64,
(camera_origin.y * 100.0) as i64,
(camera_origin.z * 100.0) as i64,
];
Self { layers, origin }
}
}
pub struct RenderParams<'a> {
pub state: &'a MapState,
pub device: &'a wgpu::Device,
pub queue: &'a wgpu::Queue,
pub color_view: &'a wgpu::TextureView,
pub visible_tiles: &'a [VisibleTile],
pub vector_meshes: &'a [VectorMeshData],
pub model_instances: &'a [ModelInstance],
pub clear_color: [f32; 4],
}
pub struct WgpuMapRenderer {
tile_pipeline: TilePipeline,
terrain_pipeline: TerrainPipeline,
terrain_data_pipeline: TerrainDataPipeline,
hillshade_pipeline: HillshadePipeline,
grid_scalar_pipeline: GridScalarPipeline,
grid_extrusion_pipeline: GridExtrusionPipeline,
column_pipeline: ColumnPipeline,
vector_pipeline: VectorPipeline,
fill_pipeline: FillPipeline,
fill_pattern_pipeline: FillPatternPipeline,
fill_extrusion_pipeline: FillExtrusionPipeline,
line_pipeline: LinePipeline,
line_pattern_pipeline: LinePatternPipeline,
circle_pipeline: CirclePipeline,
heatmap_pipeline: HeatmapPipeline,
heatmap_colormap_pipeline: HeatmapColormapPipeline,
symbol_pipeline: SymbolPipeline,
model_pipeline: ModelPipeline,
image_overlay_pipeline: ImageOverlayPipeline,
uniform_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
terrain_uniform_bind_group: wgpu::BindGroup,
terrain_data_uniform_bind_group: wgpu::BindGroup,
hillshade_uniform_bind_group: wgpu::BindGroup,
grid_scalar_uniform_bind_group: wgpu::BindGroup,
grid_extrusion_uniform_bind_group: wgpu::BindGroup,
column_uniform_bind_group: wgpu::BindGroup,
vector_uniform_bind_group: wgpu::BindGroup,
fill_extrusion_uniform_bind_group: wgpu::BindGroup,
model_uniform_bind_group: wgpu::BindGroup,
line_uniform_bind_group: wgpu::BindGroup,
circle_uniform_bind_group: wgpu::BindGroup,
heatmap_uniform_bind_group: wgpu::BindGroup,
heatmap_colormap_uniform_bind_group: wgpu::BindGroup,
heatmap_accum_texture: wgpu::Texture,
heatmap_accum_view: wgpu::TextureView,
_heatmap_ramp_texture: wgpu::Texture,
heatmap_ramp_view: wgpu::TextureView,
heatmap_colormap_textures_bind_group: wgpu::BindGroup,
symbol_uniform_bind_group: wgpu::BindGroup,
image_overlay_uniform_bind_group: wgpu::BindGroup,
sampler: wgpu::Sampler,
grid_scalar_ramp_sampler: wgpu::Sampler,
fill_pattern_sampler: wgpu::Sampler,
depth_view: wgpu::TextureView,
width: u32,
height: u32,
terrain_interaction_buffers: TerrainInteractionBuffers,
tile_atlas: TileAtlas,
hillshade_atlas: TileAtlas,
page_bind_groups: Vec<wgpu::BindGroup>,
page_terrain_bind_groups: Vec<wgpu::BindGroup>,
page_hillshade_bind_groups: Vec<wgpu::BindGroup>,
model_mesh_cache: std::collections::HashMap<ModelMeshKey, CachedModelMesh>,
shared_terrain_grids: std::collections::HashMap<u16, SharedTerrainGridMesh>,
height_texture_cache: std::collections::HashMap<TileId, CachedHeightTexture>,
shared_column_mesh: Option<SharedColumnMesh>,
grid_scalar_overlay_cache: std::collections::HashMap<LayerId, CachedGridScalarOverlay>,
grid_extrusion_overlay_cache: std::collections::HashMap<LayerId, CachedGridExtrusionOverlay>,
column_overlay_cache: std::collections::HashMap<LayerId, CachedColumnOverlay>,
point_cloud_overlay_cache: std::collections::HashMap<LayerId, CachedPointCloudOverlay>,
cached_tile_batches: Vec<TilePageBatches>,
tile_batch_cache_key: Option<TileBatchCacheKey>,
cached_vector_batches: Vec<Option<VectorBatchEntry>>,
vector_batch_cache_key: Option<VectorBatchCacheKey>,
cached_fill_extrusion_batches: Vec<Option<FillExtrusionBatchEntry>>,
cached_fill_batches: Vec<Option<FillBatchEntry>>,
cached_fill_pattern_batches: Vec<Option<FillPatternBatchEntry>>,
cached_line_batches: Vec<Option<LineBatchEntry>>,
cached_line_pattern_batches: Vec<Option<LinePatternBatchEntry>>,
cached_circle_batches: Vec<Option<CircleBatchEntry>>,
cached_heatmap_batches: Vec<Option<HeatmapBatchEntry>>,
cached_symbol_batch: Option<SymbolBatchEntry>,
symbol_atlas_texture: Option<(wgpu::Texture, wgpu::TextureView)>,
symbol_atlas_bind_group: Option<wgpu::BindGroup>,
symbol_glyph_atlas: rustial_engine::symbols::GlyphAtlas,
symbol_glyph_provider: Box<dyn rustial_engine::symbols::GlyphProvider>,
terrain_tile_bind_cache: std::collections::HashMap<TerrainTileBindKey, CachedTerrainTileBind>,
terrain_data_dirty: TerrainDataDirtyState,
cached_model_transforms: Option<CachedModelTransforms>,
cached_placeholder_batch: Option<VectorBatchEntry>,
cached_image_overlay_batches: Vec<CachedImageOverlayBatch>,
visualization_perf_stats: VisualizationPerfStats,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct ModelMeshKey {
pos_len: usize,
idx_len: usize,
fingerprint: u64,
}
impl ModelMeshKey {
fn from_mesh(mesh: &rustial_engine::ModelMesh) -> Self {
let mut fingerprint: u64 = mesh.positions.len() as u64;
if let Some(first) = mesh.positions.first() {
fingerprint = fingerprint
.wrapping_mul(31)
.wrapping_add(first[0].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(first[1].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(first[2].to_bits() as u64);
}
if let Some(&first_idx) = mesh.indices.first() {
fingerprint = fingerprint.wrapping_mul(31).wrapping_add(first_idx as u64);
}
Self {
pos_len: mesh.positions.len(),
idx_len: mesh.indices.len(),
fingerprint,
}
}
}
struct CachedModelMesh {
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
index_count: u32,
}
struct CachedModelTransforms {
#[allow(dead_code)]
buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
stride: usize,
instance_count: usize,
fingerprint: u64,
}
struct CachedImageOverlayBatch {
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
texture: wgpu::Texture,
#[allow(dead_code)]
texture_view: wgpu::TextureView,
texture_bind_group: wgpu::BindGroup,
layer_id: rustial_engine::LayerId,
tex_dimensions: (u32, u32),
data_arc_ptr: usize,
}
impl WgpuMapRenderer {
pub fn new(
device: &wgpu::Device,
_queue: &wgpu::Queue,
format: wgpu::TextureFormat,
width: u32,
height: u32,
) -> Self {
let tile_pipeline = TilePipeline::new(device, format);
let terrain_pipeline =
TerrainPipeline::new(device, format, &tile_pipeline.uniform_bind_group_layout);
let terrain_data_pipeline = TerrainDataPipeline::new(device);
let hillshade_pipeline = HillshadePipeline::new(device, format);
let grid_scalar_pipeline = GridScalarPipeline::new(device, format);
let grid_extrusion_pipeline = GridExtrusionPipeline::new(device, format);
let column_pipeline = ColumnPipeline::new(device, format);
let vector_pipeline = VectorPipeline::new(device, format);
let fill_pipeline = FillPipeline::new(device, format);
let fill_pattern_pipeline = FillPatternPipeline::new(device, format);
let fill_extrusion_pipeline = FillExtrusionPipeline::new(device, format);
let line_pipeline = LinePipeline::new(device, format);
let line_pattern_pipeline = LinePatternPipeline::new(device, format);
let circle_pipeline = CirclePipeline::new(device, format);
let heatmap_pipeline = HeatmapPipeline::new(device);
let heatmap_colormap_pipeline = HeatmapColormapPipeline::new(device, format);
let symbol_pipeline = SymbolPipeline::new(device, format);
let model_pipeline = ModelPipeline::new(device, format);
let image_overlay_pipeline = ImageOverlayPipeline::new(device, format);
let uniform_data = ViewProjUniform::from_dmat4(&glam::DMat4::IDENTITY);
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("rustial_uniform_buf"),
contents: bytemuck::bytes_of(&uniform_data),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_uniform_bg"),
layout: &tile_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let column_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_column_uniform_bg"),
layout: &column_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let grid_extrusion_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_grid_extrusion_uniform_bg"),
layout: &grid_extrusion_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let terrain_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_terrain_uniform_bg"),
layout: &terrain_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let hillshade_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_hillshade_uniform_bg"),
layout: &hillshade_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let grid_scalar_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_grid_scalar_uniform_bg"),
layout: &grid_scalar_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let vector_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_vector_uniform_bg"),
layout: &vector_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let fill_extrusion_uniform_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_fill_extrusion_uniform_bg"),
layout: &fill_extrusion_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let model_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_model_uniform_bg"),
layout: &model_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let line_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_line_uniform_bg"),
layout: &line_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let circle_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_circle_uniform_bg"),
layout: &circle_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let heatmap_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_heatmap_uniform_bg"),
layout: &heatmap_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let heatmap_colormap_uniform_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_heatmap_colormap_uniform_bg"),
layout: &heatmap_colormap_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let symbol_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_symbol_uniform_bg"),
layout: &symbol_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let image_overlay_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_image_overlay_uniform_bg"),
layout: &image_overlay_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let terrain_data_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rustial_terrain_data_uniform_bg"),
layout: &terrain_data_pipeline.uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("rustial_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
anisotropy_clamp: 16,
..Default::default()
});
let grid_scalar_ramp_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("rustial_grid_scalar_ramp_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let fill_pattern_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("rustial_fill_pattern_sampler"),
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::Repeat,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let w = width.max(1);
let h = height.max(1);
let depth_view = create_depth_texture(device, w, h);
let terrain_interaction_buffers = TerrainInteractionBuffers::new(device, w, h);
let (heatmap_accum_texture, heatmap_accum_view) =
create_heatmap_accum_texture(device, w, h);
let heatmap_ramp_texture = create_default_heatmap_ramp_texture(device, _queue);
let heatmap_ramp_view =
heatmap_ramp_texture.create_view(&wgpu::TextureViewDescriptor::default());
let heatmap_colormap_textures_bind_group =
create_heatmap_colormap_bind_group(
device,
&heatmap_colormap_pipeline.textures_bind_group_layout,
&heatmap_accum_view,
&heatmap_ramp_view,
&sampler,
);
Self {
tile_pipeline,
terrain_pipeline,
terrain_data_pipeline,
hillshade_pipeline,
grid_scalar_pipeline,
grid_extrusion_pipeline,
column_pipeline,
vector_pipeline,
fill_pipeline,
fill_pattern_pipeline,
fill_extrusion_pipeline,
line_pipeline,
line_pattern_pipeline,
circle_pipeline,
heatmap_pipeline,
heatmap_colormap_pipeline,
symbol_pipeline,
model_pipeline,
image_overlay_pipeline,
uniform_buffer,
uniform_bind_group,
terrain_uniform_bind_group,
terrain_data_uniform_bind_group,
hillshade_uniform_bind_group,
grid_scalar_uniform_bind_group,
grid_extrusion_uniform_bind_group,
column_uniform_bind_group,
vector_uniform_bind_group,
fill_extrusion_uniform_bind_group,
model_uniform_bind_group,
line_uniform_bind_group,
circle_uniform_bind_group,
heatmap_uniform_bind_group,
heatmap_colormap_uniform_bind_group,
heatmap_accum_texture,
heatmap_accum_view,
_heatmap_ramp_texture: heatmap_ramp_texture,
heatmap_ramp_view,
heatmap_colormap_textures_bind_group,
symbol_uniform_bind_group,
image_overlay_uniform_bind_group,
sampler,
grid_scalar_ramp_sampler,
fill_pattern_sampler,
depth_view,
width: w,
height: h,
terrain_interaction_buffers,
tile_atlas: TileAtlas::new(),
hillshade_atlas: TileAtlas::new(),
page_bind_groups: Vec::new(),
page_terrain_bind_groups: Vec::new(),
page_hillshade_bind_groups: Vec::new(),
model_mesh_cache: std::collections::HashMap::new(),
shared_terrain_grids: std::collections::HashMap::new(),
height_texture_cache: std::collections::HashMap::new(),
shared_column_mesh: None,
grid_scalar_overlay_cache: std::collections::HashMap::new(),
grid_extrusion_overlay_cache: std::collections::HashMap::new(),
column_overlay_cache: std::collections::HashMap::new(),
point_cloud_overlay_cache: std::collections::HashMap::new(),
cached_tile_batches: Vec::new(),
tile_batch_cache_key: None,
cached_vector_batches: Vec::new(),
vector_batch_cache_key: None,
cached_fill_extrusion_batches: Vec::new(),
cached_fill_batches: Vec::new(),
cached_fill_pattern_batches: Vec::new(),
cached_line_batches: Vec::new(),
cached_line_pattern_batches: Vec::new(),
cached_circle_batches: Vec::new(),
cached_heatmap_batches: Vec::new(),
cached_symbol_batch: None,
symbol_atlas_texture: None,
symbol_atlas_bind_group: None,
symbol_glyph_atlas: rustial_engine::symbols::GlyphAtlas::new(),
symbol_glyph_provider: Box::new(rustial_engine::symbols::ProceduralGlyphProvider::new()),
terrain_tile_bind_cache: std::collections::HashMap::new(),
terrain_data_dirty: TerrainDataDirtyState::default(),
cached_model_transforms: None,
cached_placeholder_batch: None,
cached_image_overlay_batches: Vec::new(),
visualization_perf_stats: VisualizationPerfStats::default(),
}
}
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
self.width = width.max(1);
self.height = height.max(1);
self.depth_view = create_depth_texture(device, self.width, self.height);
self.terrain_interaction_buffers.resize(device, self.width, self.height);
self.terrain_data_dirty.dirty = true;
let (tex, view) = create_heatmap_accum_texture(device, self.width, self.height);
self.heatmap_accum_texture = tex;
self.heatmap_accum_view = view;
self.heatmap_colormap_textures_bind_group = create_heatmap_colormap_bind_group(
device,
&self.heatmap_colormap_pipeline.textures_bind_group_layout,
&self.heatmap_accum_view,
&self.heatmap_ramp_view,
&self.sampler,
);
}
pub fn set_glyph_provider(
&mut self,
provider: Box<dyn rustial_engine::symbols::GlyphProvider>,
) {
self.symbol_glyph_provider = provider;
}
pub fn upload_tile(
&mut self,
device: &wgpu::Device,
tile_id: TileId,
image: &DecodedImage,
) {
if self.tile_atlas.contains(&tile_id) {
return;
}
if let Err(err) = image.validate_rgba8() {
log::warn!("wgpu upload_tile: skipping invalid tile {:?}: {}", tile_id, err);
return;
}
self.tile_atlas.insert(device, tile_id, image);
self.tile_batch_cache_key = None;
self.rebuild_page_bind_groups(device);
}
pub fn upload_hillshade(
&mut self,
device: &wgpu::Device,
tile_id: TileId,
image: &DecodedImage,
) {
if self.hillshade_atlas.contains(&tile_id) {
return;
}
if let Err(err) = image.validate_rgba8() {
log::warn!("wgpu upload_hillshade: skipping invalid tile {:?}: {}", tile_id, err);
return;
}
self.hillshade_atlas.insert(device, tile_id, image);
self.rebuild_page_bind_groups(device);
}
pub fn flush_atlas_uploads(&mut self, queue: &wgpu::Queue) {
self.tile_atlas.flush_uploads(queue);
self.hillshade_atlas.flush_uploads(queue);
}
fn get_or_create_shared_column_mesh(&mut self, device: &wgpu::Device) -> &SharedColumnMesh {
if self.shared_column_mesh.is_none() {
let (vertices, indices) = build_unit_column_mesh();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("column_unit_box_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("column_unit_box_ib"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
self.shared_column_mesh = Some(SharedColumnMesh {
vertex_buffer,
index_buffer,
index_count: indices.len() as u32,
});
}
self.shared_column_mesh.as_ref().expect("column mesh")
}
fn get_or_create_grid_scalar_overlay(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
overlay: &VisualizationOverlay,
state: &MapState,
scene_origin: DVec3,
terrain_fingerprint: u64,
) -> Option<()> {
let VisualizationOverlay::GridScalar { layer_id, grid, field, ramp } = overlay else {
return None;
};
let origin_key = [
(scene_origin.x * 100.0) as i64,
(scene_origin.y * 100.0) as i64,
(scene_origin.z * 100.0) as i64,
];
let ramp_fingerprint = grid_scalar_ramp_fingerprint(ramp);
let grid_fingerprint = grid_extrusion_grid_fingerprint(grid);
let projection = state.camera().projection();
let (vertices, indices) = build_grid_scalar_geometry(grid, state, scene_origin);
let recreate = if let Some(cached) = self.grid_scalar_overlay_cache.get(layer_id) {
cached.generation != field.generation
|| cached.ramp_fingerprint != ramp_fingerprint
|| cached.grid_fingerprint != grid_fingerprint
|| cached.projection != projection
|| cached.index_count as usize != indices.len()
|| cached.vertex_count != vertices.len()
} else {
true
};
if recreate {
self.visualization_perf_stats.grid_scalar_rebuilds += 1;
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("grid_scalar_vb_{layer_id}")),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("grid_scalar_ib_{layer_id}")),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
});
let scalar_texture = create_grid_scalar_texture(device, queue, field);
let scalar_view = scalar_texture.create_view(&wgpu::TextureViewDescriptor::default());
let ramp_texture = create_grid_scalar_ramp_texture(device, queue, ramp);
let ramp_view = ramp_texture.create_view(&wgpu::TextureViewDescriptor::default());
let uniform = build_grid_scalar_uniform(grid, field, state, scene_origin, 1.0);
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("grid_scalar_uniform_{layer_id}")),
contents: bytemuck::bytes_of(&uniform),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("grid_scalar_bg_{layer_id}")),
layout: &self.grid_scalar_pipeline.overlay_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&scalar_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&ramp_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Sampler(&self.grid_scalar_ramp_sampler),
},
],
});
self.grid_scalar_overlay_cache.insert(
*layer_id,
CachedGridScalarOverlay {
vertex_buffer,
index_buffer,
index_count: indices.len() as u32,
vertex_count: vertices.len(),
uniform_buffer,
bind_group,
scalar_texture,
ramp_texture,
generation: field.generation,
value_generation: field.value_generation,
ramp_fingerprint,
grid_fingerprint,
terrain_fingerprint,
projection,
origin_key,
},
);
return Some(());
}
if let Some(cached) = self.grid_scalar_overlay_cache.get_mut(layer_id) {
let uniform = build_grid_scalar_uniform(grid, field, state, scene_origin, 1.0);
if cached.value_generation != field.value_generation {
self.visualization_perf_stats.grid_scalar_value_updates += 1;
write_grid_scalar_texture(queue, &cached.scalar_texture, field);
cached.value_generation = field.value_generation;
queue.write_buffer(&cached.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
}
if cached.origin_key != origin_key || cached.terrain_fingerprint != terrain_fingerprint {
queue.write_buffer(
&cached.vertex_buffer,
0,
bytemuck::cast_slice::<GridScalarVertex, u8>(&vertices),
);
queue.write_buffer(&cached.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
cached.origin_key = origin_key;
cached.terrain_fingerprint = terrain_fingerprint;
}
}
Some(())
}
fn get_or_create_point_cloud_overlay(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
overlay: &VisualizationOverlay,
state: &MapState,
scene_origin: DVec3,
) -> Option<()> {
let VisualizationOverlay::Points { layer_id, points, ramp } = overlay else {
return None;
};
let origin_key = [
(scene_origin.x * 100.0) as i64,
(scene_origin.y * 100.0) as i64,
(scene_origin.z * 100.0) as i64,
];
let points_fingerprint = point_set_fingerprint(points);
let ramp_fingerprint = grid_scalar_ramp_fingerprint(ramp);
let instances = build_point_instances(points, ramp, state, scene_origin);
let needs_rebuild = if let Some(cached) = self.point_cloud_overlay_cache.get(layer_id) {
cached.generation != points.generation
|| cached.ramp_fingerprint != ramp_fingerprint
|| cached.instance_count as usize != instances.len()
} else {
true
};
if needs_rebuild {
self.visualization_perf_stats.point_cloud_rebuilds += 1;
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("point_cloud_instances_{layer_id}")),
contents: bytemuck::cast_slice(&instances),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
self.point_cloud_overlay_cache.insert(
*layer_id,
CachedPointCloudOverlay {
instance_buffer,
instance_count: instances.len() as u32,
generation: points.generation,
origin_key,
points_fingerprint,
ramp_fingerprint,
instance_data: instances,
},
);
return Some(());
}
if let Some(cached) = self.point_cloud_overlay_cache.get_mut(layer_id) {
let ranges = diff_column_instance_ranges(&cached.instance_data, &instances);
if !ranges.is_empty() {
self.visualization_perf_stats.point_cloud_partial_writes += 1;
self.visualization_perf_stats.point_cloud_partial_write_ranges += ranges.len() as u32;
}
for range in ranges {
let start = range.start;
let end = range.end;
let byte_offset =
(start * std::mem::size_of::<ColumnInstanceData>()) as wgpu::BufferAddress;
queue.write_buffer(
&cached.instance_buffer,
byte_offset,
bytemuck::cast_slice::<ColumnInstanceData, u8>(&instances[start..end]),
);
}
cached.instance_data = instances;
cached.origin_key = origin_key;
cached.points_fingerprint = points_fingerprint;
}
Some(())
}
fn get_or_create_grid_extrusion_overlay(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
overlay: &VisualizationOverlay,
state: &MapState,
scene_origin: DVec3,
terrain_fingerprint: u64,
) -> Option<()> {
let VisualizationOverlay::GridExtrusion {
layer_id,
grid,
field,
ramp,
params,
} = overlay else {
return None;
};
let origin_key = [
(scene_origin.x * 100.0) as i64,
(scene_origin.y * 100.0) as i64,
(scene_origin.z * 100.0) as i64,
];
let grid_fingerprint = grid_extrusion_grid_fingerprint(grid);
let params_fingerprint = grid_extrusion_params_fingerprint(params);
let ramp_fingerprint = grid_scalar_ramp_fingerprint(ramp);
let (vertices, indices) = build_grid_extrusion_geometry(grid, field, ramp, params, state, scene_origin);
let needs_rebuild = if let Some(cached) = self.grid_extrusion_overlay_cache.get(layer_id) {
cached.generation != field.generation
|| cached.grid_fingerprint != grid_fingerprint
|| cached.params_fingerprint != params_fingerprint
|| cached.ramp_fingerprint != ramp_fingerprint
|| cached.terrain_fingerprint != terrain_fingerprint
|| cached.index_count as usize != indices.len()
|| cached.vertex_count != vertices.len()
} else {
true
};
if needs_rebuild {
self.visualization_perf_stats.grid_extrusion_rebuilds += 1;
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("grid_extrusion_vb_{layer_id}")),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("grid_extrusion_ib_{layer_id}")),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
});
self.grid_extrusion_overlay_cache.insert(
*layer_id,
CachedGridExtrusionOverlay {
vertex_buffer,
index_buffer,
index_count: indices.len() as u32,
vertex_count: vertices.len(),
generation: field.generation,
value_generation: field.value_generation,
origin_key,
grid_fingerprint,
params_fingerprint,
ramp_fingerprint,
terrain_fingerprint,
},
);
return Some(());
}
if let Some(cached) = self.grid_extrusion_overlay_cache.get_mut(layer_id) {
if cached.value_generation != field.value_generation || cached.origin_key != origin_key {
self.visualization_perf_stats.grid_extrusion_value_updates += 1;
let vertex_bytes = bytemuck::cast_slice::<GridExtrusionVertex, u8>(&vertices);
queue.write_buffer(&cached.vertex_buffer, 0, vertex_bytes);
}
cached.value_generation = field.value_generation;
cached.origin_key = origin_key;
cached.terrain_fingerprint = terrain_fingerprint;
}
Some(())
}
fn get_or_create_column_overlay(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
overlay: &VisualizationOverlay,
state: &MapState,
scene_origin: DVec3,
) -> Option<()> {
let VisualizationOverlay::Columns { layer_id, columns, ramp } = overlay else {
return None;
};
let origin_key = [
(scene_origin.x * 100.0) as i64,
(scene_origin.y * 100.0) as i64,
(scene_origin.z * 100.0) as i64,
];
let columns_fingerprint = column_set_fingerprint(columns);
let ramp_fingerprint = grid_scalar_ramp_fingerprint(ramp);
let instances = build_column_instances(columns, ramp, state, scene_origin);
let needs_rebuild = if let Some(cached) = self.column_overlay_cache.get(layer_id) {
cached.generation != columns.generation
|| cached.ramp_fingerprint != ramp_fingerprint
|| cached.instance_count as usize != instances.len()
} else {
true
};
if needs_rebuild {
self.visualization_perf_stats.column_rebuilds += 1;
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("column_instances_{layer_id}")),
contents: bytemuck::cast_slice(&instances),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
self.column_overlay_cache.insert(
*layer_id,
CachedColumnOverlay {
instance_buffer,
instance_count: instances.len() as u32,
generation: columns.generation,
origin_key,
columns_fingerprint,
ramp_fingerprint,
instance_data: instances,
},
);
return Some(());
}
if let Some(cached) = self.column_overlay_cache.get_mut(layer_id) {
let ranges = diff_column_instance_ranges(&cached.instance_data, &instances);
if !ranges.is_empty() {
self.visualization_perf_stats.column_partial_writes += 1;
self.visualization_perf_stats.column_partial_write_ranges += ranges.len() as u32;
}
for range in ranges {
let start = range.start;
let end = range.end;
let byte_offset =
(start * std::mem::size_of::<ColumnInstanceData>()) as wgpu::BufferAddress;
queue.write_buffer(
&cached.instance_buffer,
byte_offset,
bytemuck::cast_slice::<ColumnInstanceData, u8>(&instances[start..end]),
);
}
cached.instance_data = instances;
cached.origin_key = origin_key;
cached.columns_fingerprint = columns_fingerprint;
}
Some(())
}
pub fn render(
&mut self,
state: &MapState,
device: &wgpu::Device,
queue: &wgpu::Queue,
color_view: &wgpu::TextureView,
visible_tiles: &[VisibleTile],
) {
let clear_color = state.computed_fog().clear_color;
self.render_full(&RenderParams {
state,
device,
queue,
color_view,
visible_tiles,
vector_meshes: &[],
model_instances: &[],
clear_color,
});
}
pub fn render_full(&mut self, params: &RenderParams<'_>) {
self.visualization_perf_stats = VisualizationPerfStats::default();
let scene_camera_origin = params.state.scene_world_origin();
let frame = params.state.frame_output();
let visualization = &frame.visualization;
let view = params.state.camera().view_matrix(DVec3::ZERO);
let proj = params.state.camera().projection_matrix();
let vp = proj * view;
let cam = params.state.camera();
let eye = cam.eye_offset();
let fog = params.state.computed_fog();
let clear_color = fog.clear_color;
let mut uniform = ViewProjUniform::from_dmat4(&vp);
uniform.fog_color = fog.fog_color;
uniform.eye_pos = [eye.x as f32, eye.y as f32, eye.z as f32, 0.0];
uniform.fog_params = [fog.fog_start, fog.fog_end, fog.fog_density, 0.0];
if let Some(hillshade) = params.state.hillshade() {
uniform.hillshade_highlight = hillshade.highlight_color;
uniform.hillshade_shadow = hillshade.shadow_color;
uniform.hillshade_accent = hillshade.accent_color;
uniform.hillshade_light = [
hillshade.illumination_direction,
hillshade.illumination_altitude,
hillshade.exaggeration,
hillshade.opacity,
];
}
params
.queue
.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
for vt in params.visible_tiles {
if let Some(TileData::Raster(ref img)) = vt.data {
self.upload_tile(params.device, vt.actual, img);
}
}
for raster in params.state.hillshade_rasters() {
self.upload_hillshade(params.device, raster.tile, &raster.image);
}
self.flush_atlas_uploads(params.queue);
for vt in params.visible_tiles {
self.tile_atlas.mark_used(&vt.actual);
}
let terrain_meshes = params.state.terrain_meshes();
for mesh in terrain_meshes {
if let Some(actual_tile) = find_terrain_texture_actual(mesh.tile, params.visible_tiles) {
self.tile_atlas.mark_used(&actual_tile);
}
}
for raster in params.state.hillshade_rasters() {
self.hillshade_atlas.mark_used(&raster.tile);
}
let use_shared_terrain = !terrain_meshes.is_empty()
&& matches!(
params.state.camera().projection(),
rustial_engine::CameraProjection::WebMercator
| rustial_engine::CameraProjection::Equirectangular
)
&& terrain_meshes.iter().all(|mesh| mesh.elevation_texture.is_some());
let materialized_terrain_meshes: Vec<TerrainMeshData> = if use_shared_terrain {
Vec::new()
} else {
terrain_meshes
.iter()
.map(|mesh| {
materialize_terrain_mesh(
mesh,
params.state.camera().projection(),
rustial_engine::skirt_height(
mesh.tile.zoom,
mesh.vertical_exaggeration as f64,
),
)
})
.collect()
};
if !params.model_instances.is_empty() {
self.cache_model_meshes(params.device, params.model_instances);
self.cache_model_transforms(
params.device,
params.model_instances,
scene_camera_origin,
params.state,
);
} else {
self.cached_model_transforms = None;
}
let tile_batch_key = TileBatchCacheKey::new(
params.visible_tiles,
scene_camera_origin,
params.state.camera().projection(),
);
if self.tile_batch_cache_key.as_ref() != Some(&tile_batch_key) {
self.cached_tile_batches = build_tile_batches(
params.device,
params.visible_tiles,
&self.tile_atlas,
scene_camera_origin,
params.state.camera().projection(),
);
self.tile_batch_cache_key = Some(tile_batch_key);
}
let terrain_batches = if !use_shared_terrain && !materialized_terrain_meshes.is_empty() {
build_terrain_batches(
params.device,
&materialized_terrain_meshes,
&self.tile_atlas,
scene_camera_origin,
params.visible_tiles,
)
} else {
Vec::new()
};
let hillshade_batches = if !materialized_terrain_meshes.is_empty() && !params.state.hillshade_rasters().is_empty() {
build_hillshade_batches(
params.device,
&materialized_terrain_meshes,
params.state.hillshade_rasters(),
&self.hillshade_atlas,
scene_camera_origin,
)
} else {
Vec::new()
};
let vector_batch_key = VectorBatchCacheKey::new(params.vector_meshes, scene_camera_origin);
if self.vector_batch_cache_key.as_ref() != Some(&vector_batch_key) {
self.cached_vector_batches = params
.vector_meshes
.iter()
.filter(|mesh| mesh.render_mode == VectorRenderMode::Generic)
.map(|mesh| build_vector_batch(params.device, mesh, scene_camera_origin))
.collect();
self.cached_fill_batches = params
.vector_meshes
.iter()
.filter(|mesh| mesh.render_mode == VectorRenderMode::Fill && mesh.fill_pattern.is_none())
.map(|mesh| build_fill_batch(
params.device,
mesh,
scene_camera_origin,
&self.uniform_buffer,
&self.fill_pipeline.uniform_bind_group_layout,
))
.collect();
self.cached_fill_pattern_batches = params
.vector_meshes
.iter()
.filter(|mesh| mesh.render_mode == VectorRenderMode::Fill && mesh.fill_pattern.is_some())
.map(|mesh| build_fill_pattern_batch(
params.device,
params.queue,
mesh,
scene_camera_origin,
&self.uniform_buffer,
&self.fill_pattern_pipeline.uniform_bind_group_layout,
&self.fill_pattern_pipeline.texture_bind_group_layout,
&self.fill_pattern_sampler,
))
.collect();
self.cached_fill_extrusion_batches = params
.vector_meshes
.iter()
.filter(|mesh| mesh.render_mode == VectorRenderMode::FillExtrusion)
.map(|mesh| build_fill_extrusion_batch(params.device, mesh, scene_camera_origin))
.collect();
self.cached_line_batches = params
.vector_meshes
.iter()
.filter(|mesh| mesh.render_mode == VectorRenderMode::Line && mesh.line_pattern.is_none())
.map(|mesh| build_line_batch(params.device, mesh, scene_camera_origin))
.collect();
self.cached_line_pattern_batches = params
.vector_meshes
.iter()
.filter(|mesh| mesh.render_mode == VectorRenderMode::Line && mesh.line_pattern.is_some())
.map(|mesh| build_line_pattern_batch(
params.device,
params.queue,
mesh,
scene_camera_origin,
&self.uniform_buffer,
&self.line_pattern_pipeline.uniform_bind_group_layout,
&self.line_pattern_pipeline.texture_bind_group_layout,
&self.fill_pattern_sampler,
))
.collect();
self.cached_circle_batches = params
.vector_meshes
.iter()
.filter(|mesh| mesh.render_mode == VectorRenderMode::Circle)
.map(|mesh| build_circle_batch(params.device, mesh, scene_camera_origin))
.collect();
self.cached_heatmap_batches = params
.vector_meshes
.iter()
.filter(|mesh| mesh.render_mode == VectorRenderMode::Heatmap)
.map(|mesh| build_heatmap_batch(params.device, mesh, scene_camera_origin))
.collect();
self.vector_batch_cache_key = Some(vector_batch_key);
}
{
let symbols = &frame.symbols;
if !symbols.is_empty() {
self.symbol_glyph_atlas = rustial_engine::symbols::GlyphAtlas::new();
for symbol in symbols.iter() {
if symbol.visible && symbol.opacity > 0.0 {
if let Some(text) = &symbol.text {
self.symbol_glyph_atlas.request_text(&symbol.font_stack, text);
}
}
}
self.symbol_glyph_atlas.load_requested(&*self.symbol_glyph_provider);
let dims = self.symbol_glyph_atlas.dimensions();
if dims[0] > 0 && dims[1] > 0 {
let tex = params.device.create_texture(&wgpu::TextureDescriptor {
label: Some("symbol_atlas_tex"),
size: wgpu::Extent3d {
width: dims[0] as u32,
height: dims[1] as u32,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
params.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
self.symbol_glyph_atlas.alpha(),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(dims[0] as u32),
rows_per_image: Some(dims[1] as u32),
},
wgpu::Extent3d {
width: dims[0] as u32,
height: dims[1] as u32,
depth_or_array_layers: 1,
},
);
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let atlas_bg = params.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("symbol_atlas_bg"),
layout: &self.symbol_pipeline.atlas_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
self.symbol_atlas_texture = Some((tex, view));
self.symbol_atlas_bind_group = Some(atlas_bg);
}
let mut laid_out_symbols: Vec<rustial_engine::symbols::PlacedSymbol> = symbols.to_vec();
rustial_engine::symbols::layout_symbol_glyphs(
&mut laid_out_symbols,
&self.symbol_glyph_atlas,
);
self.cached_symbol_batch = build_symbol_batch(
params.device,
&laid_out_symbols,
&self.symbol_glyph_atlas,
scene_camera_origin,
self.symbol_glyph_atlas.render_em_px(),
);
} else {
self.cached_symbol_batch = None;
self.symbol_atlas_bind_group = None;
self.symbol_atlas_texture = None;
}
}
self.cached_placeholder_batch = build_placeholder_batches(
params.device,
&frame.placeholders,
params.state.placeholder_style(),
scene_camera_origin,
);
self.build_image_overlay_batches(
params.device,
params.queue,
&frame.image_overlays,
scene_camera_origin,
);
if use_shared_terrain {
for mesh in terrain_meshes {
self.get_or_create_shared_grid(params.device, mesh.grid_resolution);
let scene_origin = scene_camera_origin;
self.get_or_create_terrain_tile_bind(
params.device, params.queue, mesh, params.state, scene_origin,
TerrainPipelineKind::Terrain,
);
self.get_or_create_terrain_tile_bind(
params.device, params.queue, mesh, params.state, scene_origin,
TerrainPipelineKind::TerrainData,
);
self.get_or_create_terrain_tile_bind(
params.device, params.queue, mesh, params.state, scene_origin,
TerrainPipelineKind::Hillshade,
);
}
}
let grid_scalar_overlays: Vec<_> = visualization
.iter()
.filter_map(|overlay| match overlay {
VisualizationOverlay::GridScalar { .. } => Some(overlay),
_ => None,
})
.collect();
let visible_grid_scalar_overlays: Vec<_> = grid_scalar_overlays
.iter()
.copied()
.filter(|overlay| visualization_overlay_intersects_scene_viewport(overlay, params.state))
.collect();
let grid_extrusion_overlays: Vec<_> = visualization
.iter()
.filter_map(|overlay| match overlay {
VisualizationOverlay::GridExtrusion { .. } => Some(overlay),
_ => None,
})
.collect();
let visible_grid_extrusion_overlays: Vec<_> = grid_extrusion_overlays
.iter()
.copied()
.filter(|overlay| visualization_overlay_intersects_scene_viewport(overlay, params.state))
.collect();
let column_overlays: Vec<_> = visualization
.iter()
.filter_map(|overlay| match overlay {
VisualizationOverlay::Columns { .. } => Some(overlay),
_ => None,
})
.collect();
let visible_column_overlays: Vec<_> = column_overlays
.iter()
.copied()
.filter(|overlay| visualization_overlay_intersects_scene_viewport(overlay, params.state))
.collect();
let point_cloud_overlays: Vec<_> = visualization
.iter()
.filter_map(|overlay| match overlay {
VisualizationOverlay::Points { .. } => Some(overlay),
_ => None,
})
.collect();
let visible_point_cloud_overlays: Vec<_> = point_cloud_overlays
.iter()
.copied()
.filter(|overlay| visualization_overlay_intersects_scene_viewport(overlay, params.state))
.collect();
let terrain_fingerprint = TerrainDataDirtyState::terrain_fingerprint(terrain_meshes);
if !visible_grid_scalar_overlays.is_empty() {
for overlay in &visible_grid_scalar_overlays {
self.get_or_create_grid_scalar_overlay(
params.device,
params.queue,
overlay,
params.state,
scene_camera_origin,
terrain_fingerprint,
);
}
}
if !visible_column_overlays.is_empty() {
self.get_or_create_shared_column_mesh(params.device);
for overlay in &visible_column_overlays {
self.get_or_create_column_overlay(
params.device,
params.queue,
overlay,
params.state,
scene_camera_origin,
);
}
}
if !visible_point_cloud_overlays.is_empty() {
self.get_or_create_shared_column_mesh(params.device);
for overlay in &visible_point_cloud_overlays {
self.get_or_create_point_cloud_overlay(
params.device,
params.queue,
overlay,
params.state,
scene_camera_origin,
);
}
}
if !visible_grid_extrusion_overlays.is_empty() {
for overlay in &visible_grid_extrusion_overlays {
self.get_or_create_grid_extrusion_overlay(
params.device,
params.queue,
overlay,
params.state,
scene_camera_origin,
terrain_fingerprint,
);
}
}
let mut encoder = params
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("rustial_encoder"),
});
let has_heatmap = !self.cached_heatmap_batches.is_empty()
&& self.cached_heatmap_batches.iter().any(|b| b.is_some());
let painter_plan = PainterPlan::new(
!terrain_meshes.is_empty(),
!params.state.hillshade_rasters().is_empty(),
has_heatmap,
);
for painter_pass in painter_plan.iter() {
match painter_pass {
PainterPass::SkyAtmosphere => {
let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("rustial_pass_sky_atmosphere"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: params.color_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: clear_color[0] as f64,
g: clear_color[1] as f64,
b: clear_color[2] as f64,
a: clear_color[3] as f64,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
});
}
PainterPass::TerrainData => {
if !self.terrain_data_dirty.needs_update(&vp, terrain_meshes) {
continue;
}
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("rustial_pass_terrain_data"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: self.terrain_interaction_buffers.coord_view(),
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: self.terrain_interaction_buffers.depth_view(),
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
});
if use_shared_terrain {
self.render_shared_terrain_data_tiles(
&mut pass,
params.state,
terrain_meshes,
);
} else {
self.render_terrain_data_batches(&mut pass, &terrain_batches);
}
}
self.terrain_data_dirty.mark_clean(&vp, terrain_meshes);
}
PainterPass::OpaqueScene => {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("rustial_pass_opaque"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: params.color_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
});
if let Some(ref ph_batch) = self.cached_placeholder_batch {
pass.set_pipeline(&self.vector_pipeline.pipeline);
pass.set_bind_group(0, &self.vector_uniform_bind_group, &[]);
pass.set_vertex_buffer(0, ph_batch.vertex_buffer.slice(..));
pass.set_index_buffer(
ph_batch.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
pass.draw_indexed(0..ph_batch.index_count, 0, 0..1);
}
if use_shared_terrain {
self.render_shared_terrain_tiles(
&mut pass,
params.state,
terrain_meshes,
params.visible_tiles,
);
} else if !terrain_batches.is_empty() {
self.render_terrain_batches(&mut pass, &terrain_batches);
} else {
self.render_tile_batches(&mut pass, &self.cached_tile_batches);
}
if !visible_grid_scalar_overlays.is_empty() {
self.render_grid_scalar_overlays(&mut pass, &visible_grid_scalar_overlays);
}
if !visible_grid_extrusion_overlays.is_empty() {
self.render_grid_extrusion_overlays(&mut pass, &visible_grid_extrusion_overlays);
}
if !visible_column_overlays.is_empty() {
self.render_column_overlays(&mut pass, &visible_column_overlays);
}
if !visible_point_cloud_overlays.is_empty() {
self.render_point_cloud_overlays(&mut pass, &visible_point_cloud_overlays);
}
self.render_vector_batches(&mut pass, &self.cached_vector_batches);
self.render_fill_batches(&mut pass, &self.cached_fill_batches);
self.render_fill_pattern_batches(&mut pass, &self.cached_fill_pattern_batches);
self.render_fill_extrusion_batches(&mut pass, &self.cached_fill_extrusion_batches);
self.render_line_batches(&mut pass, &self.cached_line_batches);
self.render_line_pattern_batches(&mut pass, &self.cached_line_pattern_batches);
self.render_circle_batches(&mut pass, &self.cached_circle_batches);
self.render_image_overlay_batches(&mut pass);
self.render_symbol_batch(&mut pass);
if !params.model_instances.is_empty() {
self.render_models(
&mut pass,
params.model_instances,
params.device,
);
}
}
PainterPass::HeatmapAccumulation => {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("rustial_pass_heatmap_accum"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.heatmap_accum_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
});
self.render_heatmap_batches(&mut pass, &self.cached_heatmap_batches);
}
PainterPass::HeatmapColormap => {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("rustial_pass_heatmap_colormap"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: params.color_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
..Default::default()
});
pass.set_pipeline(&self.heatmap_colormap_pipeline.pipeline);
pass.set_bind_group(0, &self.heatmap_colormap_uniform_bind_group, &[]);
pass.set_bind_group(1, &self.heatmap_colormap_textures_bind_group, &[]);
pass.draw(0..3, 0..1); }
PainterPass::HillshadeOverlay => {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("rustial_pass_hillshade_overlay"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: params.color_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
});
if use_shared_terrain {
self.render_shared_hillshade_tiles(
&mut pass,
params.state,
terrain_meshes,
);
} else {
self.render_hillshade_batches(&mut pass, &hillshade_batches);
}
}
}
}
params.queue.submit(std::iter::once(encoder.finish()));
self.prune_height_texture_cache(terrain_meshes);
self.prune_terrain_tile_bind_cache(terrain_meshes);
self.prune_grid_scalar_overlay_cache(&grid_scalar_overlays);
self.prune_grid_extrusion_overlay_cache(&grid_extrusion_overlays);
self.prune_column_overlay_cache(&column_overlays);
self.prune_point_cloud_overlay_cache(&point_cloud_overlays);
let tile_count_before = self.tile_atlas.len();
self.tile_atlas.end_frame();
if self.tile_atlas.len() != tile_count_before {
self.tile_batch_cache_key = None;
}
self.hillshade_atlas.end_frame();
}
fn render_tile_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
batches: &'a [TilePageBatches],
) {
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
for (page_idx, batch) in batches.iter().enumerate() {
let bg = match self.page_bind_groups.get(page_idx) {
Some(bg) => bg,
None => continue,
};
if let Some(batch) = batch.opaque.as_ref() {
pass.set_pipeline(&self.tile_pipeline.pipeline);
pass.set_bind_group(1, bg, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
if let Some(batch) = batch.translucent.as_ref() {
pass.set_pipeline(&self.tile_pipeline.translucent_pipeline);
pass.set_bind_group(1, bg, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
}
}
fn render_grid_scalar_overlays<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
overlays: &[&'a VisualizationOverlay],
) {
pass.set_pipeline(&self.grid_scalar_pipeline.pipeline);
pass.set_bind_group(0, &self.grid_scalar_uniform_bind_group, &[]);
for overlay in overlays {
let VisualizationOverlay::GridScalar { layer_id, .. } = overlay else {
continue;
};
let Some(cached) = self.grid_scalar_overlay_cache.get(layer_id) else {
continue;
};
pass.set_bind_group(1, &cached.bind_group, &[]);
pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
pass.set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..cached.index_count, 0, 0..1);
}
}
fn render_grid_extrusion_overlays<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
overlays: &[&'a VisualizationOverlay],
) {
pass.set_pipeline(&self.grid_extrusion_pipeline.pipeline);
pass.set_bind_group(0, &self.grid_extrusion_uniform_bind_group, &[]);
for overlay in overlays {
let VisualizationOverlay::GridExtrusion { layer_id, .. } = overlay else {
continue;
};
let Some(cached) = self.grid_extrusion_overlay_cache.get(layer_id) else {
continue;
};
pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
pass.set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..cached.index_count, 0, 0..1);
}
}
fn render_column_overlays<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
overlays: &[&'a VisualizationOverlay],
) {
let Some(mesh) = self.shared_column_mesh.as_ref() else {
return;
};
pass.set_pipeline(&self.column_pipeline.pipeline);
pass.set_bind_group(0, &self.column_uniform_bind_group, &[]);
for overlay in overlays {
let VisualizationOverlay::Columns { layer_id, .. } = overlay else {
continue;
};
let Some(cached) = self.column_overlay_cache.get(layer_id) else {
continue;
};
if cached.instance_count == 0 {
continue;
}
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, cached.instance_buffer.slice(..));
pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..mesh.index_count, 0, 0..cached.instance_count);
}
}
fn render_point_cloud_overlays<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
overlays: &[&'a VisualizationOverlay],
) {
let Some(mesh) = self.shared_column_mesh.as_ref() else {
return;
};
pass.set_pipeline(&self.column_pipeline.pipeline);
pass.set_bind_group(0, &self.column_uniform_bind_group, &[]);
for overlay in overlays {
let VisualizationOverlay::Points { layer_id, .. } = overlay else {
continue;
};
let Some(cached) = self.point_cloud_overlay_cache.get(layer_id) else {
continue;
};
if cached.instance_count == 0 {
continue;
}
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, cached.instance_buffer.slice(..));
pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..mesh.index_count, 0, 0..cached.instance_count);
}
}
fn render_terrain_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
batches: &'a [Option<TerrainBatch>],
) {
pass.set_pipeline(&self.terrain_pipeline.pipeline);
pass.set_bind_group(0, &self.terrain_uniform_bind_group, &[]);
for (page_idx, batch) in batches.iter().enumerate() {
let batch = match batch {
Some(b) => b,
None => continue,
};
let bg = match self.page_terrain_bind_groups.get(page_idx) {
Some(bg) => bg,
None => continue,
};
pass.set_bind_group(1, bg, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
}
fn render_terrain_data_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
batches: &'a [Option<TerrainBatch>],
) {
pass.set_pipeline(&self.terrain_data_pipeline.pipeline);
pass.set_bind_group(0, &self.terrain_data_uniform_bind_group, &[]);
for batch in batches.iter().flatten() {
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
}
fn render_hillshade_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
batches: &'a [Option<HillshadeBatch>],
) {
pass.set_pipeline(&self.hillshade_pipeline.pipeline);
pass.set_bind_group(0, &self.hillshade_uniform_bind_group, &[]);
for (page_idx, batch) in batches.iter().enumerate() {
let batch = match batch {
Some(b) => b,
None => continue,
};
let bg = match self.page_hillshade_bind_groups.get(page_idx) {
Some(bg) => bg,
None => continue,
};
pass.set_bind_group(1, bg, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
}
fn render_vector_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
batches: &'a [Option<VectorBatchEntry>],
) {
let mut pipeline_set = false;
for batch in batches.iter().flatten() {
if !pipeline_set {
pass.set_pipeline(&self.vector_pipeline.pipeline);
pass.set_bind_group(0, &self.vector_uniform_bind_group, &[]);
pipeline_set = true;
}
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
}
fn render_fill_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
batches: &'a [Option<FillBatchEntry>],
) {
for batch in batches.iter().flatten() {
pass.set_pipeline(&self.fill_pipeline.pipeline);
pass.set_bind_group(0, &batch.bind_group, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
}
fn render_fill_pattern_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
batches: &'a [Option<FillPatternBatchEntry>],
) {
for batch in batches.iter().flatten() {
pass.set_pipeline(&self.fill_pattern_pipeline.pipeline);
pass.set_bind_group(0, &batch.uniform_bind_group, &[]);
pass.set_bind_group(1, &batch.texture_bind_group, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
}
fn render_fill_extrusion_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
batches: &'a [Option<FillExtrusionBatchEntry>],
) {
let mut pipeline_set = false;
for batch in batches.iter().flatten() {
if !pipeline_set {
pass.set_pipeline(&self.fill_extrusion_pipeline.pipeline);
pass.set_bind_group(0, &self.fill_extrusion_uniform_bind_group, &[]);
pipeline_set = true;
}
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
}
fn render_line_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
batches: &'a [Option<LineBatchEntry>],
) {
let mut pipeline_set = false;
for batch in batches.iter().flatten() {
if !pipeline_set {
pass.set_pipeline(&self.line_pipeline.pipeline);
pass.set_bind_group(0, &self.line_uniform_bind_group, &[]);
pipeline_set = true;
}
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
}
fn render_line_pattern_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
batches: &'a [Option<LinePatternBatchEntry>],
) {
for batch in batches.iter().flatten() {
pass.set_pipeline(&self.line_pattern_pipeline.pipeline);
pass.set_bind_group(0, &batch.uniform_bind_group, &[]);
pass.set_bind_group(1, &batch.texture_bind_group, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
}
fn render_circle_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
batches: &'a [Option<CircleBatchEntry>],
) {
let mut pipeline_set = false;
for batch in batches.iter().flatten() {
if !pipeline_set {
pass.set_pipeline(&self.circle_pipeline.pipeline);
pass.set_bind_group(0, &self.circle_uniform_bind_group, &[]);
pipeline_set = true;
}
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
}
fn render_heatmap_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
batches: &'a [Option<HeatmapBatchEntry>],
) {
let mut pipeline_set = false;
for batch in batches.iter().flatten() {
if !pipeline_set {
pass.set_pipeline(&self.heatmap_pipeline.pipeline);
pass.set_bind_group(0, &self.heatmap_uniform_bind_group, &[]);
pipeline_set = true;
}
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
}
fn build_image_overlay_batches(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
overlays: &[rustial_engine::layers::ImageOverlayData],
camera_origin: glam::DVec3,
) {
let mut old_cache: Vec<CachedImageOverlayBatch> =
std::mem::take(&mut self.cached_image_overlay_batches);
for overlay in overlays {
if overlay.width == 0 || overlay.height == 0 || overlay.data.is_empty() {
continue;
}
let data_arc_ptr = Arc::as_ptr(&overlay.data) as usize;
let cached_idx = old_cache
.iter()
.position(|c| c.layer_id == overlay.layer_id);
let uvs = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
let vertices: Vec<ImageOverlayVertex> = overlay
.corners
.iter()
.zip(uvs.iter())
.map(|(corner, uv)| {
let rel = [
(corner[0] - camera_origin.x) as f32,
(corner[1] - camera_origin.y) as f32,
(corner[2] - camera_origin.z) as f32,
];
ImageOverlayVertex {
position: rel,
uv: *uv,
opacity: overlay.opacity,
}
})
.collect();
let indices: Vec<u32> = vec![0, 1, 2, 0, 2, 3];
if let Some(idx) = cached_idx {
let mut cached = old_cache.remove(idx);
cached.vertex_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("image_overlay_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
cached.index_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("image_overlay_ib"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
if cached.data_arc_ptr != data_arc_ptr {
if cached.tex_dimensions == (overlay.width, overlay.height) {
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &cached.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&overlay.data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(overlay.width * 4),
rows_per_image: Some(overlay.height),
},
wgpu::Extent3d {
width: overlay.width,
height: overlay.height,
depth_or_array_layers: 1,
},
);
} else {
let (texture, texture_view, texture_bind_group) =
self.create_overlay_texture(device, queue, overlay);
cached.texture = texture;
cached.texture_view = texture_view;
cached.texture_bind_group = texture_bind_group;
cached.tex_dimensions = (overlay.width, overlay.height);
}
cached.data_arc_ptr = data_arc_ptr;
}
self.cached_image_overlay_batches.push(cached);
} else {
let vertex_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("image_overlay_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("image_overlay_ib"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
let (texture, texture_view, texture_bind_group) =
self.create_overlay_texture(device, queue, overlay);
self.cached_image_overlay_batches.push(CachedImageOverlayBatch {
vertex_buffer,
index_buffer,
texture,
texture_view,
texture_bind_group,
layer_id: overlay.layer_id,
tex_dimensions: (overlay.width, overlay.height),
data_arc_ptr,
});
}
}
}
fn create_overlay_texture(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
overlay: &rustial_engine::layers::ImageOverlayData,
) -> (wgpu::Texture, wgpu::TextureView, wgpu::BindGroup) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("image_overlay_tex"),
size: wgpu::Extent3d {
width: overlay.width,
height: overlay.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&overlay.data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(overlay.width * 4),
rows_per_image: Some(overlay.height),
},
wgpu::Extent3d {
width: overlay.width,
height: overlay.height,
depth_or_array_layers: 1,
},
);
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("image_overlay_tex_bg"),
layout: &self.image_overlay_pipeline.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
(texture, texture_view, texture_bind_group)
}
fn render_image_overlay_batches<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
) {
if self.cached_image_overlay_batches.is_empty() {
return;
}
pass.set_pipeline(&self.image_overlay_pipeline.pipeline);
pass.set_bind_group(0, &self.image_overlay_uniform_bind_group, &[]);
for batch in &self.cached_image_overlay_batches {
pass.set_bind_group(1, &batch.texture_bind_group, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..6, 0, 0..1);
}
}
fn render_symbol_batch<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
) {
let batch = match &self.cached_symbol_batch {
Some(b) => b,
None => return,
};
let atlas_bg = match &self.symbol_atlas_bind_group {
Some(bg) => bg,
None => return,
};
pass.set_pipeline(&self.symbol_pipeline.pipeline);
pass.set_bind_group(0, &self.symbol_uniform_bind_group, &[]);
pass.set_bind_group(1, atlas_bg, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..batch.index_count, 0, 0..1);
}
fn render_models<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
model_instances: &[ModelInstance],
device: &wgpu::Device,
) {
if model_instances.is_empty() {
return;
}
let cached = match &self.cached_model_transforms {
Some(c) if c.instance_count == model_instances.len() => c,
_ => return,
};
pass.set_pipeline(&self.model_pipeline.pipeline);
pass.set_bind_group(0, &self.model_uniform_bind_group, &[]);
for (i, instance) in model_instances.iter().enumerate() {
let dyn_offset = (i * cached.stride) as u32;
let mesh_key = ModelMeshKey::from_mesh(&instance.mesh);
let cached_mesh = self.model_mesh_cache.get(&mesh_key);
if let Some(cached_mesh) = cached_mesh {
pass.set_bind_group(1, &cached.bind_group, &[dyn_offset]);
pass.set_vertex_buffer(0, cached_mesh.vertex_buffer.slice(..));
pass.set_index_buffer(cached_mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..cached_mesh.index_count, 0, 0..1);
} else {
let vertices = build_model_vertices(&instance.mesh);
let vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("model_inline_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("model_inline_ib"),
contents: bytemuck::cast_slice(&instance.mesh.indices),
usage: wgpu::BufferUsages::INDEX,
});
let index_count = instance.mesh.indices.len() as u32;
pass.set_bind_group(1, &cached.bind_group, &[dyn_offset]);
pass.set_vertex_buffer(0, vb.slice(..));
pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..index_count, 0, 0..1);
}
}
}
pub fn cache_model_meshes(
&mut self,
device: &wgpu::Device,
model_instances: &[ModelInstance],
) {
for instance in model_instances {
let key = ModelMeshKey::from_mesh(&instance.mesh);
if self.model_mesh_cache.contains_key(&key) {
continue;
}
let vertices = build_model_vertices(&instance.mesh);
let vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cached_model_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cached_model_ib"),
contents: bytemuck::cast_slice(&instance.mesh.indices),
usage: wgpu::BufferUsages::INDEX,
});
self.model_mesh_cache.insert(
key,
CachedModelMesh {
vertex_buffer: vb,
index_buffer: ib,
index_count: instance.mesh.indices.len() as u32,
},
);
}
}
fn cache_model_transforms(
&mut self,
device: &wgpu::Device,
model_instances: &[ModelInstance],
camera_origin: DVec3,
state: &MapState,
) {
let min_align = device.limits().min_uniform_buffer_offset_alignment as usize;
let stride = 64_usize.div_ceil(min_align) * min_align;
let mut fp: u64 = model_instances.len() as u64;
let origin_key = [
(camera_origin.x * 100.0) as i64,
(camera_origin.y * 100.0) as i64,
(camera_origin.z * 100.0) as i64,
];
fp = fp
.wrapping_mul(31)
.wrapping_add(origin_key[0] as u64)
.wrapping_mul(31)
.wrapping_add(origin_key[1] as u64)
.wrapping_mul(31)
.wrapping_add(origin_key[2] as u64);
for instance in model_instances {
fp = fp
.wrapping_mul(31)
.wrapping_add(instance.position.lat.to_bits())
.wrapping_mul(31)
.wrapping_add(instance.position.lon.to_bits())
.wrapping_mul(31)
.wrapping_add(instance.scale.to_bits())
.wrapping_mul(31)
.wrapping_add(instance.heading.to_bits());
}
if let Some(ref cached) = self.cached_model_transforms {
if cached.fingerprint == fp && cached.instance_count == model_instances.len() {
return;
}
}
let mut transform_bytes = vec![0u8; stride * model_instances.len()];
for (i, instance) in model_instances.iter().enumerate() {
let terrain_elev = state.elevation_at(&instance.position);
let altitude = instance.resolve_altitude(terrain_elev);
let world_pos = state.camera().projection().project(&instance.position);
let rel_x = (world_pos.position.x - camera_origin.x) as f32;
let rel_y = (world_pos.position.y - camera_origin.y) as f32;
let rel_z = (altitude - camera_origin.z) as f32;
let scale = instance.scale as f32;
let heading = instance.heading as f32;
let pitch = instance.pitch as f32;
let roll = instance.roll as f32;
let rotation = glam::Quat::from_rotation_z(heading)
* glam::Quat::from_rotation_x(pitch)
* glam::Quat::from_rotation_y(roll);
let transform = Mat4::from_translation(glam::Vec3::new(rel_x, rel_y, rel_z))
* Mat4::from_quat(rotation)
* Mat4::from_scale(glam::Vec3::splat(scale));
let mat = transform.to_cols_array_2d();
let mat_bytes = bytemuck::cast_slice(&mat);
let offset = i * stride;
transform_bytes[offset..offset + 64].copy_from_slice(mat_bytes);
}
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("model_transforms_cached_buf"),
contents: &transform_bytes,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("model_transforms_cached_bg"),
layout: &self.model_pipeline.model_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
});
self.cached_model_transforms = Some(CachedModelTransforms {
buffer,
bind_group,
stride,
instance_count: model_instances.len(),
fingerprint: fp,
})
}
fn rebuild_page_bind_groups(&mut self, device: &wgpu::Device) {
let tile_pages = self.tile_atlas.page_count();
while self.page_bind_groups.len() < tile_pages {
let idx = self.page_bind_groups.len();
let view = &self.tile_atlas.pages[idx].view;
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("rustial_tile_page_bg_{idx}")),
layout: &self.tile_pipeline.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
self.page_bind_groups.push(bg);
let terrain_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("rustial_terrain_page_bg_{idx}")),
layout: &self.terrain_pipeline.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
self.page_terrain_bind_groups.push(terrain_bg);
}
let hillshade_pages = self.hillshade_atlas.page_count();
while self.page_hillshade_bind_groups.len() < hillshade_pages {
let idx = self.page_hillshade_bind_groups.len();
let view = &self.hillshade_atlas.pages[idx].view;
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("rustial_hillshade_page_bg_{idx}")),
layout: &self.hillshade_pipeline.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
self.page_hillshade_bind_groups.push(bg);
}
}
fn get_or_create_shared_grid(
&mut self,
device: &wgpu::Device,
resolution: u16,
) -> &SharedTerrainGridMesh {
if !self.shared_terrain_grids.contains_key(&resolution) {
let (vertices, indices) = build_shared_terrain_grid(resolution as usize);
let vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("terrain_grid_vb_{resolution}")),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("terrain_grid_ib_{resolution}")),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
self.shared_terrain_grids.insert(
resolution,
SharedTerrainGridMesh {
vertex_buffer: vb,
index_buffer: ib,
index_count: indices.len() as u32,
},
);
}
self.shared_terrain_grids.get(&resolution).unwrap()
}
fn get_or_create_terrain_tile_bind(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
mesh: &TerrainMeshData,
state: &MapState,
scene_origin: DVec3,
pipeline_kind: TerrainPipelineKind,
) -> Option<()> {
let elevation = mesh.elevation_texture.as_ref()?;
let key = TerrainTileBindKey {
tile: mesh.tile,
pipeline: pipeline_kind,
};
let origin_key = [
(scene_origin.x * 100.0) as i64,
(scene_origin.y * 100.0) as i64,
(scene_origin.z * 100.0) as i64,
];
if let Some(cached) = self.terrain_tile_bind_cache.get(&key) {
if cached.origin_key == origin_key && cached.generation == mesh.generation {
return Some(());
}
}
let tile = mesh.tile;
let gen = mesh.generation;
let needs_height = self
.height_texture_cache
.get(&tile)
.map_or(true, |c| c.generation != gen);
if needs_height {
let size = wgpu::Extent3d {
width: elevation.width.max(1),
height: elevation.height.max(1),
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("height_tex_{:?}", tile)),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R32Float,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
bytemuck::cast_slice(&elevation.data),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(elevation.width.max(1) * 4),
rows_per_image: None,
},
size,
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.height_texture_cache
.insert(tile, CachedHeightTexture { generation: gen, view });
}
let tile_uniform = build_terrain_tile_uniform(mesh, elevation, state, scene_origin);
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("terrain_tile_uniform_{:?}_{:?}", mesh.tile, pipeline_kind)),
contents: bytemuck::bytes_of(&tile_uniform),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let layout = match pipeline_kind {
TerrainPipelineKind::Terrain => &self.terrain_pipeline.tile_bind_group_layout,
TerrainPipelineKind::TerrainData => &self.terrain_data_pipeline.tile_bind_group_layout,
TerrainPipelineKind::Hillshade => &self.hillshade_pipeline.tile_bind_group_layout,
};
let height_view_ref = &self.height_texture_cache.get(&tile)?.view;
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("terrain_tile_bg_{:?}_{:?}", mesh.tile, pipeline_kind)),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(height_view_ref),
},
],
});
self.terrain_tile_bind_cache.insert(
key,
CachedTerrainTileBind {
uniform_buffer,
bind_group,
origin_key,
generation: mesh.generation,
},
);
Some(())
}
fn render_shared_terrain_tiles<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
_state: &MapState,
terrain_meshes: &[TerrainMeshData],
visible_tiles: &[VisibleTile],
) {
pass.set_pipeline(&self.terrain_pipeline.pipeline);
pass.set_bind_group(0, &self.terrain_uniform_bind_group, &[]);
for mesh in terrain_meshes {
let grid = match self.shared_terrain_grids.get(&mesh.grid_resolution) {
Some(g) => g,
None => continue,
};
if let Some(actual) = find_terrain_texture_actual(mesh.tile, visible_tiles) {
if let Some(region) = self.tile_atlas.get(&actual) {
if let Some(bg) = self.page_terrain_bind_groups.get(region.page) {
pass.set_bind_group(1, bg, &[]);
}
}
}
let key = TerrainTileBindKey {
tile: mesh.tile,
pipeline: TerrainPipelineKind::Terrain,
};
if let Some(cached) = self.terrain_tile_bind_cache.get(&key) {
pass.set_bind_group(2, &cached.bind_group, &[]);
pass.set_vertex_buffer(0, grid.vertex_buffer.slice(..));
pass.set_index_buffer(grid.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..grid.index_count, 0, 0..1);
}
}
}
fn render_shared_terrain_data_tiles<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
_state: &MapState,
terrain_meshes: &[TerrainMeshData],
) {
pass.set_pipeline(&self.terrain_data_pipeline.pipeline);
pass.set_bind_group(0, &self.terrain_data_uniform_bind_group, &[]);
for mesh in terrain_meshes {
let grid = match self.shared_terrain_grids.get(&mesh.grid_resolution) {
Some(g) => g,
None => continue,
};
let key = TerrainTileBindKey {
tile: mesh.tile,
pipeline: TerrainPipelineKind::TerrainData,
};
if let Some(cached) = self.terrain_tile_bind_cache.get(&key) {
pass.set_bind_group(1, &cached.bind_group, &[]);
pass.set_vertex_buffer(0, grid.vertex_buffer.slice(..));
pass.set_index_buffer(grid.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..grid.index_count, 0, 0..1);
}
}
}
fn render_shared_hillshade_tiles<'a>(
&'a self,
pass: &mut wgpu::RenderPass<'a>,
_state: &MapState,
terrain_meshes: &[TerrainMeshData],
) {
pass.set_pipeline(&self.hillshade_pipeline.pipeline);
pass.set_bind_group(0, &self.hillshade_uniform_bind_group, &[]);
for mesh in terrain_meshes {
let grid = match self.shared_terrain_grids.get(&mesh.grid_resolution) {
Some(g) => g,
None => continue,
};
if let Some(region) = self.hillshade_atlas.get(&mesh.tile) {
if let Some(bg) = self.page_hillshade_bind_groups.get(region.page) {
pass.set_bind_group(1, bg, &[]);
}
}
let key = TerrainTileBindKey {
tile: mesh.tile,
pipeline: TerrainPipelineKind::Hillshade,
};
if let Some(cached) = self.terrain_tile_bind_cache.get(&key) {
pass.set_bind_group(2, &cached.bind_group, &[]);
pass.set_vertex_buffer(0, grid.vertex_buffer.slice(..));
pass.set_index_buffer(grid.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..grid.index_count, 0, 0..1);
}
}
}
fn prune_height_texture_cache(&mut self, terrain_meshes: &[TerrainMeshData]) {
let live: std::collections::HashSet<TileId> =
terrain_meshes.iter().map(|m| m.tile).collect();
self.height_texture_cache.retain(|tile, _| live.contains(tile));
}
fn prune_terrain_tile_bind_cache(&mut self, terrain_meshes: &[TerrainMeshData]) {
let live: std::collections::HashSet<TileId> =
terrain_meshes.iter().map(|m| m.tile).collect();
self.terrain_tile_bind_cache
.retain(|key, _| live.contains(&key.tile));
}
fn prune_grid_scalar_overlay_cache(&mut self, overlays: &[&VisualizationOverlay]) {
let live: std::collections::HashSet<LayerId> = overlays
.iter()
.filter_map(|overlay| match overlay {
VisualizationOverlay::GridScalar { layer_id, .. } => Some(*layer_id),
_ => None,
})
.collect();
self.grid_scalar_overlay_cache
.retain(|layer_id, _| live.contains(layer_id));
}
fn prune_grid_extrusion_overlay_cache(&mut self, overlays: &[&VisualizationOverlay]) {
let live: std::collections::HashSet<LayerId> = overlays
.iter()
.filter_map(|overlay| match overlay {
VisualizationOverlay::GridExtrusion { layer_id, .. } => Some(*layer_id),
_ => None,
})
.collect();
self.grid_extrusion_overlay_cache
.retain(|layer_id, _| live.contains(layer_id));
}
fn prune_column_overlay_cache(&mut self, overlays: &[&VisualizationOverlay]) {
let live: std::collections::HashSet<LayerId> = overlays
.iter()
.filter_map(|overlay| match overlay {
VisualizationOverlay::Columns { layer_id, .. } => Some(*layer_id),
_ => None,
})
.collect();
self.column_overlay_cache
.retain(|layer_id, _| live.contains(layer_id));
}
fn prune_point_cloud_overlay_cache(&mut self, overlays: &[&VisualizationOverlay]) {
let live: std::collections::HashSet<LayerId> = overlays
.iter()
.filter_map(|overlay| match overlay {
VisualizationOverlay::Points { layer_id, .. } => Some(*layer_id),
_ => None,
})
.collect();
self.point_cloud_overlay_cache
.retain(|layer_id, _| live.contains(layer_id));
}
pub fn render_to_buffer(
&mut self,
state: &MapState,
device: &wgpu::Device,
queue: &wgpu::Queue,
visible_tiles: &[VisibleTile],
vector_meshes: &[VectorMeshData],
model_instances: &[ModelInstance],
) -> Option<Vec<u8>> {
let format = wgpu::TextureFormat::Rgba8UnormSrgb;
let color_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("rustial_render_to_buffer_color"),
size: wgpu::Extent3d {
width: self.width,
height: self.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_tex.create_view(&wgpu::TextureViewDescriptor::default());
let clear_color = state.computed_fog().clear_color;
self.render_full(&RenderParams {
state,
device,
queue,
color_view: &color_view,
visible_tiles,
vector_meshes,
model_instances,
clear_color,
});
let bytes_per_row = self.width * 4;
let buffer_size = (bytes_per_row * self.height) as wgpu::BufferAddress;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rustial_render_to_buffer_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("rustial_render_to_buffer_encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &color_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(self.height),
},
},
wgpu::Extent3d {
width: self.width,
height: self.height,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::wait());
rx.recv().ok()?.ok()?;
let data = slice.get_mapped_range().to_vec();
readback.unmap();
Some(data)
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn visualization_perf_stats(&self) -> VisualizationPerfStats {
self.visualization_perf_stats
}
pub fn tile_atlas_diagnostics(&self) -> crate::gpu::tile_atlas::AtlasDiagnostics {
self.tile_atlas.diagnostics()
}
pub fn hillshade_atlas_diagnostics(&self) -> crate::gpu::tile_atlas::AtlasDiagnostics {
self.hillshade_atlas.diagnostics()
}
}
fn build_grid_scalar_geometry(
grid: &rustial_engine::GeoGrid,
state: &MapState,
scene_origin: DVec3,
) -> (Vec<GridScalarVertex>, Vec<u32>) {
let rows = grid.rows.max(1);
let cols = grid.cols.max(1);
let mut vertices = Vec::with_capacity((rows + 1) * (cols + 1));
let mut indices = Vec::with_capacity(rows * cols * 6);
for row in 0..=rows {
for col in 0..=cols {
let u = col as f32 / cols as f32;
let v = row as f32 / rows as f32;
let coord = grid_corner_coord(grid, row, col, state);
let projected = state.camera().projection().project(&coord);
vertices.push(GridScalarVertex {
position: [
(projected.position.x - scene_origin.x) as f32,
(projected.position.y - scene_origin.y) as f32,
(projected.position.z - scene_origin.z + 0.05) as f32,
],
uv: [u, v],
});
}
}
for row in 0..rows {
for col in 0..cols {
let tl = (row * (cols + 1) + col) as u32;
let tr = tl + 1;
let bl = ((row + 1) * (cols + 1) + col) as u32;
let br = bl + 1;
indices.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
}
}
(vertices, indices)
}
fn grid_corner_coord(
grid: &rustial_engine::GeoGrid,
row: usize,
col: usize,
state: &MapState,
) -> rustial_math::GeoCoord {
let dx = col as f64 * grid.cell_width;
let dy = row as f64 * grid.cell_height;
let (sin_r, cos_r) = grid.rotation.sin_cos();
let rx = dx * cos_r - dy * sin_r;
let ry = dx * sin_r + dy * cos_r;
let coord = offset_geo_coord(&grid.origin, rx, ry);
let altitude = resolve_grid_surface_altitude(grid, &coord, state);
rustial_math::GeoCoord::new(coord.lat, coord.lon, altitude)
}
fn create_grid_scalar_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
field: &rustial_engine::ScalarField2D,
) -> wgpu::Texture {
let size = wgpu::Extent3d {
width: field.cols.max(1) as u32,
height: field.rows.max(1) as u32,
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("grid_scalar_field_texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R32Float,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
write_grid_scalar_texture(queue, &texture, field);
texture
}
fn write_grid_scalar_texture(
queue: &wgpu::Queue,
texture: &wgpu::Texture,
field: &rustial_engine::ScalarField2D,
) {
let size = wgpu::Extent3d {
width: field.cols.max(1) as u32,
height: field.rows.max(1) as u32,
depth_or_array_layers: 1,
};
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
bytemuck::cast_slice(&field.data),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(field.cols.max(1) as u32 * 4),
rows_per_image: Some(field.rows.max(1) as u32),
},
size,
);
}
fn create_grid_scalar_ramp_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
ramp: &rustial_engine::ColorRamp,
) -> wgpu::Texture {
let width = 256u32;
let data = ramp.as_texture_data(width);
let size = wgpu::Extent3d {
width,
height: 1,
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("grid_scalar_ramp_texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width * 4),
rows_per_image: Some(1),
},
size,
);
texture
}
fn create_heatmap_accum_texture(
device: &wgpu::Device,
width: u32,
height: u32,
) -> (wgpu::Texture, wgpu::TextureView) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("heatmap_accum_texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R16Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
(texture, view)
}
fn create_default_heatmap_ramp_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
) -> wgpu::Texture {
const WIDTH: u32 = 256;
let stops: &[(f32, [u8; 4])] = &[
(0.00, [0, 0, 0, 0]),
(0.10, [65, 105, 225, 255]),
(0.30, [0, 255, 255, 255]),
(0.50, [0, 255, 0, 255]),
(0.70, [255, 255, 0, 255]),
(1.00, [255, 0, 0, 255]),
];
let mut data = vec![0u8; WIDTH as usize * 4];
for i in 0..WIDTH as usize {
let t = i as f32 / (WIDTH - 1) as f32;
let mut lo = 0;
for s in 1..stops.len() {
if stops[s].0 >= t {
lo = s - 1;
break;
}
}
let hi = (lo + 1).min(stops.len() - 1);
let range = stops[hi].0 - stops[lo].0;
let frac = if range > 0.0 {
(t - stops[lo].0) / range
} else {
0.0
};
for c in 0..4 {
let a = stops[lo].1[c] as f32;
let b = stops[hi].1[c] as f32;
data[i * 4 + c] = (a + (b - a) * frac).round() as u8;
}
}
let size = wgpu::Extent3d {
width: WIDTH,
height: 1,
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("heatmap_ramp_texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(WIDTH * 4),
rows_per_image: Some(1),
},
size,
);
texture
}
fn create_heatmap_colormap_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
accum_view: &wgpu::TextureView,
ramp_view: &wgpu::TextureView,
sampler: &wgpu::Sampler,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("heatmap_colormap_textures_bg"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(accum_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(ramp_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
})
}
fn build_grid_scalar_uniform(
grid: &rustial_engine::GeoGrid,
field: &rustial_engine::ScalarField2D,
state: &MapState,
scene_origin: DVec3,
opacity: f32,
) -> GridScalarUniform {
let projection_kind = match state.camera().projection() {
rustial_engine::CameraProjection::WebMercator => 0.0,
rustial_engine::CameraProjection::Equirectangular => 1.0,
_ => 0.0,
};
let base_altitude = match grid.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => 0.0,
rustial_engine::AltitudeMode::RelativeToGround => grid.origin.alt as f32,
rustial_engine::AltitudeMode::Absolute => grid.origin.alt as f32,
};
GridScalarUniform {
origin_counts: [grid.origin.lat as f32, grid.origin.lon as f32, grid.rows as f32, grid.cols as f32],
grid_params: [grid.cell_width as f32, grid.cell_height as f32, grid.rotation as f32, opacity],
scene_origin: [scene_origin.x as f32, scene_origin.y as f32, scene_origin.z as f32, projection_kind],
value_params: [
field.min,
field.max,
field.nan_value.unwrap_or(0.0),
if field.nan_value.is_some() { 1.0 } else { 0.0 },
],
base_altitude: [base_altitude, 0.0, 0.0, 0.0],
}
}
fn grid_scalar_ramp_fingerprint(ramp: &rustial_engine::ColorRamp) -> u64 {
let mut h = ramp.stops.len() as u64;
for stop in &ramp.stops {
h = h
.wrapping_mul(31)
.wrapping_add(stop.value.to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(stop.color[0].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(stop.color[1].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(stop.color[2].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(stop.color[3].to_bits() as u64);
}
h
}
fn grid_extrusion_params_fingerprint(params: &rustial_engine::ExtrusionParams) -> u64 {
(params.height_scale.to_bits())
.wrapping_mul(31)
.wrapping_add(params.base_meters.to_bits())
}
fn grid_extrusion_grid_fingerprint(grid: &rustial_engine::GeoGrid) -> u64 {
let mut h = 17u64;
h = h.wrapping_mul(31).wrapping_add(grid.origin.lat.to_bits());
h = h.wrapping_mul(31).wrapping_add(grid.origin.lon.to_bits());
h = h.wrapping_mul(31).wrapping_add(grid.origin.alt.to_bits());
h = h.wrapping_mul(31).wrapping_add(grid.rows as u64);
h = h.wrapping_mul(31).wrapping_add(grid.cols as u64);
h = h.wrapping_mul(31).wrapping_add(grid.cell_width.to_bits());
h = h.wrapping_mul(31).wrapping_add(grid.cell_height.to_bits());
h = h.wrapping_mul(31).wrapping_add(grid.rotation.to_bits());
h = h.wrapping_mul(31).wrapping_add(match grid.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => 0,
rustial_engine::AltitudeMode::RelativeToGround => 1,
rustial_engine::AltitudeMode::Absolute => 2,
});
h
}
fn build_grid_extrusion_geometry(
grid: &rustial_engine::GeoGrid,
field: &rustial_engine::ScalarField2D,
ramp: &rustial_engine::ColorRamp,
params: &rustial_engine::ExtrusionParams,
state: &MapState,
scene_origin: DVec3,
) -> (Vec<GridExtrusionVertex>, Vec<u32>) {
let mut vertices = Vec::new();
let mut indices = Vec::new();
for row in 0..grid.rows {
for col in 0..grid.cols {
let Some(value) = field.sample(row, col) else {
continue;
};
let t = field.normalized(row, col).unwrap_or(0.5);
let color = ramp.evaluate(t);
let corners = grid_cell_corners_world(grid, row, col, state, scene_origin, params);
append_extruded_cell_geometry(
&mut vertices,
&mut indices,
corners,
(value as f32) * params.height_scale as f32,
color,
);
}
}
(vertices, indices)
}
fn append_extruded_cell_geometry(
vertices: &mut Vec<GridExtrusionVertex>,
indices: &mut Vec<u32>,
corners: [[f32; 3]; 4],
extrusion_height: f32,
color: [f32; 4],
) {
let [nw, ne, sw, se] = corners;
let top = [
[nw[0], nw[1], nw[2] + extrusion_height],
[ne[0], ne[1], ne[2] + extrusion_height],
[sw[0], sw[1], sw[2] + extrusion_height],
[se[0], se[1], se[2] + extrusion_height],
];
let base = [
nw,
ne,
sw,
se,
];
append_quad(vertices, indices, top[0], top[1], top[2], top[3], [0.0, 0.0, 1.0], color);
append_quad(vertices, indices, base[0], base[1], top[0], top[1], [0.0, -1.0, 0.0], color);
append_quad(vertices, indices, top[2], top[3], base[2], base[3], [0.0, 1.0, 0.0], color);
append_quad(vertices, indices, base[0], top[0], base[2], top[2], [-1.0, 0.0, 0.0], color);
append_quad(vertices, indices, top[1], base[1], top[3], base[3], [1.0, 0.0, 0.0], color);
}
fn append_quad(
vertices: &mut Vec<GridExtrusionVertex>,
indices: &mut Vec<u32>,
a: [f32; 3],
b: [f32; 3],
c: [f32; 3],
d: [f32; 3],
normal: [f32; 3],
color: [f32; 4],
) {
let base_index = vertices.len() as u32;
vertices.extend_from_slice(&[
GridExtrusionVertex { position: a, normal, color },
GridExtrusionVertex { position: b, normal, color },
GridExtrusionVertex { position: c, normal, color },
GridExtrusionVertex { position: d, normal, color },
]);
indices.extend_from_slice(&[
base_index,
base_index + 2,
base_index + 1,
base_index + 1,
base_index + 2,
base_index + 3,
]);
}
fn grid_cell_corners_world(
grid: &rustial_engine::GeoGrid,
row: usize,
col: usize,
state: &MapState,
scene_origin: DVec3,
params: &rustial_engine::ExtrusionParams,
) -> [[f32; 3]; 4] {
let nw = project_grid_offset(
grid,
col as f64 * grid.cell_width,
row as f64 * grid.cell_height,
state,
scene_origin,
params,
);
let ne = project_grid_offset(
grid,
(col + 1) as f64 * grid.cell_width,
row as f64 * grid.cell_height,
state,
scene_origin,
params,
);
let sw = project_grid_offset(
grid,
col as f64 * grid.cell_width,
(row + 1) as f64 * grid.cell_height,
state,
scene_origin,
params,
);
let se = project_grid_offset(
grid,
(col + 1) as f64 * grid.cell_width,
(row + 1) as f64 * grid.cell_height,
state,
scene_origin,
params,
);
[nw, ne, sw, se]
}
fn project_grid_offset(
grid: &rustial_engine::GeoGrid,
dx: f64,
dy: f64,
state: &MapState,
scene_origin: DVec3,
params: &rustial_engine::ExtrusionParams,
) -> [f32; 3] {
let (sin_r, cos_r) = grid.rotation.sin_cos();
let rx = dx * cos_r - dy * sin_r;
let ry = dx * sin_r + dy * cos_r;
let coord = offset_geo_coord(&grid.origin, rx, ry);
let altitude = resolve_grid_base_altitude(grid, &coord, state, params) as f64;
let elevated_coord = rustial_math::GeoCoord::new(coord.lat, coord.lon, altitude);
let projected = state.camera().projection().project(&elevated_coord);
[
(projected.position.x - scene_origin.x) as f32,
(projected.position.y - scene_origin.y) as f32,
(projected.position.z - scene_origin.z) as f32,
]
}
fn offset_geo_coord(origin: &rustial_math::GeoCoord, dx_meters: f64, dy_meters: f64) -> rustial_math::GeoCoord {
const METERS_PER_DEG_LAT: f64 = 111_320.0;
let lat = origin.lat - dy_meters / METERS_PER_DEG_LAT;
let cos_lat = origin.lat.to_radians().cos().max(1e-10);
let lon = origin.lon + dx_meters / (METERS_PER_DEG_LAT * cos_lat);
rustial_math::GeoCoord::new(lat, lon, origin.alt)
}
fn resolve_grid_base_altitude(
grid: &rustial_engine::GeoGrid,
coord: &rustial_math::GeoCoord,
state: &MapState,
params: &rustial_engine::ExtrusionParams,
) -> f32 {
let terrain = state.elevation_at(coord).unwrap_or(0.0);
match grid.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => (terrain + params.base_meters) as f32,
rustial_engine::AltitudeMode::RelativeToGround => {
(terrain + grid.origin.alt + params.base_meters) as f32
}
rustial_engine::AltitudeMode::Absolute => (grid.origin.alt + params.base_meters) as f32,
}
}
fn resolve_grid_surface_altitude(
grid: &rustial_engine::GeoGrid,
coord: &rustial_math::GeoCoord,
state: &MapState,
) -> f64 {
let terrain = state.elevation_at(coord).unwrap_or(0.0);
match grid.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => terrain,
rustial_engine::AltitudeMode::RelativeToGround => terrain + grid.origin.alt,
rustial_engine::AltitudeMode::Absolute => grid.origin.alt,
}
}
fn build_unit_column_mesh() -> (Vec<ColumnVertex>, Vec<u32>) {
let vertices = vec![
ColumnVertex { position: [-0.5, -0.5, 1.0], normal: [0.0, 0.0, 1.0] },
ColumnVertex { position: [0.5, -0.5, 1.0], normal: [0.0, 0.0, 1.0] },
ColumnVertex { position: [-0.5, 0.5, 1.0], normal: [0.0, 0.0, 1.0] },
ColumnVertex { position: [0.5, 0.5, 1.0], normal: [0.0, 0.0, 1.0] },
ColumnVertex { position: [-0.5, -0.5, 0.0], normal: [0.0, 0.0, -1.0] },
ColumnVertex { position: [0.5, -0.5, 0.0], normal: [0.0, 0.0, -1.0] },
ColumnVertex { position: [-0.5, 0.5, 0.0], normal: [0.0, 0.0, -1.0] },
ColumnVertex { position: [0.5, 0.5, 0.0], normal: [0.0, 0.0, -1.0] },
ColumnVertex { position: [-0.5, -0.5, 0.0], normal: [0.0, -1.0, 0.0] },
ColumnVertex { position: [0.5, -0.5, 0.0], normal: [0.0, -1.0, 0.0] },
ColumnVertex { position: [-0.5, -0.5, 1.0], normal: [0.0, -1.0, 0.0] },
ColumnVertex { position: [0.5, -0.5, 1.0], normal: [0.0, -1.0, 0.0] },
ColumnVertex { position: [-0.5, 0.5, 1.0], normal: [0.0, 1.0, 0.0] },
ColumnVertex { position: [0.5, 0.5, 1.0], normal: [0.0, 1.0, 0.0] },
ColumnVertex { position: [-0.5, 0.5, 0.0], normal: [0.0, 1.0, 0.0] },
ColumnVertex { position: [0.5, 0.5, 0.0], normal: [0.0, 1.0, 0.0] },
ColumnVertex { position: [-0.5, -0.5, 0.0], normal: [-1.0, 0.0, 0.0] },
ColumnVertex { position: [-0.5, -0.5, 1.0], normal: [-1.0, 0.0, 0.0] },
ColumnVertex { position: [-0.5, 0.5, 0.0], normal: [-1.0, 0.0, 0.0] },
ColumnVertex { position: [-0.5, 0.5, 1.0], normal: [-1.0, 0.0, 0.0] },
ColumnVertex { position: [0.5, -0.5, 1.0], normal: [1.0, 0.0, 0.0] },
ColumnVertex { position: [0.5, -0.5, 0.0], normal: [1.0, 0.0, 0.0] },
ColumnVertex { position: [0.5, 0.5, 1.0], normal: [1.0, 0.0, 0.0] },
ColumnVertex { position: [0.5, 0.5, 0.0], normal: [1.0, 0.0, 0.0] },
];
let indices = vec![
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, 17, 17, 18, 19,
20, 22, 21, 21, 22, 23,
];
(vertices, indices)
}
fn build_column_instances(
columns: &rustial_engine::ColumnInstanceSet,
ramp: &rustial_engine::ColorRamp,
state: &MapState,
scene_origin: DVec3,
) -> Vec<ColumnInstanceData> {
let (min_height, max_height) = column_height_range(columns);
columns
.columns
.iter()
.map(|column| {
let projected = state.camera().projection().project(&column.position);
let base_z = resolve_column_base_altitude(column, state);
let normalized = if (max_height - min_height).abs() < f64::EPSILON {
0.5
} else {
((column.height - min_height) / (max_height - min_height)).clamp(0.0, 1.0)
} as f32;
let color = column.color.unwrap_or_else(|| ramp.evaluate(normalized));
ColumnInstanceData {
base_position: [
(projected.position.x - scene_origin.x) as f32,
(projected.position.y - scene_origin.y) as f32,
(base_z - scene_origin.z) as f32,
],
dimensions: [column.width as f32, column.height as f32, 0.0, 0.0],
color,
}
})
.collect()
}
fn build_point_instances(
points: &rustial_engine::PointInstanceSet,
ramp: &rustial_engine::ColorRamp,
state: &MapState,
scene_origin: DVec3,
) -> Vec<ColumnInstanceData> {
points
.points
.iter()
.map(|point| {
let projected = state.camera().projection().project(&point.position);
let center_z = resolve_point_altitude(point, state);
let diameter = (point.radius * 2.0) as f32;
let color = point.color.unwrap_or_else(|| ramp.evaluate(point.intensity.clamp(0.0, 1.0)));
ColumnInstanceData {
base_position: [
(projected.position.x - scene_origin.x) as f32,
(projected.position.y - scene_origin.y) as f32,
(center_z - scene_origin.z - point.radius) as f32,
],
dimensions: [diameter, diameter, 0.0, 0.0],
color,
}
})
.collect()
}
fn column_height_range(columns: &rustial_engine::ColumnInstanceSet) -> (f64, f64) {
let mut min_height = f64::INFINITY;
let mut max_height = f64::NEG_INFINITY;
for column in &columns.columns {
min_height = min_height.min(column.height);
max_height = max_height.max(column.height);
}
if min_height.is_infinite() || max_height.is_infinite() {
(0.0, 0.0)
} else {
(min_height, max_height)
}
}
fn resolve_point_altitude(
point: &rustial_engine::PointInstance,
state: &MapState,
) -> f64 {
let terrain = state.elevation_at(&point.position).unwrap_or(0.0);
match point.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => terrain,
rustial_engine::AltitudeMode::RelativeToGround => terrain + point.position.alt,
rustial_engine::AltitudeMode::Absolute => point.position.alt,
}
}
fn resolve_column_base_altitude(
column: &rustial_engine::ColumnInstance,
state: &MapState,
) -> f64 {
let terrain = state.elevation_at(&column.position).unwrap_or(0.0);
match column.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => terrain + column.base,
rustial_engine::AltitudeMode::RelativeToGround => terrain + column.position.alt + column.base,
rustial_engine::AltitudeMode::Absolute => column.position.alt + column.base,
}
}
fn column_set_fingerprint(columns: &rustial_engine::ColumnInstanceSet) -> u64 {
let mut h = columns.columns.len() as u64;
for column in &columns.columns {
h = h.wrapping_mul(31).wrapping_add(column.position.lat.to_bits());
h = h.wrapping_mul(31).wrapping_add(column.position.lon.to_bits());
h = h.wrapping_mul(31).wrapping_add(column.position.alt.to_bits());
h = h.wrapping_mul(31).wrapping_add(column.height.to_bits());
h = h.wrapping_mul(31).wrapping_add(column.base.to_bits());
h = h.wrapping_mul(31).wrapping_add(column.width.to_bits());
h = h.wrapping_mul(31).wrapping_add(column.pick_id);
h = h.wrapping_mul(31).wrapping_add(match column.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => 0,
rustial_engine::AltitudeMode::RelativeToGround => 1,
rustial_engine::AltitudeMode::Absolute => 2,
});
if let Some(color) = column.color {
h = h.wrapping_mul(31).wrapping_add(color[0].to_bits() as u64);
h = h.wrapping_mul(31).wrapping_add(color[1].to_bits() as u64);
h = h.wrapping_mul(31).wrapping_add(color[2].to_bits() as u64);
h = h.wrapping_mul(31).wrapping_add(color[3].to_bits() as u64);
}
}
h
}
fn point_set_fingerprint(points: &rustial_engine::PointInstanceSet) -> u64 {
let mut h = points.points.len() as u64;
for point in &points.points {
h = h.wrapping_mul(31).wrapping_add(point.position.lat.to_bits());
h = h.wrapping_mul(31).wrapping_add(point.position.lon.to_bits());
h = h.wrapping_mul(31).wrapping_add(point.position.alt.to_bits());
h = h.wrapping_mul(31).wrapping_add(point.radius.to_bits());
h = h.wrapping_mul(31).wrapping_add(point.intensity.to_bits() as u64);
h = h.wrapping_mul(31).wrapping_add(point.pick_id);
h = h.wrapping_mul(31).wrapping_add(match point.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => 0,
rustial_engine::AltitudeMode::RelativeToGround => 1,
rustial_engine::AltitudeMode::Absolute => 2,
});
if let Some(color) = point.color {
h = h.wrapping_mul(31).wrapping_add(color[0].to_bits() as u64);
h = h.wrapping_mul(31).wrapping_add(color[1].to_bits() as u64);
h = h.wrapping_mul(31).wrapping_add(color[2].to_bits() as u64);
h = h.wrapping_mul(31).wrapping_add(color[3].to_bits() as u64);
}
}
h
}
fn visualization_overlay_intersects_scene_viewport(
overlay: &VisualizationOverlay,
state: &MapState,
) -> bool {
let scene_origin = state.scene_world_origin();
let Some(bounds) = visualization_overlay_world_bounds(overlay, state, scene_origin) else {
return false;
};
bounds.intersects(state.scene_viewport_bounds())
}
fn visualization_overlay_world_bounds(
overlay: &VisualizationOverlay,
state: &MapState,
scene_origin: DVec3,
) -> Option<rustial_math::WorldBounds> {
match overlay {
VisualizationOverlay::GridScalar { grid, .. }
| VisualizationOverlay::GridExtrusion { grid, .. } => Some(grid_world_bounds(grid, state, scene_origin)),
VisualizationOverlay::Columns { columns, .. } => column_world_bounds(columns, state, scene_origin),
VisualizationOverlay::Points { points, .. } => point_world_bounds(points, state, scene_origin),
}
}
fn grid_world_bounds(
grid: &rustial_engine::GeoGrid,
state: &MapState,
scene_origin: DVec3,
) -> rustial_math::WorldBounds {
let corners = [
grid_corner_coord(grid, 0, 0, state),
grid_corner_coord(grid, 0, grid.cols, state),
grid_corner_coord(grid, grid.rows, 0, state),
grid_corner_coord(grid, grid.rows, grid.cols, state),
];
let projected: Vec<_> = corners
.iter()
.map(|coord| state.camera().projection().project(coord))
.collect();
let mut bounds = rustial_math::WorldBounds::new(
rustial_math::WorldCoord::new(
projected[0].position.x - scene_origin.x,
projected[0].position.y - scene_origin.y,
projected[0].position.z - scene_origin.z,
),
rustial_math::WorldCoord::new(
projected[0].position.x - scene_origin.x,
projected[0].position.y - scene_origin.y,
projected[0].position.z - scene_origin.z,
),
);
for projected in projected.into_iter().skip(1) {
bounds.extend_point(&rustial_math::WorldCoord::new(
projected.position.x - scene_origin.x,
projected.position.y - scene_origin.y,
projected.position.z - scene_origin.z,
));
}
bounds
}
fn point_world_bounds(
points: &rustial_engine::PointInstanceSet,
state: &MapState,
scene_origin: DVec3,
) -> Option<rustial_math::WorldBounds> {
let mut bounds: Option<rustial_math::WorldBounds> = None;
for point in &points.points {
let projected = state.camera().projection().project(&point.position);
let radius = point.radius;
let center_z = resolve_point_altitude(point, state) - scene_origin.z;
let point_bounds = rustial_math::WorldBounds::new(
rustial_math::WorldCoord::new(
projected.position.x - scene_origin.x - radius,
projected.position.y - scene_origin.y - radius,
center_z - radius,
),
rustial_math::WorldCoord::new(
projected.position.x - scene_origin.x + radius,
projected.position.y - scene_origin.y + radius,
center_z + radius,
),
);
if let Some(existing) = bounds.as_mut() {
existing.extend(&point_bounds);
} else {
bounds = Some(point_bounds);
}
}
bounds
}
fn column_world_bounds(
columns: &rustial_engine::ColumnInstanceSet,
state: &MapState,
scene_origin: DVec3,
) -> Option<rustial_math::WorldBounds> {
let mut bounds: Option<rustial_math::WorldBounds> = None;
for column in &columns.columns {
let projected = state.camera().projection().project(&column.position);
let base_z = resolve_column_base_altitude(column, state) - scene_origin.z;
let half_width = column.width * 0.5;
let column_bounds = rustial_math::WorldBounds::new(
rustial_math::WorldCoord::new(
projected.position.x - scene_origin.x - half_width,
projected.position.y - scene_origin.y - half_width,
base_z,
),
rustial_math::WorldCoord::new(
projected.position.x - scene_origin.x + half_width,
projected.position.y - scene_origin.y + half_width,
base_z + column.height,
),
);
if let Some(existing) = bounds.as_mut() {
existing.extend(&column_bounds);
} else {
bounds = Some(column_bounds);
}
}
bounds
}
fn build_shared_terrain_grid(resolution: usize) -> (Vec<TerrainGridVertex>, Vec<u32>) {
let res = resolution.max(2);
let mut vertices = Vec::with_capacity(res * res);
let mut indices = Vec::with_capacity((res - 1) * (res - 1) * 6);
for row in 0..res {
for col in 0..res {
let u = col as f32 / (res - 1) as f32;
let v = row as f32 / (res - 1) as f32;
vertices.push(TerrainGridVertex { uv: [u, v], skirt: 0.0 });
}
}
for row in 0..(res - 1) {
for col in 0..(res - 1) {
let tl = (row * res + col) as u32;
let tr = tl + 1;
let bl = ((row + 1) * res + col) as u32;
let br = bl + 1;
indices.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
}
}
let edges: [Vec<usize>; 4] = [
(0..res).collect(),
((res - 1) * res..res * res).collect(),
(0..res).map(|r| r * res).collect(),
(0..res).map(|r| r * res + res - 1).collect(),
];
for edge in &edges {
for i in 0..edge.len() - 1 {
let a = edge[i] as u32;
let b = edge[i + 1] as u32;
let uv_a = vertices[edge[i]].uv;
let uv_b = vertices[edge[i + 1]].uv;
let base_a = vertices.len() as u32;
let base_b = base_a + 1;
vertices.push(TerrainGridVertex { uv: uv_a, skirt: 1.0 });
vertices.push(TerrainGridVertex { uv: uv_b, skirt: 1.0 });
indices.extend_from_slice(&[a, base_a, b, b, base_a, base_b]);
}
}
(vertices, indices)
}
fn build_terrain_tile_uniform(
mesh: &TerrainMeshData,
elevation: &rustial_engine::TerrainElevationTexture,
state: &MapState,
scene_origin: DVec3,
) -> TerrainTileUniform {
let nw = rustial_math::tile_to_geo(&mesh.tile);
let se = rustial_math::tile_xy_to_geo(
mesh.tile.zoom,
mesh.tile.x as f64 + 1.0,
mesh.tile.y as f64 + 1.0,
);
let projection_kind = match state.camera().projection() {
rustial_engine::CameraProjection::WebMercator => 0.0,
rustial_engine::CameraProjection::Equirectangular => 1.0,
_ => 0.0,
};
let skirt = rustial_engine::skirt_height(
mesh.tile.zoom,
mesh.vertical_exaggeration as f64,
) as f32;
let skirt_base = (elevation.min_elev * mesh.vertical_exaggeration - skirt)
.max(-skirt * 3.0);
let elev_region = if mesh.tile != mesh.elevation_source_tile {
rustial_engine::elevation_region_in_texture_space(
mesh.elevation_region,
elevation.width,
elevation.height,
)
} else {
mesh.elevation_region
};
TerrainTileUniform {
geo_bounds: [nw.lat as f32, nw.lon as f32, se.lat as f32, se.lon as f32],
scene_origin: [
scene_origin.x as f32,
scene_origin.y as f32,
scene_origin.z as f32,
projection_kind,
],
elev_params: [
mesh.vertical_exaggeration,
skirt_base,
elevation.min_elev,
elevation.max_elev,
],
elev_region: [
elev_region.u_min,
elev_region.v_min,
elev_region.u_max,
elev_region.v_max,
],
}
}
fn build_model_vertices(mesh: &rustial_engine::ModelMesh) -> Vec<ModelVertex> {
debug_assert_eq!(mesh.positions.len(), mesh.normals.len());
debug_assert_eq!(mesh.positions.len(), mesh.uvs.len());
mesh.positions
.iter()
.zip(mesh.normals.iter())
.zip(mesh.uvs.iter())
.map(|((pos, normal), uv)| ModelVertex {
position: *pos,
normal: *normal,
uv: *uv,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use rustial_engine::{ColorRamp, ColorStop, ColumnInstance, ColumnInstanceSet, GeoCoord, GeoGrid, VisualizationOverlay};
fn visible_tile_with_fade(fade_opacity: f32) -> VisibleTile {
let id = TileId::new(3, 4, 2);
VisibleTile {
target: id,
actual: id,
data: None,
fade_opacity,
}
}
fn test_ramp() -> ColorRamp {
ColorRamp::new(vec![
ColorStop { value: 0.0, color: [0.0, 0.0, 1.0, 0.5] },
ColorStop { value: 1.0, color: [1.0, 0.0, 0.0, 0.8] },
])
}
#[test]
fn tile_batch_cache_key_changes_when_fade_opacity_changes() {
let a = [visible_tile_with_fade(0.25)];
let b = [visible_tile_with_fade(0.75)];
let key_a = TileBatchCacheKey::new(&a, DVec3::ZERO, rustial_engine::CameraProjection::WebMercator);
let key_b = TileBatchCacheKey::new(&b, DVec3::ZERO, rustial_engine::CameraProjection::WebMercator);
assert_ne!(key_a, key_b, "tile batch cache key must include fade-sensitive inputs");
}
#[test]
fn tile_batch_cache_key_stays_equal_when_fade_opacity_matches() {
let a = [visible_tile_with_fade(1.0)];
let b = [visible_tile_with_fade(1.0)];
let key_a = TileBatchCacheKey::new(&a, DVec3::ZERO, rustial_engine::CameraProjection::WebMercator);
let key_b = TileBatchCacheKey::new(&b, DVec3::ZERO, rustial_engine::CameraProjection::WebMercator);
assert_eq!(key_a, key_b);
}
#[test]
fn diff_column_instance_ranges_tracks_contiguous_changes() {
let old = vec![
ColumnInstanceData {
base_position: [0.0, 0.0, 0.0],
dimensions: [1.0, 2.0, 0.0, 0.0],
color: [1.0, 0.0, 0.0, 1.0],
},
ColumnInstanceData {
base_position: [1.0, 0.0, 0.0],
dimensions: [1.0, 2.0, 0.0, 0.0],
color: [0.0, 1.0, 0.0, 1.0],
},
ColumnInstanceData {
base_position: [2.0, 0.0, 0.0],
dimensions: [1.0, 2.0, 0.0, 0.0],
color: [0.0, 0.0, 1.0, 1.0],
},
ColumnInstanceData {
base_position: [3.0, 0.0, 0.0],
dimensions: [1.0, 2.0, 0.0, 0.0],
color: [1.0, 1.0, 0.0, 1.0],
},
];
let mut new = old.clone();
new[1].dimensions[1] = 4.0;
new[2].color = [1.0, 0.0, 1.0, 1.0];
assert_eq!(diff_column_instance_ranges(&old, &new), vec![1..3]);
}
#[test]
fn diff_column_instance_ranges_splits_disjoint_changes() {
let old = vec![
ColumnInstanceData {
base_position: [0.0, 0.0, 0.0],
dimensions: [1.0, 2.0, 0.0, 0.0],
color: [1.0, 0.0, 0.0, 1.0],
},
ColumnInstanceData {
base_position: [1.0, 0.0, 0.0],
dimensions: [1.0, 2.0, 0.0, 0.0],
color: [0.0, 1.0, 0.0, 1.0],
},
ColumnInstanceData {
base_position: [2.0, 0.0, 0.0],
dimensions: [1.0, 2.0, 0.0, 0.0],
color: [0.0, 0.0, 1.0, 1.0],
},
];
let mut new = old.clone();
new[0].dimensions[0] = 3.0;
new[2].base_position[2] = 5.0;
assert_eq!(diff_column_instance_ranges(&old, &new), vec![0..1, 2..3]);
}
#[test]
fn visualization_overlay_visibility_rejects_far_grid() {
let mut state = MapState::new();
state.set_viewport(1280, 720);
state.set_camera_target(GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(1_000.0);
state.update_camera(1.0 / 60.0);
let overlay = VisualizationOverlay::GridScalar {
layer_id: LayerId::next(),
grid: GeoGrid::new(GeoCoord::from_lat_lon(70.0, 120.0), 2, 2, 50.0, 50.0),
field: rustial_engine::ScalarField2D::from_data(2, 2, vec![1.0; 4]),
ramp: test_ramp(),
};
assert!(!visualization_overlay_intersects_scene_viewport(&overlay, &state));
}
#[test]
fn visualization_overlay_visibility_accepts_near_columns() {
let mut state = MapState::new();
state.set_viewport(1280, 720);
state.set_camera_target(GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(1_000.0);
state.update_camera(1.0 / 60.0);
let overlay = VisualizationOverlay::Columns {
layer_id: LayerId::next(),
columns: ColumnInstanceSet::new(vec![
ColumnInstance::new(GeoCoord::from_lat_lon(0.0, 0.0), 10.0, 5.0),
]),
ramp: test_ramp(),
};
assert!(visualization_overlay_intersects_scene_viewport(&overlay, &state));
}
}