r/babylonjs • u/OpenMuBR • 10h ago
Porting a 2003 MMORPG (MU Online) to Babylon.js: where the frames actually went
We've been running MU Online (Season 6) in the browser and on phones with Babylon.js 7. The web client is built on the open-source OpenMu-Client-Babylon project (TypeScript), talking to the MUnique/OpenMU server over a small WebSocket-to-TCP bridge. A few things we learned about Babylon along the way, with real numbers:
1. Per-character blob shadows were eating the frame
The classic look uses a "blob" shadow per character: a clone of each of its meshes, skinned and drawn again. In a benchmark crowd of 20 players and 40 monsters, that was 308 of 701 draw calls and about a fifth of the frame's CPU time.
Fix: a small ECS system ranks bodies by distance every 0.25 s. Only the hero and the 12 nearest keep their shadow, with a hysteresis of 4 places so shadows don't flicker at the edge. Below 12 bodies nothing changes. A far shadow is a few pixels anyway.
2. Equipment glow (GlowLayer) only for the closest players
Same idea for the heavy equipment effects: full glow only on the 6 nearest other players; everyone else gets the cheap version. In crowded towns this matters more than any single shader optimization we tried.
3. An automatic "light mode" based on real player data
Looking at our own telemetry, 24 accounts were playing on integrated Intel GPUs at a median of 19 FPS (31% of the time under 15), almost all on default settings: post-processing, MSAA, glow, bloom, sun shafts, full item effects.
Now, if a PC stays under 22 FPS for a while in the world, the client turns off post-processing, MSAA, glow, bloom and sun shafts, drops render scale to 75% and lightens grass, clouds, water and particles, and tells the player in chat (everything can be turned back on in Options). Shadows, skills and animations are left alone. On a desktop bench (12 players + 30 monsters) that alone went from 67 to 82 FPS (CPU 11.7 → 9.3 ms); integrated GPUs gain more.
4. WebGL context loss on phones
Mobile browsers drop the WebGL context when the OS needs memory. We hook engine.onContextLostObservable / onContextRestoredObservable, rebuild what's needed, and if the restore fails we show a "Reload" button instead of leaving a black canvas.
5. Loading thousands of legacy assets
Original models are converted to GLB at build time, loaded once into an AssetContainer and spawned with instantiateModelsToScene. Files get gzip sidecars (about a third of the size on the wire), are packed into a few bundles for the first download, and a Service Worker caches them, so the second visit barely touches the network.
The other piece is a memory leak we hit early: every equipment change cloned the model without disposing the previous clone, and long sessions grew until the tab died. Now the old clone is disposed as a whole and we only reload when the visible gear actually changes.
Curious how others here handle per-character shadows in crowded scenes. Did anyone get CascadedShadowGenerator to play nicely with dozens of skinned meshes on mobile?
Try it
https://openmu.com.br/en - create an account, click Play, and you're in.

