1use crate::render_types::DrawObject;
18use alloc::vec::Vec;
19
20use crate::frustum::{Frustum, aabb_distance_sq};
21
22#[derive(Copy, Clone, Debug)]
23struct Aabb {
24 min: [f32; 3],
25 max: [f32; 3],
26}
27
28impl Aabb {
29 fn empty() -> Self {
30 Self {
31 min: [f32::INFINITY; 3],
32 max: [f32::NEG_INFINITY; 3],
33 }
34 }
35
36 fn union(self, other: Aabb) -> Aabb {
37 Aabb {
38 min: [
39 self.min[0].min(other.min[0]),
40 self.min[1].min(other.min[1]),
41 self.min[2].min(other.min[2]),
42 ],
43 max: [
44 self.max[0].max(other.max[0]),
45 self.max[1].max(other.max[1]),
46 self.max[2].max(other.max[2]),
47 ],
48 }
49 }
50
51 fn centroid(self) -> [f32; 3] {
52 [
53 0.5 * (self.min[0] + self.max[0]),
54 0.5 * (self.min[1] + self.max[1]),
55 0.5 * (self.min[2] + self.max[2]),
56 ]
57 }
58}
59
60#[derive(Debug)]
61enum Node {
62 Internal {
63 bb: Aabb,
64 left: u32,
65 right: u32,
66 },
67 Leaf {
68 bb: Aabb,
69 cull_distance: f32,
72 index: u32,
73 },
74}
75
76#[derive(Debug, Default)]
77pub struct Bvh {
79 nodes: Vec<Node>,
80 root: Option<u32>,
81}
82
83#[derive(Copy, Clone, Debug)]
85pub(crate) struct BvhItem {
86 pub bb_min: [f32; 3],
88 pub bb_max: [f32; 3],
90 pub cull_distance: f32,
92 pub index: u32,
94}
95
96impl Bvh {
97 pub(crate) fn build(items: &[BvhItem]) -> Self {
99 let mut bvh = Bvh::default();
100 if items.is_empty() {
101 return bvh;
102 }
103 let mut working: Vec<BvhItem> = items.to_vec();
104 let root = bvh.build_recursive(&mut working);
105 bvh.root = Some(root);
106 bvh
107 }
108
109 pub fn query<F: FnMut(u32)>(&self, frustum: &Frustum, cam_pos: [f32; 3], mut visit: F) {
113 let root = match self.root {
114 Some(r) => r,
115 None => return,
116 };
117 let mut stack: [u32; 64] = [0; 64];
121 let mut sp: usize = 0;
122 stack[sp] = root;
123 sp += 1;
124
125 while sp > 0 {
126 sp -= 1;
127 let idx = stack[sp];
128 match &self.nodes[idx as usize] {
129 Node::Internal { bb, left, right } => {
130 if !frustum.intersects_aabb(bb.min, bb.max) {
131 continue;
132 }
133 if sp + 2 > stack.len() {
137 self.query_heap(idx, frustum, cam_pos, &mut visit);
141 continue;
142 }
143 stack[sp] = *right;
144 sp += 1;
145 stack[sp] = *left;
146 sp += 1;
147 }
148 Node::Leaf {
149 bb,
150 cull_distance,
151 index,
152 } => {
153 if !frustum.intersects_aabb(bb.min, bb.max) {
154 continue;
155 }
156 if *cull_distance > 0.0 {
157 let dsq = aabb_distance_sq(cam_pos, bb.min, bb.max);
158 if dsq > (*cull_distance) * (*cull_distance) {
159 continue;
160 }
161 }
162 visit(*index);
163 }
164 }
165 }
166 }
167
168 fn query_heap<F: FnMut(u32)>(
169 &self,
170 start: u32,
171 frustum: &Frustum,
172 cam_pos: [f32; 3],
173 visit: &mut F,
174 ) {
175 let mut stack: Vec<u32> = Vec::with_capacity(32);
176 stack.push(start);
177 while let Some(idx) = stack.pop() {
178 match &self.nodes[idx as usize] {
179 Node::Internal { bb, left, right } => {
180 if !frustum.intersects_aabb(bb.min, bb.max) {
181 continue;
182 }
183 stack.push(*right);
184 stack.push(*left);
185 }
186 Node::Leaf {
187 bb,
188 cull_distance,
189 index,
190 } => {
191 if !frustum.intersects_aabb(bb.min, bb.max) {
192 continue;
193 }
194 if *cull_distance > 0.0 {
195 let dsq = aabb_distance_sq(cam_pos, bb.min, bb.max);
196 if dsq > (*cull_distance) * (*cull_distance) {
197 continue;
198 }
199 }
200 visit(*index);
201 }
202 }
203 }
204 }
205
206 fn build_recursive(&mut self, items: &mut [BvhItem]) -> u32 {
207 let bb = items
208 .iter()
209 .fold(Aabb::empty(), |acc, it| acc.union(item_aabb(it)));
210
211 if items.len() == 1 {
212 let it = items[0];
213 let node_idx = self.nodes.len() as u32;
214 self.nodes.push(Node::Leaf {
215 bb,
216 cull_distance: it.cull_distance,
217 index: it.index,
218 });
219 return node_idx;
220 }
221
222 let extent = [
223 bb.max[0] - bb.min[0],
224 bb.max[1] - bb.min[1],
225 bb.max[2] - bb.min[2],
226 ];
227 let axis = if extent[0] >= extent[1] && extent[0] >= extent[2] {
228 0
229 } else if extent[1] >= extent[2] {
230 1
231 } else {
232 2
233 };
234
235 items.sort_by(|a, b| {
236 let ca = item_aabb(a).centroid()[axis];
237 let cb = item_aabb(b).centroid()[axis];
238 ca.partial_cmp(&cb).unwrap_or(core::cmp::Ordering::Equal)
239 });
240
241 let mid = items.len() / 2;
242 let self_idx = self.nodes.len() as u32;
245 self.nodes.push(Node::Internal {
246 bb,
247 left: u32::MAX,
248 right: u32::MAX,
249 });
250
251 let (left_items, right_items) = items.split_at_mut(mid);
252 let left = self.build_recursive(left_items);
253 let right = self.build_recursive(right_items);
254
255 match &mut self.nodes[self_idx as usize] {
256 Node::Internal {
257 left: l, right: r, ..
258 } => {
259 *l = left;
260 *r = right;
261 }
262 Node::Leaf { .. } => unreachable!("self_idx must point at an Internal node"),
263 }
264 self_idx
265 }
266}
267
268fn item_aabb(it: &BvhItem) -> Aabb {
269 Aabb {
270 min: it.bb_min,
271 max: it.bb_max,
272 }
273}
274
275pub fn partition_draw_objects(draw_objects: &[DrawObject]) -> (Bvh, Vec<u32>) {
280 let mut items: Vec<BvhItem> = Vec::new();
281 let mut always_draw: Vec<u32> = Vec::new();
282 for (i, obj) in draw_objects.iter().enumerate() {
283 if obj.cullable() {
284 items.push(BvhItem {
285 bb_min: obj.bb_min,
286 bb_max: obj.bb_max,
287 cull_distance: obj.cull_distance,
288 index: i as u32,
289 });
290 } else {
291 always_draw.push(i as u32);
292 }
293 }
294 (Bvh::build(&items), always_draw)
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 use alloc::vec;
302 fn ident_vp() -> [[f32; 4]; 4] {
303 [
304 [1.0, 0.0, 0.0, 0.0],
305 [0.0, 1.0, 0.0, 0.0],
306 [0.0, 0.0, 1.0, 0.0],
307 [0.0, 0.0, 0.0, 1.0],
308 ]
309 }
310
311 fn item(idx: u32, min: [f32; 3], max: [f32; 3]) -> BvhItem {
312 BvhItem {
313 bb_min: min,
314 bb_max: max,
315 cull_distance: 0.0,
316 index: idx,
317 }
318 }
319
320 fn collect(bvh: &Bvh, frustum: &Frustum) -> Vec<u32> {
321 let mut out = Vec::new();
322 bvh.query(frustum, [0.0, 0.0, 0.0], |i| out.push(i));
323 out.sort();
324 out
325 }
326
327 #[test]
328 fn empty_input_yields_empty_bvh() {
329 let bvh = Bvh::build(&[]);
330 assert!(bvh.root.is_none());
331 assert_eq!(bvh.nodes.len(), 0);
332 let f = Frustum::from_view_projection(ident_vp());
333 let mut hits = 0;
334 bvh.query(&f, [0.0, 0.0, 0.0], |_| hits += 1);
335 assert_eq!(hits, 0);
336 }
337
338 #[test]
339 fn single_item_creates_one_leaf() {
340 let bvh = Bvh::build(&[item(7, [-0.1, -0.1, -0.1], [0.1, 0.1, 0.1])]);
341 assert!(bvh.root.is_some());
342 assert_eq!(bvh.nodes.len(), 1);
343 let f = Frustum::from_view_projection(ident_vp());
344 assert_eq!(collect(&bvh, &f), vec![7]);
345 }
346
347 #[test]
348 fn visible_and_invisible_items_separate() {
349 let bvh = Bvh::build(&[
350 item(0, [-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]),
351 item(1, [5.0, -0.5, -0.5], [6.0, 0.5, 0.5]),
352 item(2, [-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]),
353 ]);
354 let f = Frustum::from_view_projection(ident_vp());
355 assert_eq!(collect(&bvh, &f), vec![0, 2]);
356 }
357
358 #[test]
359 fn distance_cutoff_prunes_far_leaf() {
360 let mut a = item(0, [-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]);
361 a.cull_distance = 1.0;
362 let mut b = item(1, [-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]);
363 b.cull_distance = 0.0;
364 let bvh = Bvh::build(&[a, b]);
365 let f = Frustum::from_view_projection(ident_vp());
366 let mut out = Vec::new();
367 bvh.query(&f, [10.0, 0.0, 0.0], |i| out.push(i));
369 out.sort();
370 assert_eq!(out, vec![1]);
371 }
372
373 #[test]
374 fn many_items_all_visible_inside_frustum() {
375 let mut items = Vec::new();
376 for i in 0..64 {
377 let x = (i as f32) * 0.001 - 0.5;
378 items.push(item(i, [x, -0.5, -0.5], [x + 0.05, 0.5, 0.5]));
379 }
380 let bvh = Bvh::build(&items);
381 let f = Frustum::from_view_projection(ident_vp());
382 let got = collect(&bvh, &f);
383 assert_eq!(got.len(), 64);
384 }
385
386 #[test]
387 fn frustum_rejecting_root_emits_nothing() {
388 let bvh = Bvh::build(&[
389 item(0, [10.0, -0.5, -0.5], [11.0, 0.5, 0.5]),
390 item(1, [10.0, -0.5, -0.5], [11.0, 0.5, 0.5]),
391 ]);
392 let f = Frustum::from_view_projection(ident_vp());
393 assert!(collect(&bvh, &f).is_empty());
394 }
395
396 fn draw_object(bb_min: [f32; 3], bb_max: [f32; 3]) -> DrawObject {
399 DrawObject {
400 vertex_offset: 0,
401 vertex_count: 0,
402 index_offset: 0,
403 index_count: 0,
404 base_vertex: 0,
405 geometry_generation: 0,
406 shader_bucket: 0,
407 model: ident_vp(),
408 texture_slot: 0,
409 normal_map_slot: 0,
410 material: crate::render_types::MaterialUniforms::DEFAULT,
411 visible: true,
412 resident: true,
413 bb_min,
414 bb_max,
415 cull_distance: 0.0,
416 lod_alternates: Vec::new(),
417 }
418 }
419
420 #[test]
421 fn partition_splits_cullable_from_always_draw() {
422 let objs = vec![
425 draw_object([-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]), draw_object([f32::NAN, 0.0, 0.0], [1.0, 1.0, 1.0]), draw_object([-0.3, -0.3, -0.3], [0.3, 0.3, 0.3]), ];
429 let (bvh, always_draw) = partition_draw_objects(&objs);
430 assert_eq!(always_draw, vec![1]);
431 assert!(bvh.root.is_some());
432 assert_eq!(bvh.nodes.len(), 3);
434 let f = Frustum::from_view_projection(ident_vp());
436 assert_eq!(collect(&bvh, &f), vec![0, 2]);
437 }
438
439 #[test]
440 fn partition_with_no_cullable_objects_yields_empty_bvh() {
441 let objs = vec![
442 draw_object([f32::NAN, 0.0, 0.0], [1.0, 1.0, 1.0]),
443 draw_object([0.0, f32::NAN, 0.0], [1.0, 1.0, 1.0]),
444 ];
445 let (bvh, always_draw) = partition_draw_objects(&objs);
446 assert_eq!(always_draw, vec![0, 1]);
447 assert!(bvh.root.is_none());
448 assert_eq!(bvh.nodes.len(), 0);
449 }
450
451 #[test]
452 fn build_splits_on_the_y_axis() {
453 let items: Vec<BvhItem> = (0..4)
456 .map(|i| {
457 let y = i as f32 * 0.2 - 0.3;
458 item(i, [-0.05, y, -0.05], [0.05, y + 0.05, 0.05])
459 })
460 .collect();
461 let bvh = Bvh::build(&items);
462 assert_eq!(bvh.nodes.len(), 7);
464 let f = Frustum::from_view_projection(ident_vp());
465 assert_eq!(collect(&bvh, &f), vec![0, 1, 2, 3]);
466 }
467
468 #[test]
469 fn build_splits_on_the_z_axis() {
470 let items: Vec<BvhItem> = (0..4)
473 .map(|i| {
474 let z = i as f32 * 0.2 - 0.3;
475 item(i, [-0.05, -0.05, z], [0.05, 0.05, z + 0.05])
476 })
477 .collect();
478 let bvh = Bvh::build(&items);
479 assert_eq!(bvh.nodes.len(), 7);
480 let f = Frustum::from_view_projection(ident_vp());
481 assert_eq!(collect(&bvh, &f), vec![0, 1, 2, 3]);
482 }
483}
484
485#[cfg(test)]
493mod bench {
494 use super::{Bvh, BvhItem};
495 use crate::frustum::Frustum;
496 use alloc::vec::Vec;
497 use std::println;
498 use std::time::Instant;
499
500 const OBJECTS: usize = 10_000;
501 const TARGET_NS: u128 = 200_000_000;
502 const MAX_ITERS: u64 = 1 << 20;
503
504 fn bench<R>(name: &str, items: u64, mut body: impl FnMut() -> R) {
505 let mut iters: u64 = 1;
506 loop {
507 let start = Instant::now();
508 for _ in 0..iters {
509 core::hint::black_box(body());
510 }
511 if start.elapsed().as_nanos() >= TARGET_NS || iters >= MAX_ITERS {
512 break;
513 }
514 iters = iters.saturating_mul(4).min(MAX_ITERS);
515 }
516
517 let start = Instant::now();
518 for _ in 0..iters {
519 core::hint::black_box(body());
520 }
521 let per_item_ns = start.elapsed().as_secs_f64() * 1e9 / (iters * items.max(1)) as f64;
522 println!(" {name:<40} {per_item_ns:>10.2} ns/item");
523 }
524
525 fn scene_items() -> Vec<BvhItem> {
529 (0..OBJECTS)
530 .map(|i| {
531 let x = (i % 100) as f32 * 3.0 - 150.0;
532 let z = (i / 100) as f32 * 6.0 - 300.0;
533 BvhItem {
534 bb_min: [x - 0.5, 0.0, z - 0.5],
535 bb_max: [x + 0.5, 1.0, z + 0.5],
536 cull_distance: 0.0,
537 index: i as u32,
538 }
539 })
540 .collect()
541 }
542
543 fn camera_frustum() -> Frustum {
546 let f = 1.0 / 35.0f32.to_radians().tan();
547 let aspect = 16.0 / 9.0;
548 let (near, far) = (0.1, 400.0);
549 let mut vp = [[0.0f32; 4]; 4];
550 vp[0][0] = f / aspect;
551 vp[1][1] = f;
552 vp[2][2] = (far + near) / (near - far);
553 vp[2][3] = -1.0;
554 vp[3][2] = 2.0 * far * near / (near - far);
555 Frustum::from_view_projection(vp)
556 }
557
558 #[test]
559 #[ignore = "benchmark; run with --ignored --test-threads=1"]
560 fn bench_bvh() {
561 let items = scene_items();
562 let frustum = camera_frustum();
563
564 bench("render/bvh_build/10k", OBJECTS as u64, || {
565 Bvh::build(&items)
566 });
567
568 let bvh = Bvh::build(&items);
569 let mut visible = 0u32;
570 bvh.query(&frustum, [0.0; 3], |_| visible += 1);
571 assert!(
572 visible > 0 && (visible as usize) < OBJECTS,
573 "the query fixture must accept some objects and reject others, saw {visible}"
574 );
575 bench("render/bvh_query/10k", OBJECTS as u64, || {
576 let mut seen = 0u32;
577 bvh.query(&frustum, [0.0; 3], |_| seen += 1);
578 seen
579 });
580 }
581}