use crate::components::ModelEntity;
use crate::plugin::MapStateResource;
use crate::systems::frame_change_detection::{frame_unchanged, FrameChangeDetection};
use bevy::mesh::{Indices, PrimitiveTopology};
use bevy::prelude::*;
use rustial_engine::{ModelInstance, ModelMesh};
use std::collections::HashMap;
#[derive(Resource, Default)]
pub struct ModelSyncState {
fingerprint: u64,
instance_count: usize,
origin_key: [i64; 3],
}
impl ModelSyncState {
fn quantise_origin(origin: glam::DVec3) -> [i64; 3] {
[
(origin.x * 100.0) as i64,
(origin.y * 100.0) as i64,
(origin.z * 100.0) as i64,
]
}
fn compute_fingerprint(instances: &[ModelInstance], origin: glam::DVec3) -> u64 {
let mut fp: u64 = instances.len() as u64;
let ok = Self::quantise_origin(origin);
fp = fp
.wrapping_mul(31)
.wrapping_add(ok[0] as u64)
.wrapping_mul(31)
.wrapping_add(ok[1] as u64)
.wrapping_mul(31)
.wrapping_add(ok[2] as u64);
for instance in instances {
fp = fp
.wrapping_mul(31)
.wrapping_add(instance.position.lat.to_bits())
.wrapping_mul(31)
.wrapping_add(instance.position.lon.to_bits())
.wrapping_mul(31)
.wrapping_add(instance.scale.to_bits())
.wrapping_mul(31)
.wrapping_add(instance.heading.to_bits());
}
fp
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct ModelMeshKey {
pos_len: usize,
idx_len: usize,
fingerprint: u64,
}
impl ModelMeshKey {
fn from_mesh(mesh: &ModelMesh) -> Self {
let mut fingerprint: u64 = mesh.positions.len() as u64;
if let Some(first) = mesh.positions.first() {
fingerprint = fingerprint
.wrapping_mul(31)
.wrapping_add(first[0].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(first[1].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(first[2].to_bits() as u64);
}
if let Some(first) = mesh.normals.first() {
fingerprint = fingerprint
.wrapping_mul(31)
.wrapping_add(first[0].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(first[1].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(first[2].to_bits() as u64);
}
if let Some(first) = mesh.uvs.first() {
fingerprint = fingerprint
.wrapping_mul(31)
.wrapping_add(first[0].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(first[1].to_bits() as u64);
}
if let Some(&first_idx) = mesh.indices.first() {
fingerprint = fingerprint.wrapping_mul(31).wrapping_add(first_idx as u64);
}
Self {
pos_len: mesh.positions.len(),
idx_len: mesh.indices.len(),
fingerprint,
}
}
}
#[derive(Resource, Default)]
pub struct CachedModelAssets {
meshes: HashMap<ModelMeshKey, Handle<Mesh>>,
material: Option<Handle<StandardMaterial>>,
}
pub fn sync_models(
mut commands: Commands,
state: Res<MapStateResource>,
existing: Query<(Entity, &ModelEntity)>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut cache: ResMut<CachedModelAssets>,
mut sync_state: ResMut<ModelSyncState>,
detection: Res<FrameChangeDetection>,
) {
if frame_unchanged(&detection, &state.0) {
return;
}
let camera_origin = state.0.scene_world_origin();
let projection = state.0.camera().projection();
let model_data = &state.0.model_instances();
if model_data.is_empty() {
for (entity, _) in existing.iter() {
commands.entity(entity).despawn();
}
sync_state.fingerprint = 0;
sync_state.instance_count = 0;
return;
}
let existing_count = existing.iter().count();
let new_fp = ModelSyncState::compute_fingerprint(model_data, camera_origin);
if existing_count == model_data.len() && existing_count == sync_state.instance_count {
if new_fp == sync_state.fingerprint {
return;
}
for (entity, marker) in existing.iter() {
let Some(instance) = model_data.get(marker.instance_index) else {
continue;
};
let terrain_elev = state.0.elevation_at(&instance.position);
let altitude = instance.resolve_altitude(terrain_elev);
let world_pos = projection.project(&instance.position);
let rel_x = (world_pos.position.x - camera_origin.x) as f32;
let rel_y = (world_pos.position.y - camera_origin.y) as f32;
let rel_z = (altitude - camera_origin.z) as f32;
let transform = build_model_transform(instance, rel_x, rel_y, rel_z);
commands.entity(entity).insert(transform);
}
sync_state.fingerprint = new_fp;
sync_state.origin_key = ModelSyncState::quantise_origin(camera_origin);
return;
}
for (entity, _) in existing.iter() {
commands.entity(entity).despawn();
}
let shared_material = cache
.material
.get_or_insert_with(|| {
materials.add(StandardMaterial {
base_color: Color::linear_rgb(0.7, 0.7, 0.7),
..Default::default()
})
})
.clone();
for (idx, instance) in model_data.iter().enumerate() {
if instance.mesh.positions.is_empty() || instance.mesh.indices.is_empty() {
continue;
}
let terrain_elev = state.0.elevation_at(&instance.position);
let altitude = instance.resolve_altitude(terrain_elev);
let world_pos = projection.project(&instance.position);
let rel_x = (world_pos.position.x - camera_origin.x) as f32;
let rel_y = (world_pos.position.y - camera_origin.y) as f32;
let rel_z = (altitude - camera_origin.z) as f32;
let transform = build_model_transform(instance, rel_x, rel_y, rel_z);
let mesh_handle = cached_model_mesh_handle(&mut meshes, &mut cache, &instance.mesh);
commands.spawn((
Mesh3d(mesh_handle),
MeshMaterial3d(shared_material.clone()),
transform,
Visibility::default(),
ModelEntity {
instance_index: idx,
layer_name: format!("model_{idx}"),
},
));
}
sync_state.fingerprint = new_fp;
sync_state.instance_count = model_data.len();
sync_state.origin_key = ModelSyncState::quantise_origin(camera_origin);
}
fn cached_model_mesh_handle(
meshes: &mut Assets<Mesh>,
cache: &mut CachedModelAssets,
mesh: &ModelMesh,
) -> Handle<Mesh> {
let key = ModelMeshKey::from_mesh(mesh);
if let Some(handle) = cache.meshes.get(&key) {
return handle.clone();
}
let mut bevy_mesh = Mesh::new(PrimitiveTopology::TriangleList, Default::default());
bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, mesh.positions.clone());
bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, mesh.normals.clone());
bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, mesh.uvs.clone());
bevy_mesh.insert_indices(Indices::U32(mesh.indices.clone()));
let handle = meshes.add(bevy_mesh);
cache.meshes.insert(key, handle.clone());
handle
}
fn build_model_transform(
instance: &ModelInstance,
rel_x: f32,
rel_y: f32,
rel_z: f32,
) -> Transform {
let scale = instance.scale as f32;
let heading = instance.heading as f32;
let pitch = instance.pitch as f32;
let roll = instance.roll as f32;
let rotation = Quat::from_rotation_z(heading)
* Quat::from_rotation_x(pitch)
* Quat::from_rotation_y(roll);
Transform {
translation: Vec3::new(rel_x, rel_y, rel_z),
rotation,
scale: Vec3::splat(scale),
}
}