use super::{Mesh, collect::Instance3D};
use crate::{Color, Ctx, Mat4, Vec3};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub struct InstanceBatch {
pub(crate) id: u32,
}
impl InstanceBatch {
pub fn id(self) -> u32 {
self.id
}
pub fn from_id(id: u32) -> Self {
Self { id }
}
}
#[derive(Clone, Copy, Debug)]
pub struct InstanceBatchOptions {
pub cell_size: f32,
pub max_distance: f32,
pub cast_shadows: bool,
pub tint: Color,
}
impl Default for InstanceBatchOptions {
fn default() -> Self {
Self {
cell_size: 32.0,
max_distance: f32::INFINITY,
cast_shadows: true,
tint: Color::WHITE,
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct Cell {
pub min: Vec3,
pub max: Vec3,
pub start: u32,
pub count: u32,
}
pub(crate) struct Batch {
pub mesh: Mesh,
pub options: InstanceBatchOptions,
pub instances: Vec<Instance3D>,
pub spheres: Vec<[f32; 4]>,
pub cells: Vec<Cell>,
}
impl Batch {
fn new(mesh: Mesh, transforms: &[Mat4], options: InstanceBatchOptions, bounds: (Vec3, f32)) -> Self {
assert!(
options.cell_size.is_finite() && options.cell_size > 0.0,
"invalid cell size"
);
assert!(options.max_distance >= 0.0, "invalid maximum distance");
assert!(transforms.len() <= 16_777_216, "too many instances");
let mut sorted: Vec<_> = transforms
.iter()
.map(|&m| {
assert!(
m.is_finite() && m.row(3) == crate::Vec4::W,
"instances require finite affine transforms"
);
let center = m.transform_point3(bounds.0);
let radius = bounds.1 * Vec3::new(m.x_axis.length(), m.y_axis.length(), m.z_axis.length()).length();
let key = (center / options.cell_size).floor().as_ivec3().to_array();
(key, m, center, radius)
})
.collect();
sorted.sort_by_key(|e| e.0);
let mut batch = Self {
mesh,
options,
instances: Vec::with_capacity(sorted.len()),
spheres: Vec::with_capacity(sorted.len()),
cells: Vec::new(),
};
let mut previous = None;
for (key, matrix, center, radius) in sorted {
if previous != Some(key) || batch.cells.last().is_some_and(|c| c.count == 256) {
batch.cells.push(Cell {
min: center - radius,
max: center + radius,
start: batch.instances.len() as u32,
count: 0,
});
previous = Some(key);
}
let cell = batch.cells.last_mut().unwrap();
cell.min = cell.min.min(center - radius);
cell.max = cell.max.max(center + radius);
cell.count += 1;
let mut model = matrix.to_cols_array_2d();
model[0][3] = super::projected::receiver_mask(None);
batch.instances.push(Instance3D {
model,
color: super::linear(options.tint),
});
batch.spheres.push(center.extend(radius).to_array());
}
batch
}
}
impl Ctx {
pub fn create_instance_batch(
&mut self,
mesh: Mesh,
transforms: &[Mat4],
options: InstanceBatchOptions,
) -> InstanceBatch {
let bounds = self.assets3d.live_bounds(mesh.id).expect("invalid mesh");
let batch = Batch::new(mesh, transforms, options, bounds);
let id = self.assets3d.instance_batches.len() as u32;
if let Some(r) = self.renderer.as_mut() {
r.set_instance_batch(id, Some(&batch));
}
self.assets3d.instance_batches.push(Some(batch));
InstanceBatch { id }
}
pub fn update_instance_batch(&mut self, batch: InstanceBatch, transforms: &[Mat4]) -> bool {
let Some(Some(old)) = self.assets3d.instance_batches.get(batch.id as usize) else {
return false;
};
let Some(bounds) = self.assets3d.live_bounds(old.mesh.id) else {
return false;
};
let new = Batch::new(old.mesh, transforms, old.options, bounds);
if let Some(r) = self.renderer.as_mut() {
r.set_instance_batch(batch.id, Some(&new));
}
self.assets3d.instance_batches[batch.id as usize] = Some(new);
true
}
pub fn free_instance_batch(&mut self, batch: InstanceBatch) {
if let Some(slot) = self.assets3d.instance_batches.get_mut(batch.id as usize) {
*slot = None;
if let Some(r) = self.renderer.as_mut() {
r.set_instance_batch(batch.id, None);
}
}
}
pub fn instance_batch_len(&self, batch: InstanceBatch) -> Option<usize> {
self.assets3d
.instance_batches
.get(batch.id as usize)?
.as_ref()
.map(|b| b.instances.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dense_cells_split_into_bounded_clusters() {
let b = Batch::new(
Mesh { id: 0 },
&vec![Mat4::IDENTITY; 1000],
Default::default(),
(Vec3::ZERO, 1.0),
);
assert_eq!(b.cells.len(), 4);
assert!(b.cells.iter().all(|c| c.count <= 256));
assert_eq!(b.cells.iter().map(|c| c.count).sum::<u32>(), 1000);
}
#[test]
fn cells_contain_transformed_bounds_and_partition_instances() {
let matrices = [-33.0, -1.0, 0.0, 2.0, 40.0].map(|x| Mat4::from_translation(Vec3::new(x, 0.0, 0.0)));
let b = Batch::new(Mesh { id: 0 }, &matrices, Default::default(), (Vec3::ZERO, 1.0));
assert_eq!(b.cells.len(), 4);
assert_eq!(b.cells.iter().map(|c| c.count).sum::<u32>(), 5);
for c in &b.cells {
for s in &b.spheres[c.start as usize..(c.start + c.count) as usize] {
let p = Vec3::from_slice(s);
assert!((p - s[3]).cmpge(c.min).all() && (p + s[3]).cmple(c.max).all());
}
}
}
}