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:
@@ -74,13 +74,11 @@ STOCKS = [
|
||||
# moving everything to `dev`, so the colour stocks stay on the released branch.
|
||||
DEV_BRANCH = {"kodak_trix", "kodak_doublex", "kodak_2302"}
|
||||
|
||||
# Which development time to ship, for a stock whose curves are measured at
|
||||
# several. Index into the profile's own `development_time` array.
|
||||
# Which development time is "normal" for a stock measured at several.
|
||||
#
|
||||
# The middle of the published range, which is the manufacturer's standard
|
||||
# process: 6.5 minutes for Double-X, 5 for 2302. The others are push and pull,
|
||||
# and are the obvious next control -- the data for them ships in the upstream
|
||||
# file and is dropped here only because nothing can yet ask for it.
|
||||
# process: 6.5 minutes for Double-X, 5 for 2302. The rest of the range is push
|
||||
# and pull, and all of it now ships -- see `development_times` in the profile.
|
||||
DEVELOPMENT_INDEX = 2
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -218,9 +216,17 @@ def convert(stock):
|
||||
|
||||
# A monochrome stock may be measured at several development times. Take one
|
||||
# column before anything else, so everything below sees the usual shape.
|
||||
development_times = data.get("development_time")
|
||||
development_set = None
|
||||
if monochrome:
|
||||
curves = data["density_curves"]
|
||||
if curves and isinstance(curves[0], list) and len(curves[0]) > 1:
|
||||
# Keep the whole axis: one 256-sample curve per development time.
|
||||
# Pushing is measured data on these stocks, not an effect, and
|
||||
# throwing away four of five columns threw the measurement away.
|
||||
development_set = [
|
||||
[[r[t]] * 3 for r in curves] for t in range(len(curves[0]))
|
||||
]
|
||||
data["density_curves"] = pick_development(curves, DEVELOPMENT_INDEX)
|
||||
base = data.get("base_density")
|
||||
if base and isinstance(base[0], list):
|
||||
@@ -286,6 +292,25 @@ def convert(stock):
|
||||
]
|
||||
for triple in columns(data["density_curves"]):
|
||||
out.append(f" - {row(triple, 5)}")
|
||||
|
||||
if development_set and development_times:
|
||||
normal = development_times[min(DEVELOPMENT_INDEX, len(development_times) - 1)]
|
||||
out += [
|
||||
"",
|
||||
"# The development axis, in minutes. Pushing is measured on this",
|
||||
"# stock rather than modelled: developing longer raises the contrast",
|
||||
"# and the maximum density, and *barely moves the speed point* --",
|
||||
"# which is why pushing buys contrast and not shadow detail.",
|
||||
"#",
|
||||
"# `development_normal` is the manufacturer's standard process and is",
|
||||
"# the curve `density_curves` above carries.",
|
||||
f"development_normal: {num(normal)}",
|
||||
f"development_times: {row(development_times)}",
|
||||
"development_curves:",
|
||||
]
|
||||
for t, curves_at_t in zip(development_times, development_set):
|
||||
out.append(f" # {t:g} minutes")
|
||||
out.append(" - " + "[" + ", ".join(row(c, 5) for c in curves_at_t) + "]")
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user