Skip to main content

polytope_playground/
projection.rs

1use glam::{Vec3, Vec4};
2use loam::math::{EuclideanR4, Projection, RasterizableSpace, SphericalS3Embedded};
3use loam::runtime::{
4    DepthEnvelope, DomainRay, DomainSpace, ImageRay, Pose, SectionCut, ViewMapping,
5};
6use loam::shape::projected_edges::stereographic_view_point;
7
8pub(crate) const STEREOGRAPHIC_POLE: Vec4 = Vec4::W;
9
10pub(crate) const PERSPECTIVE_FOCAL: f32 = 2.0;
11
12const STEREOGRAPHIC_EDGE_SAMPLES: usize = 16;
13
14const STEREOGRAPHIC_CLIP_RADIUS: f32 = 10.0;
15
16// Pharr, Jakob, and Humphreys, Physically Based Rendering, 4th ed., ยง6.2.
17fn clip_crossing(inside: Vec3, outside: Vec3) -> f32 {
18    let direction = outside - inside;
19    let a = direction.length_squared();
20    let b = inside.dot(direction);
21    let c = inside.length_squared() - STEREOGRAPHIC_CLIP_RADIUS.powi(2);
22    let discriminant = (b * b - a * c).max(0.0);
23    ((-b + discriminant.sqrt()) / a).clamp(0.0, 1.0)
24}
25
26const UNBOUNDED: DepthEnvelope = DepthEnvelope {
27    near: 0.0,
28    far: f32::INFINITY,
29};
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32pub(crate) enum Family {
33    #[default]
34    Shadow,
35    Perspective,
36    Stereographic,
37}
38
39impl Family {
40    pub(crate) const ALL: [Family; 3] =
41        [Family::Shadow, Family::Perspective, Family::Stereographic];
42
43    pub(crate) fn name(self) -> &'static str {
44        match self {
45            Family::Shadow => "shadow",
46            Family::Perspective => "perspective",
47            Family::Stereographic => "stereographic",
48        }
49    }
50
51    pub(crate) fn from_token(token: &str) -> Option<Self> {
52        Family::ALL
53            .into_iter()
54            .find(|family| family.name() == token)
55    }
56
57    pub(crate) fn mapping(self, slice: f32) -> Projected {
58        let projection = match self {
59            Family::Shadow => Projection::Identity,
60            Family::Perspective => Projection::Perspective4D {
61                focal_distance: PERSPECTIVE_FOCAL,
62            },
63            Family::Stereographic => Projection::Stereographic {
64                pole: STEREOGRAPHIC_POLE,
65            },
66        };
67        Projected {
68            name: self.name(),
69            projection,
70            slice,
71        }
72    }
73}
74
75pub(crate) struct Projected {
76    name: &'static str,
77    projection: Projection<4>,
78    slice: f32,
79}
80
81impl Projected {
82    fn project(&self, point: Vec4) -> Vec3 {
83        if point == Vec4::ZERO {
84            return Vec3::ZERO;
85        }
86        stereographic_view_point(point, &self.projection)
87    }
88
89    fn visible(&self, projected: Vec3) -> bool {
90        projected.is_finite()
91            && (!matches!(self.projection, Projection::Stereographic { .. })
92                || projected.length_squared()
93                    <= STEREOGRAPHIC_CLIP_RADIUS * STEREOGRAPHIC_CLIP_RADIUS)
94    }
95}
96
97impl ViewMapping<EuclideanR4> for Projected {
98    fn name(&self) -> &'static str {
99        self.name
100    }
101
102    fn image_point(&self, eye: &Pose<EuclideanR4>, point: Vec4) -> Option<[f32; 3]> {
103        let relative = EuclideanR4.local(eye, point).ok()?;
104        let placed = self.project(relative);
105        placed.is_finite().then(|| placed.to_array())
106    }
107
108    fn image_local(
109        &self,
110        space: &EuclideanR4,
111        _eye: &Pose<EuclideanR4>,
112        _pose: &Pose<EuclideanR4>,
113        relative: &<EuclideanR4 as DomainSpace>::Relative,
114        local: Vec4,
115    ) -> Option<[f32; 3]> {
116        let origin = space.place_relative(relative, Vec4::ZERO).ok()?;
117        let point = space.place_relative(relative, local).ok()?;
118        let projected = self.project(point - origin);
119        self.visible(projected)
120            .then(|| (projected + origin.truncate()).to_array())
121    }
122
123    fn image_segment(
124        &self,
125        space: &EuclideanR4,
126        eye: &Pose<EuclideanR4>,
127        pose: &Pose<EuclideanR4>,
128        relative: &<EuclideanR4 as DomainSpace>::Relative,
129        segment: [Vec4; 2],
130        emit: &mut dyn FnMut(f32, Option<[f32; 3]>),
131    ) {
132        let [start, end] = segment;
133        if !matches!(self.projection, Projection::Stereographic { .. }) {
134            emit(0.0, self.image_local(space, eye, pose, relative, start));
135            emit(1.0, self.image_local(space, eye, pose, relative, end));
136            return;
137        }
138        let Ok(origin) = space.place_relative(relative, Vec4::ZERO) else {
139            emit(0.0, None);
140            emit(1.0, None);
141            return;
142        };
143        let image = |local| {
144            let point = space.place_relative(relative, local).ok()?;
145            let projected = self.project(point - origin);
146            projected
147                .is_finite()
148                .then(|| (projected, (projected + origin.truncate()).to_array()))
149        };
150        let clip_radius_squared = STEREOGRAPHIC_CLIP_RADIUS * STEREOGRAPHIC_CLIP_RADIUS;
151        if start.length_squared() <= f32::MIN_POSITIVE || end.length_squared() <= f32::MIN_POSITIVE
152        {
153            let clipped = |local| {
154                image(local).and_then(|(projected, world)| self.visible(projected).then_some(world))
155            };
156            emit(0.0, clipped(start));
157            emit(1.0, clipped(end));
158            return;
159        }
160        let start_radius = start.length();
161        let end_radius = end.length();
162        let mut index = 0usize;
163        let mut previous = None;
164        <SphericalS3Embedded as RasterizableSpace<4>>::tessellate_segment(
165            start / start_radius,
166            end / end_radius,
167            STEREOGRAPHIC_EDGE_SAMPLES,
168            |unit| {
169                let t = index as f32 / STEREOGRAPHIC_EDGE_SAMPLES as f32;
170                index += 1;
171                let radius = start_radius + (end_radius - start_radius) * t;
172                let Some((projected, world)) = image(unit * radius) else {
173                    emit(t, None);
174                    previous = None;
175                    return;
176                };
177                let inside = projected.length_squared() <= clip_radius_squared;
178                if let Some((previous_t, previous_projected, previous_inside)) = previous {
179                    match (previous_inside, inside) {
180                        (true, true) => emit(t, Some(world)),
181                        (true, false) => {
182                            let crossing = clip_crossing(previous_projected, projected);
183                            let boundary = previous_projected.lerp(projected, crossing);
184                            emit(
185                                previous_t + (t - previous_t) * crossing,
186                                Some((boundary + origin.truncate()).to_array()),
187                            );
188                            emit(t, None);
189                        }
190                        (false, true) => {
191                            let crossing = clip_crossing(projected, previous_projected);
192                            let boundary = projected.lerp(previous_projected, crossing);
193                            emit(
194                                t + (previous_t - t) * crossing,
195                                Some((boundary + origin.truncate()).to_array()),
196                            );
197                            emit(t, Some(world));
198                        }
199                        (false, false) => emit(t, None),
200                    }
201                } else {
202                    emit(t, inside.then_some(world));
203                }
204                previous = Some((t, projected, inside));
205            },
206        );
207    }
208
209    fn section(&self, eye: &Pose<EuclideanR4>, pose: &Pose<EuclideanR4>) -> Option<SectionCut> {
210        let Projection::Perspective4D { focal_distance } = self.projection else {
211            return None;
212        };
213        let depth = focal_distance - self.slice;
214        let relative = EuclideanR4.local(eye, pose.point).ok()?;
215        (depth > 0.0).then(|| SectionCut {
216            offset: self.slice - relative.w,
217            scale: focal_distance / depth,
218        })
219    }
220
221    fn lift(&self, _eye: &Pose<EuclideanR4>, _ray: &ImageRay) -> Option<DomainRay<EuclideanR4>> {
222        None
223    }
224
225    fn ray_lift(&self) -> bool {
226        false
227    }
228
229    fn depth_envelope(&self) -> DepthEnvelope {
230        UNBOUNDED
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use crate::consts::BODY_SIZE;
238
239    const OFFSET: Vec4 = Vec4::new(1.8, 0.9, 0.0, 0.0);
240
241    fn placed(family: Family, local: Vec4) -> Vec3 {
242        let mapping = family.mapping(0.0);
243        let (eye, pose) = (Pose::at(Vec4::ZERO), Pose::at(OFFSET));
244        let relative = EuclideanR4.relative(&eye, &pose).expect("the eye places");
245        let image = mapping
246            .image_local(&EuclideanR4, &eye, &pose, &relative, local)
247            .expect("the map places the vertex");
248        Vec3::from_array(image)
249    }
250
251    #[test]
252    fn the_perspective_map_foreshortens_the_vertex_and_leaves_the_body_where_it_stands() {
253        let vertex = Vec4::splat(0.5) * BODY_SIZE;
254        let scale = PERSPECTIVE_FOCAL / (PERSPECTIVE_FOCAL - vertex.w);
255        let expected = Vec3::splat(vertex.x * scale) + OFFSET.truncate();
256        let image = placed(Family::Perspective, vertex);
257        assert!(
258            (image - expected).length() < 1e-5,
259            "the perspective map put the vertex at {image} rather than {expected}"
260        );
261    }
262
263    #[test]
264    fn the_stereographic_map_casts_the_vertex_onto_the_sphere_before_it_divides() {
265        let vertex = Vec4::splat(0.5) * BODY_SIZE;
266        let unit = vertex.normalize();
267        let expected = unit.truncate() / (1.0 - unit.w) + OFFSET.truncate();
268        let image = placed(Family::Stereographic, vertex);
269        assert!(
270            (image - expected).length() < 1e-5,
271            "the stereographic map put the vertex at {image} rather than {expected}"
272        );
273    }
274
275    #[test]
276    fn stereographic_edges_follow_s3_arcs_and_stop_at_the_pole_clip() {
277        let mapping = Family::Stereographic.mapping(0.0);
278        let (eye, pose) = (Pose::at(Vec4::ZERO), Pose::at(Vec4::ZERO));
279        let relative = EuclideanR4.relative(&eye, &pose).expect("the eye places");
280        let mut samples = Vec::new();
281        mapping.image_segment(
282            &EuclideanR4,
283            &eye,
284            &pose,
285            &relative,
286            [Vec4::X, Vec4::Y],
287            &mut |_, point| samples.push(point.expect("the arc stays away from the pole")),
288        );
289
290        assert_eq!(samples.len(), STEREOGRAPHIC_EDGE_SAMPLES + 1);
291        let midpoint = Vec3::from_array(samples[STEREOGRAPHIC_EDGE_SAMPLES / 2]);
292        let expected = Vec3::new(
293            std::f32::consts::FRAC_1_SQRT_2,
294            std::f32::consts::FRAC_1_SQRT_2,
295            0.0,
296        );
297        assert!((midpoint - expected).length() < 1e-5);
298
299        samples.clear();
300        mapping.image_segment(
301            &EuclideanR4,
302            &eye,
303            &pose,
304            &relative,
305            [Vec4::W, Vec4::X],
306            &mut |_, point| samples.extend(point),
307        );
308        let lengths = samples
309            .iter()
310            .map(|point| Vec3::from_array(*point).length());
311        let furthest = lengths.fold(0.0_f32, f32::max);
312        assert!((furthest - STEREOGRAPHIC_CLIP_RADIUS).abs() < 1e-3);
313        assert!(samples
314            .iter()
315            .all(|point| Vec3::from_array(*point).is_finite()));
316    }
317}