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:
@@ -2408,6 +2408,13 @@ impl DevelopSession {
|
||||
dr_pipeline::ops::film_sim::EXPOSURE,
|
||||
)
|
||||
.unwrap_or(0.0),
|
||||
push_stops: self
|
||||
.graph
|
||||
.param(
|
||||
dr_pipeline::ops::film_sim::ID,
|
||||
dr_pipeline::ops::film_sim::PUSH,
|
||||
)
|
||||
.unwrap_or(0.0),
|
||||
print_exposure_ev: self
|
||||
.graph
|
||||
.param(
|
||||
@@ -2471,6 +2478,11 @@ impl DevelopSession {
|
||||
/// how the negative was exposed. So a slider drag has to rebuild the
|
||||
/// lookup, which is why the bake is measured in milliseconds and not in
|
||||
/// frames.
|
||||
/// The stock, the paper, and how far it was developed.
|
||||
///
|
||||
/// Push rides with the other two through every path that re-bakes, because
|
||||
/// it is the same kind of fact: a decision about the material rather than
|
||||
/// an adjustment to the picture it produced.
|
||||
pub fn rebake_film(&mut self) {
|
||||
if let Some((stock, print)) = self.film().map(|(s, p)| (s.to_string(), p)) {
|
||||
self.choose_film(Some(&stock), print);
|
||||
|
||||
Reference in New Issue
Block a user