Film Grain Studio

Cinematic scene layered with animated film grain, scanlines, and color tint controlled through a custom shader.

What this code does

- ShaderPass: custom fragment shader overlays animated procedural noise on the rendered frame.
- Grain Controls: tweak strength, speed, and luminance influence to balance grain in bright vs dark areas.
- Scanlines: subtle sine-based striping simulates analog display artifacts.
- Color Tint: mix cool cinematic tint over the base scene for stylized looks.
- Cinematic Set Dressing: rotating film reels, light rigs, and projection screen emphasize the analog vibe.

JavaScript (plain)

const FilmGrainShader = {
  uniforms: {
    tDiffuse: { value: null },
    time: { value: 0 },
    strength: { value: 0.32 },
    speed: { value: 1.4 },
    luminanceInfluence: { value: 0.6 },
    scanlines: { value: 0.18 },
    tintAmount: { value: 0.22 },
    tint: { value: new THREE.Color(0x12162e) },
    seed: { value: Math.random() * 1000 }
  },
  vertexShader: `varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
  fragmentShader: `varying vec2 vUv; uniform sampler2D tDiffuse; uniform float time; uniform float strength; uniform float speed; uniform float luminanceInfluence; uniform float scanlines; uniform float tintAmount; uniform vec3 tint; uniform float seed; float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); } float noise(vec2 p){ vec2 i = floor(p); vec2 f = fract(p); float a = hash(i); float b = hash(i + vec2(1.0, 0.0)); float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0)); vec2 u = f * f * (3.0 - 2.0 * f); return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; } void main(){ vec4 base = texture2D(tDiffuse, vUv); float timeSeed = time * speed + seed; float grain = noise(gl_FragCoord.xy * (0.6 * strength) + timeSeed); float grain2 = noise((gl_FragCoord.yx + seed * 1.37) * (1.2 * strength) - timeSeed); float combined = (grain + grain2) * 0.5 - 0.5; float luminance = dot(base.rgb, vec3(0.299, 0.587, 0.114)); float influenced = mix(combined, combined * luminance, luminanceInfluence); float scan = sin(vUv.y * 1080.0 + time * 35.0) * scanlines; vec3 tinted = mix(base.rgb, base.rgb + tint, tintAmount); vec3 result = tinted + influenced + scan * 0.12; gl_FragColor = vec4(result, base.a); }`
}

const scene = new THREE.Scene()
scene.background = new THREE.Color(0x06070c)
const camera = new THREE.PerspectiveCamera(55, width / height, 0.1, 100)
camera.position.set(0, 2.8, 7)

const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)

document.querySelector('#app').appendChild(renderer.domElement)

const composer = new EffectComposer(renderer)
composer.setSize(width, height)

composer.addPass(new RenderPass(scene, camera))
const grainPass = new ShaderPass(FilmGrainShader)
composer.addPass(grainPass)

// Build simple scene
const floor = new THREE.Mesh(
  new THREE.CircleGeometry(10, 64),
  new THREE.MeshStandardMaterial({ color: 0x0a0d1a, roughness: 0.85 })
)
floor.rotation.x = -Math.PI / 2
floor.position.y = -0.5
floor.receiveShadow = true
scene.add(floor)

const ambient = new THREE.AmbientLight(0x404560, 0.45)
scene.add(ambient)

const reels = []
const reelGeometry = new THREE.CylinderGeometry(0.55, 0.55, 0.25, 48)
const reelMaterial = new THREE.MeshStandardMaterial({ color: 0x6f768a, metalness: 0.7, roughness: 0.25 })
for (let i = 0; i < 3; i++) {
  const reel = new THREE.Mesh(reelGeometry, reelMaterial)
  reel.rotation.z = Math.PI / 2
  reel.position.set(-2 + i * 2, 1.0 + (i % 2) * 0.4, 0)
  reel.userData = { speed: 0.6 + i * 0.4 }
  scene.add(reel)

  reels.push(reel)
}

const clock = new THREE.Clock()

function animate() {
  requestAnimationFrame(animate)
  const delta = clock.getDelta()
  const elapsed = clock.elapsedTime
  reels.forEach((reel) => {
    reel.rotation.x += delta * reel.userData.speed * 3.0
  })
  grainPass.uniforms.time.value = elapsed
  grainPass.uniforms.seed.value = Math.random() * 1000
  composer.render()
}

animate()