From Colored Boxes to a City at Night: Leveling Up a Web Game's Graphics With No 3D Artist


Klooboo’s Boo Runner is a three.js endless runner that kids remix — swap the sky, the road, the obstacles, the character — and share. The catch: until last week it looked like colored boxes. A sphere with one eyeball for a player, flat box obstacles, box “buildings”, a single image for the sky. Functional, but nobody would mistake it for a real game.

The brief was blunt: “make it look more like Temple Run 2.” And the hard constraint: no 3D artist, and every layer has to stay remixable by a seven-year-old. You can’t have a kid AI-swap a hand-sculpted hero character.

So we couldn’t just buy art. We had to find the things that raise perceived quality without a content pipeline. Here’s what worked, in order of impact-per-effort.

The biggest lever isn’t assets — it’s the render pipeline

The single most important realization: Temple Run 2 isn’t photoreal. It’s a unified stylized look with a real rendering pipeline. Its quality comes from lighting, tone-mapping, and post-processing — not from more polygons. And almost all of that is asset-agnostic, which means it works no matter what a kid swaps in.

We had none of it. The scene was lit by a hemisphere light and a directional light, rendered straight to the canvas with default color management. So we added a pipeline:

  • Image-based lighting (IBL). The sky image becomes the environment. Every MeshStandardMaterial is now lit by the actual scene, so the player, the road, and the obstacles share one coherent light. This is the change that made things stop looking like flat cutouts pasted over a backdrop.

    const pmrem = new THREE.PMREMGenerator(renderer);
    loader.load(skyUrl, (tex) => {
      tex.mapping = THREE.EquirectangularReflectionMapping;
      scene.background = tex;
      scene.environment = pmrem.fromEquirectangular(tex).texture; // <- the magic
    });

    Swap the sky and the whole world re-lights to match. That single coupling does more for cohesion than any model could.

  • ACES filmic tone-mapping (renderer.toneMapping = THREE.ACESFilmicToneMapping) for cinematic color instead of flat sRGB.

  • Bloom, via an EffectComposer (RenderPass → UnrealBloomPass → OutputPass), tuned to a high threshold so only genuinely bright things glow — coins, neon rails, and (later) lit windows.

  • Better shadows — a larger, bias-tuned shadow map and a stronger sun.

This pass alone was a 2–3× jump in perceived quality, and we hadn’t added a single new asset yet.

Real assets without an artist: CC0 + the Blender MCP

For the things that genuinely need to be modeled — the player’s obstacles — we leaned on CC0 assets (PolyHaven), driven through Blender via its MCP integration. We pulled a concrete road barrier, downscaled its textures to 1k, and exported a lean GLB:

  • The raw export was 11 MB (embedded 2k PNGs). Unshippable for a mobile web game.
  • Downscaling to 1k and re-exporting as JPEG-compressed GLB brought it to 1.8 MB — a 6× cut with no visible difference at gameplay distance.

Load it once, clone it per lane, and the flat pink boxes became real jersey barriers with hazard stripes, lit by the same IBL as everything else.

The dead-end worth flagging

We assumed we’d build the world — buildings, fences, poles — the same way. We were wrong. PolyHaven’s “facade” and “fence” assets aren’t models; they’re 150-piece modular construction kits meant to be assembled by hand in Blender. There are no ready-to-drop buildings, and there are no window-grid facade textures either — only plain walls.

That dead-end pushed us to a better answer.

Procedural is the answer where libraries fail (and it’s on-brand)

A city is a perfect candidate for procedural generation: repetitive, forgiving at speed, and exactly the kind of thing that should be generatable rather than authored. So we draw building facades on a <canvas> — a grid of windows, some lit, some dark — and use it as both the diffuse map and an emissive map. Then we tie the emissive intensity to the time of day:

setCityGlow(/night/.test(skyId) ? 1.7 : /dusk|sunset/.test(skyId) ? 0.7 : 0.08);

By day the windows are barely there. At night they glow — and because bloom is already in the pipeline, you get a city skyline at night, streaming past in two parallax layers, for the cost of a canvas draw. No models, no texture downloads, fully ours to remix. This is the same philosophy behind pulling all our generation into one AI service: own the generation, don’t ship a content dependency.

Juice: half of “feeling like a real game” is motion

Static screenshots lie. A runner lives or dies on feel, and feel is cheap to add relative to its impact:

  • Speed-FOV — the camera widens as you accelerate. Nothing screams “fast” louder.
  • Dust trail under the player, coin pop bursts, and camera shake on a stumble.
  • Squash-and-stretch — the character stretches on takeoff, squashes on landing, and leans into the run. Twelve lines of code; reads as “animated.”
  • A subtle speed vignette at the screen edges.

None of this is an asset. All of it is feel.

The real tension: remixable and cohesive

Here’s the part specific to a remix game. Temple Run 2 is cohesive because a studio art-directed every pixel. We can’t — a kid changes whatever they want. So cohesion has to come from the system, not from locking things down:

  • IBL means any sky a kid picks re-lights everything to match automatically.
  • Procedural layers (city, facades) inherit the palette instead of clashing.
  • And every choice has to survive a share link. A kid taps “share”, a friend opens /g/<code>, and the forked game must look identical. So each remixable layer serializes into the theme config the share snapshot stores — including the new ones (we smuggled the road-texture choice through an existing string field to avoid a schema migration, then planned a clean ThemeConfig v2 for the rest).

The lesson: on a remix platform, “art direction” is a property of your rendering and serialization systems, not a person.

What actually moved the needle

If you’re trying to make a web game look better and you don’t have an artist, spend your effort in this order:

  1. Rendering pipeline — IBL, tone-mapping, bloom, shadows. Asset-agnostic, biggest jump.
  2. Juice — FOV, particles, shake, squash-and-stretch. Cheap, huge felt-quality.
  3. A few real models where it counts (the things on-screen every second), kept lean.
  4. Procedural everything else — especially repetitive world detail.

Notably absent: “make more/better assets.” Polygon count was never the bottleneck.

What’s next

The drawer that lets kids remix all this is growing into a tabbed control surface — style, settings (speed, jump, dust), and an AI-generate tab. When that ships, the generated skies and textures will of course route through our AI gateway — the same one door, traced and cost-capped. (The hero image on this very post was generated through it.)

Colored boxes to a city at night, no artist required. Turns out the web platform is further along than it gets credit for — you just have to spend your budget on light instead of geometry.