Трассировка лучей на Rust. Часть 1. Вектор.

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

В этой части мы создадим базовую структуру вектора и основные методы для работы с векторами.

Оглавление

Реализация базовой структуры вектора

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

#[derive(Debug, Copy, Clone)]
pub struct Vec3 {
    pub components: [f64; 3],
}

Каждый вектор определяется тремя своими компонентами, записью #[derive(Debug, Copy, Clone)] мы просим компилятор Rust реализовать за нас полезные трейты Debug, Clone, Copy. Трейт Debug отвечает за вывод отладночной информации, а именно

let v = Vec3 {
        components: [1_f64, 1_f64, 1_f64],
    };
println!("v is: {:?}", v);

выведет

v is: Vec3 { components: [1.0, 1.0, 1.0] }

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

Расширение функционала вектора

Чтобы не обращаться к элементам вектора через его поле components добавим несколько человекопонятных обёрток:

impl Vec3 {
    pub fn new(c1: f64, c2: f64, c3: f64) -> Vec3 {
        Vec3 {
            components: [c1, c2, c3],
        }
    }

    pub fn unitize(&mut self) {
        *self /= self.length();
    }

    pub fn unit_vector(&self) -> Self {
        *self / self.length()
    }

    pub fn dot(&self, other: &Self) -> f64 {
        self.x() * other.x() + self.y() * other.y() + self.z() * other.z()
    }

    pub fn cross(&self, other: &Self) -> Self {
        Vec3 {
            components: [
                self.y() * other.z() - self.z() * other.y(),
                self.z() * other.x() - self.x() * other.z(),
                self.x() * other.y() - self.y() * other.x(),
            ],
        }
    }

    pub fn x(&self) -> f64 {
        self.components[0]
    }

    pub fn y(&self) -> f64 {
        self.components[1]
    }

    pub fn z(&self) -> f64 {
        self.components[2]
    }

    pub fn r(&self) -> f64 {
        self.components[0]
    }

    pub fn g(&self) -> f64 {
        self.components[1]
    }

    pub fn b(&self) -> f64 {
        self.components[2]
    }

    pub fn length2(&self) -> f64 {
        self.x() * self.x() + self.y() * self.y() + self.z() * self.z()
    }

    pub fn length(&self) -> f64 {
        self.length2().sqrt()
    }
}

Операции над векторами

Далее реализуем некоторые полезные операции над векторами и напишем для них тесты

Сложение двух векторов
impl Add for Vec3 {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self {
            components: [
                self.x() + other.x(),
                self.y() + other.y(),
                self.z() + other.z(),
            ],
        }
    }
}
#[test]
    fn add_test() {
        assert_eq!(
            Vec3 {
                components: [1_f64, 1_f64, 1_f64]
            } + Vec3 {
                components: [1_f64, 2_f64, 3_f64]
            },
            Vec3 {
                components: [2_f64, 3_f64, 4_f64]
            }
        );
    }

В результате получаем новый вектор, причём благодаря Copy трейту исходные вектора фактически не потребляются, хоть и передаются по self, таким образом вместо move (передачи владения) выполняется copy.

Вычитание двух векторов
impl Sub for Vec3 {
    type Output = Self;

    fn sub(self, other: Self) -> Self {
        Self {
            components: [
                self.x() - other.x(),
                self.y() - other.y(),
                self.z() - other.z(),
            ],
        }
    }
}
#[test]
    fn sub_test() {
        assert_eq!(
            Vec3 {
                components: [1_f64, 1_f64, 1_f64]
            } - Vec3 {
                components: [1_f64, 2_f64, 3_f64]
            },
            Vec3 {
                components: [0_f64, -1_f64, -2_f64]
            }
        );
    }

В результате получаем новый вектор, причём благодаря Copy трейту исходные вектора фактически не потребляются, хоть и передаются по self, таким образом вместо move (передачи владения) выполняется copy.

Умножение векторов
impl Mul for Vec3 {
    type Output = Self;

    fn mul(self, rhs: Self) -> Self {
        Self {
            components: [self.x() * rhs.x(), self.y() * rhs.y(), self.z() * rhs.z()],
        }
    }
}
#[test]
fn mul_test() {
    assert_eq!(
        Vec3 {
            components: [1_f64, 1_f64, 1_f64]
        } * Vec3 {
            components: [1_f64, 2_f64, 3_f64]
        },
        Vec3 {
            components: [1_f64, 2_f64, 3_f64]
        }
    );
}

В результате получаем новый вектор, причём благодаря Copy трейту исходные вектора фактически не потребляются, хоть и передаются по self, таким образом вместо move (передачи владения) выполняется copy.

Но что, если мы хотим умножить вектор на какой-то скаляр? В этом случае нужно реализовать тот же трейт, но уже для f64.

impl Mul<f64> for Vec3 {
    type Output = Self;

    fn mul(self, rhs: f64) -> Self {
        Self {
            components: [self.x() * rhs, self.y() * rhs, self.z() * rhs],
        }
    }
}

impl Mul<Vec3> for f64 {
    type Output = Vec3;

    fn mul(self, rhs: Vec3) -> Vec3 {
        Vec3 {
            components: [self * rhs.x(), self * rhs.y(), self * rhs.z()],
        }
    }
}
#[test]
fn mul_scalar_test() {
    assert_eq!(
        Vec3 {
            components: [1_f64, 1_f64, 1_f64]
        } * 2_f64,
        Vec3 {
            components: [2_f64, 2_f64, 2_f64]
        }
    );
}

#[test]
fn mul_left_scalar_test() {
    assert_eq!(
        4_f64
            * Vec3 {
                components: [1_f64, 2_f64, 3_f64]
            },
        Vec3 {
            components: [4_f64, 8_f64, 12_f64]
        }
    );
}

Деление векторов
impl Div for Vec3 {
    type Output = Self;

    fn div(self, rhs: Self) -> Self {
        Self {
            components: [self.x() / rhs.x(), self.y() / rhs.y(), self.z() / rhs.z()],
        }
    }
}
#[test]
fn div_test() {
    assert_eq!(
        Vec3 {
            components: [2_f64, 4_f64, 6_f64]
        } / Vec3 {
            components: [2_f64, 2_f64, 2_f64]
        },
        Vec3 {
            components: [1_f64, 2_f64, 3_f64]
        }
    );
}

В результате получаем новый вектор, причём благодаря Copy трейту исходные вектора фактически не потребляются, хоть и передаются по self, таким образом вместо move (передачи владения) выполняется copy.

Но что, если мы хотим разделить вектор на какой-то скаляр? В этом случае нужно реализовать тот же трейт, но уже для f64.

impl Div<f64> for Vec3 {
    type Output = Self;

    fn div(self, rhs: f64) -> Self {
        Self {
            components: [self.x() / rhs, self.y() / rhs, self.z() / rhs],
        }
    }
}

impl Div<Vec3> for f64 {
    type Output = Vec3;

    fn div(self, rhs: Vec3) -> Vec3 {
        Vec3 {
            components: [self / rhs.x(), self / rhs.y(), self / rhs.z()],
        }
    }
}
#[test]
fn div_scalar_test() {
    assert_eq!(
        Vec3 {
            components: [2_f64, 4_f64, 6_f64]
        } / 2_f64,
        Vec3 {
            components: [1_f64, 2_f64, 3_f64]
        }
    );
}

#[test]
fn div_left_scalar_test() {
    assert_eq!(
        4_f64
            / Vec3 {
                components: [1_f64, 2_f64, 4_f64]
            },
        Vec3 {
            components: [4_f64, 2_f64, 1_f64]
        }
    );
}
Изменение векторов с присвоением
impl AddAssign for Vec3 {
    fn add_assign(&mut self, other: Self) {
        self.components[0] += other.components[0];
        self.components[1] += other.components[1];
        self.components[2] += other.components[2];
    }
}

impl SubAssign for Vec3 {
    fn sub_assign(&mut self, other: Self) {
        self.components[0] -= other.components[0];
        self.components[1] -= other.components[1];
        self.components[2] -= other.components[2];
    }
}

impl MulAssign for Vec3 {
    fn mul_assign(&mut self, other: Self) {
        self.components[0] *= other.components[0];
        self.components[1] *= other.components[1];
        self.components[2] *= other.components[2];
    }
}

impl DivAssign for Vec3 {
    fn div_assign(&mut self, other: Self) {
        self.components[0] /= other.components[0];
        self.components[1] /= other.components[1];
        self.components[2] /= other.components[2];
    }
}

impl DivAssign<f64> for Vec3 {
    fn div_assign(&mut self, rhs: f64) {
        self.components[0] /= rhs;
        self.components[1] /= rhs;
        self.components[2] /= rhs;
    }
}

impl MulAssign<f64> for Vec3 {
    fn mul_assign(&mut self, rhs: f64) {
        self.components[0] *= rhs;
        self.components[1] *= rhs;
        self.components[2] *= rhs;
    }
}
#[test]
fn add_assign_test() {
    let mut a = Vec3::new(1_f64, 2_f64, 3_f64);
    let b = Vec3::new(-1_f64, -2_f64, -3_f64);

    a += b;
    assert_eq!(a, Vec3::new(0_f64, 0_f64, 0_f64));
}

#[test]
fn sub_assign_test() {
    let mut a = Vec3::new(1_f64, 2_f64, 3_f64);
    let b = Vec3::new(-1_f64, -2_f64, -3_f64);

    a -= b;
    assert_eq!(a, Vec3::new(2_f64, 4_f64, 6_f64));
}

#[test]
fn mul_assign_test() {
    let mut a = Vec3::new(1_f64, 2_f64, 3_f64);
    let b = Vec3::new(-1_f64, -1_f64, -1_f64);

    a *= b;
    assert_eq!(a, Vec3::new(-1_f64, -2_f64, -3_f64));
}

#[test]
fn mul_assign_scalar_test() {
    let mut a = Vec3::new(1_f64, 2_f64, 3_f64);

    a *= -1_f64;
    assert_eq!(a, Vec3::new(-1_f64, -2_f64, -3_f64));
}

#[test]
fn div_assign_test() {
    let mut a = Vec3::new(1_f64, 2_f64, 3_f64);
    let b = Vec3::new(-1_f64, -1_f64, -1_f64);

    a /= b;
    assert_eq!(a, Vec3::new(-1_f64, -2_f64, -3_f64));
}

#[test]
fn div_assign_scalar_test() {
    let mut a = Vec3::new(1_f64, 2_f64, 3_f64);

    a /= -1_f64;
    assert_eq!(a, Vec3::new(-1_f64, -2_f64, -3_f64));
}

В результате получаем новый вектор, причём благодаря Copy трейту исходные вектора фактически не потребляются, хоть и передаются по self, таким образом вместо move (передачи владения) выполняется copy.

Запуск тестов

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

error[E0369]: binary operation `==` cannot be applied to type `structs::vec3::Vec3`
  --> src/structs/vec3.rs:52:9
   |
52 |           assert_eq!(
   |  _________^
   | |_________|
   | |
53 | |             Vec3 {
54 | |                 components: [1_f64, 1_f64, 1_f64]
55 | |             } * 2_f64,
...  |
58 | |             }
59 | |         );
   | |          ^
   | |__________|
   | |__________structs::vec3::Vec3
   |            structs::vec3::Vec3
   |
   = note: an implementation of `std::cmp::PartialEq` might be missing for `structs::vec3::Vec3`
   = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

Причина ошибки в том, что мы не определили для структуры Vec3 правило её проверки на равенство, для этого нужно реализовать трейт PartialEq. Partial, так как для float мы не всегда можем дать ответ о равенстве двух чисел, а именно Nan!=Nan. В данном случае есть два способа реализовать трейт PartialEq:

  1. Простой - переложить всё на компилятор и попросить написать реализацию за нас:
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Vec3 {
    pub components: [f64; 3],
}
  1. Не менее простой - написать её самим, что мы и сделаем:
impl PartialEq for Vec3 {
   fn eq(&self, other: &Self) -> bool {
        self.components == other.components
    }
}

Здесь мы воспользовались тем, что для массивов трейт PartialEq уже реализован в стандартной библиотеке.

Привет графический мир!

Далее напишем классический аналог “привета миру” из области компьютерной графики, применяя только что созданный вектор.

mod structs;

use std::fs::File;
use std::io::{Write};
use structs::vec3::Vec3;

fn main() {
    let mut file = File::create("img.ppm").expect("Unable to create file!");
    let nx = 200;
    let ny = 100;
    writeln!(&mut file, "P3\n {} {} \n255", nx, ny).expect("Unable to write to file!");
    for j in (0..ny).rev() {
        for i in 0..nx {
            let col = Vec3::new((i as f64) / (nx as f64), (j as f64) / (ny as f64), 0.2_f64);
            let ir = (255.99 * col.r()) as i64;
            let ig = (255.99 * col.g()) as i64;
            let ib = (255.99 * col.b()) as i64;
            writeln!(&mut file, "{} {} {}", ir, ig, ib).expect("Unable to write to file");
        }
    }
}

В каталоге structs лежит разработанная нами структура Vec3 в файле vec3.rs. Результатом выполнения данной программы является следующий файл формата ppm:

helloworld

Примечание

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

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

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

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

К постам теперь можно оставлять комментарии

Трассировка лучей на Rust. Часть 0. Начало.

comments powered by Disqus