Three.js r128 → r185: A Field Record of an Engine Upgrade
Three.js r185 upgradeThree.js version upgradeWebGL2color pipelinecolor managementthree shimremoving CDN dependencyoffline deployment 3Drendering core migrationbrowser 3D compatibility
Engine upgrades are the thing in every 3D project that feels like looking for trouble on purpose. Everything works — so why swap the rendering core from r128 to r185? This article lays out a real upgrade end to end: what the payoff actually was before touching anything, which four kinds of breakage showed up, how we proved the visuals didn't silently change, and which legacy API calls still linger. This work is already built into the Genesis foundation; if you're considering the same move on your own engine, the ordering and the checklist should transfer directly.
One: The Payoff, Counted Before Starting
An upgrade isn't "chasing the latest version." It buys three concrete things.
First: color correctness. The default behavior around r128 was sRGB output with textures shaded as linear data — a double gamma, which makes the whole image too bright and too pink. A lot of teams treat this as "the art needs a color pass," when it's actually a pipeline bug. Three.js changed its color management after r152, and r185 follows the correct pipeline. This isn't version-number theater: the same models and textures look right after the switch.
Second: the WebGL2 capability surface. Self-written gaussian-splat shaders, BVH collision, instanced batching — all of these need a WebGL2 baseline. Without moving to r185 there's no such baseline.
Third: no more external CDN dependency. As part of the upgrade we repointed the importmap away from a public CDN, localized or stubbed six loaders, and moved the editor suite, admin console, and game page onto local dependencies. The result: intranet and offline environments run too, and pages no longer break when the outside network stutters or a version drifts.
There's a bonus engineering payoff: an upgrade forces every scattered legacy API call into the light. We ended up with a canonized list of 79 legacy calls, backed by a compatibility layer so existing user code blocks don't all have to be rewritten.
Two: The Four Kinds of Breakage That Actually Cost Time
Version 128 to 185 crosses seven minor releases; the loading path, module system, color management, and shader compilation path all changed. Breakage clustered into four categories.
1. Entry breakage: the file changed its name
The most direct layer: the new UMD bundle has a different filename, so old import paths 404 immediately. The fix is to build our own r185 UMD bundle with esbuild and put it behind the old filename — the exposed entry stays the same while the inside is upgraded. After this step, nothing downstream changes.
2. Loader breakage: six loaders with inconsistent assumptions
Texture, font, GLTF, FBX and the rest each carried their own dependency assumptions in the old version — some reached for globals, some for modules. We localized or stubbed them one by one, turning implicit global dependencies into explicit references. The most time-consuming part here was tracking down "one loader occasionally fails" — because it doesn't error, it silently falls back.
3. Symbol breakage: 87 exports that no longer exist
The new version removed or renamed a set of exports. We wrote a three-shim layer supplying 87 symbols. The rule for this layer is clear: supply signatures, not behavior. The shim makes calls resolve; behavioral differences are handled separately by the compatibility layer. Mixing the two would make attribution impossible when something breaks.
4. Semantic breakage: the color API changed names and meanings
This is the most insidious category. The old outputEncoding and texture.encoding map onto colorSpace in the new version, and it isn't a plain rename — the encoding value semantics carry detail. We wrote a patchTHREE compatibility layer that genuinely maps the old properties onto the new API instead of aliasing property names. Without this layer, the failure mode is "the colors quietly changed."
Three: How We Proved Nothing Broke
The worst outcome of touching the rendering core isn't a crash — it's that everything runs and the visuals changed, with nobody noticing. So the approach was: establish the baseline first, then cut.
- 10 baseline screenshots under a fixed protocol — fixed camera, fixed time, fixed angle, captured before any change.
- A self-built PSNR / diff tool — per-pixel comparison of baseline against post-upgrade screenshots, reporting a PSNR value and a differing-pixel ratio.
- Attribution of every difference — the admin console measured 43.4dB consistency, which is correct since its structure should be pixel-identical. The differences in the main world all land on color: composition, buildings, and characters are pixel-level identical, and the color shift comes from the color pipeline becoming correct, which is expected.
The methodology deserves its own note: baseline first, then cut. Without a baseline, nothing that "looks fine" can be falsified. With one, differences can be attributed to specific causes instead of being judged by feel.
Four: Two Supporting Pieces That Came Along
Degrade gracefully. Devices without WebGL2 shouldn't get a white screen and a console full of errors. webgl2Guard.js detects support before Three.js loads and, on failure, shows a full-screen Chinese message then calls window.stop() to halt further loading. The user reads one sentence instead of watching errors scroll past a blank page.
Closing a historical gap. Under the new version, if the importmap points at two sources at once you get a dual instance: two independent state machines that can't see each other, with the symptom "occasionally some object doesn't move." Repointing to local removed this hazard.
Five: The Numbers
| Item | Result |
|---|---|
| Rendering core | r128 → r185, WebGL2, 6-stage full acceptance, REVISION=185 |
| Color pipeline | Compatibility layer genuinely maps old properties to colorSpace; double gamma eliminated |
| Shim symbols | 87 |
| Legacy API handling | 79 calls canonized + compatibility-layer fallback |
| External dependencies | 6 loaders localized/stubbed; importmap repointed local; editor suite / admin / game page all local |
| Baseline comparison | 10 fixed-protocol screenshots; admin 43.4dB consistent; main-world differences fully attributed to color pipeline correction |
| Degradation experience | webgl2Guard.js pre-load detection, full-screen Chinese message + window.stop() |
Six: The Limits of These Numbers
- 43.4dB is the admin-console figure. The main world's difference is the expected color shift, not a botched upgrade. One number isn't the whole conclusion.
- The 87 shim symbols only guarantee that calls resolve — they don't mean behavior matches. Behavioral gaps are handled by
patchTHREE, and the two layers must be read separately. - Going CDN-free solves "can it run offline." It does not solve model size. Size belongs to the asset pipeline (LOD, texture compression), which is out of scope here.
- The 79 legacy calls are the result of a canonization pass, not a promise that legacy calls can never appear again. The next upgrade reopens this item.
FAQ
Q: Do I need to stop the project for an engine upgrade?
A: The work concentrates in the loading and compatibility layers; business code barely moves. What genuinely needs attention is regression testing, because visual changes don't throw errors.
Q: Why not just pin the old version?
A: You can — at the cost of the WebGL2 capability surface and the corrected color rendering of the correct pipeline. The price of pinning grows as browsers evolve.
Q: Will old API usage be retired immediately?
A: No. The compatibility layer keeps the old property mappings, so existing code blocks run first and can be migrated against the canonized list in batches, without a wholesale rewrite.
Q: What's hardest to judge during an upgrade?
A: "No error, but the picture changed." That class of problem only surfaces when you have baseline screenshots and a pixel-diff tool; eyeballing under normal lighting won't find it.
Source & Repository
All three addresses host identical content; the first two are faster within mainland China. The repository includes deployment docs and the acceptance scripts.
- Gitee (faster in mainland China): https://gitee.com/miduoxinxijeji/miduo.git
- GitCode (mirror): https://gitcode.com/qq_35054471/virtual-world
- GitHub: https://github.com/miduo100/3d-virtual-world
About Genesis
Genesis is a self-hosted 3D virtual world system built on Three.js + WebGL, helping individuals and businesses build their own 3D spaces. Accessible directly from a browser, compatible with both PC and mobile, it supports multiplayer online, federated teleportation, a shop system, and Agent integration—where an AI can enter your world as an embodied character. Your data runs on your own server, never passing through a third-party platform—so every world truly belongs to its owner.
Planning an engine upgrade for your own 3D frontend? Genesis Virtual World CRM is a Three.js 3D virtual world foundation you deploy on your own server — r185/WebGL2, the color pipeline, localized loaders, the compatibility layer, and graceful degradation are already handled; you write the top layer. The official site (search "Genesis Virtual World CRM") has a demo world you can walk through.
About the name: Genesis in this article refers to Genesis Virtual World CRM — they are the same self-hosted 3D virtual world product. If "Genesis" doesn't turn us up in search, search "Genesis Virtual World CRM" instead.