Three.js Experiments

Isometric Rooms in Three.js

A navigable isometric scene where clicking a nav item spins a diamond of four rooms to show the one you picked. Work, living, studio, garden. Each built from box and cylinder primitives, no models or textures.

The result is here.

Orthographic camera

Isometric projection needs an orthographic camera. A perspective camera would make far objects smaller, which breaks the flat, uniform look.

const viewSize = 10;
const camera = new THREE.OrthographicCamera(
  -viewSize * aspect, viewSize * aspect,
  viewSize, -viewSize,
  0.1, 100
);
camera.position.set(10, 10, 10);
camera.lookAt(0, 0, 0);

Position (10, 10, 10) looking at the origin gives the classic isometric angle. Equal distance along all three axes means equal foreshortening on each, which is what makes things look “isometric” rather than just “3D viewed from above”.

The diamond layout

Four rooms in a 2x2 grid. Each room is 5 units square with two walls forming an L-shape. The walls always face inward toward the centre, so from above the whole thing looks like a plus sign of walls with four open corners.

const roomConfigs = [
  { x:  2.5, z:  2.5, rot: 0 },
  { x: -2.5, z:  2.5, rot: -Math.PI / 2 },
  { x: -2.5, z: -2.5, rot: -Math.PI },
  { x:  2.5, z: -2.5, rot: -3 * Math.PI / 2 },
];

Each room is built identically in local space (walls at -x and -z, open corner at +x and +z). The rotation per room compensates so that when the pivot brings a room to the front, its effective rotation is zero. The walls end up behind the furniture from the camera’s perspective, the open corner faces you.

Rotation

A parent group holds all four rooms. Clicking a nav link sets a target angle and the animation loop interpolates toward it:

const diff = targetAngle - currentAngle;
currentAngle += diff * (1 - Math.exp(-5 * dt));
roomPivot.rotation.y = currentAngle;

Exponential smoothing gives a quick start that decelerates into the final position. No easing library needed.

The shortest-path calculation prevents the diamond from spinning 270 degrees when it could spin 90 the other way:

let diff = newTarget - targetAngle;
while (diff > Math.PI) diff -= 2 * Math.PI;
while (diff < -Math.PI) diff += 2 * Math.PI;
targetAngle += diff;

Building furniture from primitives

Every object is boxes and cylinders. A desk is a flat box on four thin box legs. A chair is a seat box, a back box, a cylinder pole, and a cylinder base. A tree is a cylinder trunk with a sphere on top.

function box(w, h, d, material) {
  const g = new THREE.BoxGeometry(w, h, d);
  const m = new THREE.Mesh(g, material);
  m.castShadow = true;
  m.receiveShadow = true;
  return m;
}

Two helper functions (box and cyl) handle geometry creation, shadow casting, and material assignment. Every piece of furniture is a few calls to these: the monitor is three boxes (screen, stand, base), the sofa is four (seat, back, two arms).

The materials are all MeshStandardMaterial with different roughness and colour values. Around 25 materials cover wood, metal, fabric, stone, water, and various paint colours. Nothing exotic, just physically-based values.

Room variety

Each room has a different wall colour and floor treatment to give them distinct character without changing the structure.

The garden replaces the standard room shell entirely. Grass floor instead of wood, low stone walls instead of full-height plaster. It has a pond (a flat cylinder with low roughness and transparency), flowers (stem cylinders with coloured cylinder heads), a tree, stepping stones, and a bench.

The studio has an easel with a canvas showing paint blobs (coloured boxes stacked on the canvas box), a tilted drafting table, and a shelf of paint tubes.

Shadows

PCFSoftShadowMap at 2048px resolution. The directional light’s shadow camera frustum covers 30 units (-15 to +15) to capture the full diamond. Without this, shadows clip at the edges as the rooms rotate.

The shadow bias is set to -0.002 to prevent shadow acne on the flat surfaces. Every mesh has castShadow and receiveShadow enabled by default through the helper functions.

Deployment

Same pattern as the parallax mountain project. One HTML file, a three-line Dockerfile, Dokploy on a Hetzner VPS, Cloudflare DNS pointing 3d-house.danieljohnmorris.com at the server.

FROM nginx:alpine
COPY . /usr/share/nginx/html/
EXPOSE 80