Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commitdd348ac6c3

README.md

lightmap

Bakes full global illumination (shadowed direct light plus multi-bounce indirect gathered by path tracing through the scene BVH) into per-entity lightmaps: the bake output is read back, dilated across UV seams, and copied into the receiver's region of ONE shared atlas that the PBR module samples through the receiver's per-instance lanes — the receiver keeps the material it was authored with. Each texel traces samples cosine-weighted chains of up to bounces surface interactions, so color bleed, emissive surfaces, and sky occlusion all land in the map.

Receiver selection is the Model.mobility axis: only static-mobility world entities bake. Movable geometry samples the published probe fields instead. Set Model.mobility = "static" to override a wrong derivation.

Every bake persists: the dilated texels land in the scene's baked-lighting container (lightmapData asset) keyed by the entity, and the receiver's Model.lightmapData ref points at it. A fresh boot restores the lightmap from the asset (applyFromAsset, called by the Model component's awake) with zero re-baking.

Exports

  • Lightmap.bake(entityId: string, opts: BakeOpts?) -> BakeReport — bake one static entity's lightmap into the shared atlas and point the receiver at it. Yields while the GPU bake + read-back complete, over many frames by design; raises with the cause when the bake cannot finish.
  • Lightmap.bakeAll(opts: BakeOpts?) -> { BakeAllEntry } — bake every static receiver in the world, reusing a single light + BVH cache across them. The result carries gpu (what the scheduler issued for the pass) and peakHeldBytes.
  • Lightmap.progress() -> table — how far the running bake has got, with gpu (frames, rays per frame, dispatches per frame) while it runs.
  • Lightmap.checkOptions(opts, limits) — raise naming the first bake option the bake cannot honour.
  • Lightmap.deriveResolution(worldArea, texelsPerMeter, cap?) -> (resolution, capped, effective) — the resolution a receiver gets, cut at cap (the largest the device bakes) or the atlas page.
  • Lightmap.applyFromAsset(entityId: string) -> (boolean, string?) — restore a receiver's baked lightmap from its persisted container entry (the boot path; no re-bake).
  • Lightmap.clear(entityId: string) — destroy the bake buffer, the runtime lightmap texture, and the per-entity material, restore the entity's original material, and remove the container entry.
  • Lightmap.bufferName(entityId: string) -> string — the name the receiver's bake output is made under ("gi_lightmap_<sanitized id>"). The buffer lives until the receiver's texels are persisted.

Types:

  • ColorRGB = { r: number, g: number, b: number }
  • BakeOpts = { resolution?, texelsPerMeter?, samples?, bounces?, intensity?, rayMax?, shadowSoftness?, sky?, rayBudget?, maxFramesInFlight?, memoryBudget?, ... }
  • BakeReport = { ok, resolution, texelCount, validTexels, ms, textureGuid, atlasLayer, atlasX, atlasY, gpu }
  • BakeAllEntry = { id: string, report: BakeReport }

How the GPU work is bounded

A bake traces rays through a software BVH, and a dispatch that runs past the platform's watchdog loses the render device, so no dispatch is allowed to be that big. Baking.planChunks splits a receiver into a direct stage (stage 0, chunks of texels) and indirect passes (stage 1, chunks of texels times a range of the samples chains), each sized from the cost model (Baking.lightmapRays) to carry at most rayBudget estimated rays (Baking.RAY_BUDGET_PER_FRAME, 2^20). Each dispatch reads its stage and range from the params uniform of lightmap_bake; a texel's result does not depend on how the work was split.

A Baking.newGpuScheduler issues the dispatches, one per pass (a bakeAll, or a lone bake), so the budget is per frame and not per receiver. It waits for the three kernels to compile first. A frame that would pass the budget ends with a fence (a read of a one-vec4 buffer the batch records after the frame's dispatches), waits until at most maxFramesInFlight fenced frames are queued, and lets a frame draw. It checks the render device on every wait: a lost or replaced device raises [Bake] the render device was lost during the bake, with what the device reported, and posts the bake.device_lost notice once.

A receiver lights up only after its texels are read back with the resolve's completion flag set; its bake buffers (144 bytes a texel) are freed once the texels are persisted. A bakeAll starts a receiver only when what it holds and what the receiver needs fit memoryBudget (half the device's memory budget for GPU content, between 64 MiB and 1 GiB, from Baking.bakeLimits); otherwise the oldest receiver finishes first.

Options the bake checks

samples is a whole number from 0, bounces a whole number from 1, a pinned resolution a power of two from 32 up to what the device bakes (Baking.bakeLimits().maxLightmapResolution: the bake output, 64 bytes a texel, must fit one storage binding, which a baseline WebGPU device limits to 128 MiB, and a receiver's whole bake must fit the memory budget), and rayBudget, memoryBudget numbers from 1. Anything else raises naming the option and the range it allows. A derived resolution above the device's limit is cut, with a warning that names the limit.

Usage

local Lightmap = require("@builtin::systems.globalIllumination.lightmap")

local report = Lightmap.bake(entityId, { resolution = 64, samples = 16, bounces = 3 })
print(report.ms, "ms")

-- Bake every static receiver in the world:
local reports = Lightmap.bakeAll({ samples = 32 })

-- Tear down:
Lightmap.clear(entityId)

Notes

  • bake refuses non-static receivers with an error naming the mobility rule — the published probe fields are the movable-geometry counterpart.
  • bakeAll builds a single BVH and gathers lights once, threading the caches through every per-entity bake. Add/move lights between invocations to invalidate.
  • A pass that raises releases its receivers' buffers and the shared BVH and clears the atlas lanes of the receivers it had applied; nothing from a lost device is reported as applied.
  • The lightmap texture is a runtime GPU resource under a stable per-entity guid (BakeReport.textureGuid); re-bakes update it in place, so the material keeps rendering the latest bake with no re-binding.
  • Texel coverage rides in the texture's alpha; the upload dilates covered texels over uncovered neighbours so bilinear filtering never bleeds black across UV island seams.
  • clear is a no-op when the entity is missing or never baked.