В этой части мы добавим настройку положения камеры и создадим простое видео.
Оглавление
Поле зрения
Для начала доработаем структуру камеры так, чтобы стало возможным задание вертикального поля зрения в градусах и соотношения сторон.
// /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(),
}
}
Камера всё так же располагается в начале координат, однако все остальные её характеристики вычисляются на основании вертикального поля зрения и соотношения сторон.
Запустив генерацию изображения при разных углах зрения получим:
FOV | IMG |
---|---|
50 | |
90 | |
120 |
Произвольное положение камеры
Ранее наша камера всегда находилась в начале координат, теперь мы добавим возможность настройки её положения и направления взгляда.
// /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),
);
получим
FOV | IMG |
---|---|
50 | |
90 | |
120 |
Генерация видео
В этом разделе мы опишем как создать видео комбинируя отдельные кадры. Для придания динамичности будем менять положение камеры так, чтобы она перемещалась по окружности над нашими объектами. Зафиксируем положение координаты $$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.