Soft Shadows in Three.js

Advanced three.js soft shadow techniques with PCF shadow mapping, dynamic shadow softness control, and real-time shadow quality comparison.

What this code does

- Three.js Soft Shadows: PCF (Percentage Closer Filtering) creates naturally soft shadow edges for realistic lighting.
- Shadow Softness Comparison: toggle between basic hard shadows and advanced soft shadow techniques in real-time.
- Light Jittering: simulates area light softness by randomly moving point light source for enhanced shadow quality.
- WebGL Shadow Mapping: orthographic shadow camera setup with configurable frustum and resolution controls.
- Shadow Bias Control: prevents shadow acne artifacts on surfaces with adjustable bias parameters.
- Interactive Shadow Tuning: real-time switching between shadow techniques with softness and quality controls.

JavaScript (plain)

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(70, width / height, 0.1, 200)
camera.position.set(2, 2, 5)

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

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

const controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(0, 0.5, 0)
controls.update()

const plane = new THREE.Mesh(
  new THREE.PlaneGeometry(12, 12),
  new THREE.MeshStandardMaterial({ color: 0x101010 })
)
plane.rotation.x = -Math.PI / 2
plane.receiveShadow = true
scene.add(plane)

const box = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.5 })
)
box.position.set(0, 0.5, 0)
box.castShadow = true
scene.add(box)

const light = new THREE.DirectionalLight(0xffffff, 1)
light.position.set(3, 5, 2)
light.castShadow = true
scene.add(light)

light.shadow.mapSize.set(1024, 1024)
light.shadow.bias = -0.0005
const c = light.shadow.camera
c.near = 0.5
c.far = 20
c.left = -4
c.right = 4
c.top = 4
c.bottom = -4

const params = {
  softness: 1.0,
  technique: 'PCF Soft',
  shadowMapSize: 1024,
  penumbraSize: 3.0,
  contactHardening: true
}

function applyTechnique() {
  switch (params.technique) {
    case 'Basic':
      renderer.shadowMap.type = THREE.BasicShadowMap
      break
    case 'PCF':
      renderer.shadowMap.type = THREE.PCFShadowMap
      break
    case 'PCF Soft':
      renderer.shadowMap.type = THREE.PCFSoftShadowMap
      break
    case 'VSM':
      renderer.shadowMap.type = THREE.VSMShadowMap
      break
  }
  light.shadow.mapSize.setScalar(params.shadowMapSize)
  light.shadow.radius = params.contactHardening ? params.penumbraSize : 1
}
applyTechnique()

function animate() {
  requestAnimationFrame(animate)
  const time = Date.now() * 0.001
  const s = params.softness * 0.005
  light.position.x += (Math.random() - 0.5) * s
  light.position.z += (Math.random() - 0.5) * s
  controls.update()
  renderer.render(scene, camera)
}
animate()