Skip to content

Ibrahim-3d/three-lightmap-baker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

163 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cornell advanced scene with path-traced global illumination baked in-browser

Solid viewport before bake Preview bake Production bake
Cornell advanced scene before lightmap baking Cornell advanced scene after Preview lightmap bake Cornell advanced scene after Production lightmap bake

Important GPU note: this baker is GPU-bound. Chrome and Edge must have Use graphics acceleration when available enabled, and chrome://gpu should report WebGL/WebGL2 as hardware accelerated. Launch capture uses installed Chrome and, on Windows, prefers ANGLE D3D11 because that is the backend that selected the NVIDIA RTX GPU in testing. The app reports the renderer it actually received, and launch automation can reject captures from the wrong GPU with BAKER_EXPECT_GPU="RTX 3050" npm run capture:launch.

🔆 Three Lightmap Baker

Path-traced lightmap baking with global illumination. In the browser. No Blender. No Unity. No round-trips.

Live Demo 🚀

Quick StartWhy This ExistsFeaturesExamplesBenchmarksHow It WorksLimitationsAPIRoadmap

Three.js TypeScript MIT License GI Bounces Status


The Problem

Three.js developers looking for real lightmap baking run into the same gaps:

  • @react-three/lightmap - built around an older hemicube workflow and no longer tracks modern R3F releases.
  • mem1b/lightbaking - targets pre-BufferGeometry-era Three.js APIs, so it is not a drop-in option for current projects.
  • three-gpu-pathtracer - excellent screen-space path tracing, but it does not publish baked lightmap atlases. Its lightmap-baking request has been open since 2021.
  • Stack Overflow answers - "just bake in Blender and export." Which is fine until your scene is procedural, user-generated, or assembled at runtime. Then you're stuck.

The result: in 2026, Three.js still lacks a maintained, browser-first lightmap baker focused on path-traced global illumination.

This repo fixes that.

Why This Exists

The Three.js ecosystem needs lightmap baking that runs in the browser. Not "export to Blender, bake for 40 minutes, re-import" - actual in-app baking where the user clicks a button and sees the result in seconds.

I looked for a library. There wasn't one. The most complete option (lucas-jones/three-lightmap-baker) had the right architecture but stopped before bounce lighting - no color bleeding, no indirect illumination, no GI.

So I'm building it. This fork keeps the proven two-pass architecture, adds multi-bounce path-traced GI, and focuses on a maintained, documented library.

The Agentic AI Angle

Every AI coding agent (Claude Code, Cursor, Devin, Copilot Workspace) can build and manipulate 3D scenes programmatically. What they can't do is bake lighting without spawning Blender and writing a Python script to drive it.

A JavaScript-native lightmap baker that runs in Node.js or the browser is infrastructure for the agentic wave. An AI agent that assembles a room from a furniture catalog needs to light that room without opening a DCC tool.

If you're building anything where 3D scenes are constructed programmatically - architectural configurators, AI interior design, procedural environments, digital twins - and you need those scenes to look lit, you're the target user.


Familiar Workflow (Unity & Unreal Style)

If you've used Unity's Progressive Lightmapper or Unreal's Lightmass, you'll feel at home. This baker implements the same industry-standard concepts directly in the browser:

  • Direct & Indirect (GI): Separate accumulation of direct light and multi-bounce global illumination.
  • Bake Presets: Toggle between high-speed Draft bakes and high-fidelity Production bakes.
  • Dilation & Padding: Automatic edge-bleeding prevention to eliminate black seams at UV island borders.
  • Denoising: Integrated bilateral noise reduction for clean soft shadows and smooth gradients.
  • Texel Density: Visualized via checkerboard overlays to ensure consistent lightmap resolution across the scene.

Features

Shipping Now

  • Path-traced global illumination - real bounce lighting, not screen-space hacks. Red walls bleed red light onto white surfaces. The Cornell Box test passes.
  • Auto UV2 unwrapping - drop in any geometry, xatlas generates non-overlapping lightmap UVs automatically. No Blender unwrap step.
  • GPU-accelerated BVH ray tracing - powered by three-mesh-bvh. Millions of rays per second in WebGL.
  • Multi-bounce - 1-4 configurable bounce depth. Each bounce adds indirect illumination. Energy-conserving (albedo < 1 guarantees convergence).
  • Per-triangle material data - albedo and emissive packed into DataTextures, indexed by BVH triangle. Materials are respected during bounces - a red wall reflects red light because the bounce ray reads the wall's albedo.
  • Progressive rendering - watch the lightmap converge in real-time. Stop early if quality is acceptable.
  • Bake presets - measured on the Cornell advanced scene from Draft through Final, with RTX 3050 Ti numbers listed below.
  • Gap flood / edge dilation - prevents black seams at UV island borders.
  • Bilateral denoiser - smooths noise while preserving shadow edges, guided by world-position and normal textures.
  • TypeScript - strict mode, fully typed API.

Coming Next

Planned work is tracked in the Roadmap.


Quick Start

# Clone
git clone https://github.com/Ibrahim-3d/three-lightmap-baker.git
cd three-lightmap-baker

# Install
npm install

# Run the Cornell Box demo
npm run start # opens http://localhost:5173

Click Bake and watch the lightmap converge. Color bleeding should be visible on the sphere within seconds.

Use as a Library

The npm package name is reserved for release, but it is not published yet.

# After the first npm release:
npm install three-lightmap-baker

# Until then, install from a generated tarball:
npm run build:package
npm pack
npm install ./three-lightmap-baker-1.0.0.tgz

If you're working in this repo, the classic baker lives in packages/baker-classic/.

import { LightmapBaker } from 'three-lightmap-baker';
import * as THREE from 'three';

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(512, 512);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x20262f);

const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 100);
camera.position.set(2.5, 2, 3);
camera.lookAt(0, 0.5, 0);

const mesh = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshStandardMaterial({ color: 0xced4da, roughness: 0.6 }),
);
mesh.position.y = 0.5;
scene.add(mesh);

const ground = new THREE.Mesh(
  new THREE.PlaneGeometry(4, 4),
  new THREE.MeshStandardMaterial({ color: 0x5c677d, roughness: 0.9 }),
);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);

const light = new THREE.DirectionalLight(0xffffff, 2.5);
light.position.set(2, 4, 1);
scene.add(light);

// Bake (clean constructor style)
const baker = new LightmapBaker({
  renderer,
  samples: 64,
  bounces: 2,
  resolution: 512,
  denoise: true,
});

const result = await baker.bake(scene, {
  onProgress: (phase, percent) => {
    console.log(`[baker] ${phase}: ${(percent * 100).toFixed(0)}%`);
  },
});

// Apply baked lightmaps to scene materials
result.apply();
renderer.render(scene, camera);

// Export lightmaps as PNG files
await result.export('lightmaps/', { format: 'png' });

// Clean up GPU resources when done
result.dispose();

Renderer-explicit style is also supported:

const baker = new LightmapBaker(renderer, { samples: 64, bounces: 2, resolution: 512 });
const result = await baker.bake(scene);

Examples

Minimal Browser

See examples/minimal-browser.ts for a copy-pasteable browser scene with a renderer, camera, MeshStandardMaterial geometry, direct light, and both supported constructor styles.

React Three Fiber

The baker needs access to the underlying Three.js renderer and scene. In R3F, get them from useThree() and run the bake from a user action:

import { useState } from 'react';
import { useThree } from '@react-three/fiber';
import { LightmapBaker } from 'three-lightmap-baker';

export function BakeButton() {
  const { gl, scene } = useThree();
  const [busy, setBusy] = useState(false);

  async function bake() {
    setBusy(true);
    try {
      const baker = new LightmapBaker({
        renderer: gl,
        resolution: 512,
        samples: 64,
        bounces: 2,
        denoise: true,
      });

      const result = await baker.bake(scene, {
        onProgress: (phase, progress) => {
          console.info(`[lightmap] ${phase}: ${Math.round(progress * 100)}%`);
        },
      });

      result.apply();
    } finally {
      setBusy(false);
    }
  }

  return (
    <button type="button" onClick={bake} disabled={busy}>
      {busy ? 'Baking...' : 'Bake'}
    </button>
  );
}

Keep the button outside the <Canvas> if you prefer native DOM controls; the important part is that the bake runs after the scene contains real MeshStandardMaterial meshes and a renderer-backed WebGL context.

Interior / Architectural Demo

The launch demo target is a small interior scene, not only a Cornell Box:

  1. Start with a flat Three.js room/configurator scene.
  2. Click Bake.
  3. Show progressive convergence, color bleeding, and soft contact shadows.
  4. Show the generated lightmap atlas.
  5. Apply the exported result back onto the same scene.

This section is intentionally a checklist until the final visual asset is captured.


Benchmarks

Measured on Windows with installed Chrome, ANGLE D3D11, and an NVIDIA GeForce RTX 3050 Ti Laptop GPU. Treat these as a reproducible baseline for the cornell.advanced scene, not a universal promise for every model or driver.

Run the local capture and benchmark helper with:

npm run capture:launch

It writes launch-artifacts/before-solid-viewport.png, launch-artifacts/after-preview-baked-combined.png, launch-artifacts/after-production-baked-combined.png, launch-artifacts/benchmark.json, and launch-artifacts/benchmark.md. The output directory is ignored by git so you can rerun it on the real launch machine and copy only the numbers/assets you want to publish.

For launch numbers, enforce the expected renderer so a dual-GPU laptop cannot accidentally publish integrated-GPU results:

BAKER_EXPECT_GPU="RTX 3050" npm run capture:launch

On Windows PowerShell:

$env:BAKER_EXPECT_GPU="RTX 3050"; npm run capture:launch

The capture helper uses installed Chrome by default. To intentionally use Playwright's bundled Chromium instead, set BAKER_CAPTURE_BROWSER_CHANNEL=bundled.

On Windows the helper tries ANGLE backends in this order when BAKER_EXPECT_GPU is set: d3d11, d3d11on12, then gl. Override that list when debugging:

$env:BAKER_CAPTURE_ANGLE="d3d11,d3d11on12,gl"; $env:BAKER_EXPECT_GPU="RTX 3050"; npm run capture:launch
Device Scene Preset Resolution Samples Bounces Denoise Bake Time
RTX 3050 Ti Laptop GPU cornell.advanced Draft 256px 4 x 32 frames (128 spp) 2 Off 4.63s
RTX 3050 Ti Laptop GPU cornell.advanced Preview 512px 5 x 96 frames (480 spp) 2 Off 5.05s
RTX 3050 Ti Laptop GPU cornell.advanced Production 1024px 6 x 256 frames (1536 spp) 2 Off 36.48s
RTX 3050 Ti Laptop GPU cornell.advanced Final 2048px 8 x 512 frames (4096 spp) 2 Off 408.7s

The script captures one solid 3D before image, then Preview and Production baked images. Use the Production image as the main after asset and keep the Preview image for visual preset comparison.


Why Not Just Bake In Blender?

Blender is still the right tool when your scene is authored offline, your assets are stable, and you can afford a DCC round-trip. This project targets the cases where that workflow breaks down:

  • Procedural scenes assembled at runtime.
  • Product configurators where users change layouts, finishes, or lights in the browser.
  • AI-generated or agent-authored 3D spaces.
  • Web architectural visualization that needs an in-app “Bake” button.
  • Pipelines where the source of truth is Three.js, not a .blend file.

The goal is not to replace offline production renderers. The goal is browser-native baked lighting for dynamic Three.js workflows.


How It Works

Two-pass architecture. No vertex shader hacks. No fighting the Three.js renderer.

Pass 1 - UV-Space Rasterization

Each mesh's geometry is rendered with gl_Position = uv2 * 2.0 - 1.0, projecting triangles into their lightmap UV layout. Two textures are generated:

Texture Contents Purpose
Position map World-space XYZ per texel (RGB = position) Ray origins for Pass 2
Normal map World-space normal per texel (RGB = normal) Ray directions for Pass 2

These textures are a 2D lookup table: for any texel coordinate, you can read the corresponding world position and surface normal.

Position map (left) and normal map (right) for a Cornell Box - each texel encodes a world-space coordinate.

Pass 2 - Path-Traced Ray Tracing

For each texel in the position/normal maps:

Read worldPos and worldNormal from textures
For each sample:
    Generate cosine-weighted hemisphere direction around worldNormal
    Trace ray from worldPos into scene BVH

    If ray hits a surface:
        Read hit surface's emissive color → add to radiance (direct)
        Read hit surface's albedo color

        // Bounce: trace again from hit point
        Generate new direction around hit normal
        Trace bounce ray into BVH
        If bounce hits emissive surface:
            radiance += hitAlbedo × bounceEmissive (indirect GI)

        // More bounces if configured (2, 3, 4...)

    Accumulate radiance into lightmap texel

Divide accumulated radiance by sample count → final lightmap

The BVH acceleration structure (from three-mesh-bvh) makes this fast - millions of ray queries per second on a mid-range GPU.

Why Two Passes?

The naive approach (what I tried first and burned 4 hours debugging) is to do everything in a single shader: override gl_Position to UV space AND trace rays in the same fragment shader. This fights Three.js at every level - modelMatrix propagation breaks, render target state leaks between frames, matrixWorldNeedsUpdate doesn't fire. 12 integration bugs later, it still didn't work.

The two-pass approach treats the renderer normally. Pass 1 uses the GPU for what it's good at (rasterization). Pass 2 uses the BVH for what it's good at (ray queries). No renderer hacks. No matrix bugs. It just works.


The Landscape

Project Last Updated GI Bounces Status
This repo Active ✅ 1-4 bounces ✅ Maintained
lucas-jones/three-lightmap-baker 2023 ❌ Direct only Archived (this fork continues it)
@react-three/lightmap 2022-era package ❌ Hemicube workflow Not current with modern R3F
mem1b/lightbaking Older Three.js era ✅ Has bounces Uses legacy Three.js APIs
three-gpu-pathtracer #5 Issue open since 2021 n/a Renderer, not a lightmap exporter
PlayCanvas lightmapper Active ❌ Direct + AO focused Engine-integrated, not a Three.js package

Bake Presets

Measured on the cornell.advanced scene only:

Preset Samples Bounces Resolution Measured Time (RTX 3050 Ti)
Draft 4 x 32 frames (128 spp) 2 256px 4.63s
Preview 5 x 96 frames (480 spp) 2 512px 5.05s
Production 6 x 256 frames (1536 spp) 2 1024px 36.48s
Final 8 x 512 frames (4096 spp) 2 2048px 408.7s

API Reference

LightmapBaker

const baker = new LightmapBaker(renderer, options?: BakeOptions);
// or:
const baker = new LightmapBaker({ renderer, ...options });
Option Type Default Description
samples number 64 Rays per texel. More = less noise, longer bake.
bounces number 2 Indirect light bounces. 1 = direct + one bounce. 4 = max.
resolution number 512 Lightmap texture size (square).
denoise boolean true Apply bilateral denoiser after bake.
dilatePx number 4 Edge dilation passes to prevent UV seam artifacts.
superSample number 1 Bake at resolution × superSample, then downscale.

baker.bake(scene, callbacks?)

const result = await baker.bake(scene, {
  onProgress: (phase: string, percent: number) => void,
});

Returns a BakeResult:

Property Type Description
lightmaps Map<Mesh, Texture> Per-mesh lightmap textures
groups BakeGroupView[] Per-atlas internals (direct/indirect/ao/composite/position/normal)
bvh MeshBVH Shared BVH built for the bake
stats object Mesh count, texel count, rays traced, and per-phase durations

result.apply()

Sets mesh.material.lightMap for each baked mesh. Three.js samples lightMap from UV2 automatically.

result.export(path, options?)

Exports lightmap textures as PNG (LDR) or EXR (HDR).

result.dispose()

Releases all GPU resources (textures, render targets).

result.refreshAO() and result.rebakeAO()

  • refreshAO({ intensity, exponent, enabled }) applies AO look changes instantly.
  • rebakeAO({ samples, distance, targetSamples }) recomputes AO rays without re-running the full GI bake.

Known Limitations

  • Verified against the current codebase: these are real current limits, not marketing disclaimers.
  • Browser/WebGL renderer required. True Node.js headless baking is planned but not shipped.
  • Requires WebGL 2 and EXT_color_buffer_float for HDR accumulation targets.
  • result.export() triggers browser downloads; it does not write directly to arbitrary filesystem paths.
  • Large atlases, high sample counts, and many bounces can still hit browser/GPU timeout behavior on weaker hardware.
  • Light/material coverage is focused on MeshStandardMaterial-style surfaces, emissive contribution, direct light collection, AO, and GI bounces. Advanced production lighting such as IES profiles, textured area lights, and full material parity remain roadmap items.
  • Auto UV2 unwrapping is designed to remove the Blender unwrap step, but pathological geometry can still need cleanup or manual UVs.
  • Playwright or other automated browser captures must record the actual WebGL renderer and should enforce the expected device with BAKER_EXPECT_GPU. Chromium GPU flags improve the odds of hardware acceleration, but they do not override OS/driver GPU assignment on every machine.
  • The current public package is prepared for npm release but is not published until the first release is cut.

Headless / Automation Status

  • Browser + injected renderer: implemented now.
  • Node.js true headless baking: not implemented yet.
  • Current pipeline depends on WebGL renderer/context, RAF-driven progressive passes, and browser-side texture export/download paths.
  • Planned direction: keep renderer-injected API stable, then add optional adapter layers for offscreen/headless contexts.

Requirements

  • Three.js r161 (see package.json)
  • WebGL 2 with EXT_color_buffer_float (required for HDR lightmap accumulation)
  • Browser/renderer context required - this release is WebGL-first (Node headless adapter is planned, not shipped)
  • GPU: any discrete GPU from the last 5 years. Intel/AMD integrated GPUs work but bake slower - the library auto-detects and warns.

Acknowledgments

This repo is a fork of lucas-jones/three-lightmap-baker, which established the two-pass architecture and proved it works. The bounce lighting, material system, denoiser, and API are new.

Built on top of:


Contributing

This is actively developed. Contributions welcome - especially:

  • Test scenes - complex interiors, outdoor scenes, edge cases
  • Performance benchmarks - bake times across GPU generations
  • Bug reports - screenshots + GPU info + sample count + resolution
  • Light type implementations - spot lights, IES profiles, textured area lights

Open an issue before starting a PR so we can coordinate. All contributions require signing the CLA via CLA Assistant.


Roadmap

See docs/ROADMAP.md for current milestones and priorities.


License

MIT - same as the original lucas-jones repo.


Built by a designer who got tired of exporting to Blender at 2am.

Releases

No releases published

Packages

 
 
 

Contributors