1use crate::vec::{Point3, Vec3};
8
9#[derive(Debug, Clone, Copy)]
13pub struct Obb3 {
14 pub center: Point3,
16 pub axes: [Vec3; 3],
18 pub half_extents: [f64; 3],
20}
21
22impl Obb3 {
23 #[must_use]
34 #[allow(clippy::missing_panics_doc)]
35 pub fn from_points(points: impl IntoIterator<Item = Point3>) -> Self {
36 let pts: Vec<Point3> = points.into_iter().collect();
37 Self::from_points_slice(&pts)
38 }
39
40 #[must_use]
49 #[allow(clippy::missing_panics_doc)]
50 pub fn from_points_slice(pts: &[Point3]) -> Self {
51 assert!(!pts.is_empty(), "OBB requires at least one point");
52
53 let n = pts.len() as f64;
54
55 let mut cx = 0.0_f64;
56 let mut cy = 0.0_f64;
57 let mut cz = 0.0_f64;
58 for p in pts {
59 cx += p.x();
60 cy += p.y();
61 cz += p.z();
62 }
63 cx /= n;
64 cy /= n;
65 cz /= n;
66
67 let mut cov = [0.0_f64; 6]; for p in pts {
70 let dx = p.x() - cx;
71 let dy = p.y() - cy;
72 let dz = p.z() - cz;
73 cov[0] += dx * dx;
74 cov[1] += dx * dy;
75 cov[2] += dx * dz;
76 cov[3] += dy * dy;
77 cov[4] += dy * dz;
78 cov[5] += dz * dz;
79 }
80
81 let axes = eigen_axes_3x3(cov);
83
84 Self::from_axes_and_points(Point3::new(cx, cy, cz), axes, pts)
85 }
86
87 #[must_use]
96 #[allow(clippy::missing_panics_doc)]
97 pub fn from_points_with_normal(points: impl IntoIterator<Item = Point3>, normal: Vec3) -> Self {
98 let pts: Vec<Point3> = points.into_iter().collect();
99 Self::from_slice_with_normal(&pts, normal)
100 }
101
102 #[must_use]
111 #[allow(clippy::missing_panics_doc)]
112 pub fn from_slice_with_normal(pts: &[Point3], normal: Vec3) -> Self {
113 assert!(!pts.is_empty(), "OBB requires at least one point");
114
115 let n = pts.len() as f64;
116
117 let mut cx = 0.0_f64;
118 let mut cy = 0.0_f64;
119 let mut cz = 0.0_f64;
120 for p in pts {
121 cx += p.x();
122 cy += p.y();
123 cz += p.z();
124 }
125 cx /= n;
126 cy /= n;
127 cz /= n;
128
129 let len =
131 (normal.x() * normal.x() + normal.y() * normal.y() + normal.z() * normal.z()).sqrt();
132 let axis2 = if len > 1e-15 {
133 Vec3::new(normal.x() / len, normal.y() / len, normal.z() / len)
134 } else {
135 return Self::from_points_slice(pts);
137 };
138
139 let abs_x = axis2.x().abs();
142 let abs_y = axis2.y().abs();
143 let abs_z = axis2.z().abs();
144 let seed = if abs_x <= abs_y && abs_x <= abs_z {
145 Vec3::new(1.0, 0.0, 0.0)
146 } else if abs_y <= abs_z {
147 Vec3::new(0.0, 1.0, 0.0)
148 } else {
149 Vec3::new(0.0, 0.0, 1.0)
150 };
151
152 let u = {
154 let v = Vec3::new(
155 seed.x() - axis2.x() * seed.dot(axis2),
156 seed.y() - axis2.y() * seed.dot(axis2),
157 seed.z() - axis2.z() * seed.dot(axis2),
158 );
159 let l = (v.x() * v.x() + v.y() * v.y() + v.z() * v.z()).sqrt();
160 Vec3::new(v.x() / l, v.y() / l, v.z() / l)
161 };
162
163 let v = axis2.cross(u);
165
166 let mut cov_uu = 0.0_f64;
168 let mut cov_uv = 0.0_f64;
169 let mut cov_vv = 0.0_f64;
170 for p in pts {
171 let d = Vec3::new(p.x() - cx, p.y() - cy, p.z() - cz);
172 let du = d.dot(u);
173 let dv = d.dot(v);
174 cov_uu += du * du;
175 cov_uv += du * dv;
176 cov_vv += dv * dv;
177 }
178
179 let (angle, _e1, _e2) = eigen_2x2(cov_uu, cov_uv, cov_vv);
181 let (sin_a, cos_a) = angle.sin_cos();
182
183 let axis0 = Vec3::new(
185 cos_a * u.x() + sin_a * v.x(),
186 cos_a * u.y() + sin_a * v.y(),
187 cos_a * u.z() + sin_a * v.z(),
188 );
189 let axis1 = Vec3::new(
190 -sin_a * u.x() + cos_a * v.x(),
191 -sin_a * u.y() + cos_a * v.y(),
192 -sin_a * u.z() + cos_a * v.z(),
193 );
194
195 Self::from_axes_and_points(Point3::new(cx, cy, cz), [axis0, axis1, axis2], pts)
196 }
197
198 fn from_axes_and_points(centroid: Point3, axes: [Vec3; 3], pts: &[Point3]) -> Self {
200 let mut min_ext = [f64::INFINITY; 3];
201 let mut max_ext = [f64::NEG_INFINITY; 3];
202
203 for p in pts {
204 let d = Vec3::new(
205 p.x() - centroid.x(),
206 p.y() - centroid.y(),
207 p.z() - centroid.z(),
208 );
209 for (i, ax) in axes.iter().enumerate() {
210 let proj = d.dot(*ax);
211 if proj < min_ext[i] {
212 min_ext[i] = proj;
213 }
214 if proj > max_ext[i] {
215 max_ext[i] = proj;
216 }
217 }
218 }
219
220 let mut center = centroid;
222 let mut half_extents = [0.0_f64; 3];
223 for i in 0..3 {
224 let mid = (min_ext[i] + max_ext[i]) * 0.5;
225 half_extents[i] = (max_ext[i] - min_ext[i]) * 0.5;
226 center = Point3::new(
227 center.x() + axes[i].x() * mid,
228 center.y() + axes[i].y() * mid,
229 center.z() + axes[i].z() * mid,
230 );
231 }
232
233 Self {
234 center,
235 axes,
236 half_extents,
237 }
238 }
239
240 #[inline]
245 #[must_use]
246 #[allow(clippy::many_single_char_names)]
247 pub fn intersects(&self, other: &Self) -> bool {
248 let t = Vec3::new(
250 other.center.x() - self.center.x(),
251 other.center.y() - self.center.y(),
252 other.center.z() - self.center.z(),
253 );
254
255 let a = &self.axes;
256 let b = &other.axes;
257 let ea = &self.half_extents;
258 let eb = &other.half_extents;
259
260 #[allow(clippy::items_after_statements)]
263 const EPS: f64 = 1e-12;
264 let mut r = [[0.0_f64; 3]; 3];
265 let mut abs_r = [[0.0_f64; 3]; 3];
266 for i in 0..3 {
267 for j in 0..3 {
268 r[i][j] = a[i].dot(b[j]);
269 abs_r[i][j] = r[i][j].abs() + EPS;
270 }
271 }
272
273 let t_a = [t.dot(a[0]), t.dot(a[1]), t.dot(a[2])];
275 let t_b = [t.dot(b[0]), t.dot(b[1]), t.dot(b[2])];
276
277 for i in 0..3 {
279 let ra = ea[i];
280 let rb = eb[0] * abs_r[i][0] + eb[1] * abs_r[i][1] + eb[2] * abs_r[i][2];
281 if t_a[i].abs() > ra + rb {
282 return false;
283 }
284 }
285
286 for j in 0..3 {
288 let ra = ea[0] * abs_r[0][j] + ea[1] * abs_r[1][j] + ea[2] * abs_r[2][j];
289 let rb = eb[j];
290 if t_b[j].abs() > ra + rb {
291 return false;
292 }
293 }
294
295 {
298 let ra = ea[1] * abs_r[2][0] + ea[2] * abs_r[1][0];
299 let rb = eb[1] * abs_r[0][2] + eb[2] * abs_r[0][1];
300 let d = (t_a[2] * r[1][0] - t_a[1] * r[2][0]).abs();
301 if d > ra + rb {
302 return false;
303 }
304 }
305 {
307 let ra = ea[1] * abs_r[2][1] + ea[2] * abs_r[1][1];
308 let rb = eb[0] * abs_r[0][2] + eb[2] * abs_r[0][0];
309 let d = (t_a[2] * r[1][1] - t_a[1] * r[2][1]).abs();
310 if d > ra + rb {
311 return false;
312 }
313 }
314 {
316 let ra = ea[1] * abs_r[2][2] + ea[2] * abs_r[1][2];
317 let rb = eb[0] * abs_r[0][1] + eb[1] * abs_r[0][0];
318 let d = (t_a[2] * r[1][2] - t_a[1] * r[2][2]).abs();
319 if d > ra + rb {
320 return false;
321 }
322 }
323 {
325 let ra = ea[0] * abs_r[2][0] + ea[2] * abs_r[0][0];
326 let rb = eb[1] * abs_r[1][2] + eb[2] * abs_r[1][1];
327 let d = (t_a[0] * r[2][0] - t_a[2] * r[0][0]).abs();
328 if d > ra + rb {
329 return false;
330 }
331 }
332 {
334 let ra = ea[0] * abs_r[2][1] + ea[2] * abs_r[0][1];
335 let rb = eb[0] * abs_r[1][2] + eb[2] * abs_r[1][0];
336 let d = (t_a[0] * r[2][1] - t_a[2] * r[0][1]).abs();
337 if d > ra + rb {
338 return false;
339 }
340 }
341 {
343 let ra = ea[0] * abs_r[2][2] + ea[2] * abs_r[0][2];
344 let rb = eb[0] * abs_r[1][1] + eb[1] * abs_r[1][0];
345 let d = (t_a[0] * r[2][2] - t_a[2] * r[0][2]).abs();
346 if d > ra + rb {
347 return false;
348 }
349 }
350 {
352 let ra = ea[0] * abs_r[1][0] + ea[1] * abs_r[0][0];
353 let rb = eb[1] * abs_r[2][2] + eb[2] * abs_r[2][1];
354 let d = (t_a[1] * r[0][0] - t_a[0] * r[1][0]).abs();
355 if d > ra + rb {
356 return false;
357 }
358 }
359 {
361 let ra = ea[0] * abs_r[1][1] + ea[1] * abs_r[0][1];
362 let rb = eb[0] * abs_r[2][2] + eb[2] * abs_r[2][0];
363 let d = (t_a[1] * r[0][1] - t_a[0] * r[1][1]).abs();
364 if d > ra + rb {
365 return false;
366 }
367 }
368 {
370 let ra = ea[0] * abs_r[1][2] + ea[1] * abs_r[0][2];
371 let rb = eb[0] * abs_r[2][1] + eb[1] * abs_r[2][0];
372 let d = (t_a[1] * r[0][2] - t_a[0] * r[1][2]).abs();
373 if d > ra + rb {
374 return false;
375 }
376 }
377
378 true
380 }
381}
382
383fn eigen_2x2(a: f64, b: f64, c: f64) -> (f64, f64, f64) {
392 if b.abs() < 1e-30 {
393 return (0.0, a, c);
394 }
395 let theta = 0.5 * (2.0 * b).atan2(a - c);
396 let trace = a + c;
397 let det = a * c - b * b;
398 let disc = (trace * trace - 4.0 * det).max(0.0).sqrt();
399 let e1 = (trace + disc) * 0.5;
400 let e2 = (trace - disc) * 0.5;
401 (theta, e1, e2)
402}
403
404#[allow(clippy::similar_names)]
410fn eigen_axes_3x3(cov: [f64; 6]) -> [Vec3; 3] {
411 let mut m = [
413 [cov[0], cov[1], cov[2]],
414 [cov[1], cov[3], cov[4]],
415 [cov[2], cov[4], cov[5]],
416 ];
417 let mut v = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
419
420 for _ in 0..50 {
422 let mut max_val = 0.0_f64;
424 let mut p = 0;
425 let mut q = 1;
426 for i in 0..3 {
427 for j in (i + 1)..3 {
428 if m[i][j].abs() > max_val {
429 max_val = m[i][j].abs();
430 p = i;
431 q = j;
432 }
433 }
434 }
435 if max_val < 1e-30 {
436 break; }
438
439 let theta = if (m[p][p] - m[q][q]).abs() < 1e-30 {
441 std::f64::consts::FRAC_PI_4
442 } else {
443 0.5 * (2.0 * m[p][q]).atan2(m[p][p] - m[q][q])
444 };
445 let (sin_t, cos_t) = theta.sin_cos();
446
447 let mut m2 = m;
449 m2[p][p] =
450 cos_t * cos_t * m[p][p] + 2.0 * sin_t * cos_t * m[p][q] + sin_t * sin_t * m[q][q];
451 m2[q][q] =
452 sin_t * sin_t * m[p][p] - 2.0 * sin_t * cos_t * m[p][q] + cos_t * cos_t * m[q][q];
453 m2[p][q] = 0.0;
454 m2[q][p] = 0.0;
455 for r in 0..3 {
456 if r != p && r != q {
457 let mp = cos_t * m[r][p] + sin_t * m[r][q];
458 let mq = -sin_t * m[r][p] + cos_t * m[r][q];
459 m2[r][p] = mp;
460 m2[p][r] = mp;
461 m2[r][q] = mq;
462 m2[q][r] = mq;
463 }
464 }
465 m = m2;
466
467 for r in 0..3 {
469 let vp = cos_t * v[r][p] + sin_t * v[r][q];
470 let vq = -sin_t * v[r][p] + cos_t * v[r][q];
471 v[r][p] = vp;
472 v[r][q] = vq;
473 }
474 }
475
476 let mut order = [0, 1, 2];
478 let eigenvalues = [m[0][0], m[1][1], m[2][2]];
479 order.sort_by(|&a, &b| {
480 eigenvalues[b]
481 .partial_cmp(&eigenvalues[a])
482 .unwrap_or(std::cmp::Ordering::Equal)
483 });
484
485 let make_axis = |col: usize| {
486 let len = (v[0][col] * v[0][col] + v[1][col] * v[1][col] + v[2][col] * v[2][col]).sqrt();
487 if len > 1e-15 {
488 Vec3::new(v[0][col] / len, v[1][col] / len, v[2][col] / len)
489 } else {
490 match col {
492 0 => Vec3::new(1.0, 0.0, 0.0),
493 1 => Vec3::new(0.0, 1.0, 0.0),
494 _ => Vec3::new(0.0, 0.0, 1.0),
495 }
496 }
497 };
498
499 [
500 make_axis(order[0]),
501 make_axis(order[1]),
502 make_axis(order[2]),
503 ]
504}
505
506#[cfg(test)]
507mod tests {
508 #![allow(clippy::unwrap_used, clippy::expect_used)]
509
510 use super::*;
511
512 #[test]
513 fn obb_from_axis_aligned_points() {
514 let pts = [
515 Point3::new(0.0, 0.0, 0.0),
516 Point3::new(2.0, 0.0, 0.0),
517 Point3::new(2.0, 1.0, 0.0),
518 Point3::new(0.0, 1.0, 0.0),
519 ];
520 let obb = Obb3::from_points(pts);
521 assert!((obb.center.x() - 1.0).abs() < 1e-10);
523 assert!((obb.center.y() - 0.5).abs() < 1e-10);
524 assert!((obb.center.z()).abs() < 1e-10);
525 }
526
527 #[test]
528 fn obb_identical_boxes_intersect() {
529 let pts = [
530 Point3::new(0.0, 0.0, 0.0),
531 Point3::new(1.0, 0.0, 0.0),
532 Point3::new(1.0, 1.0, 0.0),
533 Point3::new(0.0, 1.0, 0.0),
534 Point3::new(0.0, 0.0, 1.0),
535 Point3::new(1.0, 0.0, 1.0),
536 Point3::new(1.0, 1.0, 1.0),
537 Point3::new(0.0, 1.0, 1.0),
538 ];
539 let obb = Obb3::from_points(pts);
540 assert!(obb.intersects(&obb));
541 }
542
543 #[test]
544 fn obb_separated_boxes_dont_intersect() {
545 let a = Obb3::from_points([
546 Point3::new(0.0, 0.0, 0.0),
547 Point3::new(1.0, 0.0, 0.0),
548 Point3::new(1.0, 1.0, 0.0),
549 Point3::new(0.0, 1.0, 0.0),
550 ]);
551 let b = Obb3::from_points([
552 Point3::new(5.0, 0.0, 0.0),
553 Point3::new(6.0, 0.0, 0.0),
554 Point3::new(6.0, 1.0, 0.0),
555 Point3::new(5.0, 1.0, 0.0),
556 ]);
557 assert!(!a.intersects(&b));
558 }
559
560 #[test]
561 fn obb_overlapping_rotated_boxes_intersect() {
562 let a = Obb3::from_points([
564 Point3::new(-1.0, -1.0, 0.0),
565 Point3::new(1.0, -1.0, 0.0),
566 Point3::new(1.0, 1.0, 0.0),
567 Point3::new(-1.0, 1.0, 0.0),
568 ]);
569 let s = std::f64::consts::FRAC_1_SQRT_2;
571 let b = Obb3::from_points([
572 Point3::new(0.0, -s, 0.0),
573 Point3::new(s, 0.0, 0.0),
574 Point3::new(0.0, s, 0.0),
575 Point3::new(-s, 0.0, 0.0),
576 ]);
577 assert!(a.intersects(&b));
578 }
579
580 #[test]
581 fn obb_with_normal_planar_face() {
582 let pts = [
583 Point3::new(0.0, 0.0, 5.0),
584 Point3::new(2.0, 0.0, 5.0),
585 Point3::new(2.0, 3.0, 5.0),
586 Point3::new(0.0, 3.0, 5.0),
587 ];
588 let normal = Vec3::new(0.0, 0.0, 1.0);
589 let obb = Obb3::from_points_with_normal(pts, normal);
590
591 assert!(obb.half_extents[2] < 1e-10);
593 }
594
595 #[test]
596 fn obb_edge_touching() {
597 let a = Obb3::from_points([
599 Point3::new(0.0, 0.0, 0.0),
600 Point3::new(1.0, 0.0, 0.0),
601 Point3::new(1.0, 1.0, 0.0),
602 Point3::new(0.0, 1.0, 0.0),
603 ]);
604 let b = Obb3::from_points([
605 Point3::new(1.0, 0.0, 0.0),
606 Point3::new(2.0, 0.0, 0.0),
607 Point3::new(2.0, 1.0, 0.0),
608 Point3::new(1.0, 1.0, 0.0),
609 ]);
610 assert!(a.intersects(&b));
612 }
613
614 #[test]
615 fn obb_from_points_slice_matches_from_points() {
616 let pts = vec![
617 Point3::new(0.0, 0.0, 0.0),
618 Point3::new(2.0, 0.0, 0.0),
619 Point3::new(2.0, 1.0, 0.0),
620 Point3::new(0.0, 1.0, 0.0),
621 ];
622 let obb_iter = Obb3::from_points(pts.iter().copied());
623 let obb_slice = Obb3::from_points_slice(&pts);
624 assert!((obb_iter.center.x() - obb_slice.center.x()).abs() < 1e-15);
625 assert!((obb_iter.center.y() - obb_slice.center.y()).abs() < 1e-15);
626 assert!((obb_iter.center.z() - obb_slice.center.z()).abs() < 1e-15);
627 for i in 0..3 {
628 assert!((obb_iter.half_extents[i] - obb_slice.half_extents[i]).abs() < 1e-15);
629 }
630 }
631
632 #[test]
633 fn obb_from_slice_with_normal_matches_iterator() {
634 let pts = vec![
635 Point3::new(0.0, 0.0, 5.0),
636 Point3::new(2.0, 0.0, 5.0),
637 Point3::new(2.0, 3.0, 5.0),
638 Point3::new(0.0, 3.0, 5.0),
639 ];
640 let normal = Vec3::new(0.0, 0.0, 1.0);
641 let obb_iter = Obb3::from_points_with_normal(pts.iter().copied(), normal);
642 let obb_slice = Obb3::from_slice_with_normal(&pts, normal);
643 assert!((obb_iter.center.x() - obb_slice.center.x()).abs() < 1e-15);
644 assert!((obb_iter.center.y() - obb_slice.center.y()).abs() < 1e-15);
645 assert!((obb_iter.center.z() - obb_slice.center.z()).abs() < 1e-15);
646 for i in 0..3 {
647 assert!((obb_iter.half_extents[i] - obb_slice.half_extents[i]).abs() < 1e-15);
648 }
649 }
650
651 #[test]
652 fn eigen_2x2_correct_angle() {
653 let (theta, _e1, _e2) = super::eigen_2x2(3.0, 1.0, 1.0);
655 let expected = 0.5 * (2.0_f64).atan2(2.0);
656 assert!(
657 (theta - expected).abs() < 1e-15,
658 "theta={theta}, expected={expected}"
659 );
660 }
661}