diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4fc374 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Full-size renders stay local; commit downscaled JPEG previews to renders/ instead. +*.png diff --git a/README.md b/README.md index 4db3999..dcff04b 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,9 @@ cargo build --release ./target/release/pathtracer --list-scenes ``` -It uses all cores via rayon, shows a progress bar and (for the `cornell` scene) the region brightness table, but does not write `preview.png` or `render_metrics.png`. Without an output name it writes `.png`. +It uses all cores via rayon, shows a progress bar and (for the `cornell` scene) the region brightness table, but does not write `preview.png` or `render_metrics.png`. Without an output name it writes `.png`; an output name ending in `.jpg` or `.jpeg` writes a JPEG (quality 90) instead, which for noisy renders is several times smaller. + +PNG files are ignored by git (see `.gitignore`), so full-size renders stay local. To add a render to the repo, commit a downscaled JPEG preview to [`renders/`](renders/) (the gallery below uses 1280x720, quality 85, about 100 KB each). ### Scenes @@ -100,6 +102,16 @@ It uses all cores via rayon, shows a progress bar and (for the `cornell` scene) | `haze` | The `city` under a layer of atmospheric haze that fades the horizon and glows around the sun | | `city` | Massive scale: a procedural 400x400-lot city of ~124,000 objects (buildings with stepped tiers, mirror-glass towers, parks with trees) under a low sun. Renders 800x600 at 64 spp in ~1 s; without the BVH it is ~1000x slower | +### Gallery + +| | | +|---|---| +| ![beam](renders/beam.jpg) `beam` | ![fog](renders/fog.jpg) `fog` | +| ![caustics](renders/caustics.jpg) `caustics` | ![media](renders/media.jpg) `media` | +| ![city](renders/city.jpg) `city` | ![haze](renders/haze.jpg) `haze` | +| ![spheres](renders/spheres.jpg) `spheres` | ![night](renders/night.jpg) `night` | +| ![classic](renders/classic.jpg) `classic` | ![cornell (Python)](renders/cornell_python.jpg) `cornell`, rendered by `raytracer.py` | + Scenes are defined in [`rust/src/scenes.rs`](rust/src/scenes.rs) with a small builder (`plane`, `sphere`, `cuboid`, `box_light`, `sphere_light`, `rect_light`, `spot_light`, `fog`, `caustic_photons`, `media_photons`). Every emitter must be added with a `*_light` method so light sampling knows about it. ### Volumetrics diff --git a/renders/beam.jpg b/renders/beam.jpg new file mode 100644 index 0000000..2d703eb Binary files /dev/null and b/renders/beam.jpg differ diff --git a/renders/caustics.jpg b/renders/caustics.jpg new file mode 100644 index 0000000..e957cf0 Binary files /dev/null and b/renders/caustics.jpg differ diff --git a/renders/city.jpg b/renders/city.jpg new file mode 100644 index 0000000..f90f678 Binary files /dev/null and b/renders/city.jpg differ diff --git a/renders/classic.jpg b/renders/classic.jpg new file mode 100644 index 0000000..2964b68 Binary files /dev/null and b/renders/classic.jpg differ diff --git a/renders/cornell_python.jpg b/renders/cornell_python.jpg new file mode 100644 index 0000000..5a614be Binary files /dev/null and b/renders/cornell_python.jpg differ diff --git a/renders/fog.jpg b/renders/fog.jpg new file mode 100644 index 0000000..3dcf9f5 Binary files /dev/null and b/renders/fog.jpg differ diff --git a/renders/haze.jpg b/renders/haze.jpg new file mode 100644 index 0000000..d59fa91 Binary files /dev/null and b/renders/haze.jpg differ diff --git a/renders/media.jpg b/renders/media.jpg new file mode 100644 index 0000000..88285f5 Binary files /dev/null and b/renders/media.jpg differ diff --git a/renders/night.jpg b/renders/night.jpg new file mode 100644 index 0000000..8b89276 Binary files /dev/null and b/renders/night.jpg differ diff --git a/renders/spheres.jpg b/renders/spheres.jpg new file mode 100644 index 0000000..2aa8fe6 Binary files /dev/null and b/renders/spheres.jpg differ diff --git a/rust/Cargo.lock b/rust/Cargo.lock index b3ade10..c3ac4db 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -169,6 +169,8 @@ dependencies = [ "moxcms", "num-traits", "png", + "zune-core", + "zune-jpeg", ] [[package]] @@ -581,3 +583,18 @@ name = "zlib-rs" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 61c047b..b424962 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -8,7 +8,7 @@ description = "Cornell Box path tracer (Rust port of raytracer.py)" rand = { version = "0.8", features = ["small_rng"] } rayon = "1" indicatif = "0.17" -image = { version = "0.25", default-features = false, features = ["png"] } +image = { version = "0.25", default-features = false, features = ["png", "jpeg"] } [profile.release] opt-level = 3 diff --git a/rust/src/main.rs b/rust/src/main.rs index 793b31e..934091f 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -419,13 +419,25 @@ fn render(scene: &Scene, s: &Settings, caustics: Option<&CausticMaps>) -> (Vec image::ImageResult<()> { +/// JPEG quality for .jpg/.jpeg output: much smaller files than PNG for +/// noisy renders, with no visible loss at this setting. +const JPEG_QUALITY: u8 = 90; + +/// Saves as JPEG if `path` ends in .jpg/.jpeg, otherwise as PNG. +fn save_image(image: &[Vec3], width: usize, height: usize, path: &str) -> image::ImageResult<()> { let bytes: Vec = image .iter() .flat_map(|c| [c.x, c.y, c.z]) .map(|c| (c.clamp(0.0, 1.0) * 255.0) as u8) .collect(); - image::save_buffer(path, &bytes, width as u32, height as u32, image::ColorType::Rgb8) + let lower = path.to_ascii_lowercase(); + if lower.ends_with(".jpg") || lower.ends_with(".jpeg") { + let file = std::io::BufWriter::new(std::fs::File::create(path)?); + let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(file, JPEG_QUALITY); + encoder.encode(&bytes, width as u32, height as u32, image::ExtendedColorType::Rgb8) + } else { + image::save_buffer(path, &bytes, width as u32, height as u32, image::ColorType::Rgb8) + } } fn region_mean(image: &[Vec3], width: usize, rows: std::ops::Range, cols: std::ops::Range) -> f64 { @@ -582,7 +594,7 @@ fn main() { let elapsed = start.elapsed().as_secs_f64(); let pixels = (settings.width * settings.height) as f64; - if let Err(e) = save_png(&image, settings.width, settings.height, &settings.output) { + if let Err(e) = save_image(&image, settings.width, settings.height, &settings.output) { eprintln!("failed to write {}: {e}", settings.output); std::process::exit(1); }