Develop longer, from the measurements rather than from a contrast slider
Pushing was not a thing to simulate. It was measured data being thrown away: Double-X and 2302 each ship five characteristic curves, one per development time, and this shipped the 6.5-minute column and discarded four. All five now ship and interpolate. The axis is real. Double-X runs 4 to 12 minutes, and across it the average gradient goes 0.472 to 1.034 while Dmax goes 1.19 to 2.56. The control is in stops, because that is what a photographer means, and one stop is a factor of about 1.41 in time. That mapping is checked rather than assumed: against Double-X's own axis it lands within 2% of the 9-minute column for +1, and near 12 minutes for +2, which are the times the datasheet gives for exactly that. There is a test. **Pushing must not recover shadow detail, and this does not.** Across the whole measured range the speed point moves about a third of a stop while the gradient doubles; three stops under mid-grey, density goes from 0.008 to 0.035, which is still nothing. Developing longer multiplies what was already recorded and cannot record what never hit the film. A push built as added exposure or global contrast brightens those shadows instead and looks convincing until someone who shoots film sees it, so that property has a test of its own. Interpolated in *log* time, because development is multiplicative: 4 to 5 minutes is the same amount of push as 9 to 12, and interpolating linearly would bunch the control at one end. Clamped at both ends, because past the published range there is no data and extrapolating a contrast curve invents an emulsion nobody tested. A stock measured at one process ignores the control entirely rather than inventing a curve for it -- Portra 800's pushes are separate *measured* profiles, which is the honest way to offer those. Costs nothing per pixel and changes no shader. The curves are a per-stock table, so the interpolation happens on the CPU at bake time, where choosing a stock and moving its sliders already rebakes. The Vulkan shader is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ use crate::operation::{Operation, Uniform};
|
||||
pub const ID: OpId = OpId("film_sim");
|
||||
pub const EXPOSURE: ParamId = ParamId("exposure");
|
||||
pub const PRINT_EXPOSURE: ParamId = ParamId("print_exposure");
|
||||
pub const PUSH: ParamId = ParamId("push");
|
||||
|
||||
/// How many samples a characteristic curve carries.
|
||||
///
|
||||
@@ -64,6 +65,11 @@ static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
||||
params: &[
|
||||
ParamDescriptor::stops("exposure", "param.film_sim.exposure", -3.0, 3.0),
|
||||
ParamDescriptor::stops("print_exposure", "param.film_sim.print_exposure", -3.0, 3.0),
|
||||
// TRACES: FR-DEV-3f
|
||||
// Development, in stops of push. Bounded by what the manufacturers
|
||||
// actually published: Double-X's measured axis spans about -1 to +2,
|
||||
// and beyond a range like that a curve would have to be invented.
|
||||
ParamDescriptor::stops("push", "param.film_sim.push", -1.0, 3.0),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -122,6 +128,7 @@ impl FilmTables {
|
||||
pub struct FilmSim {
|
||||
exposure: f32,
|
||||
print_exposure: f32,
|
||||
push: f32,
|
||||
tables: Option<FilmTables>,
|
||||
}
|
||||
|
||||
@@ -165,6 +172,7 @@ impl Operation for FilmSim {
|
||||
match id {
|
||||
EXPOSURE => self.exposure = value,
|
||||
PRINT_EXPOSURE => self.print_exposure = value,
|
||||
PUSH => self.push = value,
|
||||
_ => log::warn!("film_sim: unknown parameter {id}"),
|
||||
}
|
||||
}
|
||||
@@ -173,6 +181,7 @@ impl Operation for FilmSim {
|
||||
match id {
|
||||
EXPOSURE => self.exposure,
|
||||
PRINT_EXPOSURE => self.print_exposure,
|
||||
PUSH => self.push,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user