Three.jsWebGLAnimation
WebGL & Three.js: Crafting Immersive Portfolio Backgrounds
Sep 12, 2025·7 min read
Step-by-step guide to building interactive particle systems and shader-based animations with Three.js — exactly how the animated background on this site was made.
Setting Up the Scene
A Three.js scene consists of three required objects: a `Scene` (the container), a `Camera` (the viewpoint), and a `Renderer` (the WebGL output). Mount the renderer's canvas and start the animation loop.
javascript
import * as THREE from 'three';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
camera.position.z = 5;
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();Building a Particle System
The fastest way to render thousands of points is `BufferGeometry` with a `Float32Array` of positions and `THREE.Points` as the draw call — one draw call for all particles.
javascript
const COUNT = 5000;
const positions = new Float32Array(COUNT * 3);
for (let i = 0; i < COUNT; i++) {
positions[i * 3] = (Math.random() - 0.5) * 20; // x
positions[i * 3 + 1] = (Math.random() - 0.5) * 20; // y
positions[i * 3 + 2] = (Math.random() - 0.5) * 20; // z
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({ color: 0x58a6ff, size: 0.02 });
const points = new THREE.Points(geometry, material);
scene.add(points);Adding Interactivity
Track normalised mouse coordinates and interpolate the camera or particle positions toward them each frame using linear interpolation (lerp). This creates the smooth, responsive feel without jarring jumps.
javascript
const mouse = { x: 0, y: 0 };
window.addEventListener('mousemove', (e) => {
mouse.x = (e.clientX / innerWidth - 0.5) * 2;
mouse.y = (e.clientY / innerHeight - 0.5) * 2;
});
// Inside the animation loop:
points.rotation.y += (mouse.x * 0.3 - points.rotation.y) * 0.05;
points.rotation.x += (mouse.y * 0.3 - points.rotation.x) * 0.05;Performance Checklist
- Use `BufferGeometry` — never the legacy `Geometry` class
- Limit `setPixelRatio` to 2 — retina screens don't need 3×
- Dispose geometries and materials when unmounting to avoid memory leaks
- Use `renderer.setAnimationLoop` instead of raw `requestAnimationFrame` for XR compatibility
“WebGL is just math on a GPU. Once you stop being afraid of matrices and shaders, everything clicks.”