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

Overlapping spectral records form coupled subtractive dye images Red, green, and blue wavelength bands overlap as they expose three emulsion records. Development forms cyan, magenta, and yellow dye densities, which are jointly corrected by a three-dimensional colour transform.OVERLAPPING SCENE SPECTRUMLONG λMID λSHORT λSENSITISED RECORDSRED-SENSITIVE → CYAN DYEGREEN-SENSITIVE → MAGENTA DYEBLUE-SENSITIVE → YELLOW DYESPECTRAL + PROCESS COUPLING3D COLOUR TRANSFORM
The records are not ideal RGB channels: their spectral sensitivities and dye absorptions overlap. Correction therefore maps a colour triplet to another colour triplet; it is not three independent tone curves.

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.

colourFilmDyeResponseGLSL
// 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));
}
Processing pipelineBoxes mark actual render-pass boundaries.
  1. Source texturesRGB · one texture read per pixel
  2. 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
  3. Display output

Why these steps are here

  1. Shape exposure first. Toe, shoulder, and contrast establish the nonlinear layer response.
  2. Couple before colour correction. Cross terms make each result depend on neighbouring layer records.
  3. Preserve endpoints. The interaction envelope vanishes at zero and one.
  4. Correct in three dimensions. Separation and bias operate on the coupled colour, not three unrelated scalar curves.
  5. 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.