Film dust and dirt

How particles on or close to film form compact, frame-bound silhouettes whose sharpness and displayed polarity reveal where and when contamination entered the photochemical chain.

Visible effect

Small silhouettes stay with the film frame

Film dust appears as compact flecks, dots, fibres, and irregular blobs that sit in image coordinates rather than belonging to photographed objects. A particle may hold for several frames, jump when the film advances, or disappear when the transport or cleaning system dislodges it.

Dust on or very close to the emulsion casts a sharp, dark exposure shadow. After negative-to-positive copying, the same missing density can become a light mark. Several contaminated generations may therefore contain both polarities.

Physics

Particle distance controls edge sharpness

Dust distance from film controls shadow sharpnessA particle touching the emulsion makes a compact sharp shadow, while a particle farther from the film makes a wider soft-edged shadow.CONTAMINATION · FILM CROSS-SECTIONINCIDENT LIGHTPARTICLE ON FILMPARTICLE ABOVE FILMSHARP SHADOWSOFT PENUMBRAEMULSIONFILM BASEDISTANCE
A particle touching the emulsion blocks a compact footprint. Increasing its separation lets rays arrive around the particle, widening and softening the penumbra.

Dust can enter while film is loaded, travel through a dirty gate or magazine, settle during printing, or be carried between generations. A particle lying on the emulsion blocks incident light over almost its own outline. Lift it away from the image plane and rays pass around its edge, creating a larger, softer penumbra.

Displayed tone records the contamination stage. Camera-gate dust commonly prevents exposure and leaves low density on a negative; inversion or positive printing can reverse that sign. Dust introduced on a later print can appear light directly, and an archive assembled from several generations can contain both.

Mathematics

Irregular radial masks with controlled feathering

Each particle has centre cᵢ, nominal radius r̄ᵢ, and a low-order angular perturbation that keeps the silhouette from becoming a perfect disk. Feather width fᵢ models the penumbra. Separate sets P+ and P− retain transfer polarity.

The maximum combines overlaps without accumulating opacity by loop order. Strength k composites the masks in linear light. A held integer state changes the particle layout at film-like intervals rather than every display refresh.

Shader

A bounded bank of frame-held particle silhouettes

The shader uses twenty-eight bounded particle slots with irregular radial edges, exposing size, softness, persistence, and transfer polarity independently.

filmDustMasksGLSL
// WHAT: Generate sharp, irregular dust silhouettes attached to the film frame.
// HOW: Create a bounded set of deterministic particles, vary each elliptical
// edge with angular waves, and combine separate light and dark damage masks.
// WHY: Dust close to film casts a particle-shaped mark in film coordinates;
// a broad optical blur or continuously drifting screen-space noise is different.

// A small deterministic hash turns one particle number into repeatable [0, 1]
// choices. Its constants are decorrelation values, not physical dust parameters.
float dustHash(float value) {
  return fract(sin(value * 127.1) * 43758.5453123);
}

// The returned vec2 contains two masks:
//   masks.x = light/clear dust marks
//   masks.y = dark/opaque dust marks
vec2 filmDustMasks(
  vec2 uv,
  float time,
  float amount,
  float size,
  float softness,
  float hold,
  float seed,
  float polarity
) {
  vec2 masks = vec2(0.0);

  // Hold one particle layout for a finite interval, then choose a new state.
  float layoutState = floor(max(time, 0.0) / max(hold, 0.05));

  // Evaluate at most 28 possible particles. amount controls how many are active.
  for (int index = 0; index < 28; index += 1) {
    float particleId = float(index) + floor(seed) + layoutState * 31.7;
    float enabled = step(
      1.0 - clamp(amount, 0.0, 1.0) * 0.72,
      dustHash(particleId * 2.1)
    );

    vec2 centre = vec2(
      dustHash(particleId * 3.7),
      dustHash(particleId * 5.9)
    );
    vec2 fromCentre = uv - centre;

    // Stretch x differently per particle so the population is not all circular.
    fromCentre.x *= 1.0 + 1.8 * dustHash(particleId * 7.3);
    float angle = atan(fromCentre.y, fromCentre.x);

    // Two angular harmonics roughen the otherwise smooth ellipse boundary.
    float irregularEdge = 1.0
      + 0.2 * sin(angle * 3.0 + particleId)
      + 0.12 * sin(angle * 7.0 - particleId);
    float baseRadius = mix(0.003, 0.026, clamp(size, 0.0, 1.0));
    float radiusVariation = mix(
      0.55,
      1.35,
      dustHash(particleId * 11.1)
    );
    float radius = baseRadius * radiusVariation * irregularEdge;
    float feather = mix(
      0.0004,
      radius * 0.65,
      clamp(softness, 0.0, 1.0)
    );
    float particleMask = enabled * (
      1.0 - smoothstep(
        radius - feather,
        radius + feather,
        length(fromCentre)
      )
    );

    // polarity > 0 selects light marks, < 0 dark marks, and 0 mixes both.
    float lightParticle = polarity > 0.5
      ? 1.0
      : (polarity < -0.5 ? 0.0 : step(0.5, dustHash(particleId * 17.7)));
    masks.x = max(masks.x, particleMask * lightParticle);
    masks.y = max(masks.y, particleMask * (1.0 - lightParticle));
  }

  return clamp(masks, 0.0, 1.0);
}

vec3 applyFilmDust(vec3 sourceSrgb, vec2 masks, float strength) {
  // Add/remove light in a linear-light approximation, then encode for display.
  vec3 sourceLinear = pow(max(sourceSrgb, vec3(0.0)), vec3(2.2));
  float safeStrength = clamp(strength, 0.0, 1.0);
  vec3 withLightMarks = mix(
    sourceLinear,
    vec3(1.0),
    masks.x * safeStrength
  );
  vec3 damaged = withLightMarks * (1.0 - masks.y * safeStrength);
  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. Frame-held particle compositeOne fullscreen render pass
    • Aspect-fill sample
    • Generate bounded irregular particle silhouettes
    • Hold the contamination state across film intervals
    • Split light and dark transfer polarities
    • Composite contamination in linear light
    • Encode to sRGB
  3. Display output

Why these steps are here

  1. Place particles in frame space. Marks should not follow photographed objects or camera motion.
  2. Perturb the radial edge. A few harmonics make fibres and grit less geometrically perfect.
  3. Hold the random state. Dirt persists until transport or cleaning moves it.
  4. Split transfer polarity. Geometry and generation history remain separate controls.
  5. Composite in linear light. Dark blockage and light transferred marks remain bounded.

Notes

  • Use the Source selector to test dust against footage, still images, or the diagnostic grid; edge visibility depends strongly on local contrast.
  • The procedural particles are useful for live rendering. Scanned mattes are better when matching a particular film element or restoration reference.
  • Dust on a scanner or digital sensor can also be image-bound, but its geometry and temporal behaviour follow a different acquisition path.
  • Restoration benefits from temporal evidence: real film dust may disappear abruptly between frames while scene detail moves coherently.

References

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