Продолжил работу над Ray Tracer на Rust. Результаты первой части описывал ранее.

По итогам второй части туториала сделал рендер картинки за час на 16 ядрах:

Ray tracer v2

Благодаря третьей части туториала качество рендера улучшилось. Добавил оптимизацию параллелизма и рендер картинки выше сократилось до 25 минут. Изменения качества особенно заметно в Корнеллской коробке на малом количестве лучей (все параметры рендера идентичны):

Ray tracer v2 vs v3

Итог третьей части с стеклянными и зеркальными поверхностями:

Ray tracer v3

Запомнившиеся моменты

  • Рефакторинг ссылок. Избавился от лишнего Box в Arc для текстур и материалов, чтобы удобно делиться ими между потоками.
  • Трейты Send/Sync. Перенёс Send + Sync в сами трейты, а не раскидывал их через generic-параметры.
  • Унифицированный конструктор через трейты. Ввел трейт IntoSharedMaterial, позволяющий писать единый конструктор вместо дублирования from_shared_texture, from_texture:
pub trait IntoSharedMaterial {
    fn into_arc(self) -> Arc<dyn Material>;
}

impl IntoSharedMaterial for Arc<dyn Material> {
    fn into_arc(self) -> Arc<dyn Material> {
        self
    }
}

impl<T: Material + 'static> IntoSharedMaterial for T {
    fn into_arc(self) -> Arc<dyn Material> {
        Arc::new(self)
    }
}
  • Трансформации объектов. Создал трейт Transformable для поворота и перемещения объектов через преобразование типов. Аналогично сделаны итераторы, map, filter и т.д.
pub struct RotateY<T: Hit> { // Все объекты характеризуются трейтом Hit в проекте
    object: T,
}
impl<T: Hit> Hit for RotateY<T> { ... }

pub trait Transformable: Hit + Sized {
    fn rotate_y(self, angle: f32) -> RotateY<Self> {
        RotateY::new(self, angle)
    }
    fn translate(self, offset: Coords) -> Translate<Self> {
        Translate::new(self, offset)
    }
}

impl<T: Hit> Transformable for T {}

// В вызывающем коде: object.rotate_y(45.).translate(2.)
  • Option оператор ?. Упростил обработку опциональных значений.
  • Сборка объектов через FromIterator. Реализовал конструктор с помощью трейта FromIterator и fold.
  • Функциональный стиль. Использовал итераторы, map, collect и flat_map для лаконичного и декларативного кода.
  • Параллелизм и каналы. Организовал worker pool с использованием каналов для снижения времени рендера.
  • Многопоточные генераторы чисел. Распихал по проекту thread_local! для генераторов случайных чисел.
  • Ссылки без лишних аллокаций. Применял Arc и &'a dyn там, где это возможно. Без трейта clone в проекте не обошлось, к сожалению.

Не туториалом единым

Рекомендую глянуть статью Rust в режиме «жесть», где Алексей описывает код рейтресера с минимизацией взаимодействия с std.