Skip to content

Ibrahim-3d/three-lightmap-baker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

213 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" pnpm 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 the browser today, with a planned headless/Node path, 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.
  • 3D Camera Objects - add cameras from the Asset Library, select them in the viewport, and snap your view to match any scene camera's perspective.
  • 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
corepack enable
pnpm install

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

This repo uses pnpm via Corepack. The checked-in pnpm-lock.yaml is the authoritative dependency lockfile.

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:
pnpm add three-lightmap-baker

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

Before publishing, run the full package gate:

pnpm run release:check

That command runs typecheck, lint, format check, demo build, package build, example typecheck, demo bundle budget, tarball import smoke, and npm publish --dry-run --access public. The final publish still requires an authenticated npm session. For the first real registry publish, use the manual npm Publish GitHub Actions workflow after configuring npm trusted publishing or the NPM_TOKEN repository secret; the workflow verifies the requested version, reruns pnpm run release:check, and publishes with npm provenance.

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);

Advanced automation can pass a renderer adapter when renderer/context setup is owned outside the baker:

import { LightmapBaker, createRendererAdapter } from 'three-lightmap-baker';

const adapter = createRendererAdapter(renderer, { label: 'playwright-chrome' });
const baker = new LightmapBaker({ rendererAdapter: adapter, samples: 64 });

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.

Offscreen Browser / Automation

See examples/offscreen-browser.ts for a compiled automation harness that creates a renderer for OffscreenCanvas when available, falls back to a detached browser canvas, and passes the baker a LightmapRendererAdapter. The same non-UI path is covered by tests/e2e/adapter-runtime.spec.ts.

pnpm run test:adapter-runtime

Runtime Capability Probe

See examples/node-headless-status.ts for a Node-safe capability check. It does not bake in Node yet; it reports the current runtime, whether the current environment can bake, and the limitations blocking true Node headless baking.

import { getLightmapRuntimeCapabilities } from 'three-lightmap-baker';

const capabilities = getLightmapRuntimeCapabilities();
console.log(capabilities.runtime, capabilities.canBake);

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 current launch proof uses the committed cornell.advanced before/after captures above plus the measured benchmark table below. A dedicated interior/architectural scene is still useful as a stronger showcase, but it is postponed until the custom room is designed. It is not the source of the current README screenshots, benchmark numbers, or CI visual gate.

Target flow for that future showcase: start with a flat Three.js room/configurator scene, click Bake, show progressive convergence, show the generated atlas, and apply the baked result back onto the same scene.


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:

pnpm 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.

After capture, validate the measured launch benchmark against runtime budgets:

pnpm run budget:runtime

Defaults are conservative for the RTX 3050 Ti launch baseline. Tighten a preset on a known target machine with BAKER_RUNTIME_BUDGET_DRAFT_MS, BAKER_RUNTIME_BUDGET_PREVIEW_MS, BAKER_RUNTIME_BUDGET_PRODUCTION_MS, or BAKER_RUNTIME_BUDGET_FINAL_MS.

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

BAKER_EXPECT_GPU="RTX 3050" pnpm run capture:launch

On Windows PowerShell:

$env:BAKER_EXPECT_GPU="RTX 3050"; pnpm 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"; pnpm 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

Demo Project Files

The playground File menu can save and load a versioned Project JSON file. The current v1 format round-trips the active built-in scene preset or imported GLB/glTF payload, bake/editor options, and asset-library additions with transforms and basic material values. It also persists baked final lightmaps as raw Float32 RGBA atlas payloads, so a saved project can restore baked lighting without forcing an immediate re-bake. Export a finished scene as GLB when you need to carry baked geometry/materials out of the demo.


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.
  • Renderer adapter boundary: implemented for browser/offscreen-browser test harness ownership of renderer setup and context-loss wiring.
  • Offscreen-browser example: implemented in examples/offscreen-browser.ts and covered by pnpm run typecheck:examples.
  • Runtime capability probe: implemented as getLightmapRuntimeCapabilities() with a Node-safe example in examples/node-headless-status.ts. Node currently reports canBake: false.
  • Browser smoke suite: implemented as pnpm run test:browser-smoke and wired into CI. It covers adapter runtime, Cornell visual bake, bake cancellation, Project JSON save/load, outliner selection, editor history, asset-library, and topbar/menu controls. Targeted scripts such as pnpm run test:adapter-runtime, pnpm run test:visual-cornell, and pnpm run test:bake-cancel remain available for focused runs.
  • PR preview artifact: pull requests build the demo and upload a static dist/ artifact for review without changing the production Pages deploy.
  • npm publish workflow: implemented in .github/workflows/npm-publish.yml as a manual workflow with version confirmation, dry-run mode, and npm provenance support.
  • Custom-room visual regression: postponed until the custom room/showcase scene exists.
  • 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 and prototype a Node-compatible runtime path.

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, and edge cases beyond the current Cornell advanced launch proof
  • Performance benchmarks - bake times across additional GPU generations
  • Bug reports - screenshots + GPU info + sample count + resolution
  • Lighting coverage - IES profiles, textured area lights, light probes, and material edge cases

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.

About

Browser-based Three.js lightmap baker with path-traced global illumination, auto UV2 unwrapping, BVH ray tracing, AO, denoising, and progressive preview.

Topics

Resources

License

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors