В этой части мы создадим базовую структуру вектора и основные методы для работы с векторами.
Оглавление
Реализация базовой структуры вектора
Первым делом необходимо реализовать служебные типы, в частности трёхмерный вектор. Как и в оригинальной книге вектор будет применяться как для работы с цветами, так и с вещественными числами в целом, поэтому базовый вид структуры будет следующим:
#[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
:
- Простой - переложить всё на компилятор и попросить написать реализацию за нас:
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Vec3 {
pub components: [f64; 3],
}
- Не менее простой - написать её самим, что мы и сделаем:
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
:
Примечание
Данная заметка написана в рамках реализации трассировки лучей на Rust. Остальные статьи из этой серии можно найти по следующему тегу или в первой публикации из цикла .
Исходный код проекта доступен на github.