Depth of field in 3D

A field of 600 purple beads fills the depth of the frame, and the focus moves through them from the back to the front like a camera's focus pull, with a blur computed for each pixel from its depth.

A Trillium view's draw: (renderer, scene, camera) => ... option replaces its own renderer.render(scene, camera) call, and here it renders through an EffectComposer with a BokehPass. Kinetic draws every view inside a scene through view.draw() after each seek, so seek() only sets the pass's focus from plane.

<div id="stage" data-composition-id="dof"     data-start="0" data-duration="5" data-width="1280" data-height="720" data-fps="30">
    <div class="layer" data-component="dof" data-start="0" data-duration="5">
        <canvas width="1280" height="720"></canvas>
    </div>
</div>
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { BokehPass } from 'three/addons/postprocessing/BokehPass.js';

const { scene, random } = wildflower.kinetic;
wildflower.three.use(THREE);

const BEADS = 600;
const golden = Math.PI * (3 - Math.sqrt(5));
const rnd = random(5);
// A golden-angle disc, wider than tall, with each bead at its own depth from z = -2 to 8.
const FIELD = Array.from({ length: BEADS }, (_, i) => {
    const r = 4 * Math.sqrt((i + 0.5) / BEADS), a = i * golden;
    return { id: i, x: Math.cos(a) * r * 1.8, y: Math.sin(a) * r, z: -2 + rnd() * 10, scale: 0.08, colour: 0xaa3de6 };
});

const camera = new THREE.PerspectiveCamera(40, 1280 / 720, 0.05, 100);
camera.position.set(0, 0, 16);
let bokeh;

scene('dof', {
    keyframes: {
        // The focal distance from the camera in world units, from past the farthest bead to the nearest.
        plane: { at: [0.5, 4.5], to: [24, 8], ease: 'power2.inOut' },
    },
    computed: {
        beads() { return FIELD; },
    },
    init() {
        const renderer = new THREE.WebGLRenderer({ canvas: this.element.querySelector('canvas'), antialias: true, preserveDrawingBuffer: true });
        renderer.setSize(1280, 720, false);
        const world = new THREE.Scene();
        const mesh = new THREE.InstancedMesh(new THREE.SphereGeometry(1, 20, 14), new THREE.MeshBasicMaterial(), BEADS);
        world.add(mesh);

        const composer = new EffectComposer(renderer);
        composer.addPass(new RenderPass(world, camera));
        bokeh = new BokehPass(world, camera, { focus: 14, aperture: 0.02, maxblur: 0.01 });
        composer.addPass(bokeh);

        const view = wildflower.three.view('dof-view', { renderer, scene: world, camera, render: 'change', draw: () => composer.render() });
        view.instanced(() => this.beads, mesh, { position: ['x', 'y', 'z'], scale: 'scale', color: 'colour', pickable: false });
        renderer.compile(world, camera);
        composer.render();   // compiles the pass's shaders before frame 0
    },
    seek() {
        bokeh.uniforms.focus.value = this.plane;
    },
});
html, body { margin: 0; background: #0a0b10; }

#stage { position: relative; overflow: hidden; background: #07060d; }

.layer { position: absolute; inset: 0; }

canvas { display: block; width: 1280px; height: 720px; }

[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
Depth of field in 3D

The pass's focus is a distance along the camera's look direction in world units, so plane runs from 24, past the farthest bead, to 8, the nearest. The pass blurs the rendered image by its depth, so the number of beads does not change the cost of the blur.

  • The renderer, the mesh and its binding are set up as in A camera push and a fly-through, which this pattern builds on.
  • An import map in the page maps three and three/addons/ to the three.js build and its examples/jsm/ folder.
  • The pass's own shaders are not compiled by renderer.compile(), so one composer.render() in init() compiles them before frame 0.

The Showreel demo's space shot renders through a bokeh pass this way, and its fly-out opens the aperture as the beads pass the lens. It needs the draw option of Trillium 0.2.