use crate::gpu::terrain_vertex::TerrainVertex;
use crate::gpu::tile_atlas::TileAtlas;
use crate::gpu::vector_vertex::VectorVertex;
use crate::gpu::vertex::TileVertex;
use glam::DVec3;
use rustial_engine::{
CameraProjection, PreparedHillshadeRaster, TerrainMeshData, TileId, VectorMeshData, VisibleTile,
};
use wgpu::util::DeviceExt;
const MAX_FALLBACK_DEPTH: u8 = 8;
pub(crate) fn find_terrain_texture_actual(
tile: TileId,
visible_tiles: &[VisibleTile],
) -> Option<TileId> {
if let Some(vt) = visible_tiles
.iter()
.find(|vt| vt.target == tile && vt.data.is_some())
{
return Some(vt.actual);
}
let mut current = tile;
let mut depth = 0u8;
while depth < MAX_FALLBACK_DEPTH {
let Some(parent) = current.parent() else {
break;
};
if let Some(vt) = visible_tiles
.iter()
.find(|vt| vt.target == parent && vt.data.is_some())
{
return Some(vt.actual);
}
if visible_tiles
.iter()
.any(|vt| vt.actual == parent && vt.data.is_some())
{
return Some(parent);
}
current = parent;
depth += 1;
}
None
}
pub struct TileBatch {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
}
pub struct TilePageBatches {
pub opaque: Option<TileBatch>,
pub translucent: Option<TileBatch>,
}
fn tile_opacity_for(vt: &VisibleTile) -> f32 {
let fallback_zoom_delta = if vt.actual.zoom > vt.target.zoom {
(vt.actual.zoom - vt.target.zoom) as f32
} else {
vt.target.zoom.saturating_sub(vt.actual.zoom) as f32
};
let base_opacity = if vt.target == vt.actual {
1.0_f32
} else {
(0.9_f32 - 0.12_f32 * fallback_zoom_delta).clamp(0.55_f32, 0.9_f32)
};
base_opacity * vt.fade_opacity
}
fn tile_requires_blended_pass(vt: &VisibleTile) -> bool {
tile_opacity_for(vt) < 0.999_999
}
fn build_tile_batch(
device: &wgpu::Device,
verts: Vec<TileVertex>,
idxs: Vec<u32>,
) -> Option<TileBatch> {
if verts.is_empty() {
return None;
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("tile_batch_vb"),
contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("tile_batch_ib"),
contents: bytemuck::cast_slice(&idxs),
usage: wgpu::BufferUsages::INDEX,
});
Some(TileBatch {
vertex_buffer,
index_buffer,
index_count: idxs.len() as u32,
})
}
pub fn build_tile_batches(
device: &wgpu::Device,
visible_tiles: &[VisibleTile],
atlas: &TileAtlas,
camera_origin: DVec3,
projection: CameraProjection,
) -> Vec<TilePageBatches> {
if atlas.page_count() == 0 {
return Vec::new();
}
let page_count = atlas.page_count();
let mut page_verts_opaque: Vec<Vec<TileVertex>> = vec![Vec::new(); page_count];
let mut page_idxs_opaque: Vec<Vec<u32>> = vec![Vec::new(); page_count];
let mut page_verts_translucent: Vec<Vec<TileVertex>> = vec![Vec::new(); page_count];
let mut page_idxs_translucent: Vec<Vec<u32>> = vec![Vec::new(); page_count];
for vt in visible_tiles {
let region = match atlas.get(&vt.actual) {
Some(r) => r,
None => continue,
};
let geometry_tile = if vt.actual.zoom > vt.target.zoom {
vt.actual
} else {
vt.target
};
let [sw, se, ne, nw] = projected_tile_corners(geometry_tile, projection, camera_origin);
let texture_region = vt.texture_region();
let (page_verts, page_idxs) = if tile_requires_blended_pass(vt) {
(&mut page_verts_translucent, &mut page_idxs_translucent)
} else {
(&mut page_verts_opaque, &mut page_idxs_opaque)
};
let base = page_verts[region.page].len() as u32;
let tile_opacity = tile_opacity_for(vt);
page_verts[region.page].extend_from_slice(&[
TileVertex {
position: [sw.x as f32, sw.y as f32, sw.z as f32],
uv: region.remap_uv(texture_region.u_min, texture_region.v_max),
opacity: tile_opacity,
},
TileVertex {
position: [se.x as f32, se.y as f32, se.z as f32],
uv: region.remap_uv(texture_region.u_max, texture_region.v_max),
opacity: tile_opacity,
},
TileVertex {
position: [ne.x as f32, ne.y as f32, ne.z as f32],
uv: region.remap_uv(texture_region.u_max, texture_region.v_min),
opacity: tile_opacity,
},
TileVertex {
position: [nw.x as f32, nw.y as f32, nw.z as f32],
uv: region.remap_uv(texture_region.u_min, texture_region.v_min),
opacity: tile_opacity,
},
]);
page_idxs[region.page].extend_from_slice(&[
base,
base + 1,
base + 2,
base,
base + 2,
base + 3,
]);
}
page_verts_opaque
.into_iter()
.zip(page_idxs_opaque)
.zip(
page_verts_translucent
.into_iter()
.zip(page_idxs_translucent),
)
.map(
|((opaque_verts, opaque_idxs), (translucent_verts, translucent_idxs))| {
TilePageBatches {
opaque: build_tile_batch(device, opaque_verts, opaque_idxs),
translucent: build_tile_batch(device, translucent_verts, translucent_idxs),
}
},
)
.collect()
}
fn projected_tile_corners(
tile: TileId,
projection: CameraProjection,
camera_origin: DVec3,
) -> [glam::DVec3; 4] {
let southwest =
glam::DVec3::from_array(projection.project_tile_corner(&tile, 0.0, 1.0)) - camera_origin;
let southeast =
glam::DVec3::from_array(projection.project_tile_corner(&tile, 1.0, 1.0)) - camera_origin;
let northeast =
glam::DVec3::from_array(projection.project_tile_corner(&tile, 1.0, 0.0)) - camera_origin;
let northwest =
glam::DVec3::from_array(projection.project_tile_corner(&tile, 0.0, 0.0)) - camera_origin;
[southwest, southeast, northeast, northwest]
}
pub struct TerrainBatch {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
}
pub struct HillshadeBatch {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
}
pub fn build_terrain_batches(
device: &wgpu::Device,
terrain_meshes: &[TerrainMeshData],
atlas: &TileAtlas,
camera_origin: DVec3,
visible_tiles: &[VisibleTile],
) -> Vec<Option<TerrainBatch>> {
if atlas.page_count() == 0 || terrain_meshes.is_empty() {
return Vec::new();
}
let page_count = atlas.page_count();
let mut page_verts: Vec<Vec<TerrainVertex>> = vec![Vec::new(); page_count];
let mut page_idxs: Vec<Vec<u32>> = vec![Vec::new(); page_count];
for mesh in terrain_meshes {
let actual_tile =
find_terrain_texture_actual(mesh.tile, visible_tiles).unwrap_or(mesh.tile);
let texture_region = rustial_engine::VisibleTile {
target: mesh.tile,
actual: actual_tile,
data: None,
fade_opacity: 1.0,
}
.texture_region();
let region = match atlas.get(&actual_tile) {
Some(r) => r,
None => continue,
};
debug_assert_eq!(
mesh.positions.len(),
mesh.uvs.len(),
"TerrainMeshData positions/uvs length mismatch for tile {:?}",
mesh.tile,
);
debug_assert_eq!(
mesh.positions.len(),
mesh.normals.len(),
"TerrainMeshData positions/normals length mismatch for tile {:?}",
mesh.tile,
);
let vert_count = mesh.positions.len();
let base = page_verts[region.page].len() as u32;
page_verts[region.page].reserve(vert_count);
for i in 0..vert_count {
let pos = &mesh.positions[i];
let uv = &mesh.uvs[i];
let normal = &mesh.normals[i];
let mapped_u =
texture_region.u_min + (texture_region.u_max - texture_region.u_min) * uv[0];
let mapped_v =
texture_region.v_min + (texture_region.v_max - texture_region.v_min) * uv[1];
page_verts[region.page].push(TerrainVertex {
position: [
(pos[0] - camera_origin.x) as f32,
(pos[1] - camera_origin.y) as f32,
(pos[2] - camera_origin.z) as f32,
],
uv: region.remap_uv(mapped_u, mapped_v),
normal: *normal,
});
}
page_idxs[region.page].reserve(mesh.indices.len());
for idx in &mesh.indices {
debug_assert!(
(*idx as usize) < vert_count,
"TerrainMeshData index {} out of bounds (vertex_count={}) for tile {:?}",
idx,
vert_count,
mesh.tile,
);
page_idxs[region.page].push(base + idx);
}
}
page_verts
.into_iter()
.zip(page_idxs)
.map(|(verts, idxs)| {
if verts.is_empty() {
return None;
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("terrain_batch_vb"),
contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("terrain_batch_ib"),
contents: bytemuck::cast_slice(&idxs),
usage: wgpu::BufferUsages::INDEX,
});
Some(TerrainBatch {
vertex_buffer,
index_buffer,
index_count: idxs.len() as u32,
})
})
.collect()
}
pub fn build_hillshade_batches(
device: &wgpu::Device,
terrain_meshes: &[TerrainMeshData],
hillshade_rasters: &[PreparedHillshadeRaster],
atlas: &TileAtlas,
camera_origin: DVec3,
) -> Vec<Option<HillshadeBatch>> {
if atlas.page_count() == 0 || terrain_meshes.is_empty() || hillshade_rasters.is_empty() {
return Vec::new();
}
let available: std::collections::HashSet<TileId> =
hillshade_rasters.iter().map(|r| r.tile).collect();
let page_count = atlas.page_count();
let mut page_verts: Vec<Vec<TerrainVertex>> = vec![Vec::new(); page_count];
let mut page_idxs: Vec<Vec<u32>> = vec![Vec::new(); page_count];
for mesh in terrain_meshes {
if !available.contains(&mesh.tile) {
continue;
}
let region = match atlas.get(&mesh.tile) {
Some(r) => r,
None => continue,
};
let vert_count = mesh.positions.len();
let base = page_verts[region.page].len() as u32;
page_verts[region.page].reserve(vert_count);
for i in 0..vert_count {
let pos = &mesh.positions[i];
let uv = &mesh.uvs[i];
let normal = &mesh.normals[i];
page_verts[region.page].push(TerrainVertex {
position: [
(pos[0] - camera_origin.x) as f32,
(pos[1] - camera_origin.y) as f32,
(pos[2] - camera_origin.z) as f32,
],
uv: region.remap_uv(uv[0], uv[1]),
normal: *normal,
});
}
page_idxs[region.page].reserve(mesh.indices.len());
for idx in &mesh.indices {
page_idxs[region.page].push(base + idx);
}
}
page_verts
.into_iter()
.zip(page_idxs)
.map(|(verts, idxs)| {
if verts.is_empty() {
return None;
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hillshade_batch_vb"),
contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hillshade_batch_ib"),
contents: bytemuck::cast_slice(&idxs),
usage: wgpu::BufferUsages::INDEX,
});
Some(HillshadeBatch {
vertex_buffer,
index_buffer,
index_count: idxs.len() as u32,
})
})
.collect()
}
pub struct VectorBatchEntry {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
}
pub fn build_vector_batch(
device: &wgpu::Device,
mesh: &VectorMeshData,
camera_origin: DVec3,
) -> Option<VectorBatchEntry> {
if mesh.indices.is_empty() {
return None;
}
debug_assert_eq!(
mesh.positions.len(),
mesh.colors.len(),
"VectorMeshData positions/colors length mismatch ({} vs {})",
mesh.positions.len(),
mesh.colors.len(),
);
let vertices: Vec<VectorVertex> = mesh
.positions
.iter()
.zip(mesh.colors.iter())
.map(|(pos, color)| VectorVertex {
position: [
(pos[0] - camera_origin.x) as f32,
(pos[1] - camera_origin.y) as f32,
(pos[2] - camera_origin.z) as f32,
],
color: *color,
})
.collect();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("vector_batch_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("vector_batch_ib"),
contents: bytemuck::cast_slice(&mesh.indices),
usage: wgpu::BufferUsages::INDEX,
});
Some(VectorBatchEntry {
vertex_buffer,
index_buffer,
index_count: mesh.indices.len() as u32,
})
}
pub struct FillExtrusionBatchEntry {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
}
pub fn build_fill_extrusion_batch(
device: &wgpu::Device,
mesh: &VectorMeshData,
camera_origin: DVec3,
) -> Option<FillExtrusionBatchEntry> {
use crate::gpu::fill_extrusion_vertex::FillExtrusionVertex;
if mesh.indices.is_empty() || mesh.normals.is_empty() {
return None;
}
debug_assert_eq!(
mesh.positions.len(),
mesh.normals.len(),
"FillExtrusion positions/normals length mismatch ({} vs {})",
mesh.positions.len(),
mesh.normals.len(),
);
let vertices: Vec<FillExtrusionVertex> = mesh
.positions
.iter()
.zip(mesh.normals.iter())
.zip(mesh.colors.iter())
.map(|((pos, normal), color)| FillExtrusionVertex {
position: [
(pos[0] - camera_origin.x) as f32,
(pos[1] - camera_origin.y) as f32,
(pos[2] - camera_origin.z) as f32,
],
normal: *normal,
color: *color,
})
.collect();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("fill_extrusion_batch_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("fill_extrusion_batch_ib"),
contents: bytemuck::cast_slice(&mesh.indices),
usage: wgpu::BufferUsages::INDEX,
});
Some(FillExtrusionBatchEntry {
vertex_buffer,
index_buffer,
index_count: mesh.indices.len() as u32,
})
}
pub struct FillBatchEntry {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
pub fill_params_buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
}
pub fn build_fill_batch(
device: &wgpu::Device,
mesh: &VectorMeshData,
camera_origin: DVec3,
uniform_buffer: &wgpu::Buffer,
fill_bind_group_layout: &wgpu::BindGroupLayout,
) -> Option<FillBatchEntry> {
use crate::gpu::fill_vertex::FillVertex;
use crate::pipeline::fill_pipeline::FillParamsUniform;
if mesh.indices.is_empty() {
return None;
}
let vertices: Vec<FillVertex> = mesh
.positions
.iter()
.zip(mesh.colors.iter())
.map(|(pos, color)| FillVertex {
position: [
(pos[0] - camera_origin.x) as f32,
(pos[1] - camera_origin.y) as f32,
(pos[2] - camera_origin.z) as f32,
],
color: *color,
})
.collect();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("fill_batch_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("fill_batch_ib"),
contents: bytemuck::cast_slice(&mesh.indices),
usage: wgpu::BufferUsages::INDEX,
});
let fill_params = FillParamsUniform {
fill_translate: mesh.fill_translate,
fill_opacity: mesh.fill_opacity,
fill_antialias: if mesh.fill_antialias { 1.0 } else { 0.0 },
outline_color: mesh.fill_outline_color,
};
let fill_params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("fill_params_buf"),
contents: bytemuck::bytes_of(&fill_params),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("fill_batch_bg"),
layout: fill_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: fill_params_buffer.as_entire_binding(),
},
],
});
Some(FillBatchEntry {
vertex_buffer,
index_buffer,
index_count: mesh.indices.len() as u32,
fill_params_buffer,
bind_group,
})
}
pub struct FillPatternBatchEntry {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
pub uniform_bind_group: wgpu::BindGroup,
pub texture_bind_group: wgpu::BindGroup,
pub _pattern_texture: wgpu::Texture,
pub _pattern_texture_view: wgpu::TextureView,
pub _fill_params_buffer: wgpu::Buffer,
}
#[allow(clippy::too_many_arguments)]
pub fn build_fill_pattern_batch(
device: &wgpu::Device,
queue: &wgpu::Queue,
mesh: &VectorMeshData,
camera_origin: DVec3,
uniform_buffer: &wgpu::Buffer,
uniform_bgl: &wgpu::BindGroupLayout,
texture_bgl: &wgpu::BindGroupLayout,
pattern_sampler: &wgpu::Sampler,
) -> Option<FillPatternBatchEntry> {
use crate::gpu::fill_pattern_vertex::FillPatternVertex;
use crate::pipeline::fill_pipeline::FillParamsUniform;
let pattern = mesh.fill_pattern.as_ref()?;
if mesh.indices.is_empty() || mesh.fill_pattern_uvs.is_empty() {
return None;
}
let vertices: Vec<FillPatternVertex> = (0..mesh.positions.len())
.map(|i| {
let pos = &mesh.positions[i];
FillPatternVertex {
position: [
(pos[0] - camera_origin.x) as f32,
(pos[1] - camera_origin.y) as f32,
(pos[2] - camera_origin.z) as f32,
],
color: mesh.colors[i],
uv: if i < mesh.fill_pattern_uvs.len() {
mesh.fill_pattern_uvs[i]
} else {
[0.0, 0.0]
},
}
})
.collect();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("fill_pattern_batch_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("fill_pattern_batch_ib"),
contents: bytemuck::cast_slice(&mesh.indices),
usage: wgpu::BufferUsages::INDEX,
});
let texture_size = wgpu::Extent3d {
width: pattern.width,
height: pattern.height,
depth_or_array_layers: 1,
};
let pattern_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("fill_pattern_tex"),
size: 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: &pattern_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&pattern.data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(pattern.width * 4),
rows_per_image: Some(pattern.height),
},
texture_size,
);
let pattern_texture_view = pattern_texture.create_view(&wgpu::TextureViewDescriptor::default());
let fill_params = FillParamsUniform {
fill_translate: mesh.fill_translate,
fill_opacity: mesh.fill_opacity,
fill_antialias: if mesh.fill_antialias { 1.0 } else { 0.0 },
outline_color: mesh.fill_outline_color,
};
let fill_params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("fill_pattern_params_buf"),
contents: bytemuck::bytes_of(&fill_params),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("fill_pattern_uniform_bg"),
layout: uniform_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: fill_params_buffer.as_entire_binding(),
},
],
});
let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("fill_pattern_texture_bg"),
layout: texture_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&pattern_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(pattern_sampler),
},
],
});
Some(FillPatternBatchEntry {
vertex_buffer,
index_buffer,
index_count: mesh.indices.len() as u32,
uniform_bind_group,
texture_bind_group,
_pattern_texture: pattern_texture,
_pattern_texture_view: pattern_texture_view,
_fill_params_buffer: fill_params_buffer,
})
}
pub struct LineBatchEntry {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
pub line_params: [f32; 4],
}
pub fn build_line_batch(
device: &wgpu::Device,
mesh: &VectorMeshData,
camera_origin: DVec3,
) -> Option<LineBatchEntry> {
use crate::gpu::line_vertex::LineVertex;
if mesh.indices.is_empty() || mesh.line_distances.is_empty() {
return None;
}
let vertices: Vec<LineVertex> = (0..mesh.positions.len())
.map(|i| {
let pos = &mesh.positions[i];
LineVertex {
position: [
(pos[0] - camera_origin.x) as f32,
(pos[1] - camera_origin.y) as f32,
(pos[2] - camera_origin.z) as f32,
],
color: mesh.colors[i],
line_normal: mesh.line_normals[i],
line_distance: mesh.line_distances[i],
cap_join: if i < mesh.line_cap_joins.len() {
mesh.line_cap_joins[i]
} else {
0.0
},
}
})
.collect();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("line_batch_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("line_batch_ib"),
contents: bytemuck::cast_slice(&mesh.indices),
usage: wgpu::BufferUsages::INDEX,
});
Some(LineBatchEntry {
vertex_buffer,
index_buffer,
index_count: mesh.indices.len() as u32,
line_params: mesh.line_params,
})
}
pub struct LinePatternBatchEntry {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
pub uniform_bind_group: wgpu::BindGroup,
pub texture_bind_group: wgpu::BindGroup,
pub _pattern_texture: wgpu::Texture,
pub _pattern_texture_view: wgpu::TextureView,
pub line_params: [f32; 4],
}
#[allow(clippy::too_many_arguments)]
pub fn build_line_pattern_batch(
device: &wgpu::Device,
queue: &wgpu::Queue,
mesh: &VectorMeshData,
camera_origin: DVec3,
uniform_buffer: &wgpu::Buffer,
uniform_bgl: &wgpu::BindGroupLayout,
texture_bgl: &wgpu::BindGroupLayout,
pattern_sampler: &wgpu::Sampler,
) -> Option<LinePatternBatchEntry> {
use crate::gpu::line_pattern_vertex::LinePatternVertex;
let pattern = mesh.line_pattern.as_ref()?;
if mesh.indices.is_empty() || mesh.line_pattern_uvs.is_empty() {
return None;
}
let vertices: Vec<LinePatternVertex> = (0..mesh.positions.len())
.map(|i| {
let pos = &mesh.positions[i];
LinePatternVertex {
position: [
(pos[0] - camera_origin.x) as f32,
(pos[1] - camera_origin.y) as f32,
(pos[2] - camera_origin.z) as f32,
],
color: mesh.colors[i],
line_normal: if i < mesh.line_normals.len() {
mesh.line_normals[i]
} else {
[0.0, 0.0]
},
line_distance: if i < mesh.line_distances.len() {
mesh.line_distances[i]
} else {
0.0
},
cap_join: if i < mesh.line_cap_joins.len() {
mesh.line_cap_joins[i]
} else {
0.0
},
uv: if i < mesh.line_pattern_uvs.len() {
mesh.line_pattern_uvs[i]
} else {
[0.0, 0.0]
},
}
})
.collect();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("line_pattern_batch_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("line_pattern_batch_ib"),
contents: bytemuck::cast_slice(&mesh.indices),
usage: wgpu::BufferUsages::INDEX,
});
let texture_size = wgpu::Extent3d {
width: pattern.width,
height: pattern.height,
depth_or_array_layers: 1,
};
let pattern_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("line_pattern_tex"),
size: 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: &pattern_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&pattern.data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(pattern.width * 4),
rows_per_image: Some(pattern.height),
},
texture_size,
);
let pattern_texture_view = pattern_texture.create_view(&wgpu::TextureViewDescriptor::default());
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("line_pattern_uniform_bg"),
layout: uniform_bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("line_pattern_texture_bg"),
layout: texture_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&pattern_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(pattern_sampler),
},
],
});
Some(LinePatternBatchEntry {
vertex_buffer,
index_buffer,
index_count: mesh.indices.len() as u32,
uniform_bind_group,
texture_bind_group,
_pattern_texture: pattern_texture,
_pattern_texture_view: pattern_texture_view,
line_params: mesh.line_params,
})
}
pub struct CircleBatchEntry {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
}
pub fn build_circle_batch(
device: &wgpu::Device,
mesh: &VectorMeshData,
camera_origin: DVec3,
) -> Option<CircleBatchEntry> {
use crate::gpu::circle_vertex::CircleVertex;
if mesh.circle_instances.is_empty() {
return None;
}
let instance_count = mesh.circle_instances.len();
let mut vertices: Vec<CircleVertex> = Vec::with_capacity(instance_count * 4);
let mut indices: Vec<u32> = Vec::with_capacity(instance_count * 6);
let offsets: [[f32; 2]; 4] = [[-1.0, 1.0], [1.0, 1.0], [-1.0, -1.0], [1.0, -1.0]];
for (i, ci) in mesh.circle_instances.iter().enumerate() {
let cx = (ci.center[0] - camera_origin.x) as f32;
let cy = (ci.center[1] - camera_origin.y) as f32;
let cz = (ci.center[2] - camera_origin.z) as f32;
let params = [ci.radius, ci.stroke_width, ci.blur, 0.0];
for offset in &offsets {
vertices.push(CircleVertex {
position: [cx, cy, cz],
quad_offset: *offset,
color: ci.color,
stroke_color: ci.stroke_color,
params,
});
}
let base = (i as u32) * 4;
indices.extend_from_slice(&[base, base + 2, base + 1, base + 1, base + 2, base + 3]);
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("circle_batch_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("circle_batch_ib"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
Some(CircleBatchEntry {
vertex_buffer,
index_buffer,
index_count: indices.len() as u32,
})
}
pub struct HeatmapBatchEntry {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
}
pub fn build_heatmap_batch(
device: &wgpu::Device,
mesh: &VectorMeshData,
camera_origin: DVec3,
) -> Option<HeatmapBatchEntry> {
use crate::gpu::heatmap_vertex::HeatmapVertex;
if mesh.heatmap_points.is_empty() {
return None;
}
let point_count = mesh.heatmap_points.len();
let mut vertices: Vec<HeatmapVertex> = Vec::with_capacity(point_count * 4);
let mut indices: Vec<u32> = Vec::with_capacity(point_count * 6);
let offsets: [[f32; 2]; 4] = [[-1.0, 1.0], [1.0, 1.0], [-1.0, -1.0], [1.0, -1.0]];
for (i, pt) in mesh.heatmap_points.iter().enumerate() {
let cx = (pt[0] - camera_origin.x) as f32;
let cy = (pt[1] - camera_origin.y) as f32;
let cz = 0.0f32;
let weight = pt[2] as f32;
let radius = pt[3] as f32;
let params = [weight, radius, mesh.heatmap_intensity, 0.0];
for offset in &offsets {
vertices.push(HeatmapVertex {
position: [cx, cy, cz],
quad_offset: *offset,
params,
});
}
let base = (i as u32) * 4;
indices.extend_from_slice(&[base, base + 2, base + 1, base + 1, base + 2, base + 3]);
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("heatmap_batch_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("heatmap_batch_ib"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
Some(HeatmapBatchEntry {
vertex_buffer,
index_buffer,
index_count: indices.len() as u32,
})
}
pub fn build_placeholder_batches(
device: &wgpu::Device,
placeholders: &[rustial_engine::LoadingPlaceholder],
style: &rustial_engine::PlaceholderStyle,
camera_origin: DVec3,
) -> Option<VectorBatchEntry> {
if placeholders.is_empty() {
return None;
}
let quad_count = placeholders.len();
let mut vertices: Vec<VectorVertex> = Vec::with_capacity(quad_count * 4);
let mut indices: Vec<u32> = Vec::with_capacity(quad_count * 6);
for ph in placeholders {
let opacity = style.shimmer_opacity(ph.animation_phase);
let color = [
style.background_color[0],
style.background_color[1],
style.background_color[2],
style.background_color[3] * opacity,
];
let min = ph.bounds.min.position;
let max = ph.bounds.max.position;
let ox = camera_origin.x;
let oy = camera_origin.y;
let z: f32 = -0.01;
let base = vertices.len() as u32;
vertices.push(VectorVertex {
position: [(min.x - ox) as f32, (min.y - oy) as f32, z],
color,
});
vertices.push(VectorVertex {
position: [(max.x - ox) as f32, (min.y - oy) as f32, z],
color,
});
vertices.push(VectorVertex {
position: [(max.x - ox) as f32, (max.y - oy) as f32, z],
color,
});
vertices.push(VectorVertex {
position: [(min.x - ox) as f32, (max.y - oy) as f32, z],
color,
});
indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("placeholder_batch_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("placeholder_batch_ib"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
Some(VectorBatchEntry {
vertex_buffer,
index_buffer,
index_count: indices.len() as u32,
})
}
pub struct SymbolBatchEntry {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub index_count: u32,
}
pub fn build_symbol_batch(
device: &wgpu::Device,
symbols: &[rustial_engine::symbols::PlacedSymbol],
atlas: &rustial_engine::symbols::GlyphAtlas,
camera_origin: DVec3,
render_em_px: f32,
) -> Option<SymbolBatchEntry> {
use crate::gpu::symbol_vertex::SymbolVertex;
let atlas_dims = atlas.dimensions();
if atlas_dims[0] == 0 || atlas_dims[1] == 0 {
return None;
}
let atlas_w = atlas_dims[0] as f32;
let atlas_h = atlas_dims[1] as f32;
let mut vertices: Vec<SymbolVertex> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
for symbol in symbols {
if !symbol.visible || symbol.opacity <= 0.0 {
continue;
}
if symbol.text.as_ref().is_none_or(|t| t.is_empty()) {
continue;
}
let ax = (symbol.world_anchor[0] - camera_origin.x) as f32;
let ay = (symbol.world_anchor[1] - camera_origin.y) as f32;
let az = (symbol.world_anchor[2] - camera_origin.z) as f32;
let scale = symbol.size_px / render_em_px.max(1.0);
let color = [
symbol.fill_color[0],
symbol.fill_color[1],
symbol.fill_color[2],
symbol.fill_color[3] * symbol.opacity,
];
let halo_color = symbol.halo_color;
let params = [1.0_f32, 1.0, 0.0, 0.0];
if !symbol.glyph_quads.is_empty() {
for quad in &symbol.glyph_quads {
let entry = match atlas.get(&symbol.font_stack, quad.codepoint) {
Some(e) => e,
None => continue,
};
let gw = entry.size[0] as f32 * scale;
let gh = entry.size[1] as f32 * scale;
let bearing_x = entry.bearing_x as f32 * scale;
let bearing_y = entry.bearing_y as f32 * scale;
let x0 = quad.x + bearing_x;
let y0 = quad.y + bearing_y;
let x1 = x0 + gw;
let y1 = y0 - gh;
let u0 = entry.origin[0] as f32 / atlas_w;
let v0 = entry.origin[1] as f32 / atlas_h;
let u1 = (entry.origin[0] + entry.size[0]) as f32 / atlas_w;
let v1 = (entry.origin[1] + entry.size[1]) as f32 / atlas_h;
let base = vertices.len() as u32;
vertices.push(SymbolVertex {
position: [ax, ay, az],
glyph_offset: [x0, y0],
tex_coord: [u0, v0],
color,
halo_color,
params,
});
vertices.push(SymbolVertex {
position: [ax, ay, az],
glyph_offset: [x1, y0],
tex_coord: [u1, v0],
color,
halo_color,
params,
});
vertices.push(SymbolVertex {
position: [ax, ay, az],
glyph_offset: [x0, y1],
tex_coord: [u0, v1],
color,
halo_color,
params,
});
vertices.push(SymbolVertex {
position: [ax, ay, az],
glyph_offset: [x1, y1],
tex_coord: [u1, v1],
color,
halo_color,
params,
});
indices.extend_from_slice(&[
base,
base + 2,
base + 1,
base + 1,
base + 2,
base + 3,
]);
}
} else {
let text = symbol.text.as_deref().unwrap_or("");
let mut cursor_x: f32 = 0.0;
for codepoint in text.chars() {
let entry = match atlas.get(&symbol.font_stack, codepoint) {
Some(e) => e,
None => continue,
};
let gw = entry.size[0] as f32 * scale;
let gh = entry.size[1] as f32 * scale;
let bearing_x = entry.bearing_x as f32 * scale;
let bearing_y = entry.bearing_y as f32 * scale;
let x0 = cursor_x + bearing_x;
let y0 = bearing_y;
let x1 = x0 + gw;
let y1 = y0 - gh;
let u0 = entry.origin[0] as f32 / atlas_w;
let v0 = entry.origin[1] as f32 / atlas_h;
let u1 = (entry.origin[0] + entry.size[0]) as f32 / atlas_w;
let v1 = (entry.origin[1] + entry.size[1]) as f32 / atlas_h;
let base = vertices.len() as u32;
vertices.push(SymbolVertex {
position: [ax, ay, az],
glyph_offset: [x0, y0],
tex_coord: [u0, v0],
color,
halo_color,
params,
});
vertices.push(SymbolVertex {
position: [ax, ay, az],
glyph_offset: [x1, y0],
tex_coord: [u1, v0],
color,
halo_color,
params,
});
vertices.push(SymbolVertex {
position: [ax, ay, az],
glyph_offset: [x0, y1],
tex_coord: [u0, v1],
color,
halo_color,
params,
});
vertices.push(SymbolVertex {
position: [ax, ay, az],
glyph_offset: [x1, y1],
tex_coord: [u1, v1],
color,
halo_color,
params,
});
indices.extend_from_slice(&[
base,
base + 2,
base + 1,
base + 1,
base + 2,
base + 3,
]);
cursor_x += entry.advance_x * scale;
}
}
}
if vertices.is_empty() {
return None;
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("symbol_batch_vb"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("symbol_batch_ib"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
Some(SymbolBatchEntry {
vertex_buffer,
index_buffer,
index_count: indices.len() as u32,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gpu::tile_atlas::TileAtlas;
use rustial_engine::{CameraProjection, DecodedImage};
use std::sync::Arc;
fn create_test_device() -> Option<wgpu::Device> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: None,
force_fallback_adapter: false,
}))
.ok()?;
let (device, _) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("batch_test_device"),
..Default::default()
}))
.ok()?;
Some(device)
}
fn test_image() -> DecodedImage {
DecodedImage {
width: 1,
height: 1,
data: Arc::new(vec![255, 255, 255, 255]),
}
}
#[test]
fn visible_tile_texture_region_matches_expected_subrect() {
let tile = VisibleTile {
target: TileId::new(3, 4, 2),
actual: TileId::new(1, 1, 0),
data: None,
fade_opacity: 1.0,
};
let region = tile.texture_region();
assert!((region.u_min - 0.0).abs() < 1e-6);
assert!((region.v_min - 0.5).abs() < 1e-6);
assert!((region.u_max - 0.25).abs() < 1e-6);
assert!((region.v_max - 0.75).abs() < 1e-6);
}
#[test]
fn terrain_texture_lookup_falls_back_to_visible_ancestor() {
let target = TileId::new(4, 8, 4);
let parent = TileId::new(3, 4, 2);
let visible = vec![VisibleTile {
target: parent,
actual: parent,
data: Some(rustial_engine::TileData::Raster(
rustial_engine::DecodedImage {
width: 1,
height: 1,
data: vec![255, 255, 255, 255].into(),
},
)),
fade_opacity: 1.0,
}];
assert_eq!(find_terrain_texture_actual(target, &visible), Some(parent));
}
#[test]
fn projected_tile_corners_differ_between_planar_projections() {
let tile = TileId::new(3, 4, 2);
let merc = projected_tile_corners(tile, CameraProjection::WebMercator, DVec3::ZERO);
let eq = projected_tile_corners(tile, CameraProjection::Equirectangular, DVec3::ZERO);
assert!((merc[0].y - eq[0].y).abs() > 1.0);
}
#[test]
fn exact_full_opacity_tile_uses_opaque_pass() {
let tile = VisibleTile {
target: TileId::new(3, 4, 2),
actual: TileId::new(3, 4, 2),
data: None,
fade_opacity: 1.0,
};
assert!(!tile_requires_blended_pass(&tile));
}
#[test]
fn fading_tile_uses_translucent_pass() {
let tile = VisibleTile {
target: TileId::new(3, 4, 2),
actual: TileId::new(3, 4, 2),
data: None,
fade_opacity: 0.5,
};
assert!(tile_requires_blended_pass(&tile));
}
#[test]
fn fallback_tile_uses_translucent_pass() {
let tile = VisibleTile {
target: TileId::new(4, 8, 4),
actual: TileId::new(3, 4, 2),
data: None,
fade_opacity: 1.0,
};
assert!(tile_requires_blended_pass(&tile));
}
#[test]
fn build_tile_batches_routes_exact_tile_to_opaque_batch() {
let Some(device) = create_test_device() else {
eprintln!("Skipping batch routing test: no suitable adapter/device available");
return;
};
let mut atlas = TileAtlas::new();
let tile_id = TileId::new(0, 0, 0);
atlas.insert(&device, tile_id, &test_image());
let visible = vec![VisibleTile {
target: tile_id,
actual: tile_id,
data: None,
fade_opacity: 1.0,
}];
let batches = build_tile_batches(
&device,
&visible,
&atlas,
DVec3::ZERO,
CameraProjection::WebMercator,
);
assert_eq!(batches.len(), 1);
assert!(batches[0].opaque.is_some());
assert!(batches[0].translucent.is_none());
}
#[test]
fn build_tile_batches_routes_fading_crossfade_tiles_to_translucent_batch() {
let Some(device) = create_test_device() else {
eprintln!(
"Skipping translucent batch routing test: no suitable adapter/device available"
);
return;
};
let mut atlas = TileAtlas::new();
let parent = TileId::new(0, 0, 0);
let child = TileId::new(1, 0, 0);
atlas.insert(&device, parent, &test_image());
atlas.insert(&device, child, &test_image());
let visible = vec![
VisibleTile {
target: child,
actual: child,
data: None,
fade_opacity: 0.5,
},
VisibleTile {
target: child,
actual: parent,
data: None,
fade_opacity: 0.5,
},
];
let batches = build_tile_batches(
&device,
&visible,
&atlas,
DVec3::ZERO,
CameraProjection::WebMercator,
);
assert_eq!(batches.len(), 1);
assert!(batches[0].opaque.is_none());
assert!(batches[0].translucent.is_some());
}
}