A working (but probably not the correct) pipeline to take a scene baked in Unity and display it with its lightmaps intact in vanilla Three.js.
⚠️ I am not claiming this is the canonical way. It is a way that worked for me after way too many hours of suffering. If you know a cleaner pipeline, please open an issue, I'll happily update this.
| Lightmap OFF (flat PBR, direct lights only) | Lightmap ON (Unity-baked GI) |
|---|---|
![]() |
![]() |
Same frame, same camera, same directional + ambient lights. The only difference is whether the baked lightmap is applied or bypassed.
I already know Unity well. I love Bakery / Progressive GPU. Rebuilding the same knowledge inside Blender just to bake lighting for Three.js was going to cost me days. This pipeline lets me stay in Unity for level design and baking, and ship to Three.js without rewriting everything.
Unity bakes beautiful lightmaps, but GLTF → Three.js is a minefield:
-
Unity stores lightmap UV scale/offset OUTSIDE the mesh, in a custom GLTF extension (
MX_lightmap) that most loaders ignore. If your viewer doesn't honor the extension, your meshes read the lightmap at the wrong coordinates and you get rainbow-vomit artifacts on the walls and floor. -
"High Quality" lightmaps are RGBM encoded. The alpha channel is an HDR multiplier. Three.js reads the PNG as straight RGBA and the colors come out completely wrong: black with flecks of oversaturated noise.
-
Three.js
lightMapis additive while Unity applies it multiplicatively. Out of the box, the result never matches what you see in the Unity editor.
This repo tackles problems (1) and (2) and gives a reasonable answer to (3).
Unity scene
│
│ bake lightmaps normally
▼
Unity Editor: Tools → Bake Lightmap UVs into Meshes ← this repo
│
│ GLTF export (prefrontalcortex / matrix-org UnityGLTF)
▼
Node: scripts/decode-rgbm.js Lightmap-0_comp_light.png ← this repo
│
│ drop scene into scenes/
▼
Three.js: tex.channel = 1, flipY = false, sRGB ← this repo
│
▼
Browser 🎉
- Unity 6+ (tested on 6000.0.60f1, URP)
- A GLTF exporter that writes at least a valid UV2 (
TEXCOORD_1):- prefrontalcortex/UnityGLTF, or
- matrix-org/UnityGLTF (thirdroom), branch
thirdroom/dev
- Node.js 18+ for the RGBM decoder
- Three.js r152+ in your project (we need
Texture.channel)
Copy unity/BakeLightmapUVs.cs into your Unity project:
Assets/Editor/BakeLightmapUVs.cs
Window → Rendering → Lighting → Generate Lighting
Make sure your meshes have "Generate Lightmap UVs" checked in their import settings if they don't already have a UV2 channel.
Tools → Bake Lightmap UVs into Meshes
The script will, for every MeshRenderer with a valid lightmapIndex:
- Duplicate the mesh
- Apply
renderer.lightmapScaleOffsetdirectly into the UV2 of the copy (the transformation Unity usually does at runtime via the atlas entry) - Save the copy as an asset under
Assets/BakedLM/ - Swap the filter's
sharedMeshto the baked copy - Reset the renderer's
lightmapScaleOffsetto(1,1,0,0)so the Unity viewport doesn't double-transform in play mode
The originals are kept in memory for restore in step 5.
Export your scene as usual with your GLTF exporter. Prefer separate
textures so you can inspect and decode the lightmap PNG next to the
.gltf and .bin files.
The exporter will serialize the pre-baked UV2 into the mesh. No extension support needed on the reader side.
Tools → Restore Original Meshes
- Puts the original
sharedMeshback on everyMeshFilter - Restores the original
lightmapScaleOffseton every renderer - Deletes the
Assets/BakedLM/folder
Your Unity project is back to exactly how it was.
cd scripts
npm install
node decode-rgbm.js ../scenes/Lightmap-0_comp_light.pngThis reads the PNG, multiplies each pixel's RGB by (alpha / 255) * 5.0
(the standard Unity RGBM decode), clamps to 0–255 and writes the file
back with alpha set to 255 everywhere. The decoder is idempotent: if
all alpha values are already 255, it leaves the file alone.
Tip: you can skip this step entirely if you set Player Settings → Other Settings → Lightmap Encoding → Normal Quality before baking. Unity will then export a plain sRGB PNG. The tradeoff is slightly less HDR range in the bake.
scenes/
scene.gltf
scene.bin
Lightmap-0_comp_light.png
[all your other textures]
Update scenePath and lightmapPath in main.js if your filenames
are different.
npx serve .
# open http://localhost:3000lmTex.flipY = false; // GLTF spec: no Y flip
lmTex.channel = 1; // Read UV2 (TEXCOORD_1)
lmTex.colorSpace = THREE.SRGBColorSpace; // Lightmap is sRGB-encodedThe single most important line is tex.channel = 1. Without it
Three.js defaults to UV0 (the base color UV set) and your lightmap reads
the same coordinates as your diffuse map. Hello rainbow walls.
The demo includes a floating debug panel with sliders for:
- Ambient intensity & color
- Directional intensity & color
- Lightmap intensity
- Fog enabled / color / near / far
- Bloom strength / radius / threshold
- Tone mapping exposure
A PRINT CONFIG button copies the current settings to your clipboard as JSON, so you can paste them straight into your own scene config.
-
Brightness mismatch vs Unity editor: Three.js
lightMapis additive where Unity applies it multiplicatively. The panel compensates vialightMapIntensity, ambient, and exposure, but it's not pixel-perfect. Patching the MeshStandardMaterial shader withirradiance = lightMapIrradiance(instead of+=) gets closer but is invasive and fights with direct lights. I left it out of this demo. -
Directional lightmaps (normal-aware) are ignored. Only the color lightmap is used.
-
Multiple lightmap atlases (Unity splits into Lightmap-0, -1, -2… when a scene doesn't fit) are not handled. Force everything into one by raising Lightmap Size in Lighting Settings (2048 → 4096).
-
Shadowmask / subtractive modes are untested.
-
Emissive strengths exported via
KHR_materials_emissive_strengthcan wash out the lightmap. Clamp them down to reasonable values before exporting, or post-process them in a traverse.
Honestly? No idea. This is what worked for me. If you have a
cleaner path (using MX_lightmap properly, a dedicated Three.js
extension plugin, or a better exporter), please tell me. I'll happily
update this repo and credit you.
MIT. Use, fork, remix, tear apart, improve.
Texture.channeltrick: three.js discourse- RGBM formula: the standard Unity
rgb * alpha * 5.0 - Everyone who's already written about this on Twitter, Discord and forums. This repo is mostly a concrete, runnable synthesis of advice scattered across many threads.


