use std::path::{Path, PathBuf};
use std::process;
use forge_ibl_bake::{bake_all, hdr::{load_hdr_file, HdrImage}, BakeParams};
fn main() {
match run() {
Ok(()) => {}
Err(e) => {
eprintln!("forge-ibl-bake: error: {e}");
process::exit(1);
}
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
let cfg = parse_args(&args)?;
eprintln!("forge-ibl-bake: loading {}", cfg.input.display());
let env = load_input(&cfg.input)?;
eprintln!("forge-ibl-bake: {}×{} loaded", env.width, env.height);
eprintln!("forge-ibl-bake: baking → {}", cfg.out.display());
bake_all(&env, &cfg.params, &cfg.out)?;
eprintln!("forge-ibl-bake: done");
Ok(())
}
struct Config {
input: PathBuf,
out: PathBuf,
params: BakeParams,
}
fn parse_args(args: &[String]) -> Result<Config, Box<dyn std::error::Error>> {
let mut input: Option<PathBuf> = None;
let mut out: Option<PathBuf> = None;
let mut params = BakeParams::default();
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--input" | "-i" => {
i += 1;
input = Some(PathBuf::from(next_arg(args, i, "--input")?));
}
"--out" | "-o" => {
i += 1;
out = Some(PathBuf::from(next_arg(args, i, "--out")?));
}
"--specular-size" => {
i += 1;
params.specular_size = next_arg(args, i, "--specular-size")?.parse()?;
}
"--irradiance-size" => {
i += 1;
params.irradiance_size = next_arg(args, i, "--irradiance-size")?.parse()?;
}
"--lut-size" => {
i += 1;
params.lut_size = next_arg(args, i, "--lut-size")?.parse()?;
}
"--samples" => {
i += 1;
params.samples = next_arg(args, i, "--samples")?.parse()?;
}
"--sh" => {
params.emit_sh = true;
}
"--format" => {
i += 1;
let fmt = next_arg(args, i, "--format")?;
if fmt != "ktx2" {
return Err(format!("unsupported format '{}'; only ktx2 supported in v0", fmt).into());
}
}
"--seed" => {
i += 1;
params.seed = next_arg(args, i, "--seed")?.parse()?;
}
"--help" | "-h" => {
print_usage();
process::exit(0);
}
other => {
return Err(format!("unknown flag: {other}\nRun with --help for usage.").into());
}
}
i += 1;
}
for (name, val) in [
("--specular-size", params.specular_size),
("--irradiance-size", params.irradiance_size),
("--lut-size", params.lut_size),
] {
if val > 4096 {
return Err(format!("{name} {val} exceeds maximum of 4096").into());
}
}
let input = input.ok_or("--input <path.hdr> is required")?;
let out = out.unwrap_or_else(|| PathBuf::from("./baked"));
Ok(Config { input, out, params })
}
fn next_arg<'a>(args: &'a [String], i: usize, flag: &str) -> Result<&'a str, String> {
args.get(i)
.map(|s| s.as_str())
.ok_or_else(|| format!("{flag} requires a value"))
}
fn load_input(path: &Path) -> Result<HdrImage, Box<dyn std::error::Error>> {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if ext.eq_ignore_ascii_case("exr") {
load_exr_input(path)
} else {
load_hdr_file(path).map_err(Into::into)
}
}
#[cfg(feature = "exr")]
fn load_exr_input(path: &Path) -> Result<HdrImage, Box<dyn std::error::Error>> {
forge_ibl_bake::hdr::load_exr_file(path).map_err(Into::into)
}
#[cfg(not(feature = "exr"))]
fn load_exr_input(path: &Path) -> Result<HdrImage, Box<dyn std::error::Error>> {
Err(format!(
"{}: EXR input requires building with --features exr",
path.display()
)
.into())
}
fn print_usage() {
println!(
"forge-ibl-bake v{}
USAGE:
forge-ibl-bake --input env.hdr --out ./baked/ [OPTIONS]
OPTIONS:
--specular-size <N> Cubemap face px for specular prefilter (default 256)
--irradiance-size <N> Cubemap face px for irradiance (default 32)
--lut-size <N> BRDF LUT dimension (default 128)
--samples <N> Monte-Carlo samples per texel (default 1024)
--format ktx2 Output format (default ktx2)
--sh Emit SH9 JSON instead of irradiance cubemap
--seed <N> Reserved (v0 bake is deterministic via Hammersley)
--help Show this help",
env!("CARGO_PKG_VERSION")
);
}