· 4 min read

Why a game?

A portfolio that is a real 3D game is a strange choice, and the strangeness is exactly the point — it is the largest thing on the site I could not fake.

  • process
  • webgl
  • portfolio

The obvious objection first, because it is a good one.

A recruiter has ninety seconds and forty tabs open. Handing them WASD controls instead of a bulleted list is, on the face of it, hostile. So the first design decision in this project was not about the game at all — it was that every word in the game exists as ordinary HTML on the same page, the game is opt-in, and there is a “View classic site” button visible from the first frame.

If you never press a key, you lose nothing. That is not a fallback bolted on afterwards; the panels the game opens are the classic site’s sections. Same DOM nodes. There is no second copy of the content to drift out of sync.

With that settled, the actual answer.

A portfolio should be evidence, not a claim

Every portfolio says the same things. Attention to detail. Performance-minded. Comfortable with hard problems. These are claims, and claims are free.

A 3D city that has to hold sixty frames a second on a mid-range Android phone is not free. Either it runs or it does not, and you can see which within about four seconds of arriving. The site is not telling you I care about performance; it is either demonstrating it or failing to, in front of you, on your device.

That is a genuinely uncomfortable thing to build. It is also the only reason it is worth building.

What it actually forced me to learn

Things I would not have touched otherwise:

Budgeting for the worst device, not the best. The whole thing is governed by a GPU tier check on load and a bail-out ladder underneath it — eight steps, from dropping shadow resolution all the way to unmounting the canvas entirely and leaving you with the HTML. Most of my time went into the failure path, not the happy one.

iOS Safari’s memory ceiling. Safari will kill a tab that gets greedy with GPU memory, with no warning and no error you can catch. Device pixel ratio is capped, texture budget is capped, and both numbers were chosen by watching real tabs die.

Physics as events, not polling. Every district uses a sensor collider that fires when you enter it. The naive version raycasts every frame to ask “am I near a building yet,” and the naive version costs you the frame budget you needed for everything else.

Focus management. Opening a panel from inside a canvas means trapping focus, returning it correctly on close, and making sure a keyboard user is never stranded in a 3D scene with no way out. This took longer than the physics.

The constraint that shaped everything

There is no AI in this site. No chatbot, no assistant, no model call at runtime, no AI dependency in package.json — and a CI check that fails the build if one appears.

That is a deliberate constraint, and it is load-bearing. The easy version of a 2026 portfolio is a chat box that answers questions about me. It takes an afternoon, it costs money per visitor, it can be prompt-injected, it goes down when a provider goes down, and it will occasionally invent a job I never had.

The harder version is to sit down and write the eight answers a recruiter actually asks, as static HTML, and to make the guide character’s behaviour twenty lines of deterministic logic — a distance sort over unvisited districts, a lerp toward you, a fixed list of lines. It never hallucinates my degree. It works with the network off after first load.

I build with these models professionally; two of the projects in this city are GenAI work. That is exactly why I know a portfolio is not where they earn their keep. Knowing when not to reach for the tool is the part that is hard to demonstrate — so I demonstrated it by not reaching for it here.

Was it worth it?

Ask the version of the site you are looking at. If it loaded fast, ran smoothly, and let you read everything without touching the game, then yes. If it did not, then the honest answer is in front of you, which is more than most portfolios offer.