Трассировка лучей на Rust. Часть 5. Диэлектрики.

2020-01-05 • edited 2021-02-06

В этой части список поддерживаемых материалов пополнится диэлектриками.

Оглавление

Преломление света

Преломлением называется изменение направления луча, возникающее на границе двух сред. Оно возникает, когда фазовые скорости электромагнитных волн в контактирующих средах различаются. Отношение фазовых скоростей света в вакууме и в данной среде называется показателем преломления $$n = c/v,$$ где $$c$$ - фазовая скорость света в вакууме. Различные материалы обладают разными показателями преломления, например:

СредаЗначение
Алмаз белый2.417
Берилл1.571—1.599
Изумруд1.588—1.595
Воздух1

Для описания преломления света воспользуемся законом Снеллиуса (Snell’s law ), согласно которому $$n_1 sin \Theta_1 = n_2 sin \Theta_2.$$

snell_law

Пусть $$\overline{u}$$ - входящий вектор, $$\overline{n}$$ - нормаль в точке касания, $$\overline{n}_{\bot}$$ - вектор, перпендикулярный нормали, $$\overline{r}$$ - преломлённый луч, $$n_1, n_2$$ - коэффициенты преломления между средами.

$$ \overline{u} = -\cos{\Theta_1}\overline{n}+\sin{\Theta_1}\overline{n}_{\bot} $$

$$ \overline{r} = -\cos{\Theta_2}\overline{n}+\sin{\Theta_2}\overline{n}_{\bot} $$

Выразим $$\overline{n}_{\bot}$$ через вектора $$\overline{u}$$ и $$\overline{r}$$:

$$ \overline{n}_{\bot} = \cosec{\Theta_1}\overline{u} + \ctg{\Theta_1}\overline{n} $$

$$ \overline{n}_{\bot} = \cosec{\Theta_2}\overline{r} + \ctg{\Theta_2}\overline{n} $$

Из последнего получим соотношение между $$\overline{r}$$ и $$\overline{u}:$$

$$ \cosec{\Theta_1}\overline{u} + \ctg{\Theta_1}\overline{n} = \cosec{\Theta_2}\overline{r} + \ctg{\Theta_2}\overline{n}, $$

$$ \overline{r} = \frac{\sin{\Theta_2}}{\sin{\Theta_1}}\overline{u}+(\cos{\Theta_1}\frac{\sin{\Theta_2}}{\sin{\Theta_1}}-\cos{\Theta_2})\overline{n}. $$

Из закона Снеллиуса следует, что

$$ \overline{r} = \frac{n_1}{n_2}\overline{u}+(\cos{\Theta_1}\frac{n_1}{n_2}-\cos{\Theta_2})\overline{n}, $$

$$ \overline{r} = \frac{n_1}{n_2}\overline{u}+\left(\cos{\Theta_1}\frac{n_1}{n_2}-\sqrt{1-\sin^2{\Theta_2}}\right)\overline{n}. $$

В силу того, что $$\cos{\Theta_1} = \overline{u} \cdot \overline{n}$$, а $$\sin{\Theta_2} = n_1\sqrt{1-\cos^2{\Theta_1}}/n_2:$$

$$ \overline{r} = r_i\overline{u}+\left(r_i\overline{u} \cdot \overline{n}-\sqrt{1-r^2_i + r^2_i(\overline{u} \cdot \overline{n})^2}\right)\overline{n}, $$

где $$r_i=n_1/n_2.$$

Теперь мы можем реализовать функцию расчёта преломлённого луча

// /src/materials/mod.rs

fn refract(v: &Vec3, n: &Vec3, ni_over_nt: f64) -> Option<Vec3> {
    let uv = v.unit_vector();
    let dt = uv.dot(n);
    let discriminant = 1_f64 - ni_over_nt * ni_over_nt * (1_f64 - dt * dt);

    if discriminant > 0_f64 {
        Some(ni_over_nt * (uv - *n * dt) - *n * discriminant.sqrt())
    } else {
        None
    }
}

Диэлектрики

Прозрачные материалы, такие как вода, стекло, алмазы являются диэлектриками. Когда луч попадает на поверхность диэлектрика он расщепляется на два: отражённый луч и преломлённый луч. Чтобы смоделировать это поведение мы будем случайно выбирать в качестве производного луча отраженный, либо преломлённый луч.

Сначала реализуем диэлектрик, который всегда, при возможности, преломляет проходящий через него луч:

// /src/materials/dielectric.rs

use crate::structs::ray::Ray;
use crate::structs::vec3::Vec3;

use super::Scatterable;
use crate::obj::HitRecord;

#[derive(Debug, Clone)]
pub struct Dielectric {
    pub reflection_index: f64,
}

impl Scatterable for Dielectric {
    fn scatter(
        &self,
        ray: &Ray,
        hit: &HitRecord,
        _rng: &mut impl Iterator<Item = f64>,
    ) -> Option<(Ray, Vec3)> {
        let (outward_normal, ni_over_nt) = if ray.direction.dot(&hit.n) > 0_f64 {
            (-1_f64 * hit.n, self.reflection_index)
        } else {
            (hit.n, 1_f64 / self.reflection_index)
        };

        match super::refract(&ray.direction, &outward_normal, ni_over_nt) {
            Some(refracted) => Some((Ray::new(hit.p, refracted), Vec3::new(1_f64, 1_f64, 1_f64))),
            None => Some((
                Ray::new(hit.p, super::reflect(&ray.direction, &hit.n)),
                Vec3::new(1_f64, 1_f64, 1_f64),
            )),
        }
    }
}

Корректирующий показатель для диэлектрика всегда равен $$(1,1,1)$$, так как мы полагаем, что наша поверхность не поглощает энергию входящего луча.

Изменив материал одной из сфер на диэлектрик с индексом преломления равным 1.5, получим следующий результат:

glass1

В реальности коэффициент отражения для прозрачных поверхностей зависит от угла, под которым наблюдатель смотрит на объект. Популярная аппроксимация этого явления была предложена Шликом (Schlick’s approximation ).

// /src/materials/mod.rs

fn schlick(cosine: f64, reflection_index: f64) -> f64 {
    let mut r0 = (1_f64 - reflection_index) / (1_f64 + reflection_index);

    r0 = r0 * r0;

    r0 + (1_f64 - r0) * (1_f64 - cosine).powi(5)
}

Далее доработаем функцию генерации производного луча для диэлектриков

// /src/materials/dielectric.rs

fn scatter(
        &self,
        ray: &Ray,
        hit: &HitRecord,
        rng: &mut impl Iterator<Item = f64>,
    ) -> Option<(Ray, Vec3)> {
        let (outward_normal, ni_over_nt, cosine) = if ray.direction.dot(&hit.n) > 0_f64 {
            (
                -1_f64 * hit.n,
                self.reflection_index,
                self.reflection_index * ray.direction.dot(&hit.n) / ray.direction.length(),
            )
        } else {
            (
                hit.n,
                1_f64 / self.reflection_index,
                -ray.direction.dot(&hit.n) / ray.direction.length(),
            )
        };

        let (refracted, reflection_probability) =
            match super::refract(&ray.direction, &outward_normal, ni_over_nt) {
                Some(refracted) => (
                    Some(Ray::new(hit.p, refracted)),
                    super::schlick(cosine, self.reflection_index),
                ),
                None => (None, 1_f64),
            };

        if refracted.is_none() {
            return Some((
                Ray::new(hit.p, super::reflect(&ray.direction, &hit.n)),
                Vec3::new(1_f64, 1_f64, 1_f64),
            ));
        } else {
            if rng.next().unwrap() < reflection_probability {
                return Some((
                    Ray::new(hit.p, super::reflect(&ray.direction, &hit.n)),
                    Vec3::new(1_f64, 1_f64, 1_f64),
                ));
            } else {
                return Some((refracted.unwrap(), Vec3::new(1_f64, 1_f64, 1_f64)));
            }
        }
    }

Отметим, что, задав радиус сферы отрицательным числом, мы изменим направление нормалей, это позволит смоделировать полый объект добавив ещё одну сферу отрицательного радиуса:

empty

// /src/main.rs

let world = HittableList {
        objects: vec![
            Box::new(Sphere::new(
                Vec3::new(0_f64, 0_f64, -1_f64),
                0.5,
                Material::Lambertian(Lambertian {
                    albedo: Vec3::new(0.8_f64, 0.3_f64, 0.3_f64),
                }),
            )),
            Box::new(Sphere::new(
                Vec3::new(0_f64, -100.5_f64, -1_f64),
                100_f64,
                Material::Lambertian(Lambertian {
                    albedo: Vec3::new(0.8_f64, 0.8_f64, 0_f64),
                }),
            )),
            Box::new(Sphere::new(
                Vec3::new(1_f64, 0_f64, -1_f64),
                0.5_f64,
                Material::Metal(Metal {
                    albedo: Vec3::new(0.8_f64, 0.8_f64, 0.8_f64),
                    fuzz: 0_f64,
                }),
            )),
            Box::new(Sphere::new(
                Vec3::new(-1_f64, 0_f64, -1_f64),
                0.5_f64,
                Material::Dielectric(Dielectric {
                    reflection_index: 1.5,
                }),
            )),
            Box::new(Sphere::new(
                Vec3::new(-1_f64, 0_f64, -1_f64),
                -0.45_f64,
                Material::Dielectric(Dielectric {
                    reflection_index: 1.5,
                }),
            )),
        ],
    };

Приложения

Примечание

Данная заметка написана в рамках реализации трассировки лучей на Rust. Остальные статьи из этой серии можно найти по следующему тегу или в первой публикации из цикла .

Исходный код проекта доступен на github.

Полезные материалы

  1. Лекции по программированию на Rust от Computer Science Center.
  2. Programming Rust: Fast, Safe Systems Development.
  3. Серия книг про трассировку лучей.
developmentrustdevelopmentstudyraytracerrustraytracer
License: MIT

Трассировка лучей на Rust. Часть 6. Камера.

Трассировка лучей на Rust. Часть 4. Сглаживание и материалы.

comments powered by Disqus