use glam::{Mat4, Vec3};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ClusterPage {
pub center: [f32; 3],
pub radius: f32,
pub lod_error: f32,
pub parent_error: f32,
pub lod_bounds_center: [f32; 3],
pub lod_bounds_radius: f32,
pub parent_bounds_center: [f32; 3],
pub parent_bounds_radius: f32,
pub first_index: u32,
pub index_count: u32,
}
pub fn select_cut(pages: &[ClusterPage], threshold: f32, out: &mut Vec<u32>) {
out.clear();
for (i, p) in pages.iter().enumerate() {
if p.lod_error <= threshold && threshold < p.parent_error {
out.push(i as u32);
}
}
}
pub fn select_cut_per_cluster(
pages: &[ClusterPage],
instance_world: &Mat4,
camera_pos: Vec3,
tan_half_fov_y: f32,
viewport_h: f32,
pixel_budget: f32,
out: &mut Vec<u32>,
) {
out.clear();
let scale = max_axis_scale(instance_world);
for (i, p) in pages.iter().enumerate() {
let lod_world = instance_world.transform_point3(Vec3::from(p.lod_bounds_center));
let parent_world = instance_world.transform_point3(Vec3::from(p.parent_bounds_center));
let proj_lod = cluster_projected_error(
p.lod_error,
lod_world,
camera_pos,
tan_half_fov_y,
viewport_h,
scale,
);
let proj_parent = cluster_projected_error(
p.parent_error,
parent_world,
camera_pos,
tan_half_fov_y,
viewport_h,
scale,
);
if proj_lod <= pixel_budget && pixel_budget < proj_parent {
out.push(i as u32);
}
}
}
pub fn instance_error_threshold(
instance_world: &Mat4,
camera_pos: Vec3,
tan_half_fov_y: f32,
viewport_h: f32,
pixel_budget: f32,
) -> f32 {
let center = instance_world.transform_point3(Vec3::ZERO);
let dist = (center - camera_pos).length();
let scale = max_axis_scale(instance_world);
let denom = scale * (viewport_h * 0.5);
if denom <= 1e-9 || tan_half_fov_y <= 1e-9 {
return 0.0; }
pixel_budget * dist * tan_half_fov_y / denom
}
pub fn cluster_projected_error(
error: f32,
world_center: Vec3,
camera_pos: Vec3,
tan_half_fov_y: f32,
viewport_h: f32,
world_scale: f32,
) -> f32 {
let dist = (world_center - camera_pos).length();
if dist <= 1e-6 || tan_half_fov_y <= 1e-6 {
return f32::INFINITY;
}
error * world_scale * (viewport_h * 0.5) / (dist * tan_half_fov_y)
}
pub fn max_axis_scale(m: &Mat4) -> f32 {
m.x_axis
.truncate()
.length()
.max(m.y_axis.truncate().length())
.max(m.z_axis.truncate().length())
}
pub const CLUSTER_PAGE_GPU_STRIDE: usize = 64;
pub const CLUSTER_CUT_WGSL: &str =
include_str!("render_passes/cluster_lod/shader/cluster_lod_wgsl/cluster_cut.wgsl");
pub const CLUSTER_CUT_PARAMS_SIZE: usize = 96;
#[allow(clippy::too_many_arguments)]
pub fn write_cluster_cut_params(
instance_world: &Mat4,
camera_pos: Vec3,
tan_half_fov_y: f32,
viewport_h: f32,
pixel_budget: f32,
world_scale: f32,
cluster_count: u32,
out: &mut Vec<u8>,
) {
for c in instance_world.to_cols_array() {
out.extend_from_slice(&c.to_le_bytes());
}
out.extend_from_slice(&camera_pos.x.to_le_bytes());
out.extend_from_slice(&camera_pos.y.to_le_bytes());
out.extend_from_slice(&camera_pos.z.to_le_bytes());
out.extend_from_slice(&tan_half_fov_y.to_le_bytes());
out.extend_from_slice(&viewport_h.to_le_bytes());
out.extend_from_slice(&pixel_budget.to_le_bytes());
out.extend_from_slice(&world_scale.to_le_bytes());
out.extend_from_slice(&cluster_count.to_le_bytes());
}
pub fn write_cluster_page_gpu(p: &ClusterPage, out: &mut Vec<u8>) {
let f = |out: &mut Vec<u8>, v: f32| out.extend_from_slice(&v.to_le_bytes());
let u = |out: &mut Vec<u8>, v: u32| out.extend_from_slice(&v.to_le_bytes());
f(out, p.center[0]);
f(out, p.center[1]);
f(out, p.center[2]);
f(out, p.radius);
f(out, p.lod_bounds_center[0]);
f(out, p.lod_bounds_center[1]);
f(out, p.lod_bounds_center[2]);
f(out, p.lod_bounds_radius);
f(out, p.parent_bounds_center[0]);
f(out, p.parent_bounds_center[1]);
f(out, p.parent_bounds_center[2]);
f(out, p.parent_bounds_radius);
f(out, p.lod_error);
f(out, p.parent_error);
u(out, p.first_index);
u(out, p.index_count);
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic() -> Vec<ClusterPage> {
let mk = |lod, parent, tris: u32| ClusterPage {
center: [0.0, 0.0, 0.0],
radius: 1.0,
lod_error: lod,
parent_error: parent,
lod_bounds_center: [0.0, 0.0, 0.0],
lod_bounds_radius: 1.0,
parent_bounds_center: [0.0, 0.0, 0.0],
parent_bounds_radius: 1.0,
first_index: 0,
index_count: tris * 3,
};
vec![
mk(0.0, 1.0, 10), mk(0.0, 1.0, 10),
mk(0.0, 1.0, 10),
mk(0.0, 1.0, 10),
mk(1.0, 2.0, 12), mk(1.0, 2.0, 12),
mk(2.0, f32::INFINITY, 8), ]
}
fn cut_tris(pages: &[ClusterPage], t: f32) -> u32 {
let mut out = Vec::new();
select_cut(pages, t, &mut out);
out.iter().map(|&i| pages[i as usize].index_count / 3).sum()
}
#[test]
fn finest_cut_at_zero() {
let p = synthetic();
let mut out = Vec::new();
select_cut(&p, 0.0, &mut out);
assert_eq!(out, vec![0, 1, 2, 3], "threshold 0 picks the finest level");
assert_eq!(cut_tris(&p, 0.0), 40);
}
#[test]
fn mid_and_root_cuts() {
let p = synthetic();
let mut out = Vec::new();
select_cut(&p, 1.5, &mut out);
assert_eq!(out, vec![4, 5], "1<=1.5<2 picks the mid level");
select_cut(&p, 5.0, &mut out);
assert_eq!(out, vec![6], "above all finite errors picks the root");
}
#[test]
fn triangle_count_is_monotone_non_increasing() {
let p = synthetic();
let mut prev = u32::MAX;
for t in [0.0f32, 0.5, 1.0, 1.5, 2.0, 3.0, 100.0] {
let n = cut_tris(&p, t);
assert!(n > 0, "the cut always covers the surface");
assert!(n <= prev, "coarser threshold must not increase triangles");
prev = n;
}
}
#[test]
fn every_cluster_is_selected_at_its_lower_bound() {
let p = synthetic();
for (i, page) in p.iter().enumerate() {
let mut out = Vec::new();
select_cut(&p, page.lod_error, &mut out);
assert!(
out.contains(&(i as u32)),
"cluster {i} missing at its lod_error"
);
}
}
#[test]
fn instance_threshold_coarsens_with_distance() {
let world = Mat4::IDENTITY;
let near = instance_error_threshold(&world, Vec3::new(0.0, 0.0, 2.0), 0.5, 1080.0, 1.0);
let far = instance_error_threshold(&world, Vec3::new(0.0, 0.0, 50.0), 0.5, 1080.0, 1.0);
assert!(
far > near,
"a farther instance tolerates a larger object error"
);
let p = synthetic();
let mut out = Vec::new();
select_cut(&p, near, &mut out);
let near_tris: u32 = out.iter().map(|&i| p[i as usize].index_count / 3).sum();
select_cut(&p, far, &mut out);
let far_tris: u32 = out.iter().map(|&i| p[i as usize].index_count / 3).sum();
assert!(far_tris <= near_tris);
}
#[test]
fn cut_shader_embeds_and_has_entry_point() {
assert!(CLUSTER_CUT_WGSL.contains("@compute"));
assert!(CLUSTER_CUT_WGSL.contains("fn cs_main"));
for field in [
"lod_bounds_center",
"parent_bounds_center",
"lod_error",
"parent_error",
"first_index",
"index_count",
] {
assert!(CLUSTER_CUT_WGSL.contains(field), "shader missing `{field}`");
}
}
#[test]
fn cut_params_layout() {
let mut out = Vec::new();
let world = Mat4::from_scale(Vec3::splat(2.0));
write_cluster_cut_params(
&world,
Vec3::new(1.0, 2.0, 3.0),
0.5,
1080.0,
1.5,
2.0,
7,
&mut out,
);
assert_eq!(out.len(), CLUSTER_CUT_PARAMS_SIZE, "params are 96 B");
let f = |off: usize| f32::from_le_bytes(out[off..off + 4].try_into().unwrap());
let u = |off: usize| u32::from_le_bytes(out[off..off + 4].try_into().unwrap());
assert_eq!(f(0), 2.0);
assert_eq!(f(20), 2.0);
assert_eq!(f(40), 2.0);
assert_eq!([f(64), f(68), f(72)], [1.0, 2.0, 3.0]);
assert_eq!(f(76), 0.5); assert_eq!(f(80), 1080.0); assert_eq!(f(84), 1.5); assert_eq!(f(88), 2.0); assert_eq!(u(92), 7); }
#[test]
fn gpu_page_layout_offsets_match_std430() {
let p = ClusterPage {
center: [1.0, 2.0, 3.0],
radius: 4.0,
lod_error: 13.0,
parent_error: 14.0,
lod_bounds_center: [5.0, 6.0, 7.0],
lod_bounds_radius: 8.0,
parent_bounds_center: [9.0, 10.0, 11.0],
parent_bounds_radius: 12.0,
first_index: 15,
index_count: 16,
};
let mut bytes = Vec::new();
write_cluster_page_gpu(&p, &mut bytes);
assert_eq!(bytes.len(), CLUSTER_PAGE_GPU_STRIDE, "page is 64 B");
let f = |off: usize| f32::from_le_bytes(bytes[off..off + 4].try_into().unwrap());
let u = |off: usize| u32::from_le_bytes(bytes[off..off + 4].try_into().unwrap());
assert_eq!([f(0), f(4), f(8)], [1.0, 2.0, 3.0]); assert_eq!(f(12), 4.0); assert_eq!([f(16), f(20), f(24)], [5.0, 6.0, 7.0]); assert_eq!(f(28), 8.0); assert_eq!([f(32), f(36), f(40)], [9.0, 10.0, 11.0]); assert_eq!(f(44), 12.0); assert_eq!(f(48), 13.0); assert_eq!(f(52), 14.0); assert_eq!(u(56), 15); assert_eq!(u(60), 16); }
#[test]
fn per_cluster_cut_varies_detail_by_distance() {
let page = |cx: f32, lod: f32, parent: f32, tris: u32| ClusterPage {
center: [cx, 0.0, 0.0],
radius: 1.0,
lod_error: lod,
parent_error: parent,
lod_bounds_center: [cx, 0.0, 0.0],
lod_bounds_radius: 1.0,
parent_bounds_center: [cx, 0.0, 0.0],
parent_bounds_radius: 1.0,
first_index: 0,
index_count: tris * 3,
};
let pages = vec![
page(0.0, 0.0, 0.1, 100), page(0.0, 0.1, f32::INFINITY, 30), page(100.0, 0.0, 0.1, 100), page(100.0, 0.1, f32::INFINITY, 30), ];
let mut out = Vec::new();
select_cut_per_cluster(
&pages,
&Mat4::IDENTITY,
Vec3::new(0.0, 0.0, 3.0), 0.5,
1080.0,
2.0, &mut out,
);
out.sort_unstable();
assert!(out.contains(&0), "near region keeps its FINE cluster");
assert!(out.contains(&3), "far region drops to its COARSE cluster");
assert!(
!out.contains(&1),
"near region must not pick its coarse cluster"
);
assert!(
!out.contains(&2),
"far region must not pick its fine cluster"
);
}
}