Search code examples
rustraytracing

Ray Tracing In One Weekend - Refraction issues


I'm, currently working through Ray Tracing In One Weekend to get familiar with rust. Adding the dielectric material (glass) is causing me some headaches: My refraction isn't flipping upside down! 1

Here's the code I'm using for my Vector struct:

impl Vec3 {
    pub fn new(x: f64, y: f64, z: f64) -> Vec3 { Vec3 {x, y, z} }
    pub fn x(&self) -> f64 { self.x }
    pub fn y(&self) -> f64 { self.y }
    pub fn z(&self) -> f64 { self.z }
    pub fn length_squared(&self) -> f64 {
        self.x * self.x + self.y * self.y + self.z * self.z
    }
    pub fn length(&self) -> f64 { self.distance(&Vec3::default()) }
    pub fn unit_vector(&self) -> Vec3 {
        let length = self.length();
        Vec3::new(self.x / length, self.y / length, self.z / length)
    }
    pub fn dot(&self, v:&Vec3) -> f64 {
        self.x * v.x + self.y * v.y + self.z * v.z
    }
    pub fn cross(&self, v:&Vec3) -> Vec3 {
        Vec3::new(
            self.y * v.z - self.z * v.y,
            self.z * v.x - self.x * v.z,
            self.x * v.y - self.y * v.x
        )
    }
    pub fn distance(&self, other: &Vec3) -> f64 {
        let dx = self.x - other.x();
        let dy = self.y - other.y();
        let dz = self.z - other.z();
        (dx * dx + dy * dy + dz * dz).sqrt()
    }

    pub fn random(min: f64, max:f64) -> Self {
        let between = Uniform::from(min..max);
        let mut rng = rand::thread_rng();
        Vec3::new(
            between.sample(&mut rng),
            between.sample(&mut rng),
            between.sample(&mut rng))
    }
    pub fn random_in_unit_sphere() -> Self {
        loop {
            let v = Vec3::random(-1.0, 1.0);
            if v.length_squared() < 1.0 {
                return v;
            }
        }
    }
    pub fn random_in_hemisphere(normal: &Vec3) -> Self {
        let vec = Vec3::random_in_unit_sphere();
        if vec.dot(normal) > 0.0 {
            vec
        } else {
            -vec
        }
    }
    pub fn random_unit_vector() -> Self { Vec3::random_in_unit_sphere().unit_vector() }
    pub fn near_zero(&self) -> bool {
        const MAXIMUM_DISTANCE_FROM_ZERO:f64 = 1e-8;
        self.x.abs() < MAXIMUM_DISTANCE_FROM_ZERO &&
            self.y.abs() < MAXIMUM_DISTANCE_FROM_ZERO &&
            self.z.abs() < MAXIMUM_DISTANCE_FROM_ZERO
    }
    pub fn reflected(&self, normal: &Vec3) -> Vec3 {
        let dp = self.dot(normal);
        let dp = dp * 2.0 * (*normal);
        *self - dp
    }
    pub fn refract(&self, normal: &Vec3, etai_over_etat: f64) -> Vec3 {
        let dot = (-(*self)).dot(normal);
        let cos_theta = dot.min(1.0);
        let out_perp = etai_over_etat * ((*self) + cos_theta * (*normal));
        let inner =  1.0 - out_perp.length_squared();
        let abs = inner.abs();
        let r = -(abs.sqrt());
        let out_parallel = r * (*normal);
        out_perp + out_paralle
    }
}

And this is my scatter function for the material:

fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option<Ray>, Color)> {
        let refraction_ratio = if hit_record.front_face {
            1.0/self.index_of_refraction
        } else {
            self.index_of_refraction
        };
        let unit_direction = ray.direction().unit_vector();
        let cos_theta = ((-unit_direction).dot(&hit_record.normal)).min(1.0);
        let sin_theta = (1.0 - cos_theta*cos_theta).sqrt();
        let cannot_refract = refraction_ratio * sin_theta > 1.0;
        let reflectance = Dielectric::reflectance(cos_theta, refraction_ratio);
        let mut rng = rand::thread_rng();
        let color = Color::new(1.0, 1.0, 1.0);
        if cannot_refract || reflectance > rng.gen::<f64>() {
            let reflected = unit_direction.reflected(&hit_record.normal);
            let scattered = Ray::new(hit_record.point, reflected);
            Some((Some(scattered), color))
        } else {
            let direction = unit_direction.refract(&hit_record.normal, refraction_ratio);
            let scattered = Ray::new(hit_record.point, direction);
            Some((Some(scattered), color))
        }
    }

It sort of works if I negate x and y of the refract-result, but still looks obviously wrong. Additionally, if I go a few steps back in the book and implement the 100% refraction glass, my sphere's are solid black, and I have to negate the z axis to see anything at all. So something is amiss with my refraction code, but I can't figure it out

2

Full code at: https://phlaym.net/git/phlaym/rustracer/src/commit/89a2333644a82f2645e4ad554eadf7d4f142f2c0/src


Solution

  • In the method src/hittable.rs which checks if a sphere is hit, the c code looks like this.

    // Find the nearest root that lies in the acceptable range.
    auto root = (-half_b - sqrtd) / a;
    if (root < t_min || t_max < root) {
        root = (-half_b + sqrtd) / a;
        if (root < t_min || t_max < root)
            return false;
    }
    

    You have ported it to rust code with the following listing:

    let root = (-half_b - sqrtd) / a;
    if root < t_min || t_max < root {
        let root = (-half_b + sqrtd) / a;
        if root < t_min || t_max < root {
           return None;
        }
    }
    

    The problem here is the second let root. You have created a new variable with its own scope for the inner brackets but not changed the already created variable defined before. To do this you have to make it mutable.

    let mut root = (-half_b - sqrtd) / a;
    if root < t_min || t_max < root {
        root = (-half_b + sqrtd) / a;
        if root < t_min || t_max < root {
           return None;
        }
    }
    

    Additionally I changed the following in src/ray.rs

    return match scattered {
        Some((scattered_ray, albedo)) => {
            match scattered_ray {
                Some(sr) => {
                    albedo * sr.pixel_color(world, depth-1)
                },
                None => albedo
            }
        },
        None => { return Color::default() }
    };
    

    to match the corresponding C code. Be aware of the unwrap used.

    let scattered = rect.material.scatter(self, &rect);
    if let Some((scattered_ray, albedo)) = scattered {
        return albedo * scattered_ray.unwrap().pixel_color(world, depth - 1)
    }
    return Color::default()
    

    And remove your tries to correct the reflections:

    let reflected = Vec3::new(-reflected.x(), reflected.y(), -reflected.z());
    

    correct glass sphere