You can just make 3D
The page you are reading has a city in it. Drag on it.
That is a few hundred towers at dusk, lit by a sun you can move, drawn by three.js inside the page instead of in a screenshot of one. It is one file. There was nothing to install to make it, and there is nothing to install to remix it. By the end of this post you will have built it in four pieces, and each piece is a live scene you can turn with your finger before you read a word about it.
If you make things — models, games, installations, the slide that needs one real object on it, a lesson for a room of students — you have probably been told that 3D on the web starts with a toolchain: a bundler, a project folder, a terminal you keep open, a week of reading before the first triangle. It doesn't. A vibe is a three.js scene with a URL, an audience, and (in the next post) a memory. This post is the scene.
Step 1 — a scene, a camera, a renderer
Here is the whole model of three.js, and it is the last piece of theory in this post. A scene is a list of things. A camera stands somewhere in it and looks somewhere. A renderer draws what the camera sees onto a canvas, and does it again every time the screen is ready for a new frame, about sixty times a second.
jsconst scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 100);
camera.position.set(0, 1.2, 4);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
host.appendChild(renderer.domElement);
Three objects. The camera's four numbers are its field of view in degrees, the shape of the screen, and how near and how far it can see. Then you put something in the scene — a sun, so the box has a lit side and a shaded one, and the box — and ask the renderer to draw it, forever:
jsconst sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(3, 5, 2);
scene.add(sun);
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1.5, 1.5, 1.5),
new THREE.MeshStandardMaterial({ color: 0xff5a36 })
);
scene.add(cube);
renderer.setAnimationLoop(() => {
cube.rotation.y += 0.01;
renderer.render(scene, camera);
});
A mesh is a shape plus a surface — a geometry and a material — and
scene.add is how anything gets into the world, lights included; a
standard material with no light in the scene draws black, which is the
first thing every three.js beginner sees and the reason the sun is in the
snippet. The loop is the heartbeat:
nudge the rotation, draw a frame, repeat. A still picture is a loop that
draws the same thing every time; animation is a loop where something changed.
One thing you will notice the moment you open a demo rather than read it.
Every snippet in this post is written the way three.js is taught everywhere
— new THREE.Scene(), scene.add(), a loop — because that is the form you
will meet in every tutorial and every example you go on to read. The demos
wrap that scene in a few lines that hand three.js its box in the page and
clean up after it when the page moves on. Same calls, same ideas, one
wrapper.
Get posts like this in your inbox
One email field. Real updates. No algorithm required.
So read the snippets for the idea and open the demo for the file. The third post in this series is about that wrapper — what it does, and why in a page like this one it is not optional.
Step 2 — material and light
A grey cube on a black background is a demo. Light is what makes it a thing. Three.js materials are descriptions of how a surface meets light, and the two numbers that matter most are roughness and metalness:
jsnew THREE.MeshStandardMaterial({ color: 0xd9c2a3, roughness: 1, metalness: 0 }); // clay
new THREE.MeshStandardMaterial({ color: 0xb8bcc6, roughness: 0.35, metalness: 1 }); // brushed metal
new THREE.MeshStandardMaterial({ color: 0xff5a36, roughness: 0.15, metalness: 0 }); // glossy paint
Rough clay gives a broad, subdued reflection and no sharp highlight. Metal tints its reflections with its own base colour, so the lamp reads as a smear of brushed grey. Glossy paint keeps its diffuse colour and adds a sharp highlight in the colour of the light. The demo below is the same sphere three times, told those three different things, under one lamp:
jsconst lamp = new THREE.PointLight(0xffd9a0, 45, 24, 2);
scene.add(lamp);
// every frame
lamp.position.set(Math.cos(t) * 3, 2.4, Math.sin(t) * 3);
A point light is a bulb: a position, a colour, a brightness, and a distance it reaches. Move it and every surface in the room answers at once, because nothing about the spheres changed — only the light did. That is most of what "lighting" means in 3D: you are not painting highlights on things, you are putting a lamp somewhere and letting the materials do the arithmetic.
The demo also has the first thing you will add to every scene you make: orbit controls. One line gives the camera a hand to drag with:
jsconst controls = new OrbitControls(camera, renderer.domElement);
Drag to walk around the spheres, scroll to lean in. The lamp keeps circling while you do, which is the quickest way to feel the difference between the three materials — watch where the highlight goes on each.
Step 3 — a few hundred of anything
One object is a demo. A few hundred is a medium. The trick that makes a few hundred cheap is that a mesh is only a reference to a geometry and a material — make one box and one material, and every pillar on the grid shares them:
jsconst geometry = new THREE.BoxGeometry(0.7, 1, 0.7);
geometry.translate(0, 0.5, 0); // grow from the ground, not from the middle
const material = new THREE.MeshStandardMaterial({ color: 0xff5a36 });
for (let x = 0; x < 24; x++) {
for (let z = 0; z < 24; z++) {
const pillar = new THREE.Mesh(geometry, material);
pillar.position.set(x - 12, 0, z - 12);
scene.add(pillar);
pillars.push(pillar);
}
}
That translate line is worth a second look. A box is centred on its own
origin, so scaling it taller grows it up and down. Shift the geometry up by
half its height once, and from then on scale.y grows every pillar out of
the ground like a building. One line, said once, and the whole grid behaves.
Now give the grid a reason to move. Every frame, each pillar asks one sine wave how tall it should be, based on how far it is from the centre:
jsfor (const p of pillars) {
const d = Math.hypot(p.position.x, p.position.z);
p.scale.y = 1.2 + Math.sin(d * 0.9 - t * 2);
}
Rings roll outward from the middle, forever, and no pillar knows anything about its neighbours. (The live demo adds a second, smaller wave running across the grid so the rings never look machine-cut; the radial term above is the one doing the work.) The picture is not something you drew; it is a function sampled at five hundred and seventy-six points. That is the shift worth taking from this step. You are not placing the towers. You are describing a landscape, and the towers are where it is tall.
There is a limit to this, and it is further away than you think — a few hundred is nothing, a few thousand is fine, and the post after next shows where it stops and what you do then. For now, the loop is the whole simulation.
Step 4 — knobs
Scroll back to the top of this post. That city is these three steps with four numbers pulled out where your hands can reach them, and you now know what every one of them does.
Density is the size of the grid from step 3. Height scales every
tower. Time of day moves the sun from step 2 down to the horizon, warms
its colour, fades the sky from blue through orange to night — and once the
sun is low enough, lets the windows glow, which is one line setting the
material's emissive colour from its own:
jsmaterial.emissive.copy(material.color).multiplyScalar(Math.max(0, dusk - 0.45));
And the seed is the one that is not a slider. The city is random — where
the plazas fall, how tall each block grows, which colour it gets — but the
randomness comes from a tiny seeded generator instead of Math.random(), so
the same seed produces the same city, every time, on every screen. Press
new seed and you get a different city. Press it again and that one is gone
for good — unless you wrote its number down, which is exactly what the next
post is about.
Four knobs are the difference between a demo and an instrument. The demo shows you the city the programmer found. The instrument lets you go looking.
Remix it
The whole city is one file, and it is public. Open it, press remix, and you are standing in your own copy with your own URL: swap the boxes for cylinders and you have a forest, drop the sun to the horizon and lower the fog and you have the skyline you grew up with, feed the heights from a spreadsheet and the grid is a bar chart you can walk around. There is no project to set up first, because there was never a project. There is a scene, and now it is yours.
One catch, and the next post in this series fixes it with a single hook: reload the page and the city you just made is gone.
Make a world of your own
Remix any scene in this post, or describe the one you wish existed.
Start building →