1use crate::gpu::terrain_vertex::TerrainVertex;
57use crate::gpu::tile_atlas::TileAtlas;
58use crate::gpu::vector_vertex::VectorVertex;
59use crate::gpu::vertex::TileVertex;
60use glam::DVec3;
61use rustial_engine::{CameraProjection, PreparedHillshadeRaster, TerrainMeshData, TileId, VectorMeshData, VisibleTile};
62use wgpu::util::DeviceExt;
63
64const MAX_FALLBACK_DEPTH: u8 = 8;
66
67pub(crate) fn find_terrain_texture_actual(
68 tile: TileId,
69 visible_tiles: &[VisibleTile],
70) -> Option<TileId> {
71 if let Some(vt) = visible_tiles
72 .iter()
73 .find(|vt| vt.target == tile && vt.data.is_some())
74 {
75 return Some(vt.actual);
76 }
77
78 let mut current = tile;
79 let mut depth = 0u8;
80 while depth < MAX_FALLBACK_DEPTH {
81 let Some(parent) = current.parent() else { break };
82 if let Some(vt) = visible_tiles
83 .iter()
84 .find(|vt| vt.target == parent && vt.data.is_some())
85 {
86 return Some(vt.actual);
87 }
88 if visible_tiles
89 .iter()
90 .any(|vt| vt.actual == parent && vt.data.is_some())
91 {
92 return Some(parent);
93 }
94 current = parent;
95 depth += 1;
96 }
97
98 None
99}
100
101pub struct TileBatch {
111 pub vertex_buffer: wgpu::Buffer,
113 pub index_buffer: wgpu::Buffer,
115 pub index_count: u32,
117}
118
119pub struct TilePageBatches {
126 pub opaque: Option<TileBatch>,
128 pub translucent: Option<TileBatch>,
130}
131
132fn tile_opacity_for(vt: &VisibleTile) -> f32 {
133 let fallback_zoom_delta = if vt.actual.zoom > vt.target.zoom {
134 (vt.actual.zoom - vt.target.zoom) as f32
135 } else {
136 vt.target.zoom.saturating_sub(vt.actual.zoom) as f32
137 };
138 let base_opacity = if vt.target == vt.actual {
139 1.0_f32
140 } else {
141 (0.9_f32 - 0.12_f32 * fallback_zoom_delta).clamp(0.55_f32, 0.9_f32)
142 };
143 base_opacity * vt.fade_opacity
144}
145
146fn tile_requires_blended_pass(vt: &VisibleTile) -> bool {
147 tile_opacity_for(vt) < 0.999_999
148}
149
150fn build_tile_batch(
151 device: &wgpu::Device,
152 verts: Vec<TileVertex>,
153 idxs: Vec<u32>,
154) -> Option<TileBatch> {
155 if verts.is_empty() {
156 return None;
157 }
158
159 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
160 label: Some("tile_batch_vb"),
161 contents: bytemuck::cast_slice(&verts),
162 usage: wgpu::BufferUsages::VERTEX,
163 });
164 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
165 label: Some("tile_batch_ib"),
166 contents: bytemuck::cast_slice(&idxs),
167 usage: wgpu::BufferUsages::INDEX,
168 });
169 Some(TileBatch {
170 vertex_buffer,
171 index_buffer,
172 index_count: idxs.len() as u32,
173 })
174}
175
176pub fn build_tile_batches(
197 device: &wgpu::Device,
198 visible_tiles: &[VisibleTile],
199 atlas: &TileAtlas,
200 camera_origin: DVec3,
201 projection: CameraProjection,
202) -> Vec<TilePageBatches> {
203 if atlas.page_count() == 0 {
204 return Vec::new();
205 }
206
207 let page_count = atlas.page_count();
210 let mut page_verts_opaque: Vec<Vec<TileVertex>> = vec![Vec::new(); page_count];
211 let mut page_idxs_opaque: Vec<Vec<u32>> = vec![Vec::new(); page_count];
212 let mut page_verts_translucent: Vec<Vec<TileVertex>> = vec![Vec::new(); page_count];
213 let mut page_idxs_translucent: Vec<Vec<u32>> = vec![Vec::new(); page_count];
214
215 for vt in visible_tiles {
216 let region = match atlas.get(&vt.actual) {
217 Some(r) => r,
218 None => continue,
219 };
220
221 let geometry_tile = if vt.actual.zoom > vt.target.zoom {
226 vt.actual
227 } else {
228 vt.target
229 };
230 let [sw, se, ne, nw] = projected_tile_corners(geometry_tile, projection, camera_origin);
231
232 let texture_region = vt.texture_region();
233
234 let (page_verts, page_idxs) = if tile_requires_blended_pass(vt) {
235 (&mut page_verts_translucent, &mut page_idxs_translucent)
236 } else {
237 (&mut page_verts_opaque, &mut page_idxs_opaque)
238 };
239
240 let base = page_verts[region.page].len() as u32;
242 let tile_opacity = tile_opacity_for(vt);
243
244 page_verts[region.page].extend_from_slice(&[
248 TileVertex {
249 position: [sw.x as f32, sw.y as f32, sw.z as f32],
250 uv: region.remap_uv(texture_region.u_min, texture_region.v_max),
251 opacity: tile_opacity,
252 },
253 TileVertex {
254 position: [se.x as f32, se.y as f32, se.z as f32],
255 uv: region.remap_uv(texture_region.u_max, texture_region.v_max),
256 opacity: tile_opacity,
257 },
258 TileVertex {
259 position: [ne.x as f32, ne.y as f32, ne.z as f32],
260 uv: region.remap_uv(texture_region.u_max, texture_region.v_min),
261 opacity: tile_opacity,
262 },
263 TileVertex {
264 position: [nw.x as f32, nw.y as f32, nw.z as f32],
265 uv: region.remap_uv(texture_region.u_min, texture_region.v_min),
266 opacity: tile_opacity,
267 },
268 ]);
269
270 page_idxs[region.page].extend_from_slice(&[
272 base,
273 base + 1,
274 base + 2,
275 base,
276 base + 2,
277 base + 3,
278 ]);
279 }
280
281 page_verts_opaque
282 .into_iter()
283 .zip(page_idxs_opaque)
284 .zip(page_verts_translucent.into_iter().zip(page_idxs_translucent))
285 .map(|((opaque_verts, opaque_idxs), (translucent_verts, translucent_idxs))| {
286 TilePageBatches {
287 opaque: build_tile_batch(device, opaque_verts, opaque_idxs),
288 translucent: build_tile_batch(device, translucent_verts, translucent_idxs),
289 }
290 })
291 .collect()
292}
293
294fn projected_tile_corners(tile: TileId, projection: CameraProjection, camera_origin: DVec3) -> [glam::DVec3; 4] {
295 let southwest = glam::DVec3::from_array(projection.project_tile_corner(&tile, 0.0, 1.0)) - camera_origin;
296 let southeast = glam::DVec3::from_array(projection.project_tile_corner(&tile, 1.0, 1.0)) - camera_origin;
297 let northeast = glam::DVec3::from_array(projection.project_tile_corner(&tile, 1.0, 0.0)) - camera_origin;
298 let northwest = glam::DVec3::from_array(projection.project_tile_corner(&tile, 0.0, 0.0)) - camera_origin;
299 [southwest, southeast, northeast, northwest]
300}
301
302pub struct TerrainBatch {
311 pub vertex_buffer: wgpu::Buffer,
313 pub index_buffer: wgpu::Buffer,
315 pub index_count: u32,
317}
318
319pub struct HillshadeBatch {
321 pub vertex_buffer: wgpu::Buffer,
323 pub index_buffer: wgpu::Buffer,
325 pub index_count: u32,
327}
328
329pub fn build_terrain_batches(
344 device: &wgpu::Device,
345 terrain_meshes: &[TerrainMeshData],
346 atlas: &TileAtlas,
347 camera_origin: DVec3,
348 visible_tiles: &[VisibleTile],
349) -> Vec<Option<TerrainBatch>> {
350 if atlas.page_count() == 0 || terrain_meshes.is_empty() {
351 return Vec::new();
352 }
353
354 let page_count = atlas.page_count();
355 let mut page_verts: Vec<Vec<TerrainVertex>> = vec![Vec::new(); page_count];
356 let mut page_idxs: Vec<Vec<u32>> = vec![Vec::new(); page_count];
357
358 for mesh in terrain_meshes {
359 let actual_tile = find_terrain_texture_actual(mesh.tile, visible_tiles).unwrap_or(mesh.tile);
360 let texture_region = rustial_engine::VisibleTile {
361 target: mesh.tile,
362 actual: actual_tile,
363 data: None,
364 fade_opacity: 1.0,
365 }
366 .texture_region();
367
368 let region = match atlas.get(&actual_tile) {
369 Some(r) => r,
370 None => continue,
371 };
372
373 debug_assert_eq!(
374 mesh.positions.len(),
375 mesh.uvs.len(),
376 "TerrainMeshData positions/uvs length mismatch for tile {:?}",
377 mesh.tile,
378 );
379 debug_assert_eq!(
380 mesh.positions.len(),
381 mesh.normals.len(),
382 "TerrainMeshData positions/normals length mismatch for tile {:?}",
383 mesh.tile,
384 );
385
386 let vert_count = mesh.positions.len();
387 let base = page_verts[region.page].len() as u32;
388 page_verts[region.page].reserve(vert_count);
389
390 for i in 0..vert_count {
391 let pos = &mesh.positions[i];
392 let uv = &mesh.uvs[i];
393 let normal = &mesh.normals[i];
394
395 let mapped_u = texture_region.u_min + (texture_region.u_max - texture_region.u_min) * uv[0];
396 let mapped_v = texture_region.v_min + (texture_region.v_max - texture_region.v_min) * uv[1];
397
398 page_verts[region.page].push(TerrainVertex {
399 position: [
400 (pos[0] - camera_origin.x) as f32,
401 (pos[1] - camera_origin.y) as f32,
402 (pos[2] - camera_origin.z) as f32,
403 ],
404 uv: region.remap_uv(mapped_u, mapped_v),
405 normal: *normal,
406 });
407 }
408
409 page_idxs[region.page].reserve(mesh.indices.len());
410 for idx in &mesh.indices {
411 debug_assert!(
412 (*idx as usize) < vert_count,
413 "TerrainMeshData index {} out of bounds (vertex_count={}) for tile {:?}",
414 idx,
415 vert_count,
416 mesh.tile,
417 );
418 page_idxs[region.page].push(base + idx);
419 }
420 }
421
422 page_verts
423 .into_iter()
424 .zip(page_idxs)
425 .map(|(verts, idxs)| {
426 if verts.is_empty() {
427 return None;
428 }
429 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
430 label: Some("terrain_batch_vb"),
431 contents: bytemuck::cast_slice(&verts),
432 usage: wgpu::BufferUsages::VERTEX,
433 });
434 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
435 label: Some("terrain_batch_ib"),
436 contents: bytemuck::cast_slice(&idxs),
437 usage: wgpu::BufferUsages::INDEX,
438 });
439 Some(TerrainBatch {
440 vertex_buffer,
441 index_buffer,
442 index_count: idxs.len() as u32,
443 })
444 })
445 .collect()
446}
447
448pub fn build_hillshade_batches(
450 device: &wgpu::Device,
451 terrain_meshes: &[TerrainMeshData],
452 hillshade_rasters: &[PreparedHillshadeRaster],
453 atlas: &TileAtlas,
454 camera_origin: DVec3,
455) -> Vec<Option<HillshadeBatch>> {
456 if atlas.page_count() == 0 || terrain_meshes.is_empty() || hillshade_rasters.is_empty() {
457 return Vec::new();
458 }
459
460 let available: std::collections::HashSet<TileId> =
461 hillshade_rasters.iter().map(|r| r.tile).collect();
462 let page_count = atlas.page_count();
463 let mut page_verts: Vec<Vec<TerrainVertex>> = vec![Vec::new(); page_count];
464 let mut page_idxs: Vec<Vec<u32>> = vec![Vec::new(); page_count];
465
466 for mesh in terrain_meshes {
467 if !available.contains(&mesh.tile) {
468 continue;
469 }
470
471 let region = match atlas.get(&mesh.tile) {
472 Some(r) => r,
473 None => continue,
474 };
475
476 let vert_count = mesh.positions.len();
477 let base = page_verts[region.page].len() as u32;
478 page_verts[region.page].reserve(vert_count);
479
480 for i in 0..vert_count {
481 let pos = &mesh.positions[i];
482 let uv = &mesh.uvs[i];
483 let normal = &mesh.normals[i];
484
485 page_verts[region.page].push(TerrainVertex {
486 position: [
487 (pos[0] - camera_origin.x) as f32,
488 (pos[1] - camera_origin.y) as f32,
489 (pos[2] - camera_origin.z) as f32,
490 ],
491 uv: region.remap_uv(uv[0], uv[1]),
492 normal: *normal,
493 });
494 }
495
496 page_idxs[region.page].reserve(mesh.indices.len());
497 for idx in &mesh.indices {
498 page_idxs[region.page].push(base + idx);
499 }
500 }
501
502 page_verts
503 .into_iter()
504 .zip(page_idxs)
505 .map(|(verts, idxs)| {
506 if verts.is_empty() {
507 return None;
508 }
509 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
510 label: Some("hillshade_batch_vb"),
511 contents: bytemuck::cast_slice(&verts),
512 usage: wgpu::BufferUsages::VERTEX,
513 });
514 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
515 label: Some("hillshade_batch_ib"),
516 contents: bytemuck::cast_slice(&idxs),
517 usage: wgpu::BufferUsages::INDEX,
518 });
519 Some(HillshadeBatch {
520 vertex_buffer,
521 index_buffer,
522 index_count: idxs.len() as u32,
523 })
524 })
525 .collect()
526}
527
528pub struct VectorBatchEntry {
538 pub vertex_buffer: wgpu::Buffer,
540 pub index_buffer: wgpu::Buffer,
542 pub index_count: u32,
544}
545
546pub fn build_vector_batch(
563 device: &wgpu::Device,
564 mesh: &VectorMeshData,
565 camera_origin: DVec3,
566) -> Option<VectorBatchEntry> {
567 if mesh.indices.is_empty() {
568 return None;
569 }
570
571 debug_assert_eq!(
572 mesh.positions.len(),
573 mesh.colors.len(),
574 "VectorMeshData positions/colors length mismatch ({} vs {})",
575 mesh.positions.len(),
576 mesh.colors.len(),
577 );
578
579 let vertices: Vec<VectorVertex> = mesh
580 .positions
581 .iter()
582 .zip(mesh.colors.iter())
583 .map(|(pos, color)| VectorVertex {
584 position: [
585 (pos[0] - camera_origin.x) as f32,
586 (pos[1] - camera_origin.y) as f32,
587 (pos[2] - camera_origin.z) as f32,
588 ],
589 color: *color,
590 })
591 .collect();
592
593 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
594 label: Some("vector_batch_vb"),
595 contents: bytemuck::cast_slice(&vertices),
596 usage: wgpu::BufferUsages::VERTEX,
597 });
598 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
599 label: Some("vector_batch_ib"),
600 contents: bytemuck::cast_slice(&mesh.indices),
601 usage: wgpu::BufferUsages::INDEX,
602 });
603
604 Some(VectorBatchEntry {
605 vertex_buffer,
606 index_buffer,
607 index_count: mesh.indices.len() as u32,
608 })
609}
610
611pub struct FillExtrusionBatchEntry {
619 pub vertex_buffer: wgpu::Buffer,
621 pub index_buffer: wgpu::Buffer,
623 pub index_count: u32,
625}
626
627pub fn build_fill_extrusion_batch(
631 device: &wgpu::Device,
632 mesh: &VectorMeshData,
633 camera_origin: DVec3,
634) -> Option<FillExtrusionBatchEntry> {
635 use crate::gpu::fill_extrusion_vertex::FillExtrusionVertex;
636
637 if mesh.indices.is_empty() || mesh.normals.is_empty() {
638 return None;
639 }
640
641 debug_assert_eq!(
642 mesh.positions.len(),
643 mesh.normals.len(),
644 "FillExtrusion positions/normals length mismatch ({} vs {})",
645 mesh.positions.len(),
646 mesh.normals.len(),
647 );
648
649 let vertices: Vec<FillExtrusionVertex> = mesh
650 .positions
651 .iter()
652 .zip(mesh.normals.iter())
653 .zip(mesh.colors.iter())
654 .map(|((pos, normal), color)| FillExtrusionVertex {
655 position: [
656 (pos[0] - camera_origin.x) as f32,
657 (pos[1] - camera_origin.y) as f32,
658 (pos[2] - camera_origin.z) as f32,
659 ],
660 normal: *normal,
661 color: *color,
662 })
663 .collect();
664
665 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
666 label: Some("fill_extrusion_batch_vb"),
667 contents: bytemuck::cast_slice(&vertices),
668 usage: wgpu::BufferUsages::VERTEX,
669 });
670 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
671 label: Some("fill_extrusion_batch_ib"),
672 contents: bytemuck::cast_slice(&mesh.indices),
673 usage: wgpu::BufferUsages::INDEX,
674 });
675
676 Some(FillExtrusionBatchEntry {
677 vertex_buffer,
678 index_buffer,
679 index_count: mesh.indices.len() as u32,
680 })
681}
682
683pub struct FillBatchEntry {
689 pub vertex_buffer: wgpu::Buffer,
691 pub index_buffer: wgpu::Buffer,
693 pub index_count: u32,
695 pub fill_params_buffer: wgpu::Buffer,
697 pub bind_group: wgpu::BindGroup,
699}
700
701pub fn build_fill_batch(
705 device: &wgpu::Device,
706 mesh: &VectorMeshData,
707 camera_origin: DVec3,
708 uniform_buffer: &wgpu::Buffer,
709 fill_bind_group_layout: &wgpu::BindGroupLayout,
710) -> Option<FillBatchEntry> {
711 use crate::gpu::fill_vertex::FillVertex;
712 use crate::pipeline::fill_pipeline::FillParamsUniform;
713
714 if mesh.indices.is_empty() {
715 return None;
716 }
717
718 let vertices: Vec<FillVertex> = mesh
719 .positions
720 .iter()
721 .zip(mesh.colors.iter())
722 .map(|(pos, color)| FillVertex {
723 position: [
724 (pos[0] - camera_origin.x) as f32,
725 (pos[1] - camera_origin.y) as f32,
726 (pos[2] - camera_origin.z) as f32,
727 ],
728 color: *color,
729 })
730 .collect();
731
732 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
733 label: Some("fill_batch_vb"),
734 contents: bytemuck::cast_slice(&vertices),
735 usage: wgpu::BufferUsages::VERTEX,
736 });
737 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
738 label: Some("fill_batch_ib"),
739 contents: bytemuck::cast_slice(&mesh.indices),
740 usage: wgpu::BufferUsages::INDEX,
741 });
742
743 let fill_params = FillParamsUniform {
744 fill_translate: mesh.fill_translate,
745 fill_opacity: mesh.fill_opacity,
746 fill_antialias: if mesh.fill_antialias { 1.0 } else { 0.0 },
747 outline_color: mesh.fill_outline_color,
748 };
749 let fill_params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
750 label: Some("fill_params_buf"),
751 contents: bytemuck::bytes_of(&fill_params),
752 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
753 });
754
755 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
756 label: Some("fill_batch_bg"),
757 layout: fill_bind_group_layout,
758 entries: &[
759 wgpu::BindGroupEntry {
760 binding: 0,
761 resource: uniform_buffer.as_entire_binding(),
762 },
763 wgpu::BindGroupEntry {
764 binding: 1,
765 resource: fill_params_buffer.as_entire_binding(),
766 },
767 ],
768 });
769
770 Some(FillBatchEntry {
771 vertex_buffer,
772 index_buffer,
773 index_count: mesh.indices.len() as u32,
774 fill_params_buffer,
775 bind_group,
776 })
777}
778
779pub struct FillPatternBatchEntry {
785 pub vertex_buffer: wgpu::Buffer,
787 pub index_buffer: wgpu::Buffer,
789 pub index_count: u32,
791 pub uniform_bind_group: wgpu::BindGroup,
793 pub texture_bind_group: wgpu::BindGroup,
795 pub _pattern_texture: wgpu::Texture,
797 pub _pattern_texture_view: wgpu::TextureView,
799 pub _fill_params_buffer: wgpu::Buffer,
801}
802
803pub fn build_fill_pattern_batch(
808 device: &wgpu::Device,
809 queue: &wgpu::Queue,
810 mesh: &VectorMeshData,
811 camera_origin: DVec3,
812 uniform_buffer: &wgpu::Buffer,
813 uniform_bgl: &wgpu::BindGroupLayout,
814 texture_bgl: &wgpu::BindGroupLayout,
815 pattern_sampler: &wgpu::Sampler,
816) -> Option<FillPatternBatchEntry> {
817 use crate::gpu::fill_pattern_vertex::FillPatternVertex;
818 use crate::pipeline::fill_pipeline::FillParamsUniform;
819
820 let pattern = mesh.fill_pattern.as_ref()?;
821 if mesh.indices.is_empty() || mesh.fill_pattern_uvs.is_empty() {
822 return None;
823 }
824
825 let vertices: Vec<FillPatternVertex> = (0..mesh.positions.len())
826 .map(|i| {
827 let pos = &mesh.positions[i];
828 FillPatternVertex {
829 position: [
830 (pos[0] - camera_origin.x) as f32,
831 (pos[1] - camera_origin.y) as f32,
832 (pos[2] - camera_origin.z) as f32,
833 ],
834 color: mesh.colors[i],
835 uv: if i < mesh.fill_pattern_uvs.len() {
836 mesh.fill_pattern_uvs[i]
837 } else {
838 [0.0, 0.0]
839 },
840 }
841 })
842 .collect();
843
844 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
845 label: Some("fill_pattern_batch_vb"),
846 contents: bytemuck::cast_slice(&vertices),
847 usage: wgpu::BufferUsages::VERTEX,
848 });
849 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
850 label: Some("fill_pattern_batch_ib"),
851 contents: bytemuck::cast_slice(&mesh.indices),
852 usage: wgpu::BufferUsages::INDEX,
853 });
854
855 let texture_size = wgpu::Extent3d {
857 width: pattern.width,
858 height: pattern.height,
859 depth_or_array_layers: 1,
860 };
861 let pattern_texture = device.create_texture(&wgpu::TextureDescriptor {
862 label: Some("fill_pattern_tex"),
863 size: texture_size,
864 mip_level_count: 1,
865 sample_count: 1,
866 dimension: wgpu::TextureDimension::D2,
867 format: wgpu::TextureFormat::Rgba8UnormSrgb,
868 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
869 view_formats: &[],
870 });
871
872 queue.write_texture(
873 wgpu::TexelCopyTextureInfo {
874 texture: &pattern_texture,
875 mip_level: 0,
876 origin: wgpu::Origin3d::ZERO,
877 aspect: wgpu::TextureAspect::All,
878 },
879 &pattern.data,
880 wgpu::TexelCopyBufferLayout {
881 offset: 0,
882 bytes_per_row: Some(pattern.width * 4),
883 rows_per_image: Some(pattern.height),
884 },
885 texture_size,
886 );
887
888 let pattern_texture_view =
889 pattern_texture.create_view(&wgpu::TextureViewDescriptor::default());
890
891 let fill_params = FillParamsUniform {
893 fill_translate: mesh.fill_translate,
894 fill_opacity: mesh.fill_opacity,
895 fill_antialias: if mesh.fill_antialias { 1.0 } else { 0.0 },
896 outline_color: mesh.fill_outline_color,
897 };
898 let fill_params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
899 label: Some("fill_pattern_params_buf"),
900 contents: bytemuck::bytes_of(&fill_params),
901 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
902 });
903
904 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
906 label: Some("fill_pattern_uniform_bg"),
907 layout: uniform_bgl,
908 entries: &[
909 wgpu::BindGroupEntry {
910 binding: 0,
911 resource: uniform_buffer.as_entire_binding(),
912 },
913 wgpu::BindGroupEntry {
914 binding: 1,
915 resource: fill_params_buffer.as_entire_binding(),
916 },
917 ],
918 });
919
920 let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
922 label: Some("fill_pattern_texture_bg"),
923 layout: texture_bgl,
924 entries: &[
925 wgpu::BindGroupEntry {
926 binding: 0,
927 resource: wgpu::BindingResource::TextureView(&pattern_texture_view),
928 },
929 wgpu::BindGroupEntry {
930 binding: 1,
931 resource: wgpu::BindingResource::Sampler(pattern_sampler),
932 },
933 ],
934 });
935
936 Some(FillPatternBatchEntry {
937 vertex_buffer,
938 index_buffer,
939 index_count: mesh.indices.len() as u32,
940 uniform_bind_group,
941 texture_bind_group,
942 _pattern_texture: pattern_texture,
943 _pattern_texture_view: pattern_texture_view,
944 _fill_params_buffer: fill_params_buffer,
945 })
946}
947
948pub struct LineBatchEntry {
954 pub vertex_buffer: wgpu::Buffer,
956 pub index_buffer: wgpu::Buffer,
958 pub index_count: u32,
960 pub line_params: [f32; 4],
962}
963
964pub fn build_line_batch(
968 device: &wgpu::Device,
969 mesh: &VectorMeshData,
970 camera_origin: DVec3,
971) -> Option<LineBatchEntry> {
972 use crate::gpu::line_vertex::LineVertex;
973
974 if mesh.indices.is_empty() || mesh.line_distances.is_empty() {
975 return None;
976 }
977
978 let vertices: Vec<LineVertex> = (0..mesh.positions.len())
979 .map(|i| {
980 let pos = &mesh.positions[i];
981 LineVertex {
982 position: [
983 (pos[0] - camera_origin.x) as f32,
984 (pos[1] - camera_origin.y) as f32,
985 (pos[2] - camera_origin.z) as f32,
986 ],
987 color: mesh.colors[i],
988 line_normal: mesh.line_normals[i],
989 line_distance: mesh.line_distances[i],
990 cap_join: if i < mesh.line_cap_joins.len() { mesh.line_cap_joins[i] } else { 0.0 },
991 }
992 })
993 .collect();
994
995 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
996 label: Some("line_batch_vb"),
997 contents: bytemuck::cast_slice(&vertices),
998 usage: wgpu::BufferUsages::VERTEX,
999 });
1000 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1001 label: Some("line_batch_ib"),
1002 contents: bytemuck::cast_slice(&mesh.indices),
1003 usage: wgpu::BufferUsages::INDEX,
1004 });
1005
1006 Some(LineBatchEntry {
1007 vertex_buffer,
1008 index_buffer,
1009 index_count: mesh.indices.len() as u32,
1010 line_params: mesh.line_params,
1011 })
1012}
1013
1014pub struct LinePatternBatchEntry {
1020 pub vertex_buffer: wgpu::Buffer,
1022 pub index_buffer: wgpu::Buffer,
1024 pub index_count: u32,
1026 pub uniform_bind_group: wgpu::BindGroup,
1028 pub texture_bind_group: wgpu::BindGroup,
1030 pub _pattern_texture: wgpu::Texture,
1032 pub _pattern_texture_view: wgpu::TextureView,
1034 pub line_params: [f32; 4],
1036}
1037
1038pub fn build_line_pattern_batch(
1043 device: &wgpu::Device,
1044 queue: &wgpu::Queue,
1045 mesh: &VectorMeshData,
1046 camera_origin: DVec3,
1047 uniform_buffer: &wgpu::Buffer,
1048 uniform_bgl: &wgpu::BindGroupLayout,
1049 texture_bgl: &wgpu::BindGroupLayout,
1050 pattern_sampler: &wgpu::Sampler,
1051) -> Option<LinePatternBatchEntry> {
1052 use crate::gpu::line_pattern_vertex::LinePatternVertex;
1053
1054 let pattern = mesh.line_pattern.as_ref()?;
1055 if mesh.indices.is_empty() || mesh.line_pattern_uvs.is_empty() {
1056 return None;
1057 }
1058
1059 let vertices: Vec<LinePatternVertex> = (0..mesh.positions.len())
1060 .map(|i| {
1061 let pos = &mesh.positions[i];
1062 LinePatternVertex {
1063 position: [
1064 (pos[0] - camera_origin.x) as f32,
1065 (pos[1] - camera_origin.y) as f32,
1066 (pos[2] - camera_origin.z) as f32,
1067 ],
1068 color: mesh.colors[i],
1069 line_normal: if i < mesh.line_normals.len() { mesh.line_normals[i] } else { [0.0, 0.0] },
1070 line_distance: if i < mesh.line_distances.len() { mesh.line_distances[i] } else { 0.0 },
1071 cap_join: if i < mesh.line_cap_joins.len() { mesh.line_cap_joins[i] } else { 0.0 },
1072 uv: if i < mesh.line_pattern_uvs.len() {
1073 mesh.line_pattern_uvs[i]
1074 } else {
1075 [0.0, 0.0]
1076 },
1077 }
1078 })
1079 .collect();
1080
1081 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1082 label: Some("line_pattern_batch_vb"),
1083 contents: bytemuck::cast_slice(&vertices),
1084 usage: wgpu::BufferUsages::VERTEX,
1085 });
1086 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1087 label: Some("line_pattern_batch_ib"),
1088 contents: bytemuck::cast_slice(&mesh.indices),
1089 usage: wgpu::BufferUsages::INDEX,
1090 });
1091
1092 let texture_size = wgpu::Extent3d {
1094 width: pattern.width,
1095 height: pattern.height,
1096 depth_or_array_layers: 1,
1097 };
1098 let pattern_texture = device.create_texture(&wgpu::TextureDescriptor {
1099 label: Some("line_pattern_tex"),
1100 size: texture_size,
1101 mip_level_count: 1,
1102 sample_count: 1,
1103 dimension: wgpu::TextureDimension::D2,
1104 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1105 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1106 view_formats: &[],
1107 });
1108
1109 queue.write_texture(
1110 wgpu::TexelCopyTextureInfo {
1111 texture: &pattern_texture,
1112 mip_level: 0,
1113 origin: wgpu::Origin3d::ZERO,
1114 aspect: wgpu::TextureAspect::All,
1115 },
1116 &pattern.data,
1117 wgpu::TexelCopyBufferLayout {
1118 offset: 0,
1119 bytes_per_row: Some(pattern.width * 4),
1120 rows_per_image: Some(pattern.height),
1121 },
1122 texture_size,
1123 );
1124
1125 let pattern_texture_view =
1126 pattern_texture.create_view(&wgpu::TextureViewDescriptor::default());
1127
1128 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1130 label: Some("line_pattern_uniform_bg"),
1131 layout: uniform_bgl,
1132 entries: &[wgpu::BindGroupEntry {
1133 binding: 0,
1134 resource: uniform_buffer.as_entire_binding(),
1135 }],
1136 });
1137
1138 let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1140 label: Some("line_pattern_texture_bg"),
1141 layout: texture_bgl,
1142 entries: &[
1143 wgpu::BindGroupEntry {
1144 binding: 0,
1145 resource: wgpu::BindingResource::TextureView(&pattern_texture_view),
1146 },
1147 wgpu::BindGroupEntry {
1148 binding: 1,
1149 resource: wgpu::BindingResource::Sampler(pattern_sampler),
1150 },
1151 ],
1152 });
1153
1154 Some(LinePatternBatchEntry {
1155 vertex_buffer,
1156 index_buffer,
1157 index_count: mesh.indices.len() as u32,
1158 uniform_bind_group,
1159 texture_bind_group,
1160 _pattern_texture: pattern_texture,
1161 _pattern_texture_view: pattern_texture_view,
1162 line_params: mesh.line_params,
1163 })
1164}
1165
1166pub struct CircleBatchEntry {
1172 pub vertex_buffer: wgpu::Buffer,
1174 pub index_buffer: wgpu::Buffer,
1176 pub index_count: u32,
1178}
1179
1180pub fn build_circle_batch(
1185 device: &wgpu::Device,
1186 mesh: &VectorMeshData,
1187 camera_origin: DVec3,
1188) -> Option<CircleBatchEntry> {
1189 use crate::gpu::circle_vertex::CircleVertex;
1190
1191 if mesh.circle_instances.is_empty() {
1192 return None;
1193 }
1194
1195 let instance_count = mesh.circle_instances.len();
1196 let mut vertices: Vec<CircleVertex> = Vec::with_capacity(instance_count * 4);
1197 let mut indices: Vec<u32> = Vec::with_capacity(instance_count * 6);
1198
1199 let offsets: [[f32; 2]; 4] = [[-1.0, 1.0], [1.0, 1.0], [-1.0, -1.0], [1.0, -1.0]];
1200
1201 for (i, ci) in mesh.circle_instances.iter().enumerate() {
1202 let cx = (ci.center[0] - camera_origin.x) as f32;
1203 let cy = (ci.center[1] - camera_origin.y) as f32;
1204 let cz = (ci.center[2] - camera_origin.z) as f32;
1205 let params = [ci.radius, ci.stroke_width, ci.blur, 0.0];
1206
1207 for offset in &offsets {
1208 vertices.push(CircleVertex {
1209 position: [cx, cy, cz],
1210 quad_offset: *offset,
1211 color: ci.color,
1212 stroke_color: ci.stroke_color,
1213 params,
1214 });
1215 }
1216
1217 let base = (i as u32) * 4;
1218 indices.extend_from_slice(&[base, base + 2, base + 1, base + 1, base + 2, base + 3]);
1219 }
1220
1221 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1222 label: Some("circle_batch_vb"),
1223 contents: bytemuck::cast_slice(&vertices),
1224 usage: wgpu::BufferUsages::VERTEX,
1225 });
1226 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1227 label: Some("circle_batch_ib"),
1228 contents: bytemuck::cast_slice(&indices),
1229 usage: wgpu::BufferUsages::INDEX,
1230 });
1231
1232 Some(CircleBatchEntry {
1233 vertex_buffer,
1234 index_buffer,
1235 index_count: indices.len() as u32,
1236 })
1237}
1238
1239pub struct HeatmapBatchEntry {
1245 pub vertex_buffer: wgpu::Buffer,
1247 pub index_buffer: wgpu::Buffer,
1249 pub index_count: u32,
1251}
1252
1253pub fn build_heatmap_batch(
1258 device: &wgpu::Device,
1259 mesh: &VectorMeshData,
1260 camera_origin: DVec3,
1261) -> Option<HeatmapBatchEntry> {
1262 use crate::gpu::heatmap_vertex::HeatmapVertex;
1263
1264 if mesh.heatmap_points.is_empty() {
1265 return None;
1266 }
1267
1268 let point_count = mesh.heatmap_points.len();
1269 let mut vertices: Vec<HeatmapVertex> = Vec::with_capacity(point_count * 4);
1270 let mut indices: Vec<u32> = Vec::with_capacity(point_count * 6);
1271
1272 let offsets: [[f32; 2]; 4] = [[-1.0, 1.0], [1.0, 1.0], [-1.0, -1.0], [1.0, -1.0]];
1273
1274 for (i, pt) in mesh.heatmap_points.iter().enumerate() {
1275 let cx = (pt[0] - camera_origin.x) as f32;
1276 let cy = (pt[1] - camera_origin.y) as f32;
1277 let cz = 0.0f32;
1278 let weight = pt[2] as f32;
1279 let radius = pt[3] as f32;
1280 let params = [weight, radius, mesh.heatmap_intensity, 0.0];
1281
1282 for offset in &offsets {
1283 vertices.push(HeatmapVertex {
1284 position: [cx, cy, cz],
1285 quad_offset: *offset,
1286 params,
1287 });
1288 }
1289
1290 let base = (i as u32) * 4;
1291 indices.extend_from_slice(&[base, base + 2, base + 1, base + 1, base + 2, base + 3]);
1292 }
1293
1294 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1295 label: Some("heatmap_batch_vb"),
1296 contents: bytemuck::cast_slice(&vertices),
1297 usage: wgpu::BufferUsages::VERTEX,
1298 });
1299 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1300 label: Some("heatmap_batch_ib"),
1301 contents: bytemuck::cast_slice(&indices),
1302 usage: wgpu::BufferUsages::INDEX,
1303 });
1304
1305 Some(HeatmapBatchEntry {
1306 vertex_buffer,
1307 index_buffer,
1308 index_count: indices.len() as u32,
1309 })
1310}
1311
1312pub fn build_placeholder_batches(
1327 device: &wgpu::Device,
1328 placeholders: &[rustial_engine::LoadingPlaceholder],
1329 style: &rustial_engine::PlaceholderStyle,
1330 camera_origin: DVec3,
1331) -> Option<VectorBatchEntry> {
1332 if placeholders.is_empty() {
1333 return None;
1334 }
1335
1336 let quad_count = placeholders.len();
1337 let mut vertices: Vec<VectorVertex> = Vec::with_capacity(quad_count * 4);
1338 let mut indices: Vec<u32> = Vec::with_capacity(quad_count * 6);
1339
1340 for ph in placeholders {
1341 let opacity = style.shimmer_opacity(ph.animation_phase);
1342 let color = [
1343 style.background_color[0],
1344 style.background_color[1],
1345 style.background_color[2],
1346 style.background_color[3] * opacity,
1347 ];
1348
1349 let min = ph.bounds.min.position;
1350 let max = ph.bounds.max.position;
1351 let ox = camera_origin.x;
1352 let oy = camera_origin.y;
1353 let z: f32 = -0.01;
1355
1356 let base = vertices.len() as u32;
1357 vertices.push(VectorVertex { position: [(min.x - ox) as f32, (min.y - oy) as f32, z], color });
1359 vertices.push(VectorVertex { position: [(max.x - ox) as f32, (min.y - oy) as f32, z], color });
1360 vertices.push(VectorVertex { position: [(max.x - ox) as f32, (max.y - oy) as f32, z], color });
1361 vertices.push(VectorVertex { position: [(min.x - ox) as f32, (max.y - oy) as f32, z], color });
1362 indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
1363 }
1364
1365 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1366 label: Some("placeholder_batch_vb"),
1367 contents: bytemuck::cast_slice(&vertices),
1368 usage: wgpu::BufferUsages::VERTEX,
1369 });
1370 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1371 label: Some("placeholder_batch_ib"),
1372 contents: bytemuck::cast_slice(&indices),
1373 usage: wgpu::BufferUsages::INDEX,
1374 });
1375
1376 Some(VectorBatchEntry {
1377 vertex_buffer,
1378 index_buffer,
1379 index_count: indices.len() as u32,
1380 })
1381}
1382
1383pub struct SymbolBatchEntry {
1389 pub vertex_buffer: wgpu::Buffer,
1391 pub index_buffer: wgpu::Buffer,
1393 pub index_count: u32,
1395}
1396
1397pub fn build_symbol_batch(
1405 device: &wgpu::Device,
1406 symbols: &[rustial_engine::symbols::PlacedSymbol],
1407 atlas: &rustial_engine::symbols::GlyphAtlas,
1408 camera_origin: DVec3,
1409 render_em_px: f32,
1410) -> Option<SymbolBatchEntry> {
1411 use crate::gpu::symbol_vertex::SymbolVertex;
1412
1413 let atlas_dims = atlas.dimensions();
1414 if atlas_dims[0] == 0 || atlas_dims[1] == 0 {
1415 return None;
1416 }
1417
1418 let atlas_w = atlas_dims[0] as f32;
1419 let atlas_h = atlas_dims[1] as f32;
1420
1421 let mut vertices: Vec<SymbolVertex> = Vec::new();
1422 let mut indices: Vec<u32> = Vec::new();
1423
1424 for symbol in symbols {
1425 if !symbol.visible || symbol.opacity <= 0.0 {
1426 continue;
1427 }
1428 if symbol.text.as_ref().map_or(true, |t| t.is_empty()) {
1429 continue;
1430 }
1431
1432 let ax = (symbol.world_anchor[0] - camera_origin.x) as f32;
1434 let ay = (symbol.world_anchor[1] - camera_origin.y) as f32;
1435 let az = (symbol.world_anchor[2] - camera_origin.z) as f32;
1436
1437 let scale = symbol.size_px / render_em_px.max(1.0);
1438
1439 let color = [
1440 symbol.fill_color[0],
1441 symbol.fill_color[1],
1442 symbol.fill_color[2],
1443 symbol.fill_color[3] * symbol.opacity,
1444 ];
1445 let halo_color = symbol.halo_color;
1446 let params = [1.0_f32, 1.0, 0.0, 0.0];
1447
1448 if !symbol.glyph_quads.is_empty() {
1449 for quad in &symbol.glyph_quads {
1451 let entry = match atlas.get(&symbol.font_stack, quad.codepoint) {
1452 Some(e) => e,
1453 None => continue,
1454 };
1455
1456 let gw = entry.size[0] as f32 * scale;
1457 let gh = entry.size[1] as f32 * scale;
1458 let bearing_x = entry.bearing_x as f32 * scale;
1459 let bearing_y = entry.bearing_y as f32 * scale;
1460
1461 let x0 = quad.x + bearing_x;
1464 let y0 = quad.y + bearing_y;
1465 let x1 = x0 + gw;
1466 let y1 = y0 - gh;
1467
1468 let u0 = entry.origin[0] as f32 / atlas_w;
1469 let v0 = entry.origin[1] as f32 / atlas_h;
1470 let u1 = (entry.origin[0] + entry.size[0]) as f32 / atlas_w;
1471 let v1 = (entry.origin[1] + entry.size[1]) as f32 / atlas_h;
1472
1473 let base = vertices.len() as u32;
1474 vertices.push(SymbolVertex { position: [ax, ay, az], glyph_offset: [x0, y0], tex_coord: [u0, v0], color, halo_color, params });
1475 vertices.push(SymbolVertex { position: [ax, ay, az], glyph_offset: [x1, y0], tex_coord: [u1, v0], color, halo_color, params });
1476 vertices.push(SymbolVertex { position: [ax, ay, az], glyph_offset: [x0, y1], tex_coord: [u0, v1], color, halo_color, params });
1477 vertices.push(SymbolVertex { position: [ax, ay, az], glyph_offset: [x1, y1], tex_coord: [u1, v1], color, halo_color, params });
1478 indices.extend_from_slice(&[base, base + 2, base + 1, base + 1, base + 2, base + 3]);
1479 }
1480 } else {
1481 let text = symbol.text.as_deref().unwrap_or("");
1483 let mut cursor_x: f32 = 0.0;
1484 for codepoint in text.chars() {
1485 let entry = match atlas.get(&symbol.font_stack, codepoint) {
1486 Some(e) => e,
1487 None => continue,
1488 };
1489
1490 let gw = entry.size[0] as f32 * scale;
1491 let gh = entry.size[1] as f32 * scale;
1492 let bearing_x = entry.bearing_x as f32 * scale;
1493 let bearing_y = entry.bearing_y as f32 * scale;
1494
1495 let x0 = cursor_x + bearing_x;
1496 let y0 = bearing_y;
1497 let x1 = x0 + gw;
1498 let y1 = y0 - gh;
1499
1500 let u0 = entry.origin[0] as f32 / atlas_w;
1501 let v0 = entry.origin[1] as f32 / atlas_h;
1502 let u1 = (entry.origin[0] + entry.size[0]) as f32 / atlas_w;
1503 let v1 = (entry.origin[1] + entry.size[1]) as f32 / atlas_h;
1504
1505 let base = vertices.len() as u32;
1506 vertices.push(SymbolVertex { position: [ax, ay, az], glyph_offset: [x0, y0], tex_coord: [u0, v0], color, halo_color, params });
1507 vertices.push(SymbolVertex { position: [ax, ay, az], glyph_offset: [x1, y0], tex_coord: [u1, v0], color, halo_color, params });
1508 vertices.push(SymbolVertex { position: [ax, ay, az], glyph_offset: [x0, y1], tex_coord: [u0, v1], color, halo_color, params });
1509 vertices.push(SymbolVertex { position: [ax, ay, az], glyph_offset: [x1, y1], tex_coord: [u1, v1], color, halo_color, params });
1510 indices.extend_from_slice(&[base, base + 2, base + 1, base + 1, base + 2, base + 3]);
1511
1512 cursor_x += entry.advance_x * scale;
1513 }
1514 }
1515 }
1516
1517 if vertices.is_empty() {
1518 return None;
1519 }
1520
1521 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1522 label: Some("symbol_batch_vb"),
1523 contents: bytemuck::cast_slice(&vertices),
1524 usage: wgpu::BufferUsages::VERTEX,
1525 });
1526 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1527 label: Some("symbol_batch_ib"),
1528 contents: bytemuck::cast_slice(&indices),
1529 usage: wgpu::BufferUsages::INDEX,
1530 });
1531
1532 Some(SymbolBatchEntry {
1533 vertex_buffer,
1534 index_buffer,
1535 index_count: indices.len() as u32,
1536 })
1537}
1538
1539#[cfg(test)]
1544mod tests {
1545 use super::*;
1546 use crate::gpu::tile_atlas::TileAtlas;
1547 use rustial_engine::{CameraProjection, DecodedImage};
1548 use std::sync::Arc;
1549
1550 fn create_test_device() -> Option<wgpu::Device> {
1551 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
1552 backends: wgpu::Backends::all(),
1553 ..Default::default()
1554 });
1555
1556 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1557 power_preference: wgpu::PowerPreference::LowPower,
1558 compatible_surface: None,
1559 force_fallback_adapter: false,
1560 }))
1561 .ok()?;
1562
1563 let (device, _) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
1564 label: Some("batch_test_device"),
1565 ..Default::default()
1566 }))
1567 .ok()?;
1568
1569 Some(device)
1570 }
1571
1572 fn test_image() -> DecodedImage {
1573 DecodedImage {
1574 width: 1,
1575 height: 1,
1576 data: Arc::new(vec![255, 255, 255, 255]),
1577 }
1578 }
1579
1580 #[test]
1581 fn visible_tile_texture_region_matches_expected_subrect() {
1582 let tile = VisibleTile {
1583 target: TileId::new(3, 4, 2),
1584 actual: TileId::new(1, 1, 0),
1585 data: None,
1586 fade_opacity: 1.0,
1587 };
1588
1589 let region = tile.texture_region();
1590 assert!((region.u_min - 0.0).abs() < 1e-6);
1591 assert!((region.v_min - 0.5).abs() < 1e-6);
1592 assert!((region.u_max - 0.25).abs() < 1e-6);
1593 assert!((region.v_max - 0.75).abs() < 1e-6);
1594 }
1595
1596 #[test]
1597 fn terrain_texture_lookup_falls_back_to_visible_ancestor() {
1598 let target = TileId::new(4, 8, 4);
1599 let parent = TileId::new(3, 4, 2);
1600 let visible = vec![VisibleTile {
1601 target: parent,
1602 actual: parent,
1603 data: Some(rustial_engine::TileData::Raster(rustial_engine::DecodedImage {
1604 width: 1,
1605 height: 1,
1606 data: vec![255, 255, 255, 255].into(),
1607 })),
1608 fade_opacity: 1.0,
1609 }];
1610
1611 assert_eq!(find_terrain_texture_actual(target, &visible), Some(parent));
1612 }
1613
1614 #[test]
1615 fn projected_tile_corners_differ_between_planar_projections() {
1616 let tile = TileId::new(3, 4, 2);
1617 let merc = projected_tile_corners(tile, CameraProjection::WebMercator, DVec3::ZERO);
1618 let eq = projected_tile_corners(tile, CameraProjection::Equirectangular, DVec3::ZERO);
1619
1620 assert!((merc[0].y - eq[0].y).abs() > 1.0);
1621 }
1622
1623 #[test]
1624 fn exact_full_opacity_tile_uses_opaque_pass() {
1625 let tile = VisibleTile {
1626 target: TileId::new(3, 4, 2),
1627 actual: TileId::new(3, 4, 2),
1628 data: None,
1629 fade_opacity: 1.0,
1630 };
1631
1632 assert!(!tile_requires_blended_pass(&tile));
1633 }
1634
1635 #[test]
1636 fn fading_tile_uses_translucent_pass() {
1637 let tile = VisibleTile {
1638 target: TileId::new(3, 4, 2),
1639 actual: TileId::new(3, 4, 2),
1640 data: None,
1641 fade_opacity: 0.5,
1642 };
1643
1644 assert!(tile_requires_blended_pass(&tile));
1645 }
1646
1647 #[test]
1648 fn fallback_tile_uses_translucent_pass() {
1649 let tile = VisibleTile {
1650 target: TileId::new(4, 8, 4),
1651 actual: TileId::new(3, 4, 2),
1652 data: None,
1653 fade_opacity: 1.0,
1654 };
1655
1656 assert!(tile_requires_blended_pass(&tile));
1657 }
1658
1659 #[test]
1660 fn build_tile_batches_routes_exact_tile_to_opaque_batch() {
1661 let Some(device) = create_test_device() else {
1662 eprintln!("Skipping batch routing test: no suitable adapter/device available");
1663 return;
1664 };
1665
1666 let mut atlas = TileAtlas::new();
1667 let tile_id = TileId::new(0, 0, 0);
1668 atlas.insert(&device, tile_id, &test_image());
1669
1670 let visible = vec![VisibleTile {
1671 target: tile_id,
1672 actual: tile_id,
1673 data: None,
1674 fade_opacity: 1.0,
1675 }];
1676
1677 let batches = build_tile_batches(
1678 &device,
1679 &visible,
1680 &atlas,
1681 DVec3::ZERO,
1682 CameraProjection::WebMercator,
1683 );
1684
1685 assert_eq!(batches.len(), 1);
1686 assert!(batches[0].opaque.is_some());
1687 assert!(batches[0].translucent.is_none());
1688 }
1689
1690 #[test]
1691 fn build_tile_batches_routes_fading_crossfade_tiles_to_translucent_batch() {
1692 let Some(device) = create_test_device() else {
1693 eprintln!("Skipping translucent batch routing test: no suitable adapter/device available");
1694 return;
1695 };
1696
1697 let mut atlas = TileAtlas::new();
1698 let parent = TileId::new(0, 0, 0);
1699 let child = TileId::new(1, 0, 0);
1700 atlas.insert(&device, parent, &test_image());
1701 atlas.insert(&device, child, &test_image());
1702
1703 let visible = vec![
1704 VisibleTile {
1705 target: child,
1706 actual: child,
1707 data: None,
1708 fade_opacity: 0.5,
1709 },
1710 VisibleTile {
1711 target: child,
1712 actual: parent,
1713 data: None,
1714 fade_opacity: 0.5,
1715 },
1716 ];
1717
1718 let batches = build_tile_batches(
1719 &device,
1720 &visible,
1721 &atlas,
1722 DVec3::ZERO,
1723 CameraProjection::WebMercator,
1724 );
1725
1726 assert_eq!(batches.len(), 1);
1727 assert!(batches[0].opaque.is_none());
1728 assert!(batches[0].translucent.is_some());
1729 }
1730}