Это сообщение — часть 1 из серии, в которой я рассказываю о своем пути в разработке Ruxel, простого трассировщика лучей и 3D-рендерера, написанного на языке Rust, с нуля.
Пожалуйста, ознакомьтесь с прелюдией серии для получения дополнительной информации о моих целях для v.0.1.0
этого проекта.
В идеале, к концу серии Ruxel
сможет рендерить изображение, подобное тому, что в заголовке, или даже лучше…
Примечание: Глубокое объяснение математики 3D не является целью этих сообщений. Чтобы узнать больше о математике и теории, пожалуйста, ознакомьтесь с книгами, на которые я ссылаюсь в серии «Прелюдия».
Архитектура высокого уровня
Долгосрочная цель состоит в том, что Ruxel
станет большим проектом, и, конечно, он пройдет через несколько фаз рефакторинга; однако, очень важно использовать систему модулей Rust с самого начала, чтобы сохранить хорошую организацию проекта и облегчить его сопровождение по мере его роста…
Следующая диаграмма представляет собой высокоуровневое представление архитектуры приложения, по крайней мере, для v.0.1.0
:
Диаграмма архитектуры высокого уровня Ruxel — сделано с помощью https://mermaid.live.
В Части 1 и Части 2 этой серии мы сосредоточимся на создании следующего:
Часть 1:
- Начальная структура проекта:
- Cargo.toml
- Дерево модулей
- Тестирование модулей
- Модуль геометрии:
- Векторы
- Структура
- Трейты
- Реализации
- Точки
- Структура
- Черты
- Реализации
Часть 2:
- Модуль изображения:
- Canvas
- Структура
- Черты
- Реализации
- Пиксель
- Структура
- Черты
- Реализации
- Цвета
- Структура
- Черты
- Реализации
- Изображение
- Файл
Реализация этих модулей позволит протестировать основные геометрические типы и представить первое изображение в простом формате.
Проектные леса
Для начала откройте терминал — в моем случае это комбо Alacritty + Tmux + Fish + Neovim — и начните новый проект Cargo, затем выполните несколько команд mkdir
и touch
, чтобы получить правильную структуру каталогов…
cargo new ruxel
mkdir src/geometry
mkdir src/picture
-- other commands
Если вы используете Neovim, вы можете выполнять команды оболочки в командной строке Neovim, добавляя
!
перед командами оболочки, например::!mkdir geometry
или:!touch vector.rs
.
{: .prompt-tip }
Начальная структура проекта выглядит следующим образом:
Rust/ruxel on main [✘] > v0.0.0 | v1.63.0
λ tree -L 4
.
├── Cargo.lock
├── Cargo.toml
├── images
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
└── src
├── geometry
│ ├── vector
│ │ └── tests.rs
│ └── vector.rs
├── geometry.rs
├── main.rs
├── picture
│ ├── canvas
│ │ └── tests.rs
│ ├── canvas.rs
│ ├── colors
│ │ └── tests.rs
│ └── colors.rs
└── picture.rs
8 directories, 16 files
Эта начальная структура позволяет продолжать добавлять модули и соответствующие им модульные тесты структурированным образом.
Например, добавление модуля Matrix
подразумевает создание нового каталога matrix
, его исходного файла rust и соответствующих тестов:
/src/geometry/matrix/
/src/geometry/matrix.rs
/src/geometry/matrix/tests.rs
В результате получится следующее дерево:
Rust/ruxel on main [✘] > v0.0.0 | v1.63.0
λ tree -L 4
.
├── Cargo.lock
├── Cargo.toml
├── images
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
└── src
├── geometry
** │ ├── matrix
** │ │ └── tests.rs
** │ ├── matrix.rs
│ ├── vector
│ │ └── tests.rs
│ └── vector.rs
├── geometry.rs
├── main.rs
├── picture
│ ├── canvas
│ │ └── tests.rs
│ ├── canvas.rs
│ ├── colors
│ │ └── tests.rs
│ └── colors.rs
└── picture.rs
8 directories, 16 files
Rust предлагает другие альтернативы для структурирования проектов, например, использование mod.rs
; однако я нахожу вариант использования имени файла и каталога более понятным.
Важные диагностические атрибуты
, которые нужно установить с самого начала:
/src/main.rs
#![warn(missing_docs, missing_debug_implementations)]
Это обеспечит предупреждения линтера об отсутствии doc комментариев
, используемых Rust для создания автоматической документации, и для обнаружения типов, не имеющих признака Debug
, который обеспечивает удобные способы отображения значений типов во время разработки.
И, наконец, давайте убедимся, что все наши файлы являются частью дерева модулей Rust
:
/src/main.r
/**
The geometry module implements the functionality for Points, Vectors, Matrices, and their transformations
*/
pub mod geometry;
/**
The picture module implements the functionality for Canvas and Colors in order to create an image file.
*/
pub mod picture;
/src/geometry.rs
/// Provides data structures, methods and traits for Matrix4 computations.
pub mod matrix;
/// Data structures and methods for Vector3 and Point3 computations.
pub mod vector;
/src/picture.rs
/// Provides the data structure and implementation of the Color type
pub mod colors;
/// Provides the data structure and implementation of the Canvas type
pub mod canvas;
/src/geometry/vector.rs
// Unit tests for Vector3 and Point3
#[cfg(test)]
mod tests;
/src/picture/canvas.rs
// Canvas Unit Tests
#[cfg(test)]
mod tests;
/src/picture/colors.rs
// Colors Unit Tests
#[cfg(test)]
mod tests;
Если все настроено правильно, rust-analyzer
не должен выдавать предупреждение о том, что файл не является частью дерева модулей.
Теперь можно написать некоторые типы с их ассоциированными функциями
и вынести их в область видимости main.rs
с полной помощью rust-analyzer
при завершении:
/src/main.rs
fn main() {
let v = Vector3::one();
let p = Point3::one();
println!("Vector: {:?} n Point: {:?}", v, p);
let c = ColorRgb::red();
println!("Color: {}", c);
}
Теперь, когда начальные строительные леса готовы, давайте приступим к кодированию.
Векторы и точки
Модуль vector
содержит типы Vector3
и Point3
, которые математически определяются как:
Определения типов
Математическое определение может быть переведено в код Rust следующим образом:
/// Type representing a geometric 3D Vector with x, y, z components.
#[derive(Debug, Clone, Copy)]
pub struct Vector3<T> {
/// Component on x axis
pub x: T,
/// Component on y axis
pub y: T,
/// Component on z axis
pub z: T,
}
/// Type representing a geometric 3D Point with x, y, z components.
#[derive(Debug, Clone, Copy)]
pub struct Point3<T> {
/// Component on x axis
pub x: T,
/// Component on y axis
pub y: T,
/// Component on z axis
pub z: T,
/// Component representing the 'weight'
pub w: T,
}
Оба типа:
- Являются структурами
named-field
, чтобы иметь доступ к своим компонентам по имени (например:vector.x
,point.z
). - Иметь
генеративные параметры типа
в своих компонентах, чтобы иметь возможность реализовать их какf64
илиi64
, и т.д. - Реализовать
общие черты
Debug
,Copy
иClone
. - на данный момент являются публичными через
pub
.
Наиболее важным решением здесь является то, что оба типа реализуют общие черты Copy
и Clone
.
И главная причина в том, что все их компоненты, как ожидается, будут обычными числовыми типами, такими как i64
, f64
и т.д., которые не владеют ресурсами heap
.
Черты и реализация
Из бесчисленного множества способов реализации желаемой функциональности для этих типов, предпочтительным для данного проекта является следующий способ для v.0.1.0
- Сосредоточиться на реализации
Vector
иPoint
для распространенных числовых типовf64
и для системы координат3D
. Так что никакихVector2<f64>
илиPoint3<i64>
. - Используйте дженерики и трейты как можно больше, помня о будущей расширяемости при минимальном или полном рефакторинге. Например, включение Vector2 должно быть направлено на добавление функциональности без необходимости рефакторинга существующего кода.
- Создайте публичный
Enum
с именемAxis
, который определяет ось системы координат 2D, 3D или 4D. - Создайте публичный признак
CoordInit
, который определяет методы инициализации системы координат для любого типа, который его реализует:- Это обеспечивает расширяемость для других типов векторов и точек, таких как
Vector2
илиPoint2
, или в принципе для любого типа, который требует инициализации координат: вверх, вниз, назад, вперед и т.д.
- Это обеспечивает расширяемость для других типов векторов и точек, таких как
- Реализуйте функцию
CoordInit
дляVector3
иPoint3
. - Создайте публичный трейт
VecOps
, который определяет общие возможности исключительно для векторов, такие какmagnitude
,cross product
,dot product
и т.д. - Реализуйте ассоциированную функцию
new(...)
для каждого типа. - Реализовать возможности
перегрузки операторов
для удобного написания общих бинарных операций надVector3
иPoint3
(чтобы узнать больше об этой теме, вы можете ознакомиться с этой статьей Basic Operator Overloading with Traits):- Add, AddAssign
- Sub, SubAssign
- Mul и Div
- Neg
- Реализуйте следующие
общие черты
:
Перечислитель осей
Важным, но вызывающим множество мнений решением является то, как лучше всего обрабатывать инициализацию новых типов, связанных с координатами, таких как векторы и точки, с помощью трейта с функциями, связанными с типом, с различными сигнатурами.
В сущности, как поступить в этом случае:
/src/geometry/vector.rs
trait Init{
fn new(...);
}
impl Init for Vector2<T>{
fn new(x: T, y: T) -> Self{...}
}
impl Init for Vector3<T>{
fn new(x: T, y: T, z: T) -> Self{...}
}
impl Init for Point4<T>{
fn new(x: T, y: T, z: T, w: T) -> Self{...}
}
Существуют различные альтернативы, например, использование ассоциированных функций с синтаксисом turbofish
, однако для данного проекта я предпочитаю использовать enumerator
для инкапсуляции параметров метода
:
/src/geometry/vector.rs
/**
Enumerator that encapsulates the different coordinate systems used to initialize a Vector or
Point
*/
#[derive(Debug)]
pub enum Axis<U> {
/// Coordinate system with X and Y axis.
XY(U, U),
/// Coordinate system with X, Y and Z axis.
XYZ(U, U, U),
/// Coordinate system with X, Y, Z and W axis.
XYZW(U, U, U, U),
}
А затем привести его в область видимости с помощью упрощенного псевдонима:
/src/geometry/vector.rs
use geometry::vector::{
Axis,
Axis::{XY as xy, XYZ as xyz, XYZW as xyzw},
};
Конечный результат — возможность инициализировать новый тип, связанный с координатами, последовательным, ясным и сжатым образом, используя имя метода new(...)
:
/src/main.rs
let vec3 = Vector3::new(xyz(1.0, 2.0, 3.0));
let point3 = Point3::new(xyzw(1.0, 2.0, 3.0, 4.0));
Это обеспечивает дополнительное преимущество расширяемости
для новых типов, сохраняя при этом последовательный синтаксис инициализации.
Так, например, если в будущем для Vector2
, Vector4
, Point2
или любого другого связанного с координатами типа потребуется новый метод
, мы будем использовать тот же синтаксис:
/src/main.rs
let vec2 = Vector2::new(xy(1.0, 2.0));
let vec3 = Vector3::new(xyz(1.0, 2.0, 3.0));
let vec4 = Vector4::new(xyzw(1.0, 2.0, 3.0, 4.0));
let point2 = Point2::new(xy(1.0, 2.0));
let point3 = Point3::new(xyz(1.0, 2.0, 3.0));
let point4 = Point4::new(xyzw(1.0, 2.0, 3.0, 4.0));
В следующем разделе, где определен признак CoordInit
, показано, как использовать перечислитель Axis
в функции признака.
Тит CoordInit
Основной целью трейта CoordInit
является определение функциональности для инициализации любого типа, связанного с координатами, наиболее обычными
способами, а также определенным пользователем
способом.
В случае типов, связанных с координатами, наиболее распространенными инициализациями являются: вверх, вниз, влево, вправо, вперед, назад, единица и ноль.
Также может быть удобно иметь метод self.equal()
для сравнения одного типа с другим.
В Rust этот признак можно определить следующим образом:
/src/geometry/vector.rs
/// Trait allows Types with coordinates (x, y, etc.) to be efficiently initialized with common shorthand.
pub trait CoordInit<T, U> {
/// Return a type with shorthand, for example [0, 0, -1].
fn back() -> T;
/// Return a type with shorthand, for example [0, -1, 0].
fn down() -> T;
/// Return true if a type is identical to another, else return false.
fn equal(self, rhs: Self) -> bool;
/// Return a type with shorthand, for example [0, 0, 1].
fn forward() -> T;
/// Return a type with shorthand, for example [-1, 0, 0].
fn left() -> T;
/// Return a type with user-defined Axis components.
fn new(axis: Axis<U>) -> T;
/// Return a type with shorthand, for example [1, 1, 1].
fn one() -> T;
/// Return a type with shorthand [1, 0, 0].
fn right() -> T;
/// Return a type with shorthand [0, 1, 0].
fn up() -> T;
/// Return a type with shorthand [0, 0, 0].
fn zero() -> T;
}
Признак определяется с помощью двух общих компонентов <T, U>
.
T
представляет типы, связанные с координатами (Vector
, Points
, и т.д.), которые будут инициализированы, и которые будут возвращены почти из всех функций.
U
представляет общий тип (f64
), который будет использовать Axis enum
для инициализации типов, связанных с координатами.
Следующим шагом будет реализация каждой из этих функций для конкретных типов, которые необходимы.
Как определено выше, для v.0.1.0
основное внимание уделяется только поддержке типов в трех измерениях и с плавающей точкой (f64
). Тем не менее, уже создана основа для расширения на другие измерения и примитивные типы данных.
Для получения полной информации о реализации вы можете посетить репозиторий Ruxel на GitHub.
Здесь я объясню лишь пару моментов в его реализации для Vector3<f64>
:
fn new(axis: Axis<U>) -> T
fn zero()
Rust-код выглядит следующим образом:
/src/geometry/vector.rs
impl CoordInit<Vector3<f64>, f64> for Vector3<f64> {
// other type-associated functions
// new()
fn new(axis: Axis<f64>) -> Vector3<f64> {
match axis {
Axis::XY(x, y) => Vector3 { x, y, z: 0.0 },
Axis::XYZ(x, y, z) => Vector3 { x, y, z },
Axis::XYZW(x, y, z, _w) => Vector3 { x, y, z },
}
}
fn zero() -> Self {
Vector3 {
x: 0.0,
y: 0.0,
z: 0.0,
}
}
}
Первая строка определяет реализацию признака CoordInit
для типа Vector3
, использующего f64
в своих компонентах.
Функция fn new(...)
получает перечислитель Axis<f64>
, выполняет match
по поддерживаемым системам координат (XYZ и т.д.) и возвращает Vector
, инициализированный значениями, заданными пользователем или sane.
В данном случае нормальным значением является возврат Vector3
, даже если пользователь вводит 2D систему координат.
Функция fn zero()
возвращает Vector3
со всеми его компонентами со значением 0.0
.
Перегрузка операторов
Для реализации возможностей перегрузки операторов необходимо ввести в область видимости модуль std::ops
и реализовать нужные черты в каждом из типов.
/src/geometry/vector.rs
// Bring overflow operator's traits into scope
use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}
Бинарные операции между векторами и точками должны следовать некоторой математической логике, представленной в этой таблице:
Операция | LHS | RHS | Результат |
---|---|---|---|
Добавить | V | P | P |
Добавить | P | V | P |
Добавить | V | V | V |
Добавить | P | P | N/A |
Sub | V | P | N/A |
Sub | P | V | P |
Sub | V | V | V |
Sub | P | P | V |
Мул | P | Скаляр | N/A |
Mul | V | Скаляр | V |
Mul | V | P | N/A |
Div | P | Скаляр | N/A |
Div | V | Скаляр | V |
Div | V | P | Н/А |
Отрицательный | V | N/A | -V |
Нег | P | N/A | N/A |
Таким образом, необходимо реализовать только те комбинации, которые дают логический результат. Отказ от реализации остальных дает дополнительное преимущество — компилятор Rust не будет их реализовывать.
Например, для поддержки операции Add
между Vector3
и Point3
необходимы три функции реализации:
impl Add<Point3<f64>> for Vector3<f64> {
type Output = Point3<f64>;
fn add(self, rhs: Point3<f64>) -> Point3<f64> {
Point3 {
x: self.x + rhs.x,
y: self.y + rhs.y,
z: self.z + rhs.z,
w: rhs.w,
}
}
}
impl Add<Vector3<f64>> for Point3<f64> {
type Output = Point3<f64>;
fn add(self, rhs: Vector3<f64>) -> Point3<f64> {
Point3 {
x: self.x + rhs.x,
y: self.y + rhs.y,
z: self.z + rhs.z,
w: self.w,
}
}
}
impl Add for Vector3<f64> {
type Output = Vector3<f64>;
fn add(self, rhs: Self) -> Vector3<f64> {
Vector3 {
x: self.x + rhs.x,
y: self.y + rhs.y,
z: self.z + rhs.z,
}
}
}
Проверьте репозиторий GitHub для полной реализации всех операторов.
Когда все бинарные операторы перегружены, можно «выстраивать цепочки» операций Vector и Point, как с любым обычным примитивом, что чрезвычайно полезно и удобно:
src/main.rs
let v1 = Vector3::new(xyz(2.0, 3.5, 4.0));
let v2 = Vector3::new(xyz(3.0, 7.5, 8.0));
let v3 = Vector3::new(xyz(2.55555, 7.88888, 9.34343));
let v4 = Vector3::new(xyz(2.55553, 7.88887, 9.34342));
let p1 = Point3::new(xyz(2.5, 3.5, 4.5));
let p2 = Point3::new(xyz(3.0, 7.0, 8.0));
let p3 = Point3::new(xyz(2.55555, 7.88888, 9.34343));
let p4 = Point3::new(xyz(2.55553, 7.88887, 9.34342));
println!("{:?}", v1 + v4 - v1 - v3 + (v2 - v4) / 1.522445523);
println!("{:?}", v3 + p4 + v1);
println!("{:?}", p1 - p2 / 3.7626374);
println!("{:?}", p2 - v1);
println!("{:?}", v2 + v1);
Общие черты
Руководство по API Rust предлагает обширное объяснение рекомендаций по разработке API для языка с целью создания идиоматического
кода.
Одна из ключевых задач этого проекта — максимально придерживаться этих стандартов.
По мере развития проекта, более общие черты, вероятно, будут реализованы по мере необходимости, и только если это имеет смысл.
{: .prompt-info }
Для начала, это общие черты, которые реализованы на данном этапе для типов Vector3
и Point3
:
Тип | Общий признак |
---|---|
Вектор3 | Eq, PartialEq, Display, Debug, Clone, Copy, Default |
Точка3 | Eq, PartialEq, Display, Debug, Clone, Copy, Default |
Реализации по умолчанию могут быть произведены
путем украшения определений структур атрибутами #[derive(Debug, Copy,...)]
.
Однако только в отдельных случаях Display
и Default
будет написана ручная реализация для изменения поведения default
:
Реализация по умолчанию через derive
:
src/geometry/vector.rs
#[derive(Debug)]
pub enum Axis<U>
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct Vector3<T>
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct Point3<T>
А теперь реализация manual
:
src/geometry/vector.rs
impl Display for Vector3<f64> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = format!("v: [{:^5.2},{:^5.2},{:^5.2}]", self.x, self.y, self.z);
f.write_str(&s)
}
}
impl Display for Point3<f64> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = format!(
"p: [{:^5.2},{:^5.2},{:^5.2},{:^5.2}]",
self.x, self.y, self.z, self.w
);
f.write_str(&s)
}
}
impl Default for Point3<f64> {
fn default() -> Self {
Self {
x: 0.0,
y: 0.0,
z: 0.0,
w: 1.0,
}
}
}
impl Default for Vector3<f64> {
fn default() -> Self {
Self {
x: 0.0,
y: 0.0,
z: 0.0,
}
}
}
Даже когда Default
для Vector3
реализует умолчания Rust о заполнении значения 0.0 для f64
, удобно иметь под рукой ручную реализацию для целей тестирования.
Общие векторные операции
Последние важные реализации, которые необходимы для типа Vector
, касаются их общих математических операций:
- Вычислить величину
- Нормализовать вектор
- Вычислить точечное произведение
- Вычислить кросс-продукт
- Получить минимальный компонент в векторе
- Получить максимальную компоненту вектора
- Возвращать компоненты вектора по имени и индексу.
Эти возможности могут быть определены с помощью публичного трейта VecOps<T>
, с общим параметром, чтобы расширить его реализацию для типов, отличных от Vector3<f64>
:
src/geometry/vector.rs
/// A trait that encapsulates common Vector Operations.
pub trait VecOps<T> {
/// Computes the magnitude of a Vector.
fn magnitude(&self) -> f64;
/// Returns the vector normalized (with magnitude of 1.0)
fn normalized(&mut self) -> Self;
/// Returns the Dot Product of two Vectors.
fn dot(lhs: T, rhs: T) -> f64;
/// Returns the Cross Product of two Vectors.
fn cross(lhs: T, rhs: T) -> T;
/// Returns the Smallest component in the Vector.
fn min_component(&self) -> (i8, char, f64);
/// Returns the Largest component in the Vector.
fn max_component(&self) -> (i8, char, f64);
/// Returns the component of the Vector by index. this(1)
fn this(&self, index: i8) -> Option<(i8, char, f64)>;
/// Returns the component of the Vector by name. this_n('x')
fn this_name(&self, index: char) -> Option<(i8, char, f64)>;
}
Как и в случае с трейтом CoordInit
, реализация должна быть определена для каждой функции внутри блока impl
для Vector3<f64>
:
impl VecOps<Vector3<f64>> for Vector3<f64> {
fn magnitude(&self) -> f64 {
(self.x.powf(2.0) + self.y.powf(2.0) + self.z.powf(2.0)).sqrt()
}
fn normalized(&mut self) -> Self {
let magnitude = self.magnitude();
Self {
x: self.x / magnitude,
y: self.y / magnitude,
z: self.z / magnitude,
}
}
// other functions in the VecOps<T> trait
Модульные тесты
Теперь, когда созданы основные реализации и возможности для Vector3
и Point3
, пришло время выполнить модульные тесты, чтобы убедиться в отсутствии проблем или ошибок в коде.
Как было описано в начале, тесты для каждого из модулей реализованы в test.rs
в каталоге модуля.
В случае Vector3
и Point3
, файл находится в src/geometry/vector/tests.rs
.
Есть 5 тестов, которые будут выполнены для проверки наших типов:
- целостность построения вектора и точки
- целостность перегрузки операторов вектора и точки
- целостность общих операций вектора
- Симулятор запуска ракеты, основанный на книге The Ray Tracer Challenge1
Первый шаг — ввести в область видимости модуль vector
и использовать alias
для перечислителя Axis
:
src/geometry/vector/tests.rs
/// Unit testing for Vector3 and Point3 types
use super::*;
use super::Axis::XYZ as xyz;
Затем мы определим функции тестов:
#[test]
// This test validates the construction of the Vector3 and Point3 types
fn vector_and_point_construction_integrity() {}
#[test]
// This test validates the operation overloading Add, Sub, Div, Equality, Mul, Neg, AddAssign, SubAssign for the Vector3 and Point3
fn vector_and_point_operator_overloading_integrity() {}
#[test]
// This test validates the implementation of the fuctions in the VecOps trait
fn vector_common_operations_integrity() {}
#[test]
// This test validates integrity by simulating a rocket launch
fn simulate_rocket_lauch() {}
Каждый тест представляет собой набор макросов assert!()
и assert_eq!()
, а также утверждений println!()
.
Файл тестирования довольно обширный, поэтому в этом посте я покажу только код для тестов vector_common_operations_integrity()
и ‘simulate_rocket_lauch()’:
src/geometry/vector/tests.rs
#[test]
// This test validates the implementation of the fuctions in the VecOps trait
fn vector_common_operations_integrity() {
// Magnitude
let v1 = Vector3::new(xyz(1.0, 2.0, 3.0));
assert_eq!(v1.magnitude(), 14f64.sqrt());
// Normalization
let mut v2 = v1;
assert_eq!(v2.normalized().magnitude(), 1f64);
// Dot product
let a = Vector3::new(xyz(1.0, 2.0, 3.0));
let b = Vector3::new(xyz(2.0, 3.0, 4.0));
assert_eq!(Vector3::dot(a, b), 20f64);
// Cross product
assert_eq!(Vector3::cross(a, b), Vector3::new(xyz(-1.0, 2.0, -1.0)));
assert_eq!(Vector3::cross(b, a), Vector3::new(xyz(1.0, -2.0, 1.0)));
// Min, Max and Get Components
assert_eq!(a.min_component(), (0, 'x', 1.0));
assert_eq!(a.max_component(), (2, 'z', 3.0));
assert_eq!(a.this(0).unwrap(), (0, 'x', 1.0));
assert_eq!(a.this(9), None);
assert_eq!(b.this(b.min_component().0).unwrap(), (0, 'x', 2.0));
assert_eq!(a.this_name('z').unwrap(), (2, 'z', 3.0));
}
Запуск теста с помощью cargo test vector_common_operations_integrity
дает положительный результат:
λ cargo test vector_common_operations_integrity
running 1 test
test geometry::vector::tests::vector_common_operations_integrity ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s
А теперь симуляция запуска ракеты, которая объединяет все в одном тесте. Для проверки тест выведет координаты ракеты на основе начальных условий запуска и окружающей среды (гравитация и ветер).
Ожидается, что мы увидим данные параболического запуска:
#[test]
// This test validates integrity by simulating a rocket launch
fn simulate_rocket_lauch() {
#[derive(Debug)]
struct Projectile {
position: Point3<f64>,
velocity: Vector3<f64>,
}
struct Environment {
gravity: Vector3<f64>,
wind: Vector3<f64>,
}
let mut proj = Projectile {
position: Point3::up(),
velocity: Vector3::new(xyz(1.0, 1.0, 0.0)).normalized(),
};
let env = Environment {
gravity: Vector3::down() / 10f64,
wind: Vector3::left() / 100f64,
};
fn tick<'a, 'b>(env: &'a Environment, proj: &'b mut Projectile) -> &'b mut Projectile {
proj.position = proj.position + proj.velocity;
proj.velocity = proj.velocity + env.gravity + env.wind;
proj
}
println!(
"Launch position: - x: {:^5.2}, y: {:^5.2}, z: {:^5.2}",
proj.position.x, proj.position.y, proj.position.z
);
while proj.position.y > 0.0 {
tick(&env, &mut proj);
if proj.position.y <= 0.0 {
break;
}
println!(
"Projectile position - x: {:^5.2}, y: {:^5.2}, z: {:^5.2}",
proj.position.x, proj.position.y, proj.position.z
);
}
println!("========================== End");
}
Для просмотра результатов println!()
необходимо запустить cargo test
с аргументом -- --nocapture
:
Rust/ruxel on main [!] > v0.0.0 | v1.63.0
λ cargo test simulate_rocket_lauch -- --nocapture
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/main.rs (target/debug/deps/ruxel-6b30efdff903fb79)
running 1 test
Launch position: - x: 0.00 , y: 1.00 , z: 0.00
Projectile position - x: 0.71 , y: 1.71 , z: 0.00
Projectile position - x: 1.40 , y: 2.31 , z: 0.00
Projectile position - x: 2.09 , y: 2.82 , z: 0.00
Projectile position - x: 2.77 , y: 3.23 , z: 0.00
Projectile position - x: 3.44 , y: 3.54 , z: 0.00
Projectile position - x: 4.09 , y: 3.74 , z: 0.00
Projectile position - x: 4.74 , y: 3.85 , z: 0.00
Projectile position - x: 5.38 , y: 3.86 , z: 0.00
Projectile position - x: 6.00 , y: 3.76 , z: 0.00
Projectile position - x: 6.62 , y: 3.57 , z: 0.00
Projectile position - x: 7.23 , y: 3.28 , z: 0.00
Projectile position - x: 7.83 , y: 2.89 , z: 0.00
Projectile position - x: 8.41 , y: 2.39 , z: 0.00
Projectile position - x: 8.99 , y: 1.80 , z: 0.00
Projectile position - x: 9.56 , y: 1.11 , z: 0.00
Projectile position - x: 10.11, y: 0.31 , z: 0.00
========================== End
test geometry::vector::tests::simulate_rocket_lauch ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s
Как и ожидалось, тест прошел. Это означает, что наши реализации Vector3
и Point3
были успешными.
На этом мы завершаем первую часть этой серии!
Следующие шаги
Во второй части мы сосредоточимся на:
- Созданию типов
Color
,Pixel
иCanvas
. - Определение и реализация их
трейтов
. - Написание и выполнение их
блочных тестов
. - И, наконец, создание первого изображения.
Этот пост был первоначально опубликован на:
https://rsdlt.github.io/posts/ruxel-part-1-rust-ray-tracer-renderer-3d-development/
Ссылки, упоминания и отказ от ответственности:
Фотография заголовка Rohit Choudhari на Unsplash
Книги:
-
Бак, Джамис. (2019). The ray tracer challenge: Руководство по созданию вашего первого 3D-рендерера на основе тестов. The Pragmatic Programmers.