Film scratches

How abrasive contact during film transport removes or scores emulsion into narrow longitudinal tracks whose displayed polarity depends on the damaged generation and later copying.

Visible effect

Long tracks follow the direction of film travel

Film scratches usually appear as thin, nearly vertical lines because a fixed particle, rough guide, or damaged transport surface scores the strip while it moves lengthwise. A track may persist through many frames, wander slightly, break into segments, or begin and end abruptly.

A scratch is not intrinsically white or black. Removed emulsion can transmit more light when a damaged positive print is projected, while damage on a negative may become a dark line after positive printing or digital inversion. Multiple damaged generations can contain both polarities.

Physics

A fixed contact point becomes a longitudinal wound

A fixed abrasive particle scores moving filmFilm travels across a guide where a trapped particle removes a narrow longitudinal strip of emulsion.TRANSPORT DAMAGE · PLAN VIEWABRASIVE PARTICLEFILM TRAVELLAYER CROSS-SECTIONEMULSION REMOVED
The contaminant stays at almost one lateral position while film moves beneath it, turning a local contact point into a long track parallel to transport.

Film repeatedly touches guides, rollers, pressure surfaces, and cleaning hardware. Dust, sand, a burr, or a damaged component can concentrate pressure at one lateral position. As the strip advances, that contact scores the base, protective layer, or image-bearing emulsion along the transport axis.

Damage depth and film generation determine the result. Removing image density makes a positive print locally clearer and therefore bright in projection. A clear track on a negative becomes dense after positive printing or dark after inversion. Damage introduced at several stages can mix both signs.

Mathematics

Thin line distances with longitudinal continuity

Each track has lateral centre cᵢ, small wobble ε, and half-width w. A smooth threshold creates line coverage ; segment mask b introduces breaks. Sets P+ and P− select light and dark damage.

The maximum combines overlapping tracks without making density depend on loop order. Strength k composites both polarities in linear light.

Shader

A bounded bank of persistent procedural tracks

The shader evaluates twelve bounded line slots with strong anisotropy along film travel. This avoids a costly multi-octave noise field while making continuity, width, and polarity explicit.

filmScratchMasksGLSL
// WHAT: Build narrow, mostly vertical scratch tracks in film coordinates.
// HOW: Place deterministic line slots, add small y-dependent sideways wobble,
// break each line into optional segments, and make light/dark damage masks.
// WHY: Abrasive contact stays nearly fixed across the film width while film
// travels through the mechanism, so damage extends along the transport axis.

// This visual hash makes repeatable choices from a scratch ID. Its numeric
// constants decorrelate those choices and do not represent film measurements.
float scratchHash(float value) {
  return fract(sin(value * 127.1) * 43758.5453123);
}

// masks.x contains light/clear scratches; masks.y contains dark scratches.
vec2 filmScratchMasks(
  vec2 uv,
  float time,
  float amount,
  float width,
  float continuity,
  float renewal,
  float seed,
  float polarity
) {
  vec2 masks = vec2(0.0);
  float layoutState = floor(max(time, 0.0) * max(renewal, 0.0));

  // Twelve candidate tracks give a stable performance ceiling. amount selects
  // how many candidates become visible in the current layout state.
  for (int index = 0; index < 12; index += 1) {
    float scratchId = float(index) + floor(seed);
    float enabled = step(
      1.0 - clamp(amount, 0.0, 1.0) * 0.62,
      scratchHash(scratchId + layoutState * 19.7)
    );

    // Keep tracks away from the extreme edge, then vary x position by ID.
    float centreX = 0.04
      + 0.92 * scratchHash(scratchId * 3.17 + layoutState * 2.3);

    // Quantized y bands produce a slightly crooked abrasion track. Greater
    // continuity reduces this sideways wobble.
    float verticalBand = floor(uv.y * 28.0);
    float wobble = (
      scratchHash(verticalBand + scratchId * 11.3) - 0.5
    ) * mix(0.008, 0.001, clamp(continuity, 0.0, 1.0));

    float halfWidth = mix(0.0006, 0.0045, clamp(width, 0.0, 1.0));
    float distanceFromTrack = abs(uv.x - centreX - wobble);
    float lineMask = 1.0 - smoothstep(
      halfWidth,
      halfWidth * 2.2,
      distanceFromTrack
    );

    // Segment noise can interrupt the track. At full continuity every segment
    // is retained, so the scratch becomes an unbroken line.
    float segmentId = floor(uv.y * 18.0);
    float segmentNoise = scratchHash(
      segmentId + scratchId * 29.1 + layoutState * 7.0
    );
    float segmentPresent = mix(
      step(0.68, segmentNoise),
      1.0,
      clamp(continuity, 0.0, 1.0)
    );
    float scratchMask = enabled * lineMask * segmentPresent;

    // polarity > 0 selects light marks, < 0 dark marks, and 0 mixes both.
    float lightScratch = polarity > 0.5
      ? 1.0
      : (polarity < -0.5 ? 0.0 : step(0.5, scratchHash(scratchId * 41.9)));
    masks.x = max(masks.x, scratchMask * lightScratch);
    masks.y = max(masks.y, scratchMask * (1.0 - lightScratch));
  }

  return clamp(masks, 0.0, 1.0);
}

vec3 applyFilmScratches(vec3 sourceSrgb, vec2 masks, float strength) {
  vec3 sourceLinear = pow(max(sourceSrgb, vec3(0.0)), vec3(2.2));
  float safeStrength = clamp(strength, 0.0, 1.0);
  float lightMask = masks.x * safeStrength;
  float darkMask = masks.y * safeStrength;
  vec3 withLightMarks = mix(sourceLinear, vec3(1.0), lightMask);
  vec3 damaged = withLightMarks * (1.0 - darkMask);
  return pow(max(damaged, vec3(0.0)), vec3(1.0 / 2.2));
}
Processing pipelineBoxes mark actual render-pass boundaries.
  1. Source texturesRGB · one texture read per pixel
  2. Persistent scratch-track compositeOne fullscreen render pass
    • Aspect-fill sample
    • Generate bounded longitudinal tracks
    • Split light and dark transfer polarities
    • Composite damage in linear light
    • Encode to sRGB
  3. Display output

Why these steps are here

  1. Place tracks across x. A fixed abrasive location should not move with scene objects.
  2. Vary slowly along y. Small wobble and breaks prevent perfectly synthetic ruler lines.
  3. Hold the pattern. Scratches persist across frames instead of becoming sparkling pixel noise.
  4. Separate polarity. Transfer history changes tone without changing damage geometry.
  5. Composite in linear light. Light and dark tracks remain bounded and predictable.

Notes

  • The playground is procedural; scanned scratch mattes are preferable when a specific archive element must be matched.
  • Vertical describes the displayed frame only when film travels vertically through the mechanism. The physical rule is alignment with transport.
  • A scratch can remain stable across many frames, while dust may jump or disappear when a particle moves.
  • Restoration should track long temporal evidence before interpolating a line so genuine scene edges are not erased.

References

Kodak — Handling of Processed Film — manufacturer guidance on dust, abrasion, longitudinal scratches, dirty rollers, cleaning, and storage.