Трассировка лучей на Rust. Часть 6. Камера.

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

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

Оглавление

Поле зрения

Для начала доработаем структуру камеры так, чтобы стало возможным задание вертикального поля зрения в градусах и соотношения сторон.

// /src/obj/camera.rs

pub fn new(v_fov: f64, aspect: f64) -> Self {
        let theta = v_fov * std::f64::consts::PI / 180_f64;
        let half_height = (theta / 2_f64).tan();
        let half_width = aspect * half_height;

        Self {
            lower_left_corner: Vec3::new(-half_width, -half_height, -1_f64),
            horizontal: Vec3::new(2_f64 * half_width, 0_f64, 0_f64),
            vertical: Vec3::new(0_f64, 2_f64 * half_height, 0_f64),
            origin: Vec3::default(),
        }
    }

Камера всё так же располагается в начале координат, однако все остальные её характеристики вычисляются на основании вертикального поля зрения и соотношения сторон.

Запустив генерацию изображения при разных углах зрения получим:

FOVIMG
5050
9090
120120

Произвольное положение камеры

Ранее наша камера всегда находилась в начале координат, теперь мы добавим возможность настройки её положения и направления взгляда.

lookat

// /src/obj/camera.rs

pub fn new(look_from: Vec3, look_at: Vec3, v_up: Vec3, v_fov: f64, aspect: f64) -> Self {
        let theta = v_fov * std::f64::consts::PI / 180_f64;
        let half_height = (theta / 2_f64).tan();
        let half_width = aspect * half_height;

        let w = (look_from - look_at).unit_vector();
        let u = v_up.cross(&w).unit_vector();
        let v = w.cross(&u);

        Self {
            lower_left_corner: look_from - half_width * u - half_height * v - w,
            horizontal: 2_f64 * half_width * u,
            vertical: 2_f64 * half_height * v,
            origin: look_from,
        }
    }

Изменив положение камеры

// /src/main.rs

let camera = Camera::new(
        Vec3::new(-2_f64, 2_f64, 0_f64),
        Vec3::new(0_f64, 0_f64, -1_f64),
        Vec3::new(0_f64, 1_f64, 0_f64),
        90_f64,
        (nx as f64) / (ny as f64),
    );

получим

FOVIMG
5050c
9090c
120120c

Генерация видео

В этом разделе мы опишем как создать видео комбинируя отдельные кадры. Для придания динамичности будем менять положение камеры так, чтобы она перемещалась по окружности над нашими объектами. Зафиксируем положение координаты $$x$$ в нуле, а $$y$$ и $$z$$ будем менять по следующемму правилу:

$$y = y_0+r*sin(\alpha),$$

$$z = z_0+r*cos(\alpha),$$

где $$r$$ - радиус облёта, $$\alpha$$ - угол восхождения, $$(y_0, z_0)$$ - координаты центральной сферы.

// /src/main.rs

fn get_cameras(
    look_at: Vec3,
    r: f64,
    v_fov: f64,
    aspect: f64,
    speed: i32,
) -> impl Iterator<Item = Camera> {
    (0..180 * speed)
        .map(move |a| ((a as f64) / (speed as f64)) * std::f64::consts::PI / 180_f64)
        .map(move |ar| {
            let v_up = if ar < std::f64::consts::PI / 2_f64 {
                Vec3::new(0_f64, 1_f64, 0_f64)
            } else {
                Vec3::new(0_f64, -1_f64, 0_f64)
            };
            Camera::new(
                Vec3::new(
                    0_f64,
                    look_at.y() + r * ar.sin(),
                    look_at.z() + r * ar.cos(),
                ),
                look_at,
                v_up,
                v_fov,
                aspect,
            )
        })
}

Далее сгенерируем отдельный кадр для каждого из положений камеры:

// /src/main.rs 

fn main() {
    ...

    for (frame_number, camera) in get_cameras(
        Vec3::new(0_f64, 0_f64, -1_f64),
        1_f64,
        90_f64,
        (nx as f64) / (ny as f64),
        2
    )
    .enumerate()
    {
        let mut file = File::create(format!("video/{:0>8}.ppm", frame_number))
            .expect("Unable to create file!");
        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 r_rays = g1
                    .by_ref()
                    .take(ns)
                    .zip(g2.by_ref().take(ns))
                    .map(|(r1, r2)| {
                        (
                            ((i as f64) + r1) / (nx as f64),
                            ((j as f64) + r2) / (ny as f64),
                        )
                    })
                    .map(|(u, v)| camera.get_ray(u, v));

                let mut col = Vec3::default();
                for ray in r_rays {
                    col += color(&ray, &world, &mut g, 0);
                }

                col /= ns as f64;

                col = Vec3::new(col.r().sqrt(), col.g().sqrt(), col.b().sqrt());

                let (ir, ig, ib) = col.irgb(255.99_f64);
                writeln!(&mut file, "{} {} {}", ir, ig, ib).expect("Unable to write to file");
            }
        }
    }

}

Здесь мы применили функцию enumerate, чтобы получить номер каждого из кадров для записи уникального имени File::create(format!("video/{:0>8}.ppm", frame_number)). Посмотрим, как количество лучей влияет на результат

N лучейВидео
1
10
100

Для создания видео из ppm кадров можно использовать ffmpeg

ffmpeg -framerate 60  -pattern_type glob -i "video/*.ppm" \
-filter_complex "[0:v]reverse,fifo[r];[0:v][r] concat=n=2:v=1 [v]" -map "[v]" \
-pix_fmt yuv420p -c:v libx264 -preset veryslow test.mp4

Фильтр -filter_complex "[0:v]reverse,fifo[r];[0:v][r] concat=n=2:v=1 [v]" -map "[v]" нужен, чтобы создать обратную половину видео.

Приложения

Примечание

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

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

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

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

Разделяй и властвуй.

Трассировка лучей на Rust. Часть 5. Диэлектрики.

comments powered by Disqus