psychopath/src/main.rs

337 lines
11 KiB
Rust

#![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)]
#![allow(clippy::transmute_ptr_to_ptr)]
extern crate lazy_static;
mod accel;
mod algorithm;
mod bbox;
mod bbox4;
mod boundable;
mod camera;
mod color;
mod fp_utils;
mod image;
mod lerp;
mod light;
mod math;
mod mis;
mod parse;
mod ray;
mod renderer;
mod sampling;
mod scene;
mod scramble;
mod shading;
mod space_fill;
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::bytes::complete::take_until;
use kioku::Arena;
use crate::{
accel::BVH4Node,
bbox::BBox,
parse::{parse_scene, DataTree},
surface::SurfaceIntersection,
timer::Timer,
};
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[allow(clippy::cognitive_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("bucket_size")
.short("b")
.long("bucket_size")
.value_name("N")
.help("Height and width of each render bucket in pixels.")
.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::<SurfaceIntersection>()
);
println!("BBox size: {} bytes", mem::size_of::<BBox>());
// println!("BVHNode size: {} bytes", mem::size_of::<BVHNode>());
println!("BVH4Node size: {} bytes", mem::size_of::<BVH4Node>());
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::Ok((remaining, _)) =
take_until::<&str, &[u8], ()>("__PSY_EOF__")(&input[start..end])
{
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 = Arena::new().with_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 bucket_size = if let Some(bucket_size) = args.value_of("bucket_size") {
u32::from_str(bucket_size).unwrap()
} else {
32
};
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(
bucket_size,
crop,
thread_count,
args.is_present("serialized_output"),
);
// Print render stats
if !args.is_present("serialized_output") {
let rtime = t.tick();
println!("\tRendered scene in {:.3}s", rtime);
println!("\t\t\tRays traced: {}", rstats.ray_count);
println!("\t\t\tRay/node tests: {}", rstats.accel_node_visits);
}
// 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!();
}