Depth of Field Focus

Bokeh depth of field pass with smooth auto-focus and click-to-focus controls across layered geometry.

What this code does

- EffectComposer + BokehPass: simulates camera bokeh blur based on configurable focus distance and aperture.
- Layered Scene Setup: foreground spheres, mid-ground boxes, and distant cones highlight blur falloff.
- Interactive Controls: GUI sliders adjust focus distance, aperture, and blur intensity in real time.
- Click to Focus: raycasting picks any object and moves the focal plane to its distance instantly.
- Auto Focus: optional center ray keeps the focus tracking whichever object crosses the middle of the frame.

JavaScript (plain)

const scene = new THREE.Scene()
scene.background = new THREE.Color(0x1a1a1a)
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(0, 2, 10)

const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
renderer.setSize(width, height)

renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
document.querySelector('#app').appendChild(renderer.domElement)

// Setup post-processing for depth of field
const composer = new EffectComposer(renderer)
composer.setSize(width, height)

const renderPass = new RenderPass(scene, camera)
composer.addPass(renderPass)

const bokehPass = new BokehPass(scene, camera, {
  focus: 7.0,
  aperture: 0.04,
  maxblur: 0.018
})
composer.addPass(bokehPass)

let focusTarget = 7.0
const focusLerpSpeed = 0.08
const focusSweepRange = { near: 3.5, far: 14 }
const raycaster = new THREE.Raycaster()
const pointer = new THREE.Vector2()
const centerPointer = new THREE.Vector2(0, 0)
let focusSweepEnabled = true

// Create objects at different depths
const objects = []

// Foreground spheres
for (let i = 0; i < 3; i++) {
  const geometry = new THREE.SphereGeometry(0.3, 16, 16)
  const material = new THREE.MeshStandardMaterial({
    color: new THREE.Color().setHSL(i / 3, 0.8, 0.6)
  })
  const sphere = new THREE.Mesh(geometry, material)
  sphere.position.set((i - 1) * 1.5, -1, 2 + i * 0.5)
  objects.push(sphere)
  scene.add(sphere)

}

// Middle boxes (focus area)
for (let i = 0; i < 5; i++) {
  const geometry = new THREE.BoxGeometry(0.6, 0.6, 0.6)
  const material = new THREE.MeshStandardMaterial({
    color: new THREE.Color().setHSL((i + 3) / 8, 0.8, 0.6)
  })
  const box = new THREE.Mesh(geometry, material)
  box.position.set((i - 2) * 1.8, 0, 6 + i * 0.8)
  box.rotation.x = Math.PI * 0.2
  box.rotation.y = i * Math.PI * 0.3
  objects.push(box)
  scene.add(box)

}

// Background cones
for (let i = 0; i < 4; i++) {
  const geometry = new THREE.ConeGeometry(0.4, 1.2, 8)
  const material = new THREE.MeshStandardMaterial({
    color: new THREE.Color().setHSL((i + 6) / 10, 0.8, 0.6)
  })
  const cone = new THREE.Mesh(geometry, material)
  cone.position.set((i - 1.5) * 2, 1, 15 + i * 1.2)
  objects.push(cone)
  scene.add(cone)

}

// Ground plane
const groundGeometry = new THREE.PlaneGeometry(50, 50)
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x404040 })
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
ground.rotation.x = -Math.PI / 2
ground.position.y = -2
scene.add(ground)

// Lighting
const ambientLight = new THREE.AmbientLight(0x404040, 0.3)
scene.add(ambientLight)

const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
directionalLight.position.set(5, 10, 5)
scene.add(directionalLight)

const pointLight = new THREE.PointLight(0xffa500, 0.8, 20)
pointLight.position.set(0, 3, 8)
scene.add(pointLight)

// Bright background highlights for strong bokeh
const highlightGeometry = new THREE.SphereGeometry(0.25, 16, 16)
const highlights = []
for (let i = 0; i < 18; i++) {
  const material = new THREE.MeshBasicMaterial({
    color: new THREE.Color().setHSL(0.55 + Math.random() * 0.2, 1.0, 0.7),
    transparent: true,
    opacity: 0.85
  })
  const sphere = new THREE.Mesh(highlightGeometry, material)
  sphere.position.set((Math.random() - 0.5) * 16, -1 + Math.random() * 6, 11 + Math.random() * 8)
  sphere.userData = { base: sphere.position.clone(), phase: Math.random() * Math.PI * 2 }
  highlights.push(sphere)
  objects.push(sphere)
  scene.add(sphere)

}

function updateAutoFocus() {
  raycaster.setFromCamera(centerPointer, camera)
  const hit = raycaster.intersectObjects(objects)
  if (hit.length > 0) {
    focusTarget = THREE.MathUtils.lerp(focusTarget, hit[0].distance, 0.4)
  }
}

renderer.domElement.addEventListener('click', (event) => {
  const rect = renderer.domElement.getBoundingClientRect()
  pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
  pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
  raycaster.setFromCamera(pointer, camera)
  const hit = raycaster.intersectObjects(objects)
  if (hit.length > 0) {
    focusSweepEnabled = false
    focusTarget = hit[0].distance
    bokehPass.uniforms.focus.value = focusTarget
  }
})

function animate() {
  requestAnimationFrame(animate)

  const time = Date.now() * 0.001

  // Animate objects
  objects.forEach((obj, i) => {
    obj.rotation.y += 0.01 * (i % 2 === 0 ? 1 : -1)
  })

  // Animate point light
  pointLight.position.x = Math.sin(time * 0.5) * 3
  pointLight.position.z = 8 + Math.cos(time * 0.3) * 2

  highlights.forEach((highlight) => {
    const { base, phase } = highlight.userData
    highlight.position.x = base.x + Math.sin(time * 0.2 + phase) * 1.5
    highlight.position.y = base.y + Math.cos(time * 0.25 + phase) * 0.8
  })

  if (focusSweepEnabled) {
    const sweep = THREE.MathUtils.mapLinear(
      Math.sin(time * 0.35),
      -1,
      1,
      focusSweepRange.near,
      focusSweepRange.far
    )
    focusTarget = sweep
  }

  updateAutoFocus()
  bokehPass.uniforms.focus.value = THREE.MathUtils.lerp(
    bokehPass.uniforms.focus.value,
    focusTarget,
    focusLerpSpeed
  )
  composer.render()
}

animate()