1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
use rand::{rngs::StdRng, Rng};
use rand_distr::UnitDisc;

use super::{HitRecord, Ray, Shape};
use crate::kdtree::{Bounded, BoundingBox};

/// A unit sphere centered at the origin
pub struct Sphere;

#[allow(clippy::many_single_char_names)]
impl Shape for Sphere {
    fn intersect(&self, ray: &Ray, t_min: f64, record: &mut HitRecord) -> bool {
        // Translated directly from the GLOO source code, assuming radius = 1
        let a = glm::length2(&ray.dir);
        let b = 2.0 * glm::dot(&ray.dir, &ray.origin);
        let c = glm::length2(&ray.origin) - 1.0;

        let d = b * b - 4.0 * a * c;
        if d.is_sign_negative() {
            return false;
        }

        let d = d.sqrt();
        let t_plus = (-b + d) / (2.0 * a);
        let t_minus = (-b - d) / (2.0 * a);
        let t = if t_minus < t_min {
            if t_plus < t_min {
                return false;
            }
            t_plus
        } else {
            t_minus
        };

        if t < record.time {
            record.time = t;
            record.normal = ray.at(t).normalize();
            true
        } else {
            false
        }
    }

    /// Sample a spherical light source, somewhat respecting the solid angle from a target point
    ///
    /// Currently, this implementation just generates a random point in the hemisphere facing
    /// the target point, weighted by the cosine. This isn't the most sophisticated technique,
    /// since you can sample the solid angle exactly, but it's pretty good.
    fn sample(&self, target: &glm::DVec3, rng: &mut StdRng) -> (glm::DVec3, glm::DVec3, f64) {
        let [x, y]: [f64; 2] = rng.sample(UnitDisc);
        let z = (1.0 - x * x - y * y).sqrt();
        let n = target.normalize();
        let n1 = if n.x.is_normal() {
            glm::vec3(n.y, -n.x, 0.0).normalize()
        } else {
            glm::vec3(0.0, -n.z, n.y).normalize()
        };
        let n2 = n1.cross(&n);
        let p = x * n1 + y * n2 + z * n;
        (p, p, z * std::f64::consts::FRAC_1_PI)
    }
}

impl Bounded for Sphere {
    fn bounding_box(&self) -> BoundingBox {
        BoundingBox {
            p_min: glm::vec3(-1.0, -1.0, -1.0),
            p_max: glm::vec3(1.0, 1.0, 1.0),
        }
    }
}