diff --git a/Cargo.lock b/Cargo.lock index d2391ae..1d873e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index c92fca0..96d5981 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/scenes/demo-2.lisp b/scenes/demo-2.lisp index 2dc8a0c..f8b188d 100644 --- a/scenes/demo-2.lisp +++ b/scenes/demo-2.lisp @@ -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) diff --git a/src/raytracer/camera.rs b/src/raytracer/camera.rs index 40f42f0..c5e0c7b 100644 --- a/src/raytracer/camera.rs +++ b/src/raytracer/camera.rs @@ -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 Scene, CFn: Fn(u32, &Camera) -> Camera>( + pub fn render_animation< + SFn: Fn(u32) -> Result, + CFn: Fn(u32, &Camera) -> Result, + >( &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(()) } } diff --git a/src/raytracer/lisp.rs b/src/raytracer/lisp.rs index 4778761..485e482 100644 --- a/src/raytracer/lisp.rs +++ b/src/raytracer/lisp.rs @@ -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 { - 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 = 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 { let scene_fn_call: Expression = [scene_fn.clone(), (t as i64).into()].into(); - let scn: ForeignDataWrapper = eval(env, scene_fn_call).unwrap().try_into().unwrap(); - scn.to_owned() + let scn: ForeignDataWrapper = 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 { 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 = - eval(env, update_cam_call).unwrap().try_into().unwrap(); - new_c.to_owned() + let new_c: ForeignDataWrapper = 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)] diff --git a/src/raytracer/mod.rs b/src/raytracer/mod.rs index d4f5d00..868f138 100644 --- a/src/raytracer/mod.rs +++ b/src/raytracer/mod.rs @@ -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 for RTError { + fn from(value: lispers_core::lisp::eval::EvalError) -> Self { + RTError::EvalError(value) + } +} + +impl From for RTError { + fn from(value: video_rs::Error) -> Self { + RTError::FFMpegError(value) + } +}