veloren_voxygen/scene/math.rs
1use core::{iter, mem};
2use hashbrown::HashMap;
3use num::traits::Float;
4/*
5pub use vek::{geom::repr_simd::*, mat::repr_simd::column_major::Mat4, ops::*, vec::repr_simd::*};
6*/
7pub use vek::{geom::repr_c::*, mat::repr_c::column_major::Mat4, ops::*, vec::repr_c::*};
8
9pub fn aabb_to_points<T: Float>(bounds: Aabb<T>) -> [Vec3<T>; 8] {
10 [
11 Vec3::new(bounds.min.x, bounds.min.y, bounds.min.z),
12 Vec3::new(bounds.max.x, bounds.min.y, bounds.min.z),
13 Vec3::new(bounds.max.x, bounds.max.y, bounds.min.z),
14 Vec3::new(bounds.min.x, bounds.max.y, bounds.min.z),
15 Vec3::new(bounds.min.x, bounds.min.y, bounds.max.z),
16 Vec3::new(bounds.max.x, bounds.min.y, bounds.max.z),
17 Vec3::new(bounds.max.x, bounds.max.y, bounds.max.z),
18 Vec3::new(bounds.min.x, bounds.max.y, bounds.max.z),
19 ]
20}
21
22/// Each Vec4 <a, b, c, -d> should be interpreted as reprenting plane
23/// equation
24///
25/// a(x - x0) + b(y - y0) + c(z - z0) = 0, i.e.
26/// ax + by + cz - (a * x0 + b * y0 + c * z0) = 0, i.e.
27/// ax + by + cz = (a * x0 + b * y0 + c * z0), i.e.
28/// (letting d = a * x0 + b * y0 + c * z0)
29/// ax + by + cz = d
30///
31/// where d is the distance of the plane from the origin.
32pub fn aabb_to_planes<T: Float>(bounds: Aabb<T>) -> [Vec4<T>; 6] {
33 let zero = T::zero();
34 let one = T::one();
35 let bounds = bounds.map(|e| e.abs());
36 [
37 // bottom
38 Vec4::new(zero, -one, zero, -bounds.min.y),
39 // top
40 Vec4::new(zero, one, zero, -bounds.max.y),
41 // left
42 Vec4::new(-one, zero, zero, -bounds.min.x),
43 // right
44 Vec4::new(one, zero, zero, -bounds.max.x),
45 // near
46 Vec4::new(zero, zero, -one, -bounds.min.z),
47 // far
48 Vec4::new(zero, zero, one, -bounds.max.z),
49 ]
50}
51
52pub fn mat_mul_points<T: Float + MulAdd<T, T, Output = T>>(
53 mat: Mat4<T>,
54 pts: &mut [Vec3<T>],
55 mut do_p: impl FnMut(Vec4<T>) -> Vec3<T>,
56) {
57 pts.iter_mut().for_each(|p| {
58 *p = do_p(mat * Vec4::from_point(*p));
59 });
60}
61
62/// NOTE: Expects points computed from aabb_to_points.
63pub fn calc_view_frust_object<T: Float>(pts: &[Vec3<T>; 8]) -> Vec<Vec<Vec3<T>>> {
64 vec![
65 // near (CCW)
66 vec![pts[0], pts[1], pts[2], pts[3]],
67 // far (CCW)
68 vec![pts[7], pts[6], pts[5], pts[4]],
69 // left (CCW)
70 vec![pts[0], pts[3], pts[7], pts[4]],
71 // right (CCW)
72 vec![pts[1], pts[5], pts[6], pts[2]],
73 // bottom (CCW)
74 vec![pts[4], pts[5], pts[1], pts[0]],
75 // top (CCW)
76 vec![pts[6], pts[7], pts[3], pts[2]],
77 ]
78}
79
80pub fn calc_view_frustum_world_coord<T: Float + MulAdd<T, T, Output = T>>(
81 inv_proj_view: Mat4<T>,
82) -> [Vec3<T>; 8] {
83 let mut world_pts = aabb_to_points(Aabb {
84 min: Vec3::new(-T::one(), -T::one(), T::zero()),
85 max: Vec3::one(),
86 });
87 mat_mul_points(inv_proj_view, &mut world_pts, |p| Vec3::from(p) / p.w);
88 world_pts
89}
90
91pub fn point_plane_distance<T: Float>(point: Vec3<T>, norm_dist: Vec4<T>) -> T {
92 norm_dist.dot(Vec4::from_point(point))
93}
94
95pub fn point_before_plane<T: Float>(point: Vec3<T>, plane: Vec4<T>) -> bool {
96 point_plane_distance(point, plane) > T::zero()
97}
98
99/// Returns true if and only if the final point in the polygon (i.e. the
100/// first point added to the new polygon) is outside the clipping plane
101/// (this implies that the polygon must be non-degenerate).
102pub fn clip_points_by_plane<T: Float + MulAdd<T, T, Output = T> + core::fmt::Debug>(
103 points: &mut Vec<Vec3<T>>,
104 plane: Vec4<T>,
105 intersection_points: &mut Vec<Vec3<T>>,
106) -> bool {
107 if points.len() < 3 {
108 return false;
109 }
110 // NOTE: Guaranteed to succeed since points.len() > 3.
111 let mut current_point = points[points.len() - 1];
112 let intersect_plane_edge = |a, b| {
113 let diff: Vec3<_> = b - a;
114 let t = plane.dot(Vec4::from_direction(diff));
115 if t == T::zero() {
116 None
117 } else {
118 let t = -(plane.dot(Vec4::from_point(a)) / t);
119 if t < T::zero() || T::one() < t {
120 None
121 } else {
122 Some(diff * t + a)
123 }
124 }
125 };
126 let last_is_outside = point_before_plane(current_point, plane);
127 let mut is_outside = last_is_outside;
128 let mut old_points = Vec::with_capacity((3 * points.len()) / 2);
129 mem::swap(&mut old_points, points);
130 old_points.into_iter().for_each(|point| {
131 let prev_point = mem::replace(&mut current_point, point);
132 let before_plane = point_before_plane(current_point, plane);
133 let prev_is_outside = mem::replace(&mut is_outside, before_plane);
134 if !prev_is_outside {
135 // Push previous point.
136 points.push(prev_point);
137 }
138 if prev_is_outside != is_outside {
139 if let Some(intersection_point) = intersect_plane_edge(prev_point, current_point) {
140 // Push intersection point.
141 intersection_points.push(intersection_point);
142 points.push(intersection_point);
143 }
144 }
145 });
146 last_is_outside
147}
148
149fn append_intersection_points<T: Float + core::fmt::Debug>(
150 polys: &mut Vec<Vec<Vec3<T>>>,
151 intersection_points: Vec<Vec3<T>>,
152 tolerance: T,
153) {
154 // NOTE: We use decoded versions of each polygon, with rounded entries.
155 //
156 // The line segments in intersection_points are consistently ordered as follows:
157 // each segment represents the intersection of the cutting plane with the
158 // polygon from which the segment came. The polygon can thus be split into
159 // two parts: the part "inside" the new surface (below the plane), and the
160 // part "outside" it (above the plane). Thus, when oriented
161 // with the polygon normal pointing into the camera, and the cutting plane as
162 // the x axis, with the "outside" part on top and the "inside" part on the
163 // bottom, there is a leftmost point (the point going from outside to
164 // inside, counterclockwise) and a rightmost point (the point going from
165 // inside to outside, counterclockwise). Our consistent ordering guarantees
166 // that the leftmost point comes before the rightmost point in each line
167 // segment.
168 //
169 // Why does this help us? To see that, consider the polygon adjacent to the
170 // considered polygon which also has the same right intersection point (we
171 // know there will be exactly one of these, because we have a solid
172 // structure and are only considering polygons that intersect the plane
173 // exactly two times; this means that we are ignoring polygons that intersect
174 // the plane at just one point, which means the two polygons must share a
175 // point, not be coplanar, and both intersect the plane; no theorem here,
176 // but I believe there can provably be at most one such instance given that
177 // we have at least three polygons with such a line segment).
178 //
179 // Now, for the adjacent polygon, repeat the above process. If the intersection
180 // point shared by the polygons is on the right in both cases, then we can
181 // see that the polygon's normal must be facing in the opposite direction of
182 // the original polygon despite being adjacent. But this
183 // should be impossible for a closed object! The same applies to the leftmost
184 // point.
185 //
186 // What is the practical upshot of all this? It means that we can consistently
187 // hash each line segment by its first point, which we can look up using the
188 // second point of a previous line segment. This will produce a chain of
189 // entries terminating in the original segment we looked up. As an added
190 // bonus, by going from leftmost point to leftmost point, we also ensure that
191 // we produce a polygon whose face is oriented counterclockwise around its
192 // normal; this can be seen by following the right-hand rule (TODO: provide
193 // more rigorous proof).
194 let tol = tolerance.recip();
195 let make_key = move |point: Vec3<T>| {
196 // We use floating points rounded to tolerance in order to make our HashMap
197 // lookups work. Otherwise we'd have to use a sorted structure, like a
198 // btree, which wouldn't be the end of the world but would have
199 // theoretically worse complexity.
200 //
201 // NOTE: Definitely non-ideal that we panic if the rounded value can't fit in an
202 // i64...
203 //
204 // TODO: If necessary, let the caller specify how to hash these keys, since in
205 // cases where we know the kind of floating point we're using we can
206 // just cast to bits or something.
207 point.map(|e| {
208 (e * tol)
209 .round()
210 .to_i64()
211 .expect("We don't currently try to handle floats that won't fit in an i64.")
212 })
213 };
214 let mut lines_iter = intersection_points.chunks_exact(2).filter_map(|uv| {
215 let u_key = make_key(uv[0]);
216 let v = uv[1];
217 // NOTE: The reason we need to make sure this doesn't happen is that it's
218 // otherwise possible for two points to hash to the same value due to
219 // epsilon being too low. Because of the ordering mentioned previously,
220 // we know we should *eventually* find a pair of points starting with
221 // make_key(u) and ending with a different make_key(v) in such cases, so
222 // we just discard all the other pairs (treating them as points rather
223 // than lines).
224 (u_key != make_key(v)).then_some((u_key, v))
225 });
226
227 if let Some((last_key, first)) = lines_iter.next() {
228 let lines = lines_iter.collect::<HashMap<_, _>>();
229 if lines.len() < 2 {
230 // You need at least 3 sides for a polygon
231 return;
232 }
233 // NOTE: Guaranteed to terminate, provided we have no cycles besides the one
234 // that touches every point (which should be the case given how these
235 // points were generated).
236 let poly_iter = iter::successors(Some(first), |&cur| lines.get(&make_key(cur)).copied());
237 let poly: Vec<_> = poly_iter.collect();
238 // We have to check to make sure we really went through the whole cycle.
239 // TODO: Consider adaptively decreasing precision until we can make the cycle
240 // happen.
241 if poly.last().copied().map(make_key) == Some(last_key) {
242 // Push the new polygon onto the object.
243 polys.push(poly);
244 }
245 }
246}
247
248pub fn clip_object_by_plane<T: Float + MulAdd<T, T, Output = T> + core::fmt::Debug>(
249 polys: &mut Vec<Vec<Vec3<T>>>,
250 plane: Vec4<T>,
251 tolerance: T,
252) {
253 let mut intersection_points = Vec::new();
254 polys.retain_mut(|points| {
255 let len = intersection_points.len();
256 let outside_first = clip_points_by_plane(points, plane, &mut intersection_points);
257 // Only remember intersections that are not coplanar with this side; i.e. those
258 // that have segment length 2.
259 if len + 2 != intersection_points.len() {
260 intersection_points.truncate(len);
261 } else if !outside_first {
262 // Order the two intersection points consistently, so that, when considered
263 // counterclockwise:
264 // - the first point goes from the exterior of the polygon (above the cutting
265 // plane) to its interior.
266 // - the second point goes from the interior of the polygon (below the cutting
267 // plane) to its exterior.
268 // the second is always going
269 //
270 // This allows us to uniquely map each line segment to an "owning" point (the
271 // one going from outside to inside), which happens to also point
272 // the segment in a counterclockwise direction around the new
273 // polygon normal composed of all the lines we clipped.
274 intersection_points.swap(len, len + 1);
275 }
276 // Remove polygon if it was clipped away
277 !points.is_empty()
278 });
279 // Add a polygon of all intersection points with the plane to close out the
280 // object.
281 append_intersection_points(polys, intersection_points, tolerance);
282}
283
284pub fn clip_object_by_aabb<T: Float + MulAdd<T, T, Output = T> + core::fmt::Debug>(
285 polys: &mut Vec<Vec<Vec3<T>>>,
286 bounds: Aabb<T>,
287 tolerance: T,
288) {
289 let planes = aabb_to_planes(bounds);
290 planes.iter().for_each(|&plane| {
291 clip_object_by_plane(polys, plane, tolerance);
292 });
293}
294
295/// Return value is 'Some(segment)' if line segment intersects the current
296/// test plane. Otherwise 'None' is returned in which case the line
297/// segment is entirely clipped.
298pub fn clip_test<T: Float + core::fmt::Debug>(p: T, q: T, (u1, u2): (T, T)) -> Option<(T, T)> {
299 if p == T::zero() {
300 if q >= T::zero() { Some((u1, u2)) } else { None }
301 } else {
302 let r = q / p;
303 if p < T::zero() {
304 if r > u2 {
305 None
306 } else {
307 Some((if r > u1 { r } else { u1 }, u2))
308 }
309 } else if r < u1 {
310 None
311 } else {
312 Some((u1, if r < u2 { r } else { u2 }))
313 }
314 }
315}
316
317pub fn intersection_line_aabb<T: Float + MulAdd<T, T, Output = T> + core::fmt::Debug>(
318 p: Vec3<T>,
319 dir: Vec3<T>,
320 bounds: Aabb<T>,
321) -> Option<Vec3<T>> {
322 clip_test(-dir.z, p.z - bounds.min.z, (T::zero(), T::infinity()))
323 .and_then(|t| clip_test(dir.z, bounds.max.z - p.z, t))
324 .and_then(|t| clip_test(-dir.y, p.y - bounds.min.y, t))
325 .and_then(|t| clip_test(dir.y, bounds.max.y - p.y, t))
326 .and_then(|t| clip_test(-dir.x, p.x - bounds.min.x, t))
327 .and_then(|t| clip_test(dir.x, bounds.max.x - p.x, t))
328 .and_then(|(t1, t2)| {
329 if T::zero() <= t2 {
330 Some(dir.mul_add(Vec3::broadcast(t2), p))
331 } else if T::zero() <= t1 {
332 Some(dir.mul_add(Vec3::broadcast(t1), p))
333 } else {
334 None
335 }
336 })
337}
338
339pub fn include_object_light_volume<
340 T: Float + MulAdd<T, T, Output = T> + core::fmt::Debug,
341 I: Iterator<Item = Vec3<T>>,
342>(
343 obj: I,
344 light_dir: Vec3<T>,
345 bounds: Aabb<T>,
346) -> impl Iterator<Item = Vec3<T>> {
347 obj.flat_map(move |pt| iter::once(pt).chain(intersection_line_aabb(pt, -light_dir, bounds)))
348}
349
350// NOTE: Currently specialized to skip extending to the end of the light ray,
351// since our light ray is already infinite. Correct code is commented out
352// below.
353pub fn calc_focused_light_volume_points<T: Float + MulAdd<T, T, Output = T> + core::fmt::Debug>(
354 inv_proj_view: Mat4<T>,
355 _light_dir: Vec3<T>,
356 scene_bounding_box: Aabb<T>,
357 tolerance: T,
358) -> impl Iterator<Item = Vec3<T>> {
359 let world_pts = calc_view_frustum_world_coord(inv_proj_view);
360 let mut world_frust_object = calc_view_frust_object(&world_pts);
361 clip_object_by_aabb(&mut world_frust_object, scene_bounding_box, tolerance);
362 world_frust_object.into_iter().flat_map(|e| e.into_iter())
363 /* include_object_light_volume(
364 world_frust_object.into_iter().flat_map(|e| e.into_iter()),
365 light_dir,
366 scene_bounding_box,
367 ) */
368}
369
370/// Computes an axis aligned bounding box that contains the provided points when
371/// transformed into the coordinate space specificed by `mat`.
372///
373/// "psr" stands for "Potential shadow receivers" since this function is used to
374/// get an Aabb containing the potential points (which represent the extents of
375/// the volumes of potential things that will be shadowed) that will be
376/// shadowed.
377///
378/// NOTE: Will not yield useful results if pts is empty!
379pub fn fit_psr<
380 T: Float + MulAdd<T, T, Output = T>,
381 I: Iterator<Item = Vec3<T>>,
382 F: FnMut(Vec4<T>) -> Vec4<T>,
383>(
384 mat: Mat4<T>,
385 pts: I,
386 mut do_p: F,
387) -> Aabb<T> {
388 let mut min = Vec4::broadcast(T::infinity());
389 let mut max = Vec4::broadcast(T::neg_infinity());
390 pts.map(|p| do_p(mat * Vec4::<T>::from_point(p)))
391 .for_each(|p| {
392 min = Vec4::partial_min(min, p);
393 max = Vec4::partial_max(max, p);
394 });
395 Aabb {
396 min: min.xyz(),
397 max: max.xyz(),
398 }
399}