rustic_zen/scene/
object.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/// Some considerations for adding curve support to this:
6/// https://blog.demofox.org/2016/03/05/matrix-form-of-bezier-curves/
7/// https://pomax.github.io/bezierinfo/
8/// https://computergraphics.stackexchange.com/questions/374/how-to-raytrace-bezier-surfaces/378
9/// https://en.wikipedia.org/wiki/B-spline
10/// https://en.wikipedia.org/wiki/B%C3%A9zier_curve
11///
12/// Micha's zenphoton did not actually collide curves, curves were implemented only with their normals,
13///  which seemed to work well, although I would be lying if I said I understood why. It seems to me
14/// that more comtrollable results could be achieved with besier curves, but it seems the computational
15/// cost of adding these could be very high, which might explain zenphoton's use of a hack.
16use crate::geom::{Matrix, Point, Vector};
17use crate::material::Material;
18use crate::sampler::SamplerPoint;
19
20use rand::prelude::*;
21
22/// Segments represent boundries in space for light rays to interact with.
23///
24/// These are the primitive type that scenes are constructed from in rustic zen,
25/// and can be in the form of straight lines or 2nd order Bezier curves.
26///
27/// To create dynamic and interesting scenes segments are not constructed from static
28/// points, but instead `SamplerPoint`s, which are a wrapper around two of rustic zen's `Sampler`
29/// objects. (one for x and one for y).
30///
31/// As `SamplerPoint`s do not have a constructor, construction is indirect via a tuple of `Sampler`s.
32/// # Example:
33/// ```
34/// use rustic_zen::prelude::*;
35///
36/// let m = material::hqz_legacy_default();
37///
38/// // Construct SamplerPoints in place from a tuple of Samplers.
39/// let l =Segment::line_from_points(
40///     (Sampler::new_const(0.0),Sampler::new_const(0.0)),
41///     (Sampler::new_const(10.0),Sampler::new_const(10.0)),
42///     m.clone()
43/// );
44///
45/// // Samplers also support being constructed in place though so this can be simplified to
46/// let l2 = Segment::line_from_points((0.0,0.0), (10.0, 10.0), m);
47///
48/// // You'll likely get compaints about missing type "R" if you don't add
49/// // your Segments to the scene. Adding them constrains R.
50/// let _ = Scene::new(100, 100).with_object(l).with_object(l2);
51/// ```
52pub struct Segment<R: Rng> {
53    inner: Object<R>,
54}
55
56/// Holds a definition of an object
57///
58/// Interally contains the associated logic.
59#[derive(Clone)]
60pub(crate) enum Object<R: Rng> {
61    /// Straight line variant
62    Line {
63        /// Material used
64        material: Material<R>,
65        /// Starting Position
66        p0: SamplerPoint<R>,
67        /// End Position
68        p1: SamplerPoint<R>,
69    },
70    /// Curve Variant, Implemented as a Quadratic Bezier Curve
71    Curve {
72        /// Material used
73        material: Material<R>,
74        /// Bezier Start Locationa
75        p0: SamplerPoint<R>,
76        /// Bezier Curve Point
77        p1: SamplerPoint<R>,
78        /// Bezier End Location
79        p2: SamplerPoint<R>,
80    },
81}
82
83impl<R> Segment<R>
84where
85    R: Rng,
86{
87    /// Constructs a line from the given points and material.
88    pub fn line_from_points<A, B>(start: A, end: B, material: Material<R>) -> Self
89    where
90        A: Into<SamplerPoint<R>>,
91        B: Into<SamplerPoint<R>>,
92    {
93        let start = start.into();
94        let end = end.into();
95        Self {
96            inner: Object::Line {
97                material,
98                p0: start.into(),
99                p1: end.into(),
100            },
101        }
102    }
103
104    /// Constructs a Bezier curve from the given points, and material.
105    ///
106    /// __Note__: `mid` is the control point of the Bezier curve, the resulting path is not
107    /// guarenteed to pass though this point.  
108    pub fn curve_from_points<A, B, C>(start: A, mid: B, end: C, material: Material<R>) -> Self
109    where
110        A: Into<SamplerPoint<R>>,
111        B: Into<SamplerPoint<R>>,
112        C: Into<SamplerPoint<R>>,
113    {
114        let p0 = start.into();
115        let p1 = mid.into();
116        let p2 = end.into();
117        Self {
118            inner: Object::Curve {
119                material,
120                p0,
121                p1,
122                p2,
123            },
124        }
125    }
126}
127
128impl<R> Object<R>
129where
130    R: Rng,
131{
132    /**
133     * Returns a reference to the material used in this object
134     */
135    #[inline(always)]
136    pub(crate) fn process_material(
137        &self,
138        direction: &Vector,
139        normal: &Vector,
140        wavelength: f64,
141        alpha: f64,
142        rng: &mut R,
143    ) -> Option<Vector> {
144        let material = match self {
145            Object::Curve { material, .. } => material,
146            Object::Line { material, .. } => material,
147        };
148
149        (material)(direction, normal, wavelength, alpha, rng)
150    }
151
152    #[inline(always)]
153    fn get_line_hit(
154        s1: Point,
155        s2: Point,
156        origin: &Point,
157        dir: &Vector,
158    ) -> Option<(Point, Vector, f64)> {
159        let sd = s2 - s1;
160        let mat_a = Matrix {
161            a1: sd.x,
162            b1: -dir.x,
163            a2: sd.y,
164            b2: -dir.y,
165        };
166
167        let omega = origin.clone() - s1;
168
169        let result = match mat_a.inverse() {
170            Some(m) => m * omega,
171            None => {
172                return None; // Probably cos rays are parallel
173            }
174        };
175        if (result.x >= 0.0) && (result.x <= 1.0) && (result.y > 0.0) {
176        } else {
177            return None;
178        };
179
180        let alpha = result.x;
181        let distance = result.y;
182
183        let hit = *origin + (*dir * distance);
184        let norm = Vector { x: -sd.y, y: sd.x };
185
186        return Some((hit, norm, alpha));
187    }
188
189    #[inline(always)]
190    fn get_point_on_bezier(p0: Point, p1: Point, p2: Point, alpha: f64) -> Point {
191        let beta = 1.0 - alpha;
192        ((p0.v() * beta * beta) + (p1.v() * beta * alpha * 2.0) + (p2.v() * alpha * alpha)).p()
193    }
194
195    #[inline(always)]
196    fn get_normal_on_bezier(p0: Point, p1: Point, p2: Point, alpha: f64) -> Vector {
197        let w0 = (p1 - p0) * 2.0;
198        let w1 = (p2 - p1) * 2.0;
199        (w0 * (1.0 - alpha) + w1 * alpha).normal()
200    }
201
202    #[inline(always)]
203    fn process_curve_hit(
204        p0: Point,
205        p1: Point,
206        p2: Point,
207        origin: &Point,
208        dir: &Vector,
209        alpha: f64,
210    ) -> Option<(f64, Point)> {
211        if 0.0 >= alpha || alpha >= 1.0 {
212            return None;
213        }
214        let hit = Self::get_point_on_bezier(p0, p1, p2, alpha);
215        // bail out early if hit is behind emmitter;
216        let ray_alpha = (hit.x - origin.x) / dir.x;
217        if ray_alpha < 0.0 {
218            return None;
219        } else {
220            Some((ray_alpha, hit))
221        }
222    }
223
224    #[inline(always)]
225    fn get_curve_hit(
226        p0: Point,
227        p1: Point,
228        p2: Point,
229        origin: &Point,
230        dir: &Vector,
231    ) -> Option<(Point, Vector, f64)> {
232        // align coordinate system such that thee roots of the curve are the solutions.
233        let rotation_matrix = Matrix {
234            a1: dir.x,
235            a2: dir.y,
236            b1: dir.y,
237            b2: -dir.x,
238        };
239
240        let pa0 = rotation_matrix * (p0 - *origin).p();
241        let pa1 = rotation_matrix * (p1 - *origin).p();
242        let pa2 = rotation_matrix * (p2 - *origin).p();
243        let a = pa0.y;
244        let b = pa1.y;
245        let c = pa2.y;
246        let d = a - 2.0 * b + c;
247
248        if d.abs() > 0.0001 {
249            let m1 = -f64::sqrt(b * b - a * c);
250            let m2 = b - a;
251            let v1 = -(m1 + m2) / d;
252            let v2 = -(-m1 + m2) / d;
253            let r1 = Self::process_curve_hit(p0, p1, p2, origin, dir, v1);
254            let r2 = Self::process_curve_hit(p0, p1, p2, origin, dir, v2);
255            if r1.is_none() && r2.is_none() {
256                return None;
257            }
258            let (d1, hit1) = r1.unwrap_or((f64::MAX, (0.0, 0.0).into()));
259            let (d2, hit2) = r2.unwrap_or((f64::MAX, (0.0, 0.0).into()));
260            if d1 < d2 {
261                let norm = Self::get_normal_on_bezier(p0, p1, p2, v1);
262                Some((hit1, norm, v1))
263            } else {
264                let norm = Self::get_normal_on_bezier(p0, p1, p2, v2);
265                Some((hit2, norm, v2))
266            }
267        } else {
268            if b != c {
269                let t = (2.0 * b - c) / (2.0 * b - 2.0 * c);
270                let (_, hit) = Self::process_curve_hit(p0, p1, p2, origin, dir, t)?;
271                let norm = Self::get_normal_on_bezier(p0, p1, p2, t);
272                Some((hit, norm, t))
273            } else {
274                None
275            }
276        }
277    }
278
279    /**
280     * Tests if the inbound ray actually hit the object,
281     * if so it returns the coords of the hit, followed by the normal
282     * to the hit surface.
283     *
284     * If miss it returns None
285     *
286     * This test assumes you have done a box test to
287     * check the bounds of the line first
288     */
289    #[inline(always)]
290    pub(crate) fn get_hit(
291        &self,
292        origin: &Point,
293        dir: &Vector,
294        rng: &mut R,
295    ) -> Option<(Point, Vector, f64)> {
296        match self {
297            Object::Curve { p0, p1, p2, .. } => {
298                Self::get_curve_hit(p0.get(rng), p1.get(rng), p2.get(rng), origin, dir)
299            }
300            Object::Line { p0, p1, .. } => {
301                Self::get_line_hit(p0.get(rng), p1.get(rng), origin, dir)
302            }
303        }
304    }
305}
306
307impl<R> From<Segment<R>> for Object<R>
308where
309    R: Rng,
310{
311    fn from(value: Segment<R>) -> Self {
312        value.inner
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    type RandGen = rand_pcg::Pcg64Mcg;
319
320    use crate::material::hqz_legacy_default;
321
322    use super::Object;
323    use super::Segment;
324
325    use crate::geom::{Point, Vector};
326    use crate::material::hqz_legacy;
327
328    use rand::prelude::*;
329
330    #[test]
331    fn segment_into() {
332        let s = Segment::line_from_points((0.0, 0.0), (10.0, 10.0), hqz_legacy_default());
333        let _: Object<RandGen> = s.into();
334        // we can't actually check for equlaity on the object or any of it's fields, so if this compiles we assume all is good.
335    }
336
337    #[test]
338    /// Ray hits object test
339    /// Test result should be a hit at (5,5)
340    fn hit_line_1() {
341        let mut rng = RandGen::from_entropy();
342
343        let m = hqz_legacy(0.3, 0.3, 0.3);
344
345        let obj = Object::Line {
346            p0: (0.0, 0.0).into(),
347            p1: (10.0, 10.0).into(),
348            material: m,
349        };
350
351        let origin = Point { x: 10.0, y: 0.0 };
352        let dir = Vector { x: -1.0, y: 1.0 };
353
354        let a = obj.get_hit(&origin, &dir, &mut rng);
355
356        let (a, b, _) = a.expect("A was not meant to be `None`");
357
358        //assert hit is at (5,5)
359        assert_eq!(a.x, 5.0);
360        assert_eq!(a.y, 5.0);
361
362        //assert normal is (-10,10)
363        assert_eq!(b.x, -10.0);
364        assert_eq!(b.y, 10.0);
365        //normal is not normalised
366    }
367
368    #[test]
369    /// Ray hits object test
370    /// Test result should be a hit at (5,5)
371    fn hit_curve_1() {
372        let mut rng = RandGen::from_entropy();
373
374        let m = hqz_legacy(0.3, 0.3, 0.3);
375
376        let obj = Object::Curve {
377            p0: (0.0, 0.0).into(),
378            p1: (5.0, 5.0).into(),
379            p2: (10.0, 10.0).into(),
380            material: m,
381        };
382
383        let origin = Point { x: 0.0, y: 5.0 };
384        let dir = Vector { x: 1.0, y: 0.0 };
385
386        let a = obj.get_hit(&origin, &dir, &mut rng);
387
388        let (a, b, _) = a.expect("A was not meant to be `None`");
389
390        //assert hit is at (5,5)
391        assert_eq!(a.x, 5.0);
392        assert_eq!(a.y, 5.0);
393
394        // //assert normal is (-10,10)
395        assert_eq!(b.x, -10.0);
396        assert_eq!(b.y, 10.0);
397        //normal is not normalised
398    }
399
400    #[test]
401    /// Ray hits object test
402    /// Test result should be a hit at (5,5)
403    fn hit_curve_2() {
404        let mut rng = RandGen::from_entropy();
405
406        let m = hqz_legacy(0.3, 0.3, 0.3);
407
408        let obj = Segment::curve_from_points((0.0, 0.0), (5.0, 5.0), (10.0, 10.0), m).inner;
409
410        let origin = Point { x: 0.0, y: 10.0 };
411        let dir = Vector { x: 1.0, y: -1.0 };
412
413        let a = obj.get_hit(&origin, &dir, &mut rng);
414
415        let (a, b, _) = a.expect("A was not meant to be `None`");
416
417        //assert hit is at (5,5)
418        assert_eq!(a.x, 5.0);
419        assert_eq!(a.y, 5.0);
420
421        //assert normal is (-10,10)
422        assert_eq!(b.x, -10.0);
423        assert_eq!(b.y, 10.0);
424        //normal is not normalised
425    }
426
427    #[test]
428    /// Ray hits object test
429    /// Test result should be a hit at (5,5)
430    fn hit_curve_horz() {
431        let mut rng = RandGen::from_entropy();
432
433        let m = hqz_legacy(0.3, 0.3, 0.3);
434
435        let obj = Segment::curve_from_points((0.0, 5.0), (5.0, 5.0), (10.0, 5.0), m).inner;
436
437        let origin = Point { x: 5.0, y: 0.0 };
438        let dir = Vector { x: 0.0, y: 1.0 };
439
440        let a = obj.get_hit(&origin, &dir, &mut rng);
441
442        let (a, b, _) = a.expect("A was not meant to be `None`");
443
444        //assert hit is at (5,5)
445        assert_eq!(a.x.round(), 5.0);
446        assert_eq!(a.y.round(), 5.0);
447
448        //assert normal is (-10,10)
449        assert_eq!(b.x.round(), 0.0);
450        assert_eq!(b.y.round(), 10.0);
451        //normal is not normalised
452    }
453
454    #[test]
455    /// Ray hits object test
456    /// Test result should be a hit at (5,5)
457    fn hit_curve_vert() {
458        let mut rng = RandGen::from_entropy();
459
460        let m = hqz_legacy(0.3, 0.3, 0.3);
461
462        let obj = Segment::curve_from_points((5.0, 0.0), (5.0, 5.0), (5.0, 10.0), m).inner;
463
464        let origin = Point { x: 0.0, y: 5.0 };
465        let dir = Vector { x: 1.0, y: 0.0 };
466
467        let a = obj.get_hit(&origin, &dir, &mut rng);
468
469        let (a, b, _) = a.expect("A was not meant to be `None`");
470
471        //assert hit is at (5,5)
472        assert_eq!(a.x, 5.0);
473        assert_eq!(a.y, 5.0);
474
475        //assert normal is (-10,10)
476        assert_eq!(b.x.round(), -10.0);
477        assert_eq!(b.y.round(), 0.0);
478        //normal is not normalised
479    }
480
481    #[test]
482    /// Vector misses the object test
483    /// Test result should be None as Ray crosses dy/dx
484    /// Past the end of the object.
485    fn miss_line_1() {
486        let mut rng = RandGen::from_entropy();
487
488        let m = hqz_legacy(0.3, 0.3, 0.3);
489
490        let obj = Segment::line_from_points((0.0, 0.0), (10.0, 10.0), m).inner;
491
492        let origin = Point { x: 30.0, y: 0.0 };
493        let dir = Vector { x: -1.0, y: 1.0 };
494
495        let a = obj.get_hit(&origin, &dir, &mut rng);
496
497        assert!(a.is_none());
498    }
499
500    #[test]
501    /// Vector Going the wrong way test
502    /// Test result should be None, as Ray is going 180°
503    /// in the wrong direction to hit the object.
504    fn miss_line_2() {
505        let mut rng = RandGen::from_entropy();
506
507        let m = hqz_legacy(0.3, 0.3, 0.3);
508
509        let obj = Segment::line_from_points((0.0, 0.0), (10.0, 10.0), m).inner;
510
511        let origin = Point { x: 10.0, y: 0.0 };
512        let dir = Vector { x: 1.0, y: 1.0 };
513
514        let a = obj.get_hit(&origin, &dir, &mut rng);
515
516        assert!(a.is_none());
517    }
518
519    #[test]
520    /// Vector misses the object test
521    /// Test result should be None as Ray crosses dy/dx
522    /// Past the end of the object.
523    fn miss_curve_1() {
524        let mut rng = RandGen::from_entropy();
525
526        let m = hqz_legacy(0.3, 0.3, 0.3);
527
528        let obj = Segment::curve_from_points((0.0, 0.0), (5.0, 5.0), (10.0, 10.0), m).inner;
529
530        let origin = Point { x: 30.0, y: 0.0 };
531        let dir = Vector { x: -1.0, y: 1.0 };
532
533        let a = obj.get_hit(&origin, &dir, &mut rng);
534
535        assert!(a.is_none());
536    }
537
538    #[test]
539    /// Vector Going the wrong way test
540    /// Test result should be None, as Ray is going 180°
541    /// in the wrong direction to hit the object.
542    fn miss_curve_2() {
543        let mut rng = RandGen::from_entropy();
544
545        let m = hqz_legacy(0.3, 0.3, 0.3);
546
547        let obj = Segment::curve_from_points((0.0, 0.0), (5.0, 5.0), (10.0, 10.0), m).inner;
548
549        let origin = Point { x: 10.0, y: 0.0 };
550        let dir = Vector { x: 1.0, y: 1.0 };
551
552        let a = obj.get_hit(&origin, &dir, &mut rng);
553
554        assert!(a.is_none());
555    }
556}