Graphics Town

Interactive 3D world
The town seen from the sea: boats and a swimmer in the water, amusement rides and umbrellas on the beach, a village around a wizard tower, oaks and snow-capped mountains behind

A tropical coast that turns into a fantasy village and then a rain-soaked mountain range, rendered live in the browser. Every object in it is either assembled from primitives or generated procedurally, animated in JavaScript and GLSL, and the camera can ride the boat, the swimmer or the flock of geese. Open the demo; a laptop GPU is enough.

It began as the final project for CS559 Computer Graphics at UW–Madison in spring 2025, on the course's thin Three.js framework, and grew over the semester: the amusement rides came from an earlier workbook, the village buildings from the next, then the sea, the beach, the weather and the shaders for the final town. The skybox and the environment lighting came later, after the course had ended.

Apart from the campfire and the sky, nothing here is a downloaded asset. The textures are painted by hand, the trees, mushrooms, mountains and dunes are procedural, and the figures are hierarchies of primitives with their own joints.

By the numbers

Objects registered in the world and stepped every frame328
Object classes written from scratch on top of the framework's GrObject26
Of those, classes with their own per-frame animation16
Lines of JavaScript across five modules3,709
Geometry primitives instantiated in code, across 14 geometry types141
Textures painted by hand18
Water vertices moved on the CPU every frame4,257
Dune vertices displaced on the GPU by a 4-octave noise shader3,025
Oak trees placed at random on every load, with a mushroom cluster beside 40% of them130
Rain particles recycled through the cloud layer800
Geese on a closed Catmull-Rom spline, each flapping out of phase10
Rideable objects, and camera modes to see them from4 and 4
Shadow map, PCF soft, covering the whole town from one sun2048 × 2048
Build steps, bundlers, npm packages0

What's in it

The math in it

Nothing is keyframed. Every shape and every motion comes out of a formula that runs on load or every frame. The pieces doing the work, each linked to the line where it happens:

Cubic Bézier curves. Each palm frond is a tube swept along a four-point cubic Bézier, ten of them rotated 2π/10 apart around the trunk. The roundabout handle is a curve path of two line segments joined by a cubic Bézier, swept the same way.08-06-buildings.js:504 Quadratic Béziers as outlines. A goose wing is a closed 2D shape drawn from three quadratic Bézier segments, then triangulated into a mesh.07-08-parkobjects.js:736 Catmull-Rom splines. The flock rides a closed spline through eight points whose radius is modulated by a sine; the swimmer rides another through five. Position is the curve sampled at t = time mod 1, heading is the sample at t + 0.01.07-08-parkobjects.js:619 Quaternions. The swimmer's heading is the rotation taking its forward axis onto the normalized difference of two spline samples; body roll is an axis-angle quaternion multiplied on afterwards. The flock is oriented with a look-at rotation matrix.extras.js:740 Surfaces of revolution and ellipses. A teacup is a profile curve lathed around its axis, with a handle swept along an ellipse.07-08-parkobjects.js:896 Matrix transforms and kinematic chains. Symmetric parts are mirrored by applying a scale(−1, 1, 1) matrix to the vertices. Limbs are parented in sequence, shoulder to elbow, thigh to knee to ankle, goose body to three neck segments to head, so one rotation carries everything below it.08-06-buildings.js:286 Summed sinusoids. A teacup tilts by 0.08·sin(1.5t + φ) + 0.06·cos(0.7t + 2φ) while bobbing at two other frequencies; the tilt-a-wheel adds a term that depends on the platform's angle; each car, seat and light gets its own phase φ so nothing moves in lockstep. The boat bobs at ω and rolls at 1.3ω.07-08-parkobjects.js:1034 A swim cycle from phase arithmetic. Shoulders windmill through a 2π-wrapped phase, opposite arms π apart; the elbow bends only through the underwater half of the stroke; knees follow |sin|, ankles lead by a quarter period.extras.js:663 Traveling waves. Sea height is A₁·sin(0.3y + 2t) + A₂·cos(0.2x − 1.5t) per vertex, normals recomputed every frame so the lighting follows the water.extras.js:805 Value noise and fBm in GLSL. A hash from fract(sin(dot(p, k))·43758.5453), bilinear interpolation with the cubic Hermite curve f²(3 − 2f), four octaves with lacunarity 2 and gain ½, displacement along the vertex normal, then smoothstep and mix in the fragment shader for the crest tint.sandShader.js:35 Radial falloff terrain. Mountain height is (1 − r)·peak for r the normalized distance from the summit, plus random jitter; snow is a threshold on height written into per-vertex colors, with flat-shaded normals.extras.js:396 Polar coordinates and mesh deformation. Cloud puffs are placed by random angle and radius, stretched along one axis. The boat hull starts as a box whose vertices are tapered linearly toward bow and stern and curved underneath with a cosine.extras.js:837 Euler integration and wrap-around. Every raindrop steps yyv·Δt and reappears at the cloud layer; the boats step x the same way and flip direction at the bounds.extras.js:925 Randomness with intent. Uniform placement inside each strip, a 40% Bernoulli draw for mushrooms beside a tree, random start phases for clouds and geese, so the town is never the same twice but always reads the same.final-grtown.js:399 Lighting setup. One directional sun with a hand-fitted orthographic shadow frustum and a 2048² PCF soft shadow map, a cube map as the image-based environment for the PBR materials, sRGB output.final-grtown.js:89

Controls

  • Orbit: drag to rotate, scroll to zoom, right-drag to pan
  • Fly: W A S D, R/F for height, drag to look
  • Drive or Follow the boat, the swimmer or the geese
  • LookAt jumps to any of the 328 objects
  • Speed scales every animation; Run pauses it
  • Solo isolates one object against a blank scene