D19 retired the per-body base curve, moved the matrix ahead of the edits and the film into the view transform's place, but a dozen doc comments still listed the curve among what a pixel passes through, or said the film skipped it. The detail stage's module doc still drew the matrix after the edits and the last detail pass encoding, which the view pass took over. The film crate's README gave the base curves as its reason for being data, and the ops README's list of hand-written nodes had neither the view transform nor three of the five kernels. FR-MRG-2 gave the base curve as why the merge cuts below the profile; the view transform is why now. The decision table still said colour defaults were a per-body curve, and FR-DEV-3j said only the default view transform skips a JPEG, where the node skips one whatever its sliders say. frame-budget.md records the view pass as unmeasured.
99 lines
4.9 KiB
Markdown
99 lines
4.9 KiB
Markdown
# Film stocks
|
||
|
||
One file per stock in [`profiles/`](profiles/). Adding a stock is adding a
|
||
file — no code change, no shader, no new operation — because under the GPLv3
|
||
a stock should be contributable without a release.
|
||
|
||
## What a profile is
|
||
|
||
Three measured tables, all of them published in the manufacturer's datasheet:
|
||
|
||
| Field | What it decides |
|
||
|---|---|
|
||
| `log_sensitivity` | what each emulsion layer *sees*, per wavelength |
|
||
| `density_curves` | contrast, latitude, and where the stock clips |
|
||
| `dye_density` | what the developed stock *looks* like, per wavelength |
|
||
| `base_density` | the support: film base, and a colour negative's orange mask |
|
||
|
||
Plus `kind` (negative or positive), `support` (film or paper), and the two
|
||
illuminants the data is referenced to. A print paper is a stock like any
|
||
other; `support` exists so an interface can offer papers separately, not
|
||
because the renderer treats them differently.
|
||
|
||
## Why it is not a LUT
|
||
|
||
Because the parameters stay physical. Opening up a stop moves the picture along
|
||
the film's own characteristic curve — toe, shoulder and all — instead of
|
||
scaling a number somebody baked at one exposure. A scanned negative comes out
|
||
orange and inverted because that is what a negative *is*, and it becomes a
|
||
photograph when a paper profile prints it, exactly as it would in a darkroom.
|
||
|
||
The data cost runs the other way from a LUT collection too: a stock is about
|
||
17 kB of measurements, where one HaldCLUT is roughly 800 kB of one person's
|
||
grade.
|
||
|
||
## How it runs
|
||
|
||
The spectral chain reduces to three tables, and the reduction is exact where it
|
||
matters — see [`src/bake.rs`](src/bake.rs) for the argument:
|
||
|
||
1. **A 3×3 matrix**, linear sRGB to the three layers' exposure. Exact, not an
|
||
approximation: the reconstructed scene spectrum is linear in the sRGB
|
||
triple, so the integral collapses into nine numbers.
|
||
2. **Three 1D curves**, log exposure to density, sampled at 256 points — one
|
||
row per development time the datasheet measures. Push picks between the
|
||
rows, and interpolating them is exact, because density is linear in push
|
||
between two measured processes.
|
||
3. **One 32³ lookup**, density to linear sRGB — dye absorption, the viewing
|
||
illuminant and the chromatic adaptation, all of which take exactly three
|
||
numbers in. A printed negative is two: the film's cube ends at the paper's
|
||
log exposure through the negative, the enlarger's exposure is added there,
|
||
and the paper's own curve row and cube take it to linear sRGB.
|
||
|
||
The stock is the last thing that happens to the picture. It runs in the view
|
||
transform's place (D19): handed linear sRGB, scene-referred, after every other
|
||
adjustment and after sharpening and noise reduction, and handing back the
|
||
rendering the output transform encodes. So every other slider decides the
|
||
exposure the negative receives, and the default tone mapping is not applied
|
||
on top.
|
||
|
||
Per pixel that is a matrix multiply, a handful of curve taps and one texture
|
||
fetch — two for a print. Splitting 2 from 3, rather than baking one LUT over exposure, is measured
|
||
rather than assumed: the curve carries all the sharp shape and the dye mixing
|
||
is smooth, so folding the curve into the 3D lookup would need it three times
|
||
larger for the same error. At 32³ the worst interpolation error is about 0.003
|
||
in linear sRGB, below one 8-bit code value, and there is a test that says so.
|
||
|
||
**No slider is baked.** Camera exposure is a gain before the matrix, push
|
||
chooses between curve rows, print exposure is the addition between the two
|
||
cubes, and format sets the grain; each reaches the shader as a uniform that is
|
||
linear in what it does. That is what lets a mask layer hold its own film
|
||
settings, and a pixel under several layers take the weighted average of them.
|
||
Only the enlarger's filtration is solved at bake time, against the
|
||
photograph's exposure — an enlarger has one filtration for the whole print —
|
||
so the film's Exposure, set on the whole photograph, is the one slider that
|
||
rebakes. The stock and its
|
||
paper are the photograph's; a layer has no picker.
|
||
|
||
## Adding a stock
|
||
|
||
If spektrafilm has it, add its name to `STOCKS` in
|
||
[`tools/film-profiles/convert.py`](../../tools/film-profiles/convert.py) and
|
||
re-run it. Otherwise write the YAML by hand from the datasheet; the loader
|
||
validates the table lengths and says which file and field is wrong.
|
||
|
||
Either way, list it in `BUILT_IN` in [`src/lib.rs`](src/lib.rs) to compile it
|
||
in — or drop it in the profile directory at runtime, which is the path meant
|
||
for stocks that ship separately from the binary.
|
||
|
||
## Provenance
|
||
|
||
The shipped profiles are converted from
|
||
[spektrafilm](https://github.com/andreavolpato/spektrafilm) by Andrea Volpato,
|
||
licensed CC BY-SA 4.0. See [`profiles/LICENSE-PROFILES.txt`](profiles/LICENSE-PROFILES.txt)
|
||
for the licence and [`profiles/CHANGELOG.txt`](profiles/CHANGELOG.txt) for what
|
||
the conversion changed and what it deliberately did not.
|
||
|
||
The sRGB reflectance basis is Mallett & Yuksel (2019); the observer is the CIE
|
||
1931 2°.
|