🎯

Coding Challenges

Sharpen your WebGL and simulation skills through hands-on tasks. Each challenge comes with a starter scaffold, progressive hints, and an unlock-able reference solution.

Your progress 0 / 6 completed
2Easy challenges
2Medium challenges
2Hard challenges
~12hTotal learning time

All Challenges

Starting from a sphere floating in mid-air, add a gravity force that accelerates the ball downward each frame. When it hits the floor, apply an energy-absorbing bounce so it eventually comes to rest. Learn Euler integration, velocity accumulation and coefficient of restitution.

  • Ball falls with vy += g * dt applied every frame
  • Ball bounces off the floor plane with vy *= -restitution
  • Ball slows down and settles (restitution < 1)
  • Controls: slider for gravity strength (0 → 30 m/s²)
// Three.js r160 is already loaded
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 100);
camera.position.set(0, 2, 8);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

// Ball
const ball = new THREE.Mesh(
  new THREE.SphereGeometry(0.5, 32, 32),
  new THREE.MeshStandardMaterial({ color: 0x3b82f6 })
);
ball.position.y = 5;
scene.add(ball);

// Floor
scene.add(new THREE.Mesh(
  new THREE.PlaneGeometry(20, 20),
  new THREE.MeshStandardMaterial({ color: 0x1e293b })
).rotateX(-Math.PI / 2));

scene.add(new THREE.AmbientLight(0xffffff, 0.5));
scene.add(Object.assign(new THREE.DirectionalLight(0xffffff, 1), { position: { set: (x,y,z) => { } } }));

// TODO: add gravity physics here
let vy = 0;
const GRAVITY = 9.8;        // m/s²
const RESTITUTION = 0.7;    // bounciness
const FLOOR_Y = 0.5;        // ball radius

let last = performance.now();
function animate() {
  requestAnimationFrame(animate);
  const now = performance.now();
  const dt = Math.min((now - last) / 1000, 0.05);
  last = now;

  // ← Your code goes here

  renderer.render(scene, camera);
}
animate();
Each frame, add gravity to vertical velocity: vy -= GRAVITY * dt. Then move the ball: ball.position.y += vy * dt. The minus sign makes gravity pull downward (negative Y).
After moving the ball, check if (ball.position.y < FLOOR_Y). If true, clamp it back: ball.position.y = FLOOR_Y, then flip and attenuate velocity: vy = Math.abs(vy) * RESTITUTION. If vy < 0.1, set it to 0 so the ball truly stops.
Add an <input type="range" min="0" max="30" value="9.8"> overlay on top of the canvas. Read its value inside the animation loop as const g = parseFloat(slider.value). Don't forget a <label> for accessibility.
Open in Playground ↗
⚠️ Try coding it yourself first — you'll learn more that way.
vy -= GRAVITY * dt;
ball.position.y += vy * dt;

if (ball.position.y < FLOOR_Y) {
  ball.position.y = FLOOR_Y;
  vy = Math.abs(vy) * RESTITUTION;
  if (vy < 0.05) vy = 0;
}

Create a smooth animated sine wave using a THREE.BufferGeometry line. The wave should travel in time (phase shift every frame) and respond to sliders for amplitude, frequency and speed. This is the foundation of many wave-physics visualisations.

  • Render a smooth line using THREE.Line with BufferGeometry
  • Animate the wave: y = A * sin(f*x - speed*t)
  • Update vertex positions each frame (reuse buffer — no re-allocation)
  • HUD sliders: Amplitude (0.1 → 3), Frequency (0.5 → 10), Speed (0 → 5)
const NUM_POINTS = 512;
const X_RANGE = 20; // wave spans -10 to +10

// Build geometry once
const positions = new Float32Array(NUM_POINTS * 3);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const wave = new THREE.Line(
  geometry,
  new THREE.LineBasicMaterial({ color: 0x38bdf8, linewidth: 2 })
);
scene.add(wave);

let t = 0;
function updateWave(amplitude, frequency, speed) {
  // TODO: fill positions[] with sine wave data
  // x goes from -X_RANGE/2 to +X_RANGE/2
  // y = amplitude * Math.sin(frequency * x - speed * t)
  // z = 0
  geometry.attributes.position.needsUpdate = true;
}
Loop i from 0 to NUM_POINTS. Compute x = (i / (NUM_POINTS - 1)) * X_RANGE - X_RANGE / 2 to spread evenly from -10 to +10. Store as positions[i*3] = x.
Increment a time counter each frame: t += dt. Use it in the sine: y = A * Math.sin(freq * x - spd * t). The - spd * t term creates rightward travel.
Increase NUM_POINTS (try 512+). Also check that you set geometry.attributes.position.needsUpdate = true every frame, otherwise Three.js won't re-upload the buffer.
Open in Playground ↗
⚠️ Try coding it yourself first.
function updateWave(amplitude, frequency, speed) {
  for (let i = 0; i < NUM_POINTS; i++) {
    const x = (i / (NUM_POINTS - 1)) * X_RANGE - X_RANGE / 2;
    const y = amplitude * Math.sin(frequency * x - speed * t);
    positions[i * 3]     = x;
    positions[i * 3 + 1] = y;
    positions[i * 3 + 2] = 0;
  }
  geometry.attributes.position.needsUpdate = true;
}

Implement Craig Reynolds' Boids algorithm: 100 agents that flock together through only three local rules. Each boid looks at neighbours within a perception radius and steers to separate, align and cohere. Emergent flocking behaviour appears with no global coordination.

  • Separation: steer away from boids that are too close
  • Alignment: steer toward the average heading of nearby boids
  • Cohesion: steer toward the average position of nearby boids
  • Wrap-around boundaries (boids teleport across edges)
  • HUD sliders for perception radius and the 3 rule weights
Each boid needs: { pos: THREE.Vector3, vel: THREE.Vector3 }. Keep a flat array of N boids. Speed-cap velocity each frame: if (vel.length() > MAX_SPEED) vel.setLength(MAX_SPEED).
For each boid A, loop all boids B. If A.pos.distanceTo(B.pos) < PERCEPTION (and B ≠ A), B is a neighbour. Store the neighbour positions and velocities in temporary arrays, then average them for cohesion and alignment.
For each close neighbour B, compute the vector away = A.pos.clone().sub(B.pos). Scale it inversely by distance: away.divideScalar(dist * dist). Sum all these vectors, then add steerSeparation.multiplyScalar(wSeparation) to the velocity.
Check that you cap velocity after summing all three rules. Also ensure the separation zone is smaller than the perception radius. A typical setup: perception = 50, separation = 25. Tune weights: separation > alignment > cohesion often works best.
Open in Playground ↗ See the full simulation
⚠️ Try coding it yourself first.
function applyBoidRules(boids, wSep, wAli, wCoh, perception, sepDist) {
  const steer = new THREE.Vector3();
  boids.forEach((b, i) => {
    const sep = new THREE.Vector3();
    const ali = new THREE.Vector3();
    const coh = new THREE.Vector3();
    let nAli = 0, nCoh = 0;
    boids.forEach((o, j) => {
      if (i === j) return;
      const d = b.pos.distanceTo(o.pos);
      if (d < sepDist) {
        sep.add(b.pos.clone().sub(o.pos).divideScalar(d * d));
      }
      if (d < perception) {
        ali.add(o.vel); nAli++;
        coh.add(o.pos); nCoh++;
      }
    });
    if (nAli) ali.divideScalar(nAli).sub(b.vel).multiplyScalar(wAli);
    if (nCoh) coh.divideScalar(nCoh).sub(b.pos).multiplyScalar(wCoh);
    sep.multiplyScalar(wSep);
    b.vel.add(sep).add(ali).add(coh);
    if (b.vel.length() > MAX_SPEED) b.vel.setLength(MAX_SPEED);
  });
}

The Gerstner wave model moves vertices both vertically and horizontally, producing the iconic peaked crests of ocean waves. Implement it as a GLSL vertex shader using Three.js ShaderMaterial. Superimpose 4 wave trains with different directions and frequencies for a realistic sea surface.

  • Vertex shader computes Gerstner displacement for each grid vertex
  • Superimpose at least 4 wave trains (vary direction, frequency, amplitude)
  • Fragment shader uses normals for Phong/Blinn lighting with a sky reflection
  • HUD: wind speed slider that scales wave amplitudes & steepness
For wave with wavenumber k, direction D, amplitude A, steepness Q, speed c:

x += Q * A * D.x * cos(k * dot(D, pos.xz) - c * t)
z += Q * A * D.y * cos(k * dot(D, pos.xz) - c * t)
y += A * sin(k * dot(D, pos.xz) - c * t)

Where Q is steepness (0=sinusoidal, 1=sharp crests).
Use ShaderMaterial.uniforms. Pass an array of vec4s: { waveParams: { value: [ new THREE.Vector4(amp, freq, dir.x, dir.y), ... ] } }. In GLSL declare uniform vec4 waveParams[4];. Also pass a uniform float time; updated each frame.
Differentiate the Gerstner displacement analytically. For each wave: N.x -= D.x * k * A * cos(phase), N.z -= D.y * k * A * cos(phase), N.y -= Q * k * A * sin(phase) (accumulated, then start with N=(0,1,0) and negate). Normalize the result.
⚠️ Try coding it yourself first.
// GLSL Vertex Shader (simplified single wave)
vec3 gerstner(vec3 pos, vec2 D, float amp, float freq, float steep, float t) {
  float phase = freq * dot(D, pos.xz) - freq * 1.5 * t;
  return vec3(
    steep * amp * D.x * cos(phase),
    amp * sin(phase),
    steep * amp * D.y * cos(phase)
  );
}

// In main():
vec3 displaced = position;
for (int i = 0; i < 4; i++) {
  vec4 wp = waveParams[i]; // x=amp, y=freq, z=dirX, w=dirZ
  displaced += gerstner(position, vec2(wp.z, wp.w), wp.x, wp.y, 0.5, time);
}

Smoothed Particle Hydrodynamics (SPH) treats fluid as a cloud of particles that exert pressure and viscosity forces on each other. Implement the Müller et al. SPH formulation with 200 particles in 2D, including density estimation, pressure and viscosity forces, and wall boundary conditions.

  • Density estimation via poly6 kernel: ρᵢ = Σ m · W_poly6(rᵢⱼ, h)
  • Pressure force via spiky gradient kernel
  • Viscosity force via Laplacian kernel
  • Box boundary with damping (particles bounce off walls)
  • Render with instanced spheres coloured by speed
Three standard kernels with smoothing radius h:
Poly6 (density): W = (315/(64π h⁹)) · (h²−r²)³ when r < h
Spiky gradient (pressure): ∇W = −(45/(π h⁶)) · (h−r)² · r̂
Viscosity Laplacian: ∇²W = (45/(π h⁶)) · (h−r)
Use the ideal gas state equation: P = GAS_CONST * (density - REST_DENSITY). REST_DENSITY ≈ 1000 kg/m³, GAS_CONST ≈ 2000. Pressure force on particle i from j: F = −m · (Pᵢ+Pⱼ)/(2·ρⱼ) · ∇W_spiky.
The naïve O(N²) neighbour search works for 200 particles at 60fps. For more particles, build a spatial hash grid: divide space into cells of size h. Each particle only checks its 9 neighbouring cells instead of all N particles.
This usually means your time step is too large. Cap dt at 0.005s. Also add a maximum acceleration clamp: if force.length() > MAX_FORCE, scale it down. Ensure you skip self-interaction (i !== j) in the force loop.
⚠️ A full SPH implementation is 150+ lines — the hints above contain all the key equations. Study the Fluid simulation source for a complete reference.

Simulate N gravitational bodies using the Leapfrog / Velocity-Verlet integrator, which conserves energy far better than Euler integration. Add a softening parameter to prevent singularities at close approach. Visualise orbital trails and measure the total energy drift over time.

  • Compute gravitational acceleration: a = G·m/|r|² (with softening ε)
  • Leapfrog integration: kick–drift–kick scheme
  • Trail rendering using THREE.Line circular buffers
  • HUD: total kinetic + potential energy displayed in real time
  • Presets: Solar system, binary star, figure-eight orbit
Leapfrog is symplectic — it conserves a modified energy exactly, so orbits don't spiral in or out over long times. The "kick-drift-kick" form is:

v_half = v + (a/2) * dt
pos += v_half * dt
a_new = computeAccel(pos)
v = v_half + (a_new/2) * dt
Add a softening length ε² to the denominator: a = G * m * r / (|r|² + ε²)^(3/2). A typical softening is 0.1–1% of the mean inter-particle separation. This prevents infinite forces when two bodies get very close.
Pre-allocate a Float32Array of length TRAIL_LENGTH * 3 for each body. Each frame, shift the buffer left by one vertex (or use a circular index). Write the current position at the active index, then set geometry.attributes.position.needsUpdate = true.
⚠️ A full N-body system with trails is 200+ lines. The hints contain all key equations. Study the N-Body simulation source for a complete reference.