Skip to content
← writing

Restyling someone's WebGL terrain into night mode

three
webgl
r3f
frontend

mesq open-sourced an infinite-terrain scene (MIT) that stopped my scroll: endless procedural grass, streamed trees, wind flow lines, and a little physics ball you roll around to explore it. I wanted my own take, so instead of rebuilding it, I reskinned it for night. Click in and roll around (WASD / arrows):

loading terrain…

The cheapest way to make something yours: a theme

The smart thing mesq did was build the scene around a theme object. Every color the scene uses (terrain, sky, grass base and tip, the ball, leaves, the fresnel rim on foliage) comes from one palette, and there's a switcher that swaps day for a warmer "light" set. That one decision is what made my job easy.

I added a night palette next to the existing ones:

night: {
  terrain: '#2b3c63',        // cool slate-blue ground
  background: '#0b1126',     // deep indigo sky
  grassBase: '#1d4a52',      // dark teal roots
  grassTop: '#57b291',       // moonlit green tips
  ball: '#9a7bff',           // a glowing violet you
  leaves: '#356a58',         // dim foliage
  bushFresnelColor: '#a6d4ff', // cold blue rim light
}

Then I dropped the lighting from a bright sun to a low, cool "moonlight": a faint blue directional plus a blue hemisphere fill, so nothing reads as warm. The shaders didn't change at all. They were already driven by those uniforms, so recoloring the inputs recolors the whole world. Day became midnight by editing a handful of hex values and three lights.

Once one new mood was that cheap, I made a few more. Each preset now bundles its palette and a lighting rig, so switching it changes the whole feel: aurora (polar green-cyan), synthwave (neon magenta + cyan), sakura (a bright pink-blossom day), ember (volcanic dusk), and noir (moonlit grayscale). The picker in the demo above swaps between them live, and you can toggle the trees and stones while you're there.

Getting it into the site

This is the heaviest thing on here by far. It pulls in three.js, react-three- fiber, drei, rapier physics, and a stack of GLSL shaders. Two things made it behave:

  • Client-only. The site server-renders on Cloudflare Workers, where WebGL and physics don't exist. So the scene is loaded with a dynamic import() inside an effect, which keeps three and rapier out of the server bundle entirely. The page ships a tiny placeholder and swaps in the real scene once it's on the client.
  • GLSL as modules. A Vite plugin lets the shaders import as strings with their #include directives resolved, so the materials port over untouched.

The infinite part is the nicest trick: it streams a 3×3 grid of chunks around the ball and rescales the field at its border, so you can roll forever and never hit an edge. I left all of that intact.

Full credit to mesq — the scene is his (MIT), I just turned out the lights. Grab my night build on the Infinite Terrain page.

Ask your agent to implement this

Read the full writeup at https://seangeng.com/writing/night-infinite-terrain.md and implement it in my project.

It covers: Restyling someone's WebGL terrain into night mode — mesq shipped a gorgeous open-source infinite-terrain scene in r3f. I didn't rebuild it — I reskinned it. A new theme palette and moonlit lighting turn a sunny field into a midnight one. Same shaders, different mood.

Requirements:
- Follow the technique/approach exactly as described in the writeup.
- Adapt names, colors, and styling to my project's existing conventions.
- If it's a component, make it reusable with sensible props and TypeScript types.
- Keep it accessible: semantic HTML, keyboard support, and respect prefers-reduced-motion.
- When done, tell me which files you created or changed and how to use it.

Paste into Claude Code, Codex, Cursor, or any agent. view raw .md