feat(camera): render anim using ffmpeg

This commit is contained in:
2026-03-31 20:15:30 +02:00
parent d0840759b3
commit fc40e0b798
6 changed files with 111 additions and 26 deletions

32
Cargo.lock generated
View File

@@ -894,6 +894,7 @@ dependencies = [
"lispers-core",
"lispers-macro",
"nalgebra",
"ndarray",
"nix",
"rayon",
"video-rs",
@@ -1033,6 +1034,21 @@ dependencies = [
"syn",
]
[[package]]
name = "ndarray"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
dependencies = [
"matrixmultiply",
"num-complex",
"num-integer",
"num-traits",
"portable-atomic",
"portable-atomic-util",
"rawpointer",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@@ -1194,6 +1210,21 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "portable-atomic"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "portable-atomic-util"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
dependencies = [
"portable-atomic",
]
[[package]]
name = "potential_utf"
version = "0.1.4"
@@ -1669,6 +1700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2633ec4c2a8aeb7c0e970f75ba99122a75841e9f7b34d5225366d0e61a870a8c"
dependencies = [
"ffmpeg-next",
"ndarray",
"tracing",
"url",
]

View File

@@ -43,4 +43,5 @@ nix = "0.31.2"
rayon = "1.11.0"
lispers-core = {workspace = true}
lispers-macro = {workspace = true}
video-rs = "0.11.0"
video-rs = { version = "0.11.0", features = ["ndarray"] }
ndarray = "0.17.2"

View File

@@ -80,15 +80,15 @@
(defun cam-fn (t c)
(let '((pos . (point 0 4 8))
(cnt . (point 0 0 0))
(to . (point -3 3 -8))
(to . (point -2 3 -6))
(up . (vector 0 1 0))
(fovy . 40)
(pct . (/ t 300.0)))
(let '((tpos . (vadd pos (vmul (vsub to pos) pct)))
(tfovy . (+ fovy (* 40 pct)))
)
(camera-reposition c tpos cnt up fovy)
(camera-reposition c tpos cnt up tfovy)
)
))
(render-animation cam scene-fn cam-fn 300 1 4 2)
(render-animation cam "demo-animation-2.mp4" scene-fn cam-fn 300 30 4 2)

View File

@@ -1,11 +1,15 @@
use std::fmt::Display;
use std::{fmt::Display, path::Path};
use super::{
scene::Scene,
types::{Color, Point3, Ray, Scalar, Vector3},
RTError,
};
use image::RgbImage;
use lispers_core::lisp::eval::EvalError;
use ndarray::Array3;
use rayon::prelude::*;
use video_rs::{encode::Settings, Encoder, Time};
/// A camera that can render a scene.
#[derive(Clone, PartialEq, Debug)]
@@ -119,26 +123,47 @@ impl Camera {
Camera::new(position, center, up, fovy, self.width, self.height)
}
pub fn render_animation<SFn: Fn(u32) -> Scene, CFn: Fn(u32, &Camera) -> Camera>(
pub fn render_animation<
SFn: Fn(u32) -> Result<Scene, EvalError>,
CFn: Fn(u32, &Camera) -> Result<Camera, EvalError>,
>(
&self,
path: &Path,
scene_fn: SFn,
update_cam: CFn,
frames: u32,
fps: u32,
depth: u32,
subp: u32,
) {
) -> Result<(), RTError> {
let mut encoder = Encoder::new(
path,
Settings::preset_h264_yuv420p(self.width, self.height, false),
)?;
let frame_duration = Time::from_nth_of_a_second(fps as usize);
let mut timestamp = Time::zero();
let mut cam = self.to_owned();
for t in 0..frames {
println!("Rendering frame {}/{}", t + 1, frames);
cam = update_cam(t, &cam);
let img = cam.render(&scene_fn(t), depth, subp);
println!(
"Rendering frame {}/{} for {}",
t + 1,
frames,
path.display()
);
cam = update_cam(t, &cam)?;
let img = cam.render(&scene_fn(t)?, depth, subp);
match img.save(format!("frame_{:04}.png", t)) {
Ok(_) => {}
Err(e) => print!("Could not render frame: {}", e),
}
let frame = Array3::from_shape_fn((self.height, self.width, 3), |(y, x, c)| {
img.get_pixel(x as u32, y as u32)[c]
});
encoder.encode(&frame, timestamp)?;
timestamp = timestamp.aligned_with(frame_duration).add();
}
encoder.finish()?;
Ok(())
}
}

View File

@@ -1,3 +1,5 @@
use std::path::PathBuf;
use crate::raytracer::{scene::Scene, types::Light};
use lispers_macro::{native_lisp_function, native_lisp_function_proxy};
@@ -14,6 +16,7 @@ use super::{
plane::{Checkerboard, Plane},
sphere::Sphere,
types::{Color, Material, Point3, RTObjectWrapper, Vector3},
RTError,
};
#[native_lisp_function(eval)]
@@ -179,38 +182,44 @@ pub fn render(
}
pub fn render_animation(env: &Environment, expr: Expression) -> Result<Expression, EvalError> {
let [cam, scene_fn, update_cam, frames, fps, depth, subp]: [Expression; 7] = expr.try_into()?;
let [cam, path, scene_fn, update_cam, frames, fps, depth, subp]: [Expression; 8] =
expr.try_into()?;
let cam: ForeignDataWrapper<Camera> = eval(env, cam)?.try_into()?;
let path: String = eval(env, path)?.try_into()?;
let frames: i64 = eval(env, frames)?.try_into()?;
let fps: i64 = eval(env, fps)?.try_into()?;
let depth: i64 = eval(env, depth)?.try_into()?;
let subp: i64 = eval(env, subp)?.try_into()?;
let sfn = |t: u32| -> Scene {
let sfn = |t: u32| -> Result<Scene, EvalError> {
let scene_fn_call: Expression = [scene_fn.clone(), (t as i64).into()].into();
let scn: ForeignDataWrapper<Scene> = eval(env, scene_fn_call).unwrap().try_into().unwrap();
scn.to_owned()
let scn: ForeignDataWrapper<Scene> = eval(env, scene_fn_call)?.try_into()?;
Ok(scn.to_owned())
};
let ucm = |t: u32, c: &Camera| -> Camera {
let ucm = |t: u32, c: &Camera| -> Result<Camera, EvalError> {
let c = ForeignDataWrapper::new(c.to_owned());
let update_cam_call: Expression = [update_cam.clone(), (t as i64).into(), c.into()].into();
let new_c: ForeignDataWrapper<Camera> =
eval(env, update_cam_call).unwrap().try_into().unwrap();
new_c.to_owned()
let new_c: ForeignDataWrapper<Camera> = eval(env, update_cam_call)?.try_into()?;
Ok(new_c.to_owned())
};
cam.render_animation(
let path: PathBuf = path.into();
match cam.render_animation(
&path,
sfn,
ucm,
frames as u32,
fps as u32,
depth as u32,
subp as u32,
);
Ok(Expression::Nil)
) {
Ok(()) => Ok(Expression::Nil),
Err(RTError::EvalError(e)) => Err(e),
Err(RTError::FFMpegError(e)) => Err(EvalError::RuntimeError(e.to_string())),
}
}
#[native_lisp_function(eval)]

View File

@@ -5,3 +5,21 @@ pub mod scene;
pub mod sphere;
pub mod types;
mod vec;
#[derive(Debug, Clone)]
pub enum RTError {
EvalError(lispers_core::lisp::eval::EvalError),
FFMpegError(video_rs::Error),
}
impl From<lispers_core::lisp::eval::EvalError> for RTError {
fn from(value: lispers_core::lisp::eval::EvalError) -> Self {
RTError::EvalError(value)
}
}
impl From<video_rs::Error> for RTError {
fn from(value: video_rs::Error) -> Self {
RTError::FFMpegError(value)
}
}