В этой части мы создадим базовую структуру сферы и отобразим её в ppm
формате.
Оглавление
Проверка на пересечение между лучом и сферой
Для начала добавим обработку первого объекта в наш трассировщик лучей. Наиболее простым объектом для трассировки является сфера, определяемая уравнением:
В случае, если центр сферы расположен не в начале координат, а в точке , то последнее уравнение примет вид
в векторной форме:
где через обозначено скалярное произведение разности векторов и
Подставив в эту формулу вместо определяющий вектор луча в момент времени , получим
что, в свою очередь, можно переписать в виде следующего квадратного уравнения:
Последнее уравнение, в зависимости от значения его дискриминанта (), может иметь одно, два или ни одного решения, что позволит нам определить в какой момент времени луч пересекает (), касается сферы (), либо убедиться, что луч не пересекает сферу ().
Реализуем эту проверку в коде
// src/main.rs
fn hit_sphere(center: Vec3, radius: f64, ray: &Ray) -> bool {
let oc = ray.origin - center; // A - C
let a = ray.direction.dot(&ray.direction); // dot(B, B)
let b = 2_f64 * oc.dot(&ray.direction); // 2 * dot(A-C, B)
let c = oc.dot(&oc) - radius * radius; // dot(A-C, A-C) - R * R
(b * b - 4_f64 * a * c) > 0_f64
}
и отметим красным те точки, в которых луч пересекает сферу
// src/main.rs
fn color(ray: &Ray) -> Vec3 {
if (hit_sphere(Vec3::new(0_f64, 0_f64, -1_f64), 0.5_f64, ray)) {
return Vec3::new(1_f64, 0_f64, 0_f64);
}
let unit_direction = ray.direction.unit_vector();
let t = 0.5_f64 * (unit_direction.y() + 1_f64);
(1_f64 - t) * Vec3::new(1_f64, 1_f64, 1_f64) + t * Vec3::new(0.5_f64, 0.7_f64, 1.0_f64)
}
После запуска программы получим следующий результат:
Мы успешно отобразили точки пересечения сферы с центром в точке с нашим виртуальным лучом. Однако если мы попробуем запустить ту же программу для сферы с центром в то мы получим то же изображение, так как пока не учитывается условие что позволяет “видеть” объекты, находящиеся у нас за “спиной”. Это не является желаемым поведение и будет исправлено далее.
Вычисление нормали к поверхности сферы
Далее изменим функцию hit_sphere
так, чтобы она возвращала момент времени первого пересечения луча со сферой:
// scr/main.rs
fn hit_sphere(center: Vec3, radius: f64, ray: &Ray) -> Option<f64> {
let oc = ray.origin - center;
let a = ray.direction.dot(&ray.direction);
let b = 2_f64 * oc.dot(&ray.direction);
let c = oc.dot(&oc) - radius * radius;
let discriminant = (b * b - 4_f64 * a * c);
if (discriminant < 0_f64) {
None
} else {
Some((-b - discriminant.sqrt()) / (2_f64 * a)) // - так как мы хотим получить момент первого пересечения
}
}
Теперь вместо bool
, мы возвращаем Option<f64>
, которое равно None
в случае, если нет пересечения со сферой и Some
в противном случае. Some
содержит в себе момент первого пересечения луча со сферой.
Далее раскрасим сферу согласно направлению нормалей к её поверхности. Вычислить нормаль сферы достаточно просто, так как она вычисляется просто как нормализованный вектор
// scr/main.rs
fn color(ray: &Ray) -> Vec3 {
match hit_sphere(Vec3::new(0_f64, 0_f64, -1_f64), 0.5_f64, ray) {
Some(t) => {
let n = (ray.point_at(t) - Vec3::new(0_f64, 0_f64, -1_f64)).unit_vector();
return 0.5_f64 * Vec3::new(n.x() + 1_f64, n.y() + 1_f64, n.z() + 1_f64);
}
None => (),
}
...
}
В функции color
мы применили тот же приём, что и при вычислении значения t
для закраски фона - так как нормаль является вектором единичной длины со значениями компонент из отрезка то добавив к каждой её компоненте и разделив пополам мы приведём из к значениям из интервала Последнее позволяет нам сопоставить каждой точке на поверхности сферы цвет, отвечающий направлению нормали к сфере в этой точке.
Отображение нескольких объектов
Давайте теперь отобразим несколько сфер. Можно просто создать вектор сфер внутри функции color
, однако, лучше использовать более общий подход. Создадим трейт Hittable
, который будет отвечать за проверку на пересечение между лучом и любым объектом, для которого верна характеристика Hittable
.
// src/obj/mod.rs
use crate::structs::ray::Ray;
use crate::structs::vec3::Vec3;
pub trait Hittable {
fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord>;
}
/// Record of hit event between hittable object and a ray
#[derive(Debug, Clone, Copy)]
pub struct HitRecord {
/// Time when the hit had happened
pub t: f64,
/// Point where the ray had hit the object
pub p: Vec3,
/// Normal to the object hit with ray in the point of the hit
pub n: Vec3,
}
Функция hit
возвращает возможное пересечение луча с объектом, если пересечения нет, то она вернёт None
. Структура HitRecord
состоит из момента времени пересечения луча с объектом t
, координат пересечения луча с объектом p
и нормали к объекту в этой точке n
.
Далее реализуем структуру сферы и трейт Hittable
для неё.
// src/obj/sphere.rs
use super::HitRecord;
use super::Hittable;
use crate::structs::ray::Ray;
use crate::structs::vec3::Vec3;
mod tests {
use super::*;
#[test]
fn hit_test() {
let s = Sphere::new(Vec3::new(0_f64, 0_f64, -1_f64), 0.5);
let r = Ray::new(Vec3::default(), s.center);
let m_r = Ray::new(Vec3::default(), -1_f64 * s.center);
let hit = s.hit(&r, 0_f64, std::f64::MAX);
let miss = s.hit(&m_r, 0_f64, std::f64::MAX);
assert_eq!(hit.is_some(), true);
assert_eq!(miss.is_none(), true);
}
}
#[derive(Debug, Clone, Copy)]
pub struct Sphere {
pub center: Vec3,
pub radius: f64,
}
impl Sphere {
pub fn new(center: Vec3, radius: f64) -> Self {
Self {
center: center,
radius: radius,
}
}
}
impl Hittable for Sphere {
fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
let oc = ray.origin - self.center;
let a = ray.direction.dot(&ray.direction);
let b = oc.dot(&ray.direction);
let c = oc.dot(&oc) - self.radius * self.radius;
let discriminant = b * b - a * c;
if discriminant > 0_f64 {
let t_fi = (-b - discriminant.sqrt()) / a;
if t_fi > t_min && t_fi < t_max {
let p_fi = ray.point_at(t_fi);
return Some(HitRecord {
t: t_fi,
p: p_fi,
n: (p_fi - self.center) / self.radius,
});
}
let t_si = (-b + discriminant.sqrt()) / a;
if t_si > t_min && t_si < t_max {
let p_si = ray.point_at(t_si);
return Some(HitRecord {
t: t_si,
p: p_si,
n: (p_si - self.center) / self.radius,
});
}
}
None
}
}
Обратите внимание на use
блоки в начале файла, до структур мы добираемся через спецификатор crate
, а до HitRecord, Hittable
через super
. Метод hit
по сути повторяет функционал метода hit_sphere
с тем отличием, что в зависимости от рассматриваемого промежутка времени мы получим либо первое пересечение со сферой, либо второе (ранее мы всегда брали первое).
Также реализуем структуру для представления нашей сцены - набора объектов с которыми мы ищем пересечение:
// src/obj/hittablelist.rs
use super::HitRecord;
use super::Hittable;
use crate::structs::ray::Ray;
pub struct HittableList {
pub objects: Vec<Box<dyn Hittable>>,
}
impl Hittable for HittableList {
fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
let mut current_hit: Option<HitRecord> = None;
let mut best_hit_time = t_max;
for object in &self.objects {
let hit_candidate = object.hit(ray, t_min, best_hit_time);
match hit_candidate {
Some(hit) => {
best_hit_time = hit.t;
current_hit = Some(hit);
}
None => (),
}
}
current_hit
}
}
Обратите внимание, что поле objects
имеет тип Vec<Box<dyn Hittable>>
, а не Vec<dyn Hittable>
, причиной этого является то, что Vec
хранит объекты одинакового размера, а объекты, для которых реализована характеристика Hittable
могут иметь произвольный размер. Однако ничего нам не мешает иметь вектор указателей на области в куче, где лежат нашит объекты, ведь указатели будут иметь одинаковый размер. Отметим также, что здесь мы применили паттерн матчинг, чтобы обработать случаи, когда было найдено пересечение с одним из объектов в коллекции.
Далее добавим в src/obj/mod.rs
экспорт только что реализованных типов:
// src/obj/mod.rs
...
pub mod hittablelist;
pub mod sphere;
...
Теперь перепишем функцию color
использовав реализованный функционал:
// src/obj/main.rs
fn color(ray: &Ray, world: &HittableList) -> Vec3 {
match world.hit(ray, 0_f64, std::f64::MAX) {
Some(hit) => 0.5_f64 * Vec3::new(hit.n.x() + 1_f64, hit.n.y() + 1_f64, hit.n.z() + 1_f64),
None => {
let unit_direction = ray.direction.unit_vector();
let t = 0.5_f64 * (unit_direction.y() + 1_f64);
(1_f64 - t) * Vec3::new(1_f64, 1_f64, 1_f64) + t * Vec3::new(0.5_f64, 0.7_f64, 1.0_f64)
}
}
}
Если обнаружено пересечение, то закрашиваем в соответствии с направлением нормали в точки пересечения, иначе закрашиваем традиционным бело-голубым градиентом.
// src/obj/main.rs
fn main {
...
let world = HittableList {
objects: vec![
Box::new(Sphere::new(Vec3::new(0_f64, 0_f64, -1_f64), 0.5)),
Box::new(Sphere::new(Vec3::new(0_f64, -100.5_f64, -1_f64), 100_f64)),
],
};
let col = color(&r, &world);
...
}
В функции main
мы добавили на сцену две сферы, одну из прошлых примеров и другую заметно большую, которая будет выступать в роли “земли”.
Приложения
Примечание
Данная заметка написана в рамках реализации трассировки лучей на Rust. Остальные статьи из этой серии можно найти по следующему тегу или в первой публикации из цикла .
Исходный код проекта доступен на github.