What started as a weekend experiment to render a calm, digital glade turned into a months-long exercise in pushing WebGL 2.0 and Three.js to their limits. I wanted an environment that felt alive, not just pretty-looking, but responsive, season-aware, and fast enough to run on a mid-range laptop. Below is the short version of that journey: the problems that kept me up at night, the solutions that actually worked, and the few hard lessons I carried into every subsystem.
The original spark for Elemental Serenity came from watching Bruno Simon's portfolio devlogs (https://youtu.be/MXpML0B2MJc?si=rePA5w2j7CL7Jm9s), specifically his breakdowns of how a playful, interactive 3D experience can still be technically rigorous. Seeing how he approached world-building in the browser reframed how I thought about WebGL projects: not as static demos, but as places users can explore. His emphasis on performance-aware creativity pushed me to treat every visual decision as an engineering problem waiting to be solved.
At the same time, Jordan Breton's portfolio (https://jordan-breton.com/) heavily influenced the overall mood and restraint of the scene. Where Bruno's work inspired interactivity and technical ambition, Jordan's reminded me of the power of atmosphere, subtle motion, carefully chosen colour palettes, and environments that feel intentional rather than busy. That balance between expressiveness and calm became a guiding principle throughout the project.
I intentionally kept the tech simple but modern:
vite-plugin-glsl — absolute game changer for
shader hot-reload.
These choices gave me quick iteration during development while allowing me to drop into GLSL when performance mattered.
I wanted thousands of grass blades that bend and whisper in the wind. Naively creating tens of thousands of separate meshes was an instant FPS death sentence. The goal became: how do I keep visual richness with minimal geometry and GPU overhead?
I didn't want a single color toggle. I wanted completely different moods.
SeasonManager singleton broadcasts events and
exposes a small set of uniforms every shader subscribes to
(palette colors, shadow tint, water reflectance, smoke
opacity).
u_seasonBlend.
The result is smooth, event-driven transitions with zero frame drops even on large scenes.
Visuals were only half the immersion. Sound had to be reactive, not just background music.
AmbientSoundManager controls both
spatialized sounds (bird pings, crackling
fire) and ambience tracks (wind, brook,
seasonal pads).
This was deceptively tricky: poorly handled crossfades or too many simultaneous sound nodes easily produce audible artefacts or memory leaks, so lifecycle management was critical.
A few engineering choices repeatedly paid dividends:
EventEmitter allowed decoupled components to
react to seasonal or quality changes without washing the scene
with polling logic.
localStorage.
WebGL resources are explicit. I ran into crashes and VRAM leaks until I audited the cleanup.
.dispose() on geometries, materials,
and textures when removed.
After enforcing strict cleanup patterns, stability improved dramatically, especially on devices with constrained memory.
This project was equal parts art and systems engineering. The biggest wins came from moving logic onto the GPU (instancing, wind, particles), centralising state (SeasonManager), and being ruthless about cleanup. If you want to poke around the code, focus on the instanced grass shader and the event-driven SeasonManager; they're the heart of the system.
Version 1.0.0 | Built with 💜 for the web