pub mod addr;
pub mod billboard;
pub mod cavegen;
pub mod chunks;
pub mod edit;
pub mod lod;
pub mod render;
pub mod snapshot;
pub mod streaming;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use glam::{DQuat, DVec3, IVec3, UVec3};
use roxlap_formats::vxl::Vxl;
use serde::{Deserialize, Serialize};
pub use addr::{grid_local_to_world, voxel_global, voxel_split, world_to_grid_local, GridLocalPos};
pub use billboard::{canonical_viewpoints, BillboardCache, BillboardSnapshot};
pub use lod::{select_lod, Lod, LodThresholds};
pub use streaming::{ChunkGenerator, StreamRadius};
pub const CHUNK_SIZE_XY: u32 = 128;
pub const CHUNK_SIZE_Z: u32 = 256;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct GridId(u32);
impl GridId {
#[must_use]
pub const fn raw(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct GridTransform {
pub origin: DVec3,
pub rotation: DQuat,
}
impl GridTransform {
#[must_use]
pub fn identity() -> Self {
Self {
origin: DVec3::ZERO,
rotation: DQuat::IDENTITY,
}
}
#[must_use]
pub fn at(origin: DVec3) -> Self {
Self {
origin,
rotation: DQuat::IDENTITY,
}
}
}
impl Default for GridTransform {
fn default() -> Self {
Self::identity()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GridAddr {
pub grid: GridId,
pub chunk: IVec3,
pub voxel: UVec3,
}
#[derive(Debug)]
pub struct Grid {
pub transform: GridTransform,
pub chunks: HashMap<IVec3, Vxl>,
pub render_sky: bool,
pub mip_levels_override: Option<u32>,
pub lod_thresholds: LodThresholds,
pub billboards: Option<BillboardCache>,
pub generator: Option<Arc<dyn ChunkGenerator>>,
pub stream_radius: StreamRadius,
pub chunk_versions: HashMap<IVec3, u64>,
pub pending_gen: HashSet<IVec3>,
}
impl Grid {
#[must_use]
pub fn new(transform: GridTransform) -> Self {
Self {
transform,
chunks: HashMap::new(),
render_sky: true,
mip_levels_override: None,
lod_thresholds: LodThresholds::always_near(),
billboards: None,
generator: None,
stream_radius: StreamRadius::DISABLED,
chunk_versions: HashMap::new(),
pending_gen: HashSet::new(),
}
}
#[must_use]
pub fn chunk_version(&self, chunk_idx: IVec3) -> u64 {
self.chunk_versions.get(&chunk_idx).copied().unwrap_or(0)
}
pub(crate) fn bump_chunk_version(&mut self, chunk_idx: IVec3) {
let entry = self.chunk_versions.entry(chunk_idx).or_insert(0);
*entry = entry.saturating_add(1);
}
pub fn set_generator(&mut self, generator: Option<Arc<dyn ChunkGenerator>>) {
self.generator = generator;
}
pub fn ensure_chunk_generated(&mut self, chunk_idx: IVec3) -> bool {
if self.chunks.contains_key(&chunk_idx) {
return false;
}
let Some(generator) = self.generator.as_ref() else {
return false;
};
if !generator.should_generate(chunk_idx) {
return false;
}
let chunk = generator.generate(chunk_idx);
self.chunks.insert(chunk_idx, chunk);
self.billboards = None;
true
}
#[must_use]
pub fn bounding_radius(&self) -> f64 {
if self.chunks.is_empty() {
return 0.0;
}
let mut min = IVec3::splat(i32::MAX);
let mut max = IVec3::splat(i32::MIN);
for &idx in self.chunks.keys() {
min = min.min(idx);
max = max.max(idx);
}
let sx = f64::from(CHUNK_SIZE_XY);
let sz = f64::from(CHUNK_SIZE_Z);
let lo = DVec3::new(
f64::from(min.x) * sx,
f64::from(min.y) * sx,
f64::from(min.z) * sz,
);
let hi = DVec3::new(
f64::from(max.x + 1) * sx,
f64::from(max.y + 1) * sx,
f64::from(max.z + 1) * sz,
);
let half_extent = (hi - lo) * 0.5;
half_extent.length()
}
#[must_use]
pub fn select_lod(&self, camera_world_pos: DVec3) -> Lod {
select_lod(camera_world_pos, &self.transform, self.lod_thresholds)
}
}
#[derive(Debug, Default)]
pub struct Scene {
grids: HashMap<GridId, Grid>,
next_grid_id: u32,
#[cfg(not(target_arch = "wasm32"))]
streaming: streaming::StreamingState,
}
impl Scene {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn grid_count(&self) -> usize {
self.grids.len()
}
pub fn add_grid(&mut self, transform: GridTransform) -> GridId {
let id = GridId(self.next_grid_id);
self.next_grid_id += 1;
self.grids.insert(id, Grid::new(transform));
id
}
pub fn remove_grid(&mut self, id: GridId) -> Option<Grid> {
self.grids.remove(&id)
}
#[must_use]
pub fn grid(&self, id: GridId) -> Option<&Grid> {
self.grids.get(&id)
}
pub fn grid_mut(&mut self, id: GridId) -> Option<&mut Grid> {
self.grids.get_mut(&id)
}
pub fn grids(&self) -> impl Iterator<Item = (GridId, &Grid)> {
self.grids.iter().map(|(id, g)| (*id, g))
}
pub fn grids_mut(&mut self) -> impl Iterator<Item = (GridId, &mut Grid)> {
self.grids.iter_mut().map(|(id, g)| (*id, g))
}
#[cfg(not(target_arch = "wasm32"))]
pub fn set_streaming_threads(&mut self, n: usize) {
self.streaming.set_thread_count(n);
}
#[cfg(target_arch = "wasm32")]
pub fn set_streaming_threads(&mut self, _n: usize) {
}
pub fn pump_streaming(&mut self, camera_world_pos: DVec3) {
#[cfg(target_arch = "wasm32")]
{
self.pump_streaming_sync(camera_world_pos);
}
#[cfg(not(target_arch = "wasm32"))]
{
self.pump_streaming_native(camera_world_pos);
}
}
#[cfg(not(target_arch = "wasm32"))]
fn pump_streaming_native(&mut self, camera_world_pos: DVec3) {
while let Ok(result) = self.streaming.rx.try_recv() {
let Some(grid) = self.grids.get_mut(&result.grid_id) else {
continue;
};
let was_pending = grid.pending_gen.remove(&result.chunk_idx);
if !was_pending {
continue;
}
if grid.chunks.contains_key(&result.chunk_idx) {
continue;
}
if grid.chunk_version(result.chunk_idx) != result.version_at_dispatch {
continue;
}
grid.chunks.insert(result.chunk_idx, result.vxl);
grid.billboards = None;
}
self.streaming.ensure_pool();
let pool: &rayon::ThreadPool = self.streaming.pool.as_ref().expect("ensure_pool just ran");
let tx_template = &self.streaming.tx;
for (grid_id, grid) in &mut self.grids {
evict_grid_chunks(grid, camera_world_pos);
dispatch_grid_async(*grid_id, grid, camera_world_pos, pool, tx_template);
}
}
pub fn pump_streaming_sync(&mut self, camera_world_pos: DVec3) {
for grid in self.grids.values_mut() {
pump_grid_streaming_sync(grid, camera_world_pos);
}
}
}
fn pump_grid_streaming_sync(grid: &mut Grid, camera_world_pos: DVec3) {
let radius = grid.stream_radius;
if radius.is_disabled() {
return;
}
let cam_local = streaming::world_to_grid_local_pos(camera_world_pos, &grid.transform);
if radius.r_active > 0.0 && grid.generator.is_some() {
for_each_chunk_in_radius(cam_local, radius.r_active, |idx| {
grid.ensure_chunk_generated(idx);
});
}
evict_grid_chunks_with_cam(grid, cam_local);
}
#[cfg(not(target_arch = "wasm32"))]
fn evict_grid_chunks(grid: &mut Grid, camera_world_pos: DVec3) {
let radius = grid.stream_radius;
if radius.is_disabled() {
return;
}
let cam_local = streaming::world_to_grid_local_pos(camera_world_pos, &grid.transform);
evict_grid_chunks_with_cam(grid, cam_local);
}
fn evict_grid_chunks_with_cam(grid: &mut Grid, cam_local: DVec3) {
let radius = grid.stream_radius;
if !radius.r_evict.is_finite() {
return;
}
let r_sq = radius.r_evict * radius.r_evict;
let to_evict: Vec<IVec3> = grid
.chunks
.keys()
.filter(|&&idx| streaming::chunk_aabb_dist_sq(cam_local, idx) > r_sq)
.copied()
.collect();
let to_evict_pending: Vec<IVec3> = grid
.pending_gen
.iter()
.filter(|&&idx| streaming::chunk_aabb_dist_sq(cam_local, idx) > r_sq)
.copied()
.collect();
if to_evict.is_empty() && to_evict_pending.is_empty() {
return;
}
for idx in &to_evict {
grid.chunks.remove(idx);
grid.chunk_versions.remove(idx);
grid.pending_gen.remove(idx);
}
for idx in &to_evict_pending {
grid.pending_gen.remove(idx);
}
if !to_evict.is_empty() {
grid.billboards = None;
}
}
fn for_each_chunk_in_radius<F>(cam_local: DVec3, r_active: f64, mut f: F)
where
F: FnMut(IVec3),
{
let r_sq = r_active * r_active;
let sxy = f64::from(CHUNK_SIZE_XY);
let sz = f64::from(CHUNK_SIZE_Z);
#[allow(clippy::cast_possible_truncation)]
let r_chunks_xy = (r_active / sxy).ceil() as i32 + 1;
#[allow(clippy::cast_possible_truncation)]
let r_chunks_z = (r_active / sz).ceil() as i32 + 1;
#[allow(clippy::cast_possible_truncation)]
let cx_chunk = (cam_local.x / sxy).floor() as i32;
#[allow(clippy::cast_possible_truncation)]
let cy_chunk = (cam_local.y / sxy).floor() as i32;
#[allow(clippy::cast_possible_truncation)]
let cz_chunk = (cam_local.z / sz).floor() as i32;
for chz in (cz_chunk - r_chunks_z)..=(cz_chunk + r_chunks_z) {
for chy in (cy_chunk - r_chunks_xy)..=(cy_chunk + r_chunks_xy) {
for chx in (cx_chunk - r_chunks_xy)..=(cx_chunk + r_chunks_xy) {
let idx = IVec3::new(chx, chy, chz);
if streaming::chunk_aabb_dist_sq(cam_local, idx) <= r_sq {
f(idx);
}
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn dispatch_grid_async(
grid_id: GridId,
grid: &mut Grid,
camera_world_pos: DVec3,
pool: &rayon::ThreadPool,
tx: &crossbeam_channel::Sender<streaming::ChunkResult>,
) {
let radius = grid.stream_radius;
if radius.is_disabled() || radius.r_active <= 0.0 {
return;
}
let Some(generator) = grid.generator.as_ref().map(Arc::clone) else {
return;
};
let cam_local = streaming::world_to_grid_local_pos(camera_world_pos, &grid.transform);
for_each_chunk_in_radius(cam_local, radius.r_active, |idx| {
if grid.chunks.contains_key(&idx) {
return; }
if grid.pending_gen.contains(&idx) {
return; }
if !generator.should_generate(idx) {
return;
}
grid.pending_gen.insert(idx);
let version_at_dispatch = grid.chunk_version(idx);
let tx_clone = tx.clone();
let gen_clone = Arc::clone(&generator);
pool.spawn(move || {
let vxl = gen_clone.generate(idx);
let _ = tx_clone.send(streaming::ChunkResult {
grid_id,
chunk_idx: idx,
version_at_dispatch,
vxl,
});
});
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_scene_has_no_grids() {
let scene = Scene::new();
assert_eq!(scene.grid_count(), 0);
assert!(scene.grids().next().is_none());
}
#[test]
fn add_grid_returns_fresh_ids() {
let mut scene = Scene::new();
let a = scene.add_grid(GridTransform::identity());
let b = scene.add_grid(GridTransform::at(DVec3::new(100.0, 0.0, 0.0)));
assert_ne!(a, b);
assert_eq!(a.raw(), 0);
assert_eq!(b.raw(), 1);
assert_eq!(scene.grid_count(), 2);
}
#[test]
fn grid_lookup_round_trips() {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::at(DVec3::new(10.0, 20.0, 30.0)));
let g = scene.grid(id).expect("grid registered");
assert_eq!(g.transform.origin, DVec3::new(10.0, 20.0, 30.0));
assert_eq!(g.transform.rotation, DQuat::IDENTITY);
assert!(g.chunks.is_empty());
}
#[test]
fn remove_grid_drops_it_from_scene() {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::identity());
let removed = scene.remove_grid(id);
assert!(removed.is_some());
assert_eq!(scene.grid_count(), 0);
assert!(scene.grid(id).is_none());
let id2 = scene.add_grid(GridTransform::identity());
assert_ne!(id, id2);
assert_eq!(id2.raw(), 1);
}
#[test]
fn remove_unknown_grid_is_none() {
let mut scene = Scene::new();
let bogus = GridId(999);
assert!(scene.remove_grid(bogus).is_none());
}
#[test]
fn grid_mut_can_modify_transform() {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::identity());
scene.grid_mut(id).unwrap().transform.origin = DVec3::new(1.0, 2.0, 3.0);
assert_eq!(
scene.grid(id).unwrap().transform.origin,
DVec3::new(1.0, 2.0, 3.0)
);
}
#[test]
fn chunk_size_constants_match_plan() {
assert_eq!(CHUNK_SIZE_XY, 128);
assert_eq!(CHUNK_SIZE_Z, 256);
}
#[test]
fn new_grid_defaults_to_always_near_lod() {
let g = Grid::new(GridTransform::identity());
assert_eq!(g.lod_thresholds.r_near, f64::INFINITY);
assert_eq!(g.lod_thresholds.r_mid, f64::INFINITY);
assert_eq!(g.select_lod(DVec3::new(1e9, 0.0, 0.0)), Lod::Near);
}
#[test]
fn bounding_radius_empty_grid_is_zero() {
let g = Grid::new(GridTransform::identity());
assert_eq!(g.bounding_radius(), 0.0);
}
#[test]
fn bounding_radius_single_chunk_at_origin() {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::identity());
let g = scene.grid_mut(id).unwrap();
g.set_voxel(IVec3::new(0, 0, 0), Some(0x80_88_88_88));
let r = g.bounding_radius();
let expected = ((64.0_f64).powi(2) * 2.0 + (128.0_f64).powi(2)).sqrt();
assert!(
(r - expected).abs() < 1e-9,
"bounding_radius={r} expected={expected}"
);
}
#[test]
fn bounding_radius_grows_with_chunk_extent() {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::identity());
let g = scene.grid_mut(id).unwrap();
g.set_voxel(IVec3::new(0, 0, 0), Some(0x80_88_88_88));
g.set_voxel(IVec3::new(384, 0, 0), Some(0x80_88_88_88));
assert_eq!(g.chunks.len(), 2);
let r = g.bounding_radius();
let expected = (256.0_f64.powi(2) + 64.0_f64.powi(2) + 128.0_f64.powi(2)).sqrt();
assert!(
(r - expected).abs() < 1e-9,
"bounding_radius={r} expected={expected}"
);
}
#[test]
fn grid_select_lod_respects_lod_thresholds_field() {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::at(DVec3::new(100.0, 0.0, 0.0)));
let g = scene.grid_mut(id).unwrap();
g.lod_thresholds = LodThresholds {
r_near: 50.0,
r_mid: 200.0,
..LodThresholds::always_near()
};
assert_eq!(g.select_lod(DVec3::new(125.0, 0.0, 0.0)), Lod::Near);
assert_eq!(g.select_lod(DVec3::new(200.0, 0.0, 0.0)), Lod::Mid);
assert_eq!(g.select_lod(DVec3::new(600.0, 0.0, 0.0)), Lod::Far);
}
}