Added the gallery
This commit is contained in:
1 parent
d44f159c98
commit
8861858f0d
8 files changed
+482
-11
No files matched your search
@@ -9,6 +9,7 @@ use std::time::Instant;
|
||||
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use resolved::gpu::AdapterChoice;
|
||||
use resolved::radiometry::{Radiometry, AU};
|
||||
use resolved::{
|
||||
CameraTier, EarthTexture, Fidelity, Frame, LightingTier, MoonTexture, Renderer, RendererOptions,
|
||||
};
|
||||
@@ -45,6 +46,11 @@ struct RenderArgs {
|
||||
/// An 8-bit sRGB PNG for viewing: bias subtracted, saturation as white.
|
||||
#[arg(long)]
|
||||
preview: Option<PathBuf>,
|
||||
/// Make the preview look like a photograph rather than the raw signal: white
|
||||
/// balanced to sunlight, and a tone curve that brightens shadows and midtones and
|
||||
/// rolls highlights off to white at saturation. --out is unchanged.
|
||||
#[arg(long)]
|
||||
pretty: bool,
|
||||
/// Lighting fidelity tier: L0, or L1 (adds Earth's surface, shadows on targets,
|
||||
/// light from Earth and the Moon, eclipses on them, and the Sun's limb darkening;
|
||||
/// needs Blue Marble data).
|
||||
@@ -267,7 +273,21 @@ fn render(args: &RenderArgs) -> Result<(), String> {
|
||||
// Bias reads as black and the saturation level as white.
|
||||
let black = detector.bias;
|
||||
let white = detector.dn(detector.full_well) as f64;
|
||||
write_preview(&frame, preview, black, white)?;
|
||||
let look = if args.pretty {
|
||||
// Each channel scaled to the weakest one's response to a white surface in
|
||||
// sunlight, so sunlit white comes out neutral and no channel is amplified.
|
||||
let radiometry = Radiometry::new(renderer.camera());
|
||||
let white_surface: Vec<f64> = (0..frame.channels as usize)
|
||||
.map(|k| radiometry.lambert_radiance(k, 1.0, 1.0, AU))
|
||||
.collect();
|
||||
let weakest = white_surface.iter().copied().fold(f64::INFINITY, f64::min);
|
||||
Look::Pretty {
|
||||
balance: white_surface.iter().map(|w| weakest / w).collect(),
|
||||
}
|
||||
} else {
|
||||
Look::Raw
|
||||
};
|
||||
write_preview(&frame, preview, black, white, &look)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -303,7 +323,33 @@ fn write_dn(frame: &Frame, path: &Path, adc_bits: u32) -> Result<(), String> {
|
||||
.map_err(|e| format!("{}: {e}", path.display()))
|
||||
}
|
||||
|
||||
fn write_preview(frame: &Frame, path: &Path, black: f64, white: f64) -> Result<(), String> {
|
||||
/// How the preview turns DN into an image.
|
||||
enum Look {
|
||||
/// The signal as recorded: linear from the bias (black) to saturation (white).
|
||||
Raw,
|
||||
/// Like a photograph: each channel scaled by `balance`, then [`tone`].
|
||||
Pretty { balance: Vec<f64> },
|
||||
}
|
||||
|
||||
/// How much the pretty tone curve brightens the darkest values: 3, about 1.6 stops.
|
||||
const PRETTY_LIFT: f64 = 3.0;
|
||||
|
||||
/// The pretty tone curve, from the fraction of saturation `x` to linear display value:
|
||||
/// Reinhard's operator with its white point at saturation. It starts with slope
|
||||
/// [`PRETTY_LIFT`] and rises ever more slowly to 1 at `x = 1`, so midtones brighten
|
||||
/// and highlights compress instead of clipping.
|
||||
fn tone(x: f64) -> f64 {
|
||||
let x = x.max(0.0);
|
||||
PRETTY_LIFT * x * (1.0 + x / PRETTY_LIFT) / (1.0 + PRETTY_LIFT * x)
|
||||
}
|
||||
|
||||
fn write_preview(
|
||||
frame: &Frame,
|
||||
path: &Path,
|
||||
black: f64,
|
||||
white: f64,
|
||||
look: &Look,
|
||||
) -> Result<(), String> {
|
||||
let srgb = |x: f64| {
|
||||
let x = x.clamp(0.0, 1.0);
|
||||
let v = if x <= 0.003_130_8 {
|
||||
@@ -313,10 +359,25 @@ fn write_preview(frame: &Frame, path: &Path, black: f64, white: f64) -> Result<(
|
||||
};
|
||||
(v * 255.0).round() as u8
|
||||
};
|
||||
let level = |d: u16| (d as f64 - black) / (white - black);
|
||||
let bytes: Vec<u8> = frame
|
||||
.dn
|
||||
.iter()
|
||||
.map(|&d| srgb((d as f64 - black) / (white - black)))
|
||||
.chunks(frame.channels as usize)
|
||||
.flat_map(|pixel| -> Vec<u8> {
|
||||
match look {
|
||||
Look::Raw => pixel.iter().map(|&d| srgb(level(d))).collect(),
|
||||
// A channel at saturation lost how bright it was, and balancing the
|
||||
// others would tint it: saturated pixels are white.
|
||||
Look::Pretty { .. } if pixel.iter().any(|&d| d as f64 >= white) => {
|
||||
vec![255; pixel.len()]
|
||||
}
|
||||
Look::Pretty { balance } => pixel
|
||||
.iter()
|
||||
.zip(balance)
|
||||
.map(|(&d, b)| srgb(tone(level(d) * b)))
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
image::save_buffer(
|
||||
path,
|
||||
@@ -327,3 +388,22 @@ fn write_preview(frame: &Frame, path: &Path, black: f64, white: f64) -> Result<(
|
||||
)
|
||||
.map_err(|e| format!("{}: {e}", path.display()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tone_lifts_the_darks_and_reaches_white_at_saturation() {
|
||||
assert_eq!(tone(0.0), 0.0);
|
||||
assert!((tone(1.0) - 1.0).abs() < 1e-12);
|
||||
// Slope PRETTY_LIFT at zero; brighter than raw everywhere between.
|
||||
assert!((tone(1e-6) / 1e-6 - PRETTY_LIFT).abs() < 1e-3);
|
||||
let mut last = 0.0;
|
||||
for i in 1..100 {
|
||||
let x = i as f64 / 100.0;
|
||||
assert!(tone(x) > x && tone(x) > last);
|
||||
last = tone(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,34 @@ fn example_scene_on_gpu_and_cpu() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pretty_previews_lift_the_darks_and_leave_space_black() {
|
||||
let preview = |pretty: bool| {
|
||||
let path = out_dir().join(format!("preview-pretty-{pretty}.png"));
|
||||
let path_str = path.to_str().unwrap();
|
||||
let mut args = vec![
|
||||
"render",
|
||||
"--scene",
|
||||
"examples/scenes/boxwing.toml",
|
||||
"--preview",
|
||||
path_str,
|
||||
];
|
||||
if pretty {
|
||||
args.push("--pretty");
|
||||
}
|
||||
let result = resolved(&args);
|
||||
assert!(result.status.success(), "{result:?}");
|
||||
image::open(&path).unwrap().into_rgb8()
|
||||
};
|
||||
let (raw, pretty) = (preview(false), preview(true));
|
||||
// Space is the bias, black either way.
|
||||
assert_eq!(raw.get_pixel(0, 0).0, [0; 3]);
|
||||
assert_eq!(pretty.get_pixel(0, 0).0, [0; 3]);
|
||||
// Earth's dayside below the limb, well short of saturation: brighter when pretty.
|
||||
let (r, p) = (raw.get_pixel(0, 1079).0, pretty.get_pixel(0, 1079).0);
|
||||
assert!((0..3).all(|k| p[k] > r[k]), "raw {r:?}, pretty {p:?}");
|
||||
}
|
||||
|
||||
#[cfg(feature = "astrokit")]
|
||||
#[test]
|
||||
fn lunar_eclipse_darkens_the_moon() {
|
||||
|
||||
Reference in new issue
Block a user