use crate::components::DeferredAssetDrop;
use crate::components::TerrainEntity;
use crate::plugin::MapStateResource;
use crate::systems::frame_change_detection::{frame_unchanged, FrameChangeDetection};
use crate::tile_fog_material::{TerrainUniforms, TileFogMaterial};
use bevy::asset::RenderAssetUsages;
use bevy::camera::visibility::NoFrustumCulling;
use bevy::mesh::{Indices, PrimitiveTopology};
use bevy::prelude::*;
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
use rustial_engine::{materialize_terrain_mesh, CameraProjection, TerrainMeshData, TileId};
use rustial_engine as rustial_math;
use std::collections::{HashMap, HashSet};
const PROJECTION_WEB_MERCATOR: f32 = 0.0;
const PROJECTION_EQUIRECTANGULAR: f32 = 1.0;
#[derive(Resource)]
pub struct TerrainPlaceholderTexture(pub Handle<Image>);
#[derive(Resource)]
pub struct TerrainHeightPlaceholderTexture(pub Handle<Image>);
#[derive(Resource)]
pub struct HillshadePlaceholderTexture(pub Handle<Image>);
#[derive(Resource, Default)]
pub struct SharedTerrainGridMeshes {
handles: HashMap<u16, Handle<Mesh>>,
}
#[derive(Resource, Default)]
pub struct UploadedTerrainHeightTextures {
handles: HashMap<(TileId, u64), Handle<Image>>,
}
pub fn init_placeholder_texture(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
) {
let image = Image::new(
Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
TextureDimension::D2,
vec![255, 255, 255, 255], TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::default(),
);
let handle = images.add(image);
commands.insert_resource(TerrainPlaceholderTexture(handle));
let height = Image::new(
Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
TextureDimension::D2,
0.0f32.to_le_bytes().to_vec(),
TextureFormat::R32Float,
RenderAssetUsages::default(),
);
let height_handle = images.add(height);
commands.insert_resource(TerrainHeightPlaceholderTexture(height_handle));
let hillshade = Image::new(
Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
TextureDimension::D2,
vec![128, 128, 255, 255],
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::default(),
);
let hillshade_handle = images.add(hillshade);
commands.insert_resource(HillshadePlaceholderTexture(hillshade_handle));
}
pub(crate) fn supports_gpu_terrain_path(mesh: &TerrainMeshData, projection: CameraProjection) -> bool {
mesh.elevation_texture.is_some()
&& matches!(
projection,
CameraProjection::WebMercator | CameraProjection::Equirectangular
)
}
pub(crate) fn terrain_uniforms_for_mesh(
mesh: &TerrainMeshData,
camera_origin: glam::DVec3,
projection: CameraProjection,
) -> TerrainUniforms {
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 effective_skirt = rustial_engine::skirt_height(
mesh.tile.zoom,
mesh.vertical_exaggeration as f64,
) as f32;
let (skirt_base, min_elev, max_elev) = mesh
.elevation_texture
.as_ref()
.map(|elevation| {
let raw_base =
elevation.min_elev * mesh.vertical_exaggeration - effective_skirt;
let base = raw_base.max(-effective_skirt * 3.0);
(
base,
elevation.min_elev,
elevation.max_elev,
)
})
.unwrap_or((0.0, 0.0, 0.0));
let elev_region = mesh
.elevation_texture
.as_ref()
.map(|elevation| {
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
}
})
.unwrap_or(mesh.elevation_region);
TerrainUniforms {
geo_bounds: Vec4::new(nw.lat as f32, nw.lon as f32, se.lat as f32, se.lon as f32),
scene_origin: scene_origin_uniform(camera_origin, projection),
elev_params: Vec4::new(mesh.vertical_exaggeration, skirt_base, min_elev, max_elev),
elev_region: Vec4::new(
elev_region.u_min,
elev_region.v_min,
elev_region.u_max,
elev_region.v_max,
),
options: Vec4::new(1.0, 0.0, 0.0, 0.0),
}
}
pub(crate) fn scene_origin_uniform(
camera_origin: glam::DVec3,
projection: CameraProjection,
) -> Vec4 {
let projection_kind = match projection {
CameraProjection::WebMercator => PROJECTION_WEB_MERCATOR,
CameraProjection::Equirectangular => PROJECTION_EQUIRECTANGULAR,
_ => PROJECTION_WEB_MERCATOR,
};
Vec4::new(
camera_origin.x as f32,
camera_origin.y as f32,
camera_origin.z as f32,
projection_kind,
)
}
pub(crate) fn get_or_create_shared_grid_mesh_handle(
meshes: &mut Assets<Mesh>,
shared_grids: &mut SharedTerrainGridMeshes,
resolution: u16,
) -> Handle<Mesh> {
if let Some(handle) = shared_grids.handles.get(&resolution) {
return handle.clone();
}
let res = (resolution as usize).max(2);
let (positions, uvs, indices) = build_shared_grid_mesh(res);
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, Default::default());
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; uvs.len()]);
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
mesh.insert_indices(Indices::U32(indices));
let handle = meshes.add(mesh);
shared_grids.handles.insert(resolution, handle.clone());
handle
}
pub(crate) fn get_or_create_height_texture_handle(
images: &mut Assets<Image>,
height_cache: &mut UploadedTerrainHeightTextures,
mesh: &TerrainMeshData,
fallback: &Handle<Image>,
) -> Handle<Image> {
let Some(elevation) = mesh.elevation_texture.as_ref() else {
return fallback.clone();
};
let key = (mesh.tile, mesh.generation);
if let Some(handle) = height_cache.handles.get(&key) {
return handle.clone();
}
let bytes = bytemuck::cast_slice::<f32, u8>(&elevation.data).to_vec();
let image = Image::new(
Extent3d {
width: elevation.width.max(1),
height: elevation.height.max(1),
depth_or_array_layers: 1,
},
TextureDimension::D2,
bytes,
TextureFormat::R32Float,
RenderAssetUsages::default(),
);
let handle = images.add(image);
height_cache.handles.insert(key, handle.clone());
handle
}
fn build_shared_grid_mesh(resolution: usize) -> (Vec<[f32; 3]>, Vec<[f32; 2]>, Vec<u32>) {
let mut positions = Vec::with_capacity(resolution * resolution);
let mut uvs = Vec::with_capacity(resolution * resolution);
let mut indices = Vec::with_capacity((resolution - 1) * (resolution - 1) * 6);
for row in 0..resolution {
for col in 0..resolution {
let u = col as f32 / (resolution - 1) as f32;
let v = row as f32 / (resolution - 1) as f32;
positions.push([u, v, 0.0]);
uvs.push([u, v]);
}
}
for row in 0..(resolution - 1) {
for col in 0..(resolution - 1) {
let tl = (row * resolution + col) as u32;
let tr = tl + 1;
let bl = ((row + 1) * resolution + col) as u32;
let br = bl + 1;
indices.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
}
}
let edges: [Vec<usize>; 4] = [
(0..resolution).collect(),
((resolution - 1) * resolution..resolution * resolution).collect(),
(0..resolution).map(|r| r * resolution).collect(),
(0..resolution).map(|r| r * resolution + resolution - 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 = uvs[edge[i]];
let uv_b = uvs[edge[i + 1]];
let base_a = positions.len() as u32;
let base_b = base_a + 1;
positions.push([uv_a[0], uv_a[1], 1.0]);
positions.push([uv_b[0], uv_b[1], 1.0]);
uvs.push(uv_a);
uvs.push(uv_b);
indices.extend_from_slice(&[a, base_a, b, b, base_a, base_b]);
}
}
(positions, uvs, indices)
}
#[derive(Resource, Default)]
pub struct LastSceneOrigin {
origin: Option<[i64; 3]>,
}
impl LastSceneOrigin {
fn quantise(origin: glam::DVec3) -> [i64; 3] {
[
(origin.x * 100.0) as i64,
(origin.y * 100.0) as i64,
(origin.z * 100.0) as i64,
]
}
fn changed(&self, origin: glam::DVec3) -> bool {
let q = Self::quantise(origin);
self.origin.as_ref() != Some(&q)
}
fn update(&mut self, origin: glam::DVec3) {
self.origin = Some(Self::quantise(origin));
}
}
pub fn sync_terrain(
mut commands: Commands,
state: Res<MapStateResource>,
mut existing: Query<(
Entity,
&TerrainEntity,
&mut Transform,
&Mesh3d,
&MeshMaterial3d<TileFogMaterial>,
)>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<TileFogMaterial>>,
mut images: ResMut<Assets<Image>>,
mut deferred: ResMut<DeferredAssetDrop>,
mut shared_grids: ResMut<SharedTerrainGridMeshes>,
mut height_cache: ResMut<UploadedTerrainHeightTextures>,
placeholder: Option<Res<TerrainPlaceholderTexture>>,
height_placeholder: Option<Res<TerrainHeightPlaceholderTexture>>,
mut last_origin: ResMut<LastSceneOrigin>,
detection: Res<FrameChangeDetection>,
) {
if frame_unchanged(&detection, &state.0)
&& !(state.0.terrain().enabled() && !state.0.terrain_meshes().is_empty() && existing.is_empty())
{
return;
}
let terrain_meshes = state.0.terrain_meshes();
let camera_origin = state.0.scene_world_origin();
let projection = state.0.camera().projection();
let origin_changed = last_origin.changed(camera_origin);
if !state.0.terrain().enabled() {
for (entity, _, _, mesh_handle, mat_handle) in existing.iter() {
deferred.keep_mesh(mesh_handle.0.clone());
deferred.keep_material(mat_handle.0.clone());
commands.entity(entity).despawn();
log::trace!("terrain_sync: despawned (terrain disabled)");
}
last_origin.update(camera_origin);
return;
}
if terrain_meshes.is_empty() {
if origin_changed {
for (_, terrain, mut transform, _, material_handle) in existing.iter_mut() {
if terrain.gpu_displaced {
transform.translation = Vec3::ZERO;
if let Some(material) = materials.get_mut(&material_handle.0) {
material.terrain.scene_origin = scene_origin_uniform(camera_origin, projection);
}
} else {
let offset = terrain.spawn_origin - camera_origin;
transform.translation = Vec3::new(offset.x as f32, offset.y as f32, offset.z as f32);
}
}
last_origin.update(camera_origin);
}
return;
}
let desired: HashSet<TileId> = terrain_meshes.iter().map(|m| m.tile).collect();
let engine_generations: HashMap<TileId, u64> = terrain_meshes
.iter()
.map(|m| (m.tile, m.generation))
.collect();
let engine_gpu_path: HashMap<TileId, bool> = terrain_meshes
.iter()
.map(|m| (m.tile, supports_gpu_terrain_path(m, projection)))
.collect();
for (entity, terrain, _, mesh_handle, mat_handle) in existing.iter() {
let dominated = !desired.contains(&terrain.tile_id);
let stale = engine_generations
.get(&terrain.tile_id)
.map_or(false, |&generation| generation != terrain.mesh_generation);
let gpu_path_changed = engine_gpu_path
.get(&terrain.tile_id)
.map_or(false, |&gpu| gpu != terrain.gpu_displaced);
if dominated || stale || terrain.projection != projection || gpu_path_changed {
deferred.keep_mesh(mesh_handle.0.clone());
deferred.keep_material(mat_handle.0.clone());
commands.entity(entity).despawn();
log::trace!(
"terrain_sync: despawned tile {:?} (dominated={dominated}, stale={stale}, gpu_path_changed={gpu_path_changed})",
terrain.tile_id,
);
}
}
let mut existing_ids: HashSet<TileId> = HashSet::with_capacity(desired.len());
for (_, terrain, mut transform, _, material_handle) in existing.iter_mut() {
if !desired.contains(&terrain.tile_id) {
continue;
}
if engine_generations
.get(&terrain.tile_id)
.map_or(false, |&generation| generation != terrain.mesh_generation)
{
continue;
}
if terrain.projection != projection {
continue;
}
if engine_gpu_path
.get(&terrain.tile_id)
.map_or(false, |&gpu| gpu != terrain.gpu_displaced)
{
continue;
}
existing_ids.insert(terrain.tile_id);
if origin_changed {
if terrain.gpu_displaced {
transform.translation = Vec3::ZERO;
if let Some(material) = materials.get_mut(&material_handle.0) {
material.terrain.scene_origin = scene_origin_uniform(camera_origin, projection);
}
} else {
let offset = terrain.spawn_origin - camera_origin;
transform.translation = Vec3::new(offset.x as f32, offset.y as f32, offset.z as f32);
}
}
}
let Some(placeholder) = placeholder else {
return;
};
let Some(height_placeholder) = height_placeholder else {
return;
};
let needed_height_keys: HashSet<(TileId, u64)> = terrain_meshes
.iter()
.filter(|mesh| supports_gpu_terrain_path(mesh, projection))
.map(|mesh| (mesh.tile, mesh.generation))
.collect();
for terrain_mesh in terrain_meshes {
if existing_ids.contains(&terrain_mesh.tile) {
continue;
}
let gpu_displaced = supports_gpu_terrain_path(terrain_mesh, projection);
let (mesh_handle, material, transform) = if gpu_displaced {
let mesh_handle = get_or_create_shared_grid_mesh_handle(
&mut meshes,
&mut shared_grids,
terrain_mesh.grid_resolution,
);
let height_handle = get_or_create_height_texture_handle(
&mut images,
&mut height_cache,
terrain_mesh,
&height_placeholder.0,
);
let material = TileFogMaterial {
tile_texture: Some(placeholder.0.clone()),
flags: Default::default(),
terrain: terrain_uniforms_for_mesh(
terrain_mesh,
camera_origin,
projection,
),
height_texture: height_handle,
..TileFogMaterial::default()
};
(mesh_handle, material, Transform::IDENTITY)
} else {
let materialized = materialize_terrain_mesh(
terrain_mesh,
projection,
rustial_engine::skirt_height(
terrain_mesh.tile.zoom,
terrain_mesh.vertical_exaggeration as f64,
),
);
if materialized.positions.is_empty() || materialized.indices.is_empty() {
log::trace!(
"terrain_sync: skipping degenerate mesh for tile {:?}",
terrain_mesh.tile,
);
continue;
}
let positions: Vec<[f32; 3]> = materialized
.positions
.iter()
.map(|p| {
[
(p[0] - camera_origin.x) as f32,
(p[1] - camera_origin.y) as f32,
(p[2] - camera_origin.z) as f32,
]
})
.collect();
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, Default::default());
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, materialized.normals);
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, materialized.uvs);
mesh.insert_indices(Indices::U32(materialized.indices));
let mesh_handle = meshes.add(mesh);
let material = TileFogMaterial {
tile_texture: Some(placeholder.0.clone()),
height_texture: height_placeholder.0.clone(),
..TileFogMaterial::default()
};
(mesh_handle, material, Transform::IDENTITY)
};
let material_handle = materials.add(material);
commands.spawn((
Mesh3d(mesh_handle),
MeshMaterial3d(material_handle),
transform,
Visibility::Visible,
NoFrustumCulling,
TerrainEntity {
tile_id: terrain_mesh.tile,
spawn_origin: camera_origin,
projection,
gpu_displaced,
mesh_generation: terrain_mesh.generation,
has_exact_texture: false,
},
));
log::trace!("terrain_sync: spawned tile {:?}", terrain_mesh.tile);
}
let cache_headroom = needed_height_keys.len().max(16) * 2;
if height_cache.handles.len() > cache_headroom {
height_cache
.handles
.retain(|key, _| needed_height_keys.contains(key));
}
last_origin.update(camera_origin);
}