Colour-film dye response
How spectrally overlapping emulsion layers form cyan, magenta, and yellow dye records whose nonlinear interactions reshape hue, saturation, and tone — and why faithful correction is a three-dimensional colour transform.
Visible effect
Colour paths bend as exposure and neighbouring channels change
Colour film does not record three isolated display channels. Light-sensitive layers overlap spectrally, and development forms subtractive cyan, magenta, and yellow dye images. A change in one exposure component can therefore alter more than one output component after the negative is printed or scanned.
The visible result is a stock- and process-specific relation between colour and exposure: hue paths bend, saturation changes through the tonal range, and neutral balance can drift warm or cool. Three separate one-dimensional curves cannot encode those cross-channel dependencies.
Physics
Overlapping records form subtractive dye images
A colour negative contains multiple emulsion layers sensitised to broad, overlapping wavelength bands. During colour development, the red-sensitive record forms cyan dye, the green-sensitive record forms magenta dye, and the blue-sensitive record forms yellow dye. Those dyes absorb light when the negative is printed or scanned.
Real stocks use additional layers, interlayers, masks, and carefully engineered dye spectra. The final colour also depends on development, printing, scanning, and display conversion. “RGB layers” is therefore useful shorthand, not a literal set of ideal digital primaries.
Mathematics
A bounded analytic stand-in for a measured 3D LUT
The playground first applies one bounded toe/shoulder functionf to exposed linear-light channels. It then adds interactions whose strength is K. Each interaction vanishes at black, white, and neutral equality:
Colour separation expands or contracts the result around luminance; warm bias shifts midtones through a bounded envelope. A production emulation would normally sample a measured three-dimensional LUT: the output at (r,g,b) is interpolated from neighbouring lattice points, so every output component may depend on all three inputs.
Shader
GLSL coupled linear-light transform
The runtime compiles the exact function shown below. Its wrapper performs aspect-fill sampling, sRGB decoding, and sRGB encoding.
// WHAT: Approximate colour-negative exposure response and coupled dye records.
// HOW: Shape each linear-light channel with toe, shoulder, and contrast; couple
// the records; adjust colour separation and warm bias; then mix with the source.
// WHY: Film colour is not three independent display curves, so controlled
// cross-channel terms make the model's dye-layer interaction visible.
float colourFilmChannelResponse(
float value,
float exposureStops,
float toe,
float shoulder,
float contrast
) {
// Exposure acts before the nonlinear characteristic curve.
float exposed = clamp(value * exp2(exposureStops), 0.0, 1.0);
// Toe compresses shadows; shoulder compresses highlights.
float toeShaped = pow(exposed, 1.0 + clamp(toe, 0.0, 1.0) * 2.0);
float shoulderShaped =
1.0 - pow(1.0 - toeShaped, 1.0 + clamp(shoulder, 0.0, 1.0) * 2.0);
float safeContrast = max(0.25, contrast);
float low = pow(shoulderShaped, safeContrast);
float high = pow(1.0 - shoulderShaped, safeContrast);
return low / max(low + high, 0.000001);
}
vec3 colourFilmDyeResponse(
vec3 sourceLinear,
float exposureStops,
float toe,
float shoulder,
float contrast,
float layerCoupling,
float colourSeparation,
float warmBias,
float effectMix
) {
vec3 shaped = vec3(
colourFilmChannelResponse(sourceLinear.r, exposureStops, toe, shoulder, contrast),
colourFilmChannelResponse(sourceLinear.g, exposureStops, toe, shoulder, contrast),
colourFilmChannelResponse(sourceLinear.b, exposureStops, toe, shoulder, contrast)
);
// Coupling is strongest in mid-range dye density and vanishes at endpoints.
float coupling = clamp(layerCoupling, 0.0, 1.0);
vec3 coupled = shaped;
coupled.r += coupling * shaped.r * (1.0 - shaped.r) *
(0.36 * (shaped.g - shaped.r) - 0.18 * (shaped.b - shaped.r));
coupled.g += coupling * shaped.g * (1.0 - shaped.g) *
(0.18 * (shaped.r - shaped.g) + 0.12 * (shaped.b - shaped.g));
coupled.b += coupling * shaped.b * (1.0 - shaped.b) *
(0.28 * (shaped.g - shaped.b) - 0.12 * (shaped.r - shaped.b));
// Separate colour around constant luminance rather than scaling RGB directly.
float luminance = dot(coupled, vec3(0.2126, 0.7152, 0.0722));
vec3 separated = vec3(luminance) +
(coupled - vec3(luminance)) * max(0.0, colourSeparation);
// Keep the warm bias in midtones so black and white endpoints remain stable.
float biasEnvelope = luminance * (1.0 - luminance) * warmBias;
vec3 corrected = clamp(
separated + biasEnvelope * vec3(0.18, 0.04, -0.12),
0.0,
1.0
);
// Mix last so zero effectMix remains an exact identity.
return mix(sourceLinear, corrected, clamp(effectMix, 0.0, 1.0));
}- Source texturesRGB · one texture read per pixel
- Dye-response transformOne fullscreen render pass
- Aspect-fill sample
- Decode sRGB
- Apply exposure response
- Couple the colour layers
- Apply separation and colour bias
- Mix and encode to sRGB
- Display output
Why these steps are here
- Shape exposure first. Toe, shoulder, and contrast establish the nonlinear layer response.
- Couple before colour correction. Cross terms make each result depend on neighbouring layer records.
- Preserve endpoints. The interaction envelope vanishes at zero and one.
- Correct in three dimensions. Separation and bias operate on the coupled colour, not three unrelated scalar curves.
- Mix last. Zero mix is a tested identity.
Notes
- This is a pedagogical display-referred approximation, not a profile for a named film stock.
- A calibrated pipeline needs measured targets, a defined illuminant, negative or print characterization, scanner profiling, and a documented output colour space.
- A 3D LUT captures cross-channel mapping but does not by itself simulate grain, halation, dye clouds, chemistry variation, fading, or spatial nonuniformity.
- The model clamps to the display gamut; production grading should use an explicit scene/display transform and a deliberate gamut-mapping policy.
References
Kodak VISION Color Print Film 2383/3383 — Technical Information — manufacturer data for red, green, and blue characteristic curves, spectral response, and granularity.
Kodak — Exploring the Color Image — manufacturer educational reference for subtractive dye layers, colour reproduction, exposure, and image-forming light spread.