Replace committed PNG renders with small JPEG previews

Full-size PNG renders are now ignored; downscaled 1280x720 JPEG
previews live in renders/ and are shown in a README gallery. The
renderer writes JPEG when the output name ends in .jpg/.jpeg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-26 14:30:10 +02:00
parent 7b5fc1f385
commit e7e258cb41
15 changed files with 48 additions and 5 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# Full-size renders stay local; commit downscaled JPEG previews to renders/ instead.
*.png

View file

@ -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 `<scene>.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 `<scene>.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

BIN
renders/beam.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
renders/caustics.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
renders/city.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
renders/classic.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
renders/cornell_python.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

BIN
renders/fog.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
renders/haze.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

BIN
renders/media.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
renders/night.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
renders/spheres.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

17
rust/Cargo.lock generated
View file

@ -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",
]

View file

@ -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

View file

@ -419,14 +419,26 @@ fn render(scene: &Scene, s: &Settings, caustics: Option<&CausticMaps>) -> (Vec<V
(image, total_samples)
}
fn save_png(image: &[Vec3], width: usize, height: usize, path: &str) -> 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<u8> = image
.iter()
.flat_map(|c| [c.x, c.y, c.z])
.map(|c| (c.clamp(0.0, 1.0) * 255.0) as u8)
.collect();
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<usize>, cols: std::ops::Range<usize>) -> f64 {
let n = rows.len() * cols.len();
@ -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);
}