retrofire_core/render/clip.rs
1//! Clipping geometric shapes against planes.
2//!
3//! Clipping means converting a shape into another, such that only the points
4//! inside a volume enclosed by one or more planes remain; "inside" is defined
5//! as the half-space that the plane's normal vector points away from.
6//! In other words, clipping computes the intersection between a shape and
7//! a (possibly unbounded) convex polyhedron defined by the planes.
8//!
9//! In particular, this module implements clipping of geometry against the six
10//! planes comprising the [*view frustum*][view_frustum], in order to avoid
11//! drawing objects that are behind the camera or outside the bounds of the
12//! viewport. Clipping geometry before the raster stage is more efficient than
13//! doing it for every scanline individually.
14//!
15
16use alloc::vec::Vec;
17use core::{iter::zip, mem::swap};
18
19use view_frustum::{outcode, status};
20
21use crate::geom::{Edge, Tri, Vertex, vertex};
22use crate::math::{Lerp, vec::ProjVec3};
23
24/// Trait for types that can be [clipped][self] against convex volumes.
25///
26/// # Note to implementors
27/// This trait is primarily meant to be implemented on slices or other
28/// composites, so that several primitives can be clipped in a single call.
29/// This allows reuse of temporary buffers, for instance.
30///
31/// Implementations should avoid creating degenerate primitives, such as
32/// triangles with only two unique vertices.
33pub trait Clip {
34 /// Type of the clipped object. For example, `Self` if implemented for
35 /// the type itself, or `T` if implemented for `[T]`.
36 type Item;
37
38 /// Clips `self` against `planes`, returning the resulting zero or more
39 /// primitives in the out parameter `out`.
40 ///
41 /// If a primitive being clipped lies entirely within the bounding volume,
42 /// it is emitted as it is. If it is entirely outside the volume, it is
43 /// skipped. If it is partially inside, it is clipped such that no points
44 /// outside the volume remain in the result.
45 ///
46 /// The result is unspecified if `out` is nonempty.
47 ///
48 /// TODO Investigate returning an iterator
49 fn clip(&self, planes: &[ClipPlane], out: &mut Vec<Self::Item>);
50}
51
52/// A vector in clip space.
53pub type ClipVec = ProjVec3;
54
55/// A vertex in clip space.
56#[derive(Copy, Clone, Debug, PartialEq)]
57pub struct ClipVert<A> {
58 pub pos: ClipVec,
59 pub attrib: A,
60 outcode: u8,
61}
62
63/// Visibility of a shape in the view frustum.
64#[derive(Copy, Clone, Debug, Eq, PartialEq)]
65pub enum Status {
66 /// Entirely inside view frustum
67 Visible,
68 /// Either outside or partly inside, needs clipping
69 Clipped,
70 /// Entirely outside view frustum
71 Hidden,
72}
73
74#[derive(Debug, Copy, Clone)]
75pub struct ClipPlane(ClipVec, u8);
76
77impl ClipPlane {
78 /// Creates a clip plane given a normal, offset, and outcode bit.
79 const fn new(x: f32, y: f32, z: f32, off: f32, bit: u8) -> Self {
80 Self(ClipVec::new([x, y, z, -off]), bit)
81 }
82
83 /// Returns the signed distance between `pt` and `self`.
84 ///
85 /// The return value is positive if `pt` is "outside" the plane,
86 /// defined as the half-space in the direction of the normal vector,
87 /// negative if `pt` is in the other half-space, and zero if `pt`
88 /// is exactly coincident with the plane:
89 /// ```text
90 /// n
91 /// ^ d > 0
92 /// | x
93 /// | |
94 /// |_ |
95 /// -----x-----+-'------------ self
96 /// d = 0 |
97 /// x
98 /// d < 0
99 /// ```
100 #[inline]
101 pub fn signed_dist(&self, pt: &ClipVec) -> f32 {
102 self.0.dot(pt)
103 }
104
105 /// Computes this plane's outcode bit for a point.
106 ///
107 /// The result is `self.1` if `pt` is outside this plane, 0 otherwise.
108 #[inline]
109 pub fn outcode(&self, pt: &ClipVec) -> u8 {
110 (self.signed_dist(pt) > 0.0) as u8 * self.1
111 }
112
113 /// Checks the outcode of a clip vertex against `self`.
114 ///
115 /// Returns `true` if this plane's outcode bit in `v` is 0, `false` otherwise.
116 #[inline]
117 pub fn is_inside<A>(&self, v: &ClipVert<A>) -> bool {
118 self.1 & v.outcode == 0
119 }
120
121 pub fn intersect<A: Lerp>(
122 &self,
123 [v0, v1]: [&ClipVert<A>; 2],
124 ) -> Option<ClipVert<A>> {
125 // TODO Doesn't use is_inside because it can't distinguish the case
126 // where a vertex lies exactly on the plane. Though that's mostly
127 // a theoretical edge case (heh).
128 let d0 = self.signed_dist(&v0.pos);
129 let d1 = self.signed_dist(&v1.pos);
130 (d0 * d1 < 0.0).then(|| {
131 // The edge crosses the plane surface. Split the edge in two
132 // by interpolating and emitting a new vertex at intersection.
133 // The new vertex becomes one of the endpoints of a new "clip"
134 // edge coincident with the plane.
135
136 // `t` is the fractional distance from `v0` to the intersection
137 // point. If condition guarantees that `d1 - d0` is nonzero.
138 let t = -d0 / (d1 - d0);
139
140 ClipVert::new(vertex(
141 v0.pos.lerp(&v1.pos, t),
142 v0.attrib.lerp(&v1.attrib, t),
143 ))
144 })
145 }
146
147 /// Clips a convex polygon against `self`.
148 ///
149 /// Returns the resulting vertices in the out parameter `verts_out`.
150 ///
151 /// In the diagram below, clipping triangle ABC results in quad ABPQ,
152 /// where P and Q are new vertices generated by interpolating between
153 /// A and C, and B and C, respectively.
154 ///
155 /// ```text
156 ///
157 /// n
158 /// ^ C
159 /// | / \ outside
160 /// | / \
161 /// ----+-------Q-------P--------self-----
162 /// / \
163 /// A--___ \ inside
164 /// `---__ \
165 /// `---B
166 /// ```
167 pub fn clip_simple_polygon<A: Lerp + Clone>(
168 &self,
169 verts_in: &[ClipVert<A>],
170 verts_out: &mut Vec<ClipVert<A>>,
171 ) {
172 let mut verts = verts_in.iter().chain(&verts_in[..1]);
173
174 let Some(mut v0) = verts.next() else {
175 return;
176 };
177
178 for v1 in verts {
179 if self.is_inside(v0) {
180 // v0 is inside; emit it as-is. If v1 is also inside, we don't
181 // have to do anything; it is emitted on the next iteration.
182 verts_out.push((*v0).clone());
183 } else {
184 // v0 is outside, discard it. If v1 is also outside, we don't
185 // have to do anything; it is discarded on the next iteration.
186 }
187
188 if let Some(v) = self.intersect([v0, v1]) {
189 verts_out.push(v);
190 }
191 v0 = v1;
192 }
193 }
194}
195
196/// A view frustum is a truncated, sideways pyramid representing the volume of
197/// space that is visible in a viewport with perspective projection. The left,
198/// top, right, and bottom sides of the frustum correspond to the edges of the
199/// viewport, while the near and far sides (the top and bottom of the pyramid)
200/// limit how close-up or far away objects can be drawn.
201///
202/// The far plane can be used to limit the amount of geometry that needs to be
203/// rendered per frame, usually combined with a fog effect so objects are not
204/// abruptly clipped. The near plane must have a positive offset from origin,
205/// and the offset plays a large role in dictating the distribution of depth
206/// values used for hidden surface removal.
207///
208/// TODO Describe clip space
209pub mod view_frustum {
210 use super::*;
211
212 /// The near, far, left, right, bottom, and top clipping planes,
213 /// in that order.
214 #[rustfmt::skip]
215 pub const PLANES: [ClipPlane; 6] = [
216 ClipPlane::new( 0.0, 0.0, -1.0, 1.0, 0x01), // Near
217 ClipPlane::new( 0.0, 0.0, 1.0, 1.0, 0x02), // Far
218 ClipPlane::new(-1.0, 0.0, 0.0, 1.0, 0x04), // Left
219 ClipPlane::new( 1.0, 0.0, 0.0, 1.0, 0x08), // Right
220 ClipPlane::new( 0.0, -1.0, 0.0, 1.0, 0x10), // Bottom
221 ClipPlane::new( 0.0, 1.0, 0.0, 1.0, 0x20), // Top
222 ];
223
224 /// Clips geometry against the standard view frustum.
225 ///
226 /// Returns the part that is within the frustum in the out parameter `out`.
227 ///
228 /// This is the main entry point to clipping.
229 pub fn clip<G: Clip + ?Sized>(geom: &G, out: &mut Vec<G::Item>) {
230 geom.clip(&PLANES, out);
231 }
232
233 /// Returns the outcode of the given point.
234 ///
235 /// The outcode is a bitset where the bit of each plane is 0 if the point
236 /// is inside the plane, and 1 otherwise. It is used to determine whether
237 /// a primitive is fully inside, partially inside, or fully outside the
238 /// frustum.
239 pub fn outcode(pt: &ClipVec) -> u8 {
240 PLANES.iter().map(|p| p.outcode(pt)).sum()
241 }
242
243 /// Returns the visibility status of the convex hull given by `vs`.
244 pub fn status<V>(vs: &[ClipVert<V>]) -> Status {
245 // The set of planes outside which all vertices are
246 let all_outside = vs.iter().fold(!0, |code, v| code & v.outcode);
247
248 // The set of planes outside which at least one vertex is.
249 let any_outside = vs.iter().fold(0, |code, v| code | v.outcode);
250
251 if all_outside != 0 {
252 // If all vertices are outside at least one plane, the whole
253 // object is hidden and can be culled. Note that they must be
254 // outside the *same* plane; it isn't enough that they are all
255 // outside at least *some* plane!
256 Status::Hidden
257 } else if any_outside == 0 {
258 // If no vertex is outside any plane, the whole polygon is visible
259 Status::Visible
260 } else {
261 // Otherwise, at least one of the vertices is outside the frustum
262 // and the polygon will have to be clipped (and may end up getting
263 // culled completely).
264 Status::Clipped
265 }
266 }
267}
268
269/// Computes the intersection of a simple polygon and a convex volume.
270///
271/// Returns the part of the polygon that is inside all planes, if any, in
272/// `verts_out`. This function uses an out parameter rather than the return
273/// value in order to avoid extra allocations. The result is unspecified if
274/// `verts_out` is not empty. `verts_in` is left empty by the function.
275///
276/// The algorithm used is Sutherland–Hodgman [^1].
277///
278/// TODO Describe algorithm
279///
280/// [^1]: Ivan Sutherland, Gary W. Hodgman: Reentrant Polygon Clipping.
281/// Communications of the ACM, vol. 17, pp. 32–42, 1974
282pub fn clip_simple_polygon<'a, A: Lerp + Clone>(
283 planes: &[ClipPlane],
284 verts_in: &'a mut Vec<ClipVert<A>>,
285 verts_out: &'a mut Vec<ClipVert<A>>,
286) {
287 debug_assert!(verts_out.is_empty());
288
289 for (p, i) in zip(planes, 0..) {
290 p.clip_simple_polygon(verts_in, verts_out);
291 verts_in.clear();
292 if verts_out.is_empty() {
293 // Nothing left to clip; the polygon was fully outside
294 break;
295 } else if i < planes.len() - 1 {
296 // Use the result of this iteration as the input of the next
297 swap(verts_in, verts_out);
298 }
299 }
300}
301
302impl<V> ClipVert<V> {
303 pub fn new(Vertex { pos, attrib }: Vertex<ClipVec, V>) -> Self {
304 let outcode = outcode(&pos);
305 Self { pos, attrib, outcode }
306 }
307}
308
309impl<A: Lerp + Clone> Clip for [Edge<ClipVert<A>>] {
310 type Item = Edge<ClipVert<A>>;
311
312 fn clip(&self, planes: &[ClipPlane], out: &mut Vec<Self::Item>) {
313 'lines: for Edge(a, b) in self {
314 let both_outside = a.outcode & b.outcode != 0;
315 let neither_outside = a.outcode | b.outcode == 0;
316
317 let mut a = a.clone();
318 let mut b = b.clone();
319
320 if both_outside {
321 continue;
322 }
323 if neither_outside {
324 out.push(Edge(a, b));
325 continue;
326 }
327 // Otherwise, clipping is needed
328 for p in planes {
329 let a_in = p.is_inside(&a);
330 let b_in = p.is_inside(&b);
331 // TODO Why not handled by both_outside check?
332 if !a_in && !b_in {
333 continue 'lines;
334 }
335 if let Some(v) = p.intersect([&a, &b]) {
336 if a_in {
337 b = v;
338 } else if b_in {
339 a = v;
340 }
341 }
342 }
343 out.push(Edge(a, b));
344 }
345 }
346}
347
348impl<A: Lerp + Clone> Clip for [Tri<ClipVert<A>>] {
349 type Item = Tri<ClipVert<A>>;
350
351 fn clip(&self, planes: &[ClipPlane], out: &mut Vec<Self::Item>) {
352 debug_assert!(out.is_empty());
353
354 // Avoid unnecessary allocations by reusing these
355 let mut verts_in = Vec::with_capacity(10);
356 let mut verts_out = Vec::with_capacity(10);
357
358 for tri @ Tri(vs) in self {
359 match status(vs) {
360 Status::Visible => {
361 out.push(tri.clone());
362 continue;
363 }
364 Status::Hidden => continue,
365 Status::Clipped => { /* go on and clip */ }
366 }
367
368 verts_in.extend(vs.clone());
369 clip_simple_polygon(planes, &mut verts_in, &mut verts_out);
370
371 if let [p, rest @ ..] = &verts_out[..] {
372 // Clipping a triangle results in an n-gon, where n depends on
373 // how many planes the triangle intersects. For example, here
374 // clipping triangle ABC generated three new vertices, resulting
375 // in quad APQR. Convert the quad into triangles PRA and PQR
376 // (or APQ and AQR, it is arbitrary):
377 //
378 // B
379 //
380 // / \
381 // Q_____P___________
382 // / |..../..\
383 // |.../.....\
384 // / |./.........\
385 // |/............\
386 // C _ _ _R_______________A
387 // |
388 // |
389 //
390 out.extend(
391 rest.windows(2)
392 .map(|e| Tri([p.clone(), e[0].clone(), e[1].clone()])),
393 );
394 }
395 verts_out.clear();
396 }
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use alloc::vec;
403
404 use crate::{geom::vertex, math::Vary};
405
406 use super::{view_frustum::*, *};
407
408 const FAR_PLANE: ClipPlane = PLANES[1];
409
410 fn vec(x: f32, y: f32, z: f32) -> ClipVec {
411 [x, y, z, 1.0].into()
412 }
413
414 fn vtx(pos: ClipVec) -> ClipVert<f32> {
415 ClipVert::new(vertex(pos, 0.0))
416 }
417
418 fn tri(a: ClipVec, b: ClipVec, c: ClipVec) -> Tri<ClipVert<f32>> {
419 Tri([a, b, c].map(vtx))
420 }
421
422 #[test]
423 fn signed_distance() {
424 assert_eq!(FAR_PLANE.signed_dist(&vec(0.0, 0.0, -1.0)), -2.0);
425 assert_eq!(FAR_PLANE.signed_dist(&vec(1.0, 0.0, 0.0)), -1.0);
426 assert_eq!(FAR_PLANE.signed_dist(&vec(0.0, 2.0, 1.0)), 0.0);
427 assert_eq!(FAR_PLANE.signed_dist(&vec(-3.0, 0.0, 2.0)), 1.0);
428 }
429
430 #[test]
431 fn outcode_inside() {
432 assert_eq!(outcode(&vec(0.0, 0.0, 0.0)), 0);
433 assert_eq!(outcode(&vec(1.0, 0.0, 0.0)), 0);
434 assert_eq!(outcode(&vec(0.0, -1.0, 0.0)), 0);
435 assert_eq!(outcode(&vec(0.0, 1.0, 1.0)), 0);
436 }
437
438 #[test]
439 fn outcode_outside() {
440 // Top Btm Rgt Lft Far Near
441 // 32 16 8 4 2 1
442
443 // Outside near == 1
444 assert_eq!(outcode(&vec(0.0, 0.0, -1.5)), 0b00_0_01);
445 // Outside right == 8
446 assert_eq!(outcode(&vec(2.0, 0.0, 0.0)), 0b00_10_00);
447 // Outside bottom == 16
448 assert_eq!(outcode(&vec(0.0, -1.01, 0.0)), 0b01_00_00);
449 // Outside far left == 2|4
450 assert_eq!(outcode(&vec(-2.0, 0.0, 2.0)), 0b00_01_10);
451 }
452
453 #[test]
454 fn edge_clip_inside() {
455 let e = [vec(2.0, 0.0, -1.0), vec(-1.0, 1.0, 1.0)].map(vtx);
456 let mut res = vec![];
457 FAR_PLANE.clip_simple_polygon(&e, &mut res);
458 assert_eq!(res, e);
459 }
460 #[test]
461 fn edge_clip_outside() {
462 let e = [vec(2.0, 0.0, 1.5), vec(-1.0, 1.0, 2.0)].map(vtx);
463 let mut res = vec![];
464 FAR_PLANE.clip_simple_polygon(&e, &mut res);
465 assert_eq!(res, []);
466 }
467 #[test]
468 fn edge_clip_in_out() {
469 let e = [vec(2.0, 0.0, 0.0), vec(-1.0, 1.0, 2.0)].map(vtx);
470 let mut res = vec![];
471 FAR_PLANE.clip_simple_polygon(&e, &mut res);
472 // clip_simple_polygon treats a single edge as a degenerate polygon,
473 // inserting an additional vertex
474 assert_eq!(res[..2], [e[0], vtx(vec(0.5, 0.5, 1.0))]);
475 }
476 #[test]
477 fn edge_clip_out_in() {
478 let e = [vec(2.0, 0.0, 4.0), vec(-1.0, 1.0, 0.0)].map(vtx);
479 let mut res = vec![];
480 FAR_PLANE.clip_simple_polygon(&e, &mut res);
481 // clip_simple_polygon treats a single edge as a degenerate polygon,
482 // inserting an additional vertex
483 assert_eq!(res[..2], [vtx(vec(-0.25, 0.75, 1.0)), e[1]]);
484 }
485
486 #[test]
487 fn tri_clip_fully_inside() {
488 let tri =
489 tri(vec(0.0, -1.0, 0.0), vec(2.0, 0.0, 0.5), vec(-1.0, 1.5, 0.0));
490 let res = &mut vec![];
491 [tri].clip(&[FAR_PLANE], res);
492 assert_eq!(res, &[tri]);
493 }
494 #[test]
495 fn tri_clip_fully_outside() {
496 let tri =
497 tri(vec(0.0, -1.0, 1.5), vec(2.0, 0.0, 1.5), vec(-1.0, 1.5, 2.0));
498 let res = &mut vec![];
499 [tri].clip(&[FAR_PLANE], res);
500 assert_eq!(res, &[]);
501 }
502
503 #[test]
504 fn tri_clip_inside_on_on() {
505 //
506 // 1.0 --on1------------on2-- plane
507 // \ /
508 // \ /
509 // \ /
510 // 0.0 ins
511 // -1.0 0.0 1.0 2.0
512 let tri =
513 tri(vec(0.0, -1.0, 0.0), vec(2.0, 0.0, 1.0), vec(-1.0, 1.5, 1.0));
514 let res = &mut vec![];
515 [tri].clip(&[FAR_PLANE], res);
516 assert_eq!(res, &[tri]);
517 }
518
519 #[test]
520 fn tri_clip_outside_inside_inside() {
521 // 2.0 out
522 // | \
523 // | \
524 // 1.0 -----+---+----- plane
525 // | \
526 // | \
527 // 0.0 in1----in2
528 // 0.0 1.0
529 let out = vec(0.0, 0.0, 2.0);
530 let in1 = vec(0.0, 1.0, 0.0);
531 let in2 = vec(1.0, 0.0, 0.0);
532 let tr = tri(out, in1, in2);
533
534 let res = &mut vec![];
535 [tr].clip(&[FAR_PLANE], res);
536 assert_eq!(
537 res,
538 &[
539 // Clipping `out` leaves a quadrilateral
540 tri(vec(0.0, 0.5, 1.0), in1, in2),
541 tri(vec(0.0, 0.5, 1.0), in2, vec(0.5, 0.0, 1.0))
542 ]
543 );
544 }
545 #[test]
546 fn tri_clip_outside_on_inside() {
547 // 2.0 out
548 // | \
549 // | \
550 // 1.0 -----+----on--- plane
551 // | /
552 // | /
553 // 0.0 . ins . . .
554 // 0.0 1.0
555 let out = vec(0.0, 0.0, 2.0);
556 let on = vec(1.0, 0.0, 1.0);
557 let ins = vec(0.0, -1.0, 0.0);
558 let tr = tri(out, on, ins);
559
560 let res = &mut vec![];
561 [tr].clip(&[FAR_PLANE], res);
562 assert_eq!(res, &[tri(on, ins, vec(0.0, -0.5, 1.0))]);
563 }
564 #[test]
565 fn tri_clip_outside_on_on() {
566 // 2.0 out
567 // | \
568 // | \
569 // 1.0 ---on2---on1-- plane
570 // .
571 // .
572 // 0.0 . o . . .
573 // 0.0 1.0
574 let out = vec(0.0, 0.0, 2.0);
575 let on1 = vec(1.0, 0.0, 1.0);
576 let on2 = vec(0.0, -1.0, 1.0);
577 let tr = tri(out, on1, on2);
578
579 let res = &mut vec![];
580 [tr].clip(&[FAR_PLANE], res);
581 assert_eq!(res, &[]);
582 }
583
584 #[test]
585 fn tri_clip_against_frustum_fully_inside() {
586 let tr = tri(
587 vec(-1.0, -1.0, -1.0),
588 vec(1.0, 1.0, 0.0),
589 vec(0.0, 1.0, 1.0),
590 );
591 let res = &mut vec![];
592 [tr].clip(&PLANES, res);
593 assert_eq!(res, &[tr]);
594 }
595 #[test]
596 fn tri_clip_against_frustum_fully_outside() {
597 // z
598 // ^
599 // 2-------0
600 // · \ |
601 // --1---+ |
602 // · | \ |
603 // + - 1 - 2 - - > x
604 // |
605 // ------+
606
607 let tr =
608 tri(vec(2.0, 2.0, 2.0), vec(2.0, -2.0, 0.0), vec(0.0, -1.0, 2.0));
609
610 let res = &mut vec![];
611 [tr].clip(&PLANES, res);
612 assert_eq!(res, &[]);
613 }
614 #[test]
615 fn tri_clip_against_frustum_result_is_quad() {
616 // z
617 // ^
618 // 2
619 // | \
620 // - 1---+
621 // | | \
622 // 0---1---2 - - > x
623 // |
624 // - ----+
625
626 let tr =
627 tri(vec(0.0, 0.0, 0.0), vec(2.0, 0.0, 0.0), vec(0.0, 0.0, 2.0));
628
629 let res = &mut vec![];
630 [tr].clip(&PLANES, res);
631 assert_eq!(
632 res,
633 &[
634 tri(vec(0.0, 0.0, 0.0), vec(1.0, 0.0, 0.0), vec(1.0, 0.0, 1.0)),
635 tri(vec(0.0, 0.0, 0.0), vec(1.0, 0.0, 1.0), vec(0.0, 0.0, 1.0))
636 ]
637 );
638 }
639
640 #[test]
641 fn tri_clip_against_frustum_result_is_heptagon() {
642 // z
643 // ^ 2
644 // · / /
645 // +---/---+ /
646 // / · |/
647 // 0 | · o · / · · > x
648 // \ · /|
649 // +-\---/-+
650 // 1
651
652 let tr =
653 tri(vec(-1.5, 0.0, 0.0), vec(0.0, 0.0, -1.5), vec(2.0, 0.0, 2.0));
654
655 let res = &mut vec![];
656 [tr].clip(&PLANES, res);
657
658 // 7 intersection points -> clipped shape made of 5 triangles
659 assert_eq!(res.len(), 5);
660 assert!(res.iter().all(in_bounds));
661 }
662
663 #[test]
664 #[allow(unused)]
665 fn tri_clip_against_frustum_all_cases() {
666 // Methodically go through every possible combination of every
667 // vertex inside/outside every plane, including degenerate cases.
668
669 let xs = || (-2.0).vary(1.0, Some(5));
670
671 let pts: Vec<_> = xs()
672 .flat_map(move |x| {
673 xs().flat_map(move |y| xs().map(move |z| vec(x, y, z)))
674 })
675 .collect();
676
677 let tris = pts.iter().flat_map(|a| {
678 pts.iter()
679 .flat_map(|b| pts.iter().map(|c| tri(*a, *b, *c)))
680 });
681
682 let mut in_tris = 0;
683 let mut in_degen = 0;
684 let mut out_tris = [0; 8];
685 let mut out_degen = 0;
686 let mut out_total = 0;
687 for tr in tris {
688 let res = &mut vec![];
689 [tr].clip(&PLANES, res);
690 assert!(
691 res.iter().all(in_bounds),
692 "clip returned oob vertex:\n\
693 input: {:#?}\n\
694 output: {:#?}",
695 tr,
696 &res
697 );
698 in_tris += 1;
699 in_degen += is_degenerate(&tr) as u32;
700 out_tris[res.len()] += 1;
701 out_total += res.len();
702 out_degen += res.iter().filter(|t| is_degenerate(t)).count()
703 }
704 #[cfg(feature = "std")]
705 {
706 use std::dbg;
707 dbg!(in_tris);
708 dbg!(in_degen);
709 dbg!(out_degen);
710 dbg!(out_total);
711 }
712 assert_eq!(in_tris, 5i32.pow(9));
713 assert_eq!(
714 out_tris,
715 [559754, 536199, 537942, 254406, 58368, 6264, 192, 0]
716 );
717 }
718
719 fn is_degenerate(Tri([a, b, c]): &Tri<ClipVert<f32>>) -> bool {
720 a.pos == b.pos || a.pos == c.pos || b.pos == c.pos
721 }
722
723 fn in_bounds(Tri(vs): &Tri<ClipVert<f32>>) -> bool {
724 vs.iter()
725 .flat_map(|v| (v.pos / v.pos.w()).0)
726 .all(|a| a.abs() <= 1.00001)
727 }
728}