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}