#![allow(clippy::float_cmp)] #![allow(clippy::inline_always)] #![allow(clippy::many_single_char_names)] #![allow(clippy::needless_lifetimes)] #![allow(clippy::needless_return)] #![allow(clippy::or_fun_call)] #![allow(clippy::too_many_arguments)] #![allow(clippy::redundant_field_names)] #![allow(clippy::enum_variant_names)] #![allow(clippy::cast_lossless)] #![allow(clippy::needless_range_loop)] #![allow(clippy::excessive_precision)] extern crate lazy_static; mod accel; mod algorithm; mod bbox; mod bbox4; mod boundable; mod camera; mod color; mod fp_utils; mod hash; mod hilbert; mod image; mod lerp; mod light; mod math; mod mis; mod parse; mod ray; mod renderer; mod sampling; mod scene; mod shading; mod surface; mod timer; mod tracer; mod transform_stack; use std::{fs::File, io, io::Read, mem, path::Path, str::FromStr}; use clap::{App, Arg}; use nom::{error_position, take_until}; use mem_arena::MemArena; use crate::{ accel::BVH4Node, bbox::BBox, parse::{parse_scene, DataTree}, renderer::LightPath, surface::SurfaceIntersection, timer::Timer, }; const VERSION: &str = env!("CARGO_PKG_VERSION"); #[allow(clippy::cyclomatic_complexity)] fn main() { let mut t = Timer::new(); // Parse command line arguments. let args = App::new("Psychopath") .version(VERSION) .about("A slightly psychotic path tracer") .arg( Arg::with_name("input") .short("i") .long("input") .value_name("FILE") .help("Input .psy file") .takes_value(true) .required_unless_one(&["dev", "use_stdin"]), ) .arg( Arg::with_name("spp") .short("s") .long("spp") .value_name("N") .help("Number of samples per pixel") .takes_value(true) .validator(|s| { usize::from_str(&s) .and(Ok(())) .or(Err("must be an integer".to_string())) }), ) .arg( Arg::with_name("max_bucket_samples") .short("b") .long("spb") .value_name("N") .help("Target number of samples per bucket (determines bucket size)") .takes_value(true) .validator(|s| { usize::from_str(&s) .and(Ok(())) .or(Err("must be an integer".to_string())) }), ) .arg( Arg::with_name("crop") .long("crop") .value_name("X1 Y1 X2 Y2") .help( "Only render the image between pixel coordinates (X1, Y1) \ and (X2, Y2). Coordinates are zero-indexed and inclusive.", ) .takes_value(true) .number_of_values(4) .validator(|s| { usize::from_str(&s) .and(Ok(())) .or(Err("must be four integers".to_string())) }), ) .arg( Arg::with_name("threads") .short("t") .long("threads") .value_name("N") .help( "Number of threads to render with. Defaults to the number of logical \ cores on the system.", ) .takes_value(true) .validator(|s| { usize::from_str(&s) .and(Ok(())) .or(Err("must be an integer".to_string())) }), ) .arg( Arg::with_name("stats") .long("stats") .help("Print additional statistics about rendering"), ) .arg( Arg::with_name("dev") .long("dev") .help("Show useful dev/debug info."), ) .arg( Arg::with_name("serialized_output") .long("serialized_output") .help("Serialize and send render output to standard output.") .hidden(true), ) .arg( Arg::with_name("use_stdin") .long("use_stdin") .help("Take scene file in from stdin instead of a file path.") .hidden(true), ) .get_matches(); // Print some misc useful dev info. if args.is_present("dev") { println!( "SurfaceIntersection size: {} bytes", mem::size_of::() ); println!("LightPath size: {} bytes", mem::size_of::()); println!("BBox size: {} bytes", mem::size_of::()); // println!("BVHNode size: {} bytes", mem::size_of::()); println!("BVH4Node size: {} bytes", mem::size_of::()); return; } let crop = args.values_of("crop").map(|mut vals| { let coords = ( u32::from_str(vals.next().unwrap()).unwrap(), u32::from_str(vals.next().unwrap()).unwrap(), u32::from_str(vals.next().unwrap()).unwrap(), u32::from_str(vals.next().unwrap()).unwrap(), ); if coords.0 > coords.2 { panic!("Argument '--crop': X1 must be less than or equal to X2"); } if coords.1 > coords.3 { panic!("Argument '--crop': Y1 must be less than or equal to Y2"); } coords }); // Parse data tree of scene file if !args.is_present("serialized_output") { println!("Parsing scene file...",); } t.tick(); let psy_contents = if args.is_present("use_stdin") { // Read from stdin let mut input = Vec::new(); let tmp = std::io::stdin(); let mut stdin = tmp.lock(); let mut buf = vec![0u8; 4096]; loop { let count = stdin .read(&mut buf) .expect("Unexpected end of scene input."); let start = if input.len() < 11 { 0 } else { input.len() - 11 }; let end = input.len() + count; input.extend(&buf[..count]); let mut done = false; let mut trunc_len = 0; if let nom::IResult::Done(remaining, _) = take_until!(&input[start..end], "__PSY_EOF__") { done = true; trunc_len = input.len() - remaining.len(); } if done { input.truncate(trunc_len); break; } } String::from_utf8(input).unwrap() } else { // Read from file let mut input = String::new(); let fp = args.value_of("input").unwrap(); let mut f = io::BufReader::new(File::open(fp).unwrap()); let _ = f.read_to_string(&mut input); input }; let dt = DataTree::from_str(&psy_contents).unwrap(); if !args.is_present("serialized_output") { println!("\tParsed scene file in {:.3}s", t.tick()); } // Iterate through scenes and render them if let DataTree::Internal { ref children, .. } = dt { for child in children { t.tick(); if child.type_name() == "Scene" { if !args.is_present("serialized_output") { println!("Building scene..."); } let arena = MemArena::with_min_block_size((1 << 20) * 4); let mut r = parse_scene(&arena, child).unwrap_or_else(|e| { e.print(&psy_contents); panic!("Parse error."); }); if let Some(spp) = args.value_of("spp") { if !args.is_present("serialized_output") { println!("\tOverriding scene spp: {}", spp); } r.spp = usize::from_str(spp).unwrap(); } let max_samples_per_bucket = if let Some(max_samples_per_bucket) = args.value_of("max_bucket_samples") { u32::from_str(max_samples_per_bucket).unwrap() } else { 4096 }; let thread_count = if let Some(threads) = args.value_of("threads") { u32::from_str(threads).unwrap() } else { num_cpus::get() as u32 }; if !args.is_present("serialized_output") { println!("\tBuilt scene in {:.3}s", t.tick()); } if !args.is_present("serialized_output") { println!("Rendering scene with {} threads...", thread_count); } let (mut image, rstats) = r.render( max_samples_per_bucket, crop, thread_count, args.is_present("serialized_output"), ); // Print render stats if !args.is_present("serialized_output") { let rtime = t.tick(); let ntime = rtime as f64 / rstats.total_time; println!("\tRendered scene in {:.3}s", rtime); println!( "\t\tTrace: {:.3}s", ntime * rstats.trace_time ); println!("\t\t\tRays traced: {}", rstats.ray_count); println!( "\t\t\tRays/sec: {}", (rstats.ray_count as f64 / (ntime * rstats.trace_time) as f64) as u64 ); println!( "\t\t\tTraversal: {:.3}s", ntime * rstats.accel_traversal_time ); println!("\t\t\tRay/node tests: {}", rstats.accel_node_visits); println!( "\t\tInitial ray generation: {:.3}s", ntime * rstats.initial_ray_generation_time ); println!( "\t\tRay generation: {:.3}s", ntime * rstats.ray_generation_time ); println!( "\t\tSample writing: {:.3}s", ntime * rstats.sample_writing_time ); } // Write to disk if !args.is_present("serialized_output") { println!("Writing image to disk into '{}'...", r.output_file); if r.output_file.ends_with(".png") { image .write_png(Path::new(&r.output_file)) .expect("Failed to write png..."); } else if r.output_file.ends_with(".exr") { image.write_exr(Path::new(&r.output_file)); } else { panic!("Unknown output file extension."); } println!("\tWrote image in {:.3}s", t.tick()); } // Print memory stats if stats are wanted. if args.is_present("stats") { let arena_stats = arena.stats(); let mib_occupied = arena_stats.0 as f64 / 1_048_576.0; let mib_allocated = arena_stats.1 as f64 / 1_048_576.0; println!("MemArena stats:"); if mib_occupied >= 1.0 { println!("\tOccupied: {:.1} MiB", mib_occupied); } else { println!("\tOccupied: {:.4} MiB", mib_occupied); } if mib_allocated >= 1.0 { println!("\tUsed: {:.1} MiB", mib_allocated); } else { println!("\tUsed: {:.4} MiB", mib_allocated); } println!("\tTotal blocks: {}", arena_stats.2); } } } } // End with blank line println!(); }