enginerenderer 0.0.1

A zero-dependency offline rendering engine in pure Rust — CPU path tracing, BVH acceleration, 16-band spectral rendering, PBR materials, animation & video export.
Documentation
use std::path::PathBuf;

use enginerenderer::api::animation::AnimationClip;
use enginerenderer::api::engine::EngineApi;
use enginerenderer::api::scenes::SceneDescriptor;
use enginerenderer::api::types::core::{Quality, RenderRequest};

pub fn is_video_mode(args: &[String]) -> bool {
    args.iter().any(|arg| {
        arg == "video"
            || arg == "--video"
            || arg.starts_with("--duration")
            || arg.starts_with("--output-dir")
            || arg.starts_with("--output-mp4")
            || arg.starts_with("--prefix")
            || arg.starts_with("--scene")
            || arg.starts_with("--scene-file")
            || arg.starts_with("--quality")
    })
}

pub fn run(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
    let defaults = RenderRequest::preview();
    let duration = parse_u32_arg(args, "--duration", 5).max(1);
    let fps = parse_u32_arg(args, "--fps", 30).max(1);
    let width = parse_u32_arg(args, "--width", defaults.width as u32).max(1);
    let height = parse_u32_arg(args, "--height", defaults.height as u32).max(1);
    let quality = parse_quality(args);
    let output_dir =
        parse_path_arg(args, "--output-dir").unwrap_or_else(|| PathBuf::from("output/video"));
    let output_mp4 =
        parse_path_arg(args, "--output-mp4").unwrap_or_else(|| output_dir.join("animation.mp4"));
    let frame_prefix = parse_string_arg(args, "--prefix").unwrap_or_else(|| String::from("frame"));

    std::fs::create_dir_all(&output_dir)?;
    if let Some(parent) = output_mp4.parent()
        && !parent.as_os_str().is_empty()
    {
        std::fs::create_dir_all(parent)?;
    }

    let frame_output_dir = output_dir.join(".frames_tmp");
    if frame_output_dir.exists() {
        std::fs::remove_dir_all(&frame_output_dir)?;
    }
    std::fs::create_dir_all(&frame_output_dir)?;

    let request = RenderRequest::preview()
        .with_quality(quality)
        .with_resolution(width as usize, height as usize)
        .with_output(frame_output_dir.clone(), String::from("frame.ppm"));

    let clip = AnimationClip::new(duration as f64, fps as f64);
    let base = load_scene(args)?;
    let api = EngineApi::new();
    let sequence = api.render_animation(base, clip, &request, &frame_prefix)?;
    api.encode_animation_mp4(&sequence, &output_mp4)?;

    for frame in &sequence.frames {
        if frame.output_path.exists() {
            std::fs::remove_file(&frame.output_path)?;
        }
    }
    if frame_output_dir.exists() {
        std::fs::remove_dir_all(&frame_output_dir)?;
    }

    eprintln!("video generated: {}", output_mp4.display());
    Ok(())
}

fn load_scene(args: &[String]) -> Result<SceneDescriptor, Box<dyn std::error::Error>> {
    if let Some(path) = parse_path_arg(args, "--scene-file") {
        return Ok(SceneDescriptor::load_from_file(path)?);
    }
    Ok(SceneDescriptor::default())
}

fn parse_u32_arg(args: &[String], key: &str, default_value: u32) -> u32 {
    if let Some(value) = parse_string_arg(args, key)
        && let Ok(parsed) = value.parse::<u32>()
    {
        return parsed;
    }
    default_value
}

fn parse_string_arg(args: &[String], key: &str) -> Option<String> {
    for (index, arg) in args.iter().enumerate() {
        if let Some(value) = arg.strip_prefix(&(String::from(key) + "=")) {
            return Some(value.to_string());
        }
        if arg == key {
            return args.get(index + 1).cloned();
        }
    }
    None
}

fn parse_path_arg(args: &[String], key: &str) -> Option<PathBuf> {
    parse_string_arg(args, key).map(PathBuf::from)
}

fn parse_quality(args: &[String]) -> Quality {
    let Some(raw) = parse_string_arg(args, "--quality") else {
        return Quality::Preview;
    };

    match raw.trim().to_ascii_lowercase().as_str() {
        "preview" => Quality::Preview,
        "hd" => Quality::Hd,
        "production" => Quality::Production,
        _ => Quality::Preview,
    }
}