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
16fn 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}