Builtin Library
README.md
volumeProbe
Bake a 3D grid of SH L2 irradiance probes and publish it into the renderer's irradiance-volume set: every standard-PBR fragment inside the volume's bounds (movers, avatars, freshly spawned props) takes its ambient term from the field, sampled in fragment via trilinear interpolation across the 8 nearest grid points — no per-receiver setup.
The bake path-traces every probe sample through the scene BVH for up to
bounces surface interactions, so probes capture indirect light, color
bleed, emissive surfaces, and sky occlusion. Probe storage is a flat
array of SH coefficients — 9 vec4 per probe (RGB packed in xyz,
padding in w). Re-bake when geometry or lights change.
Every bake persists: the SH brick lands in the scene's baked-lighting
container (lightmapData asset) keyed by the volume's fieldId, and the
volume's lightmapData ref points at it. A fresh boot restores and
republishes the field from the asset (restore, called by the
VolumeProbe component's awake) with zero re-baking.
Exports
VolumeProbe.bake(volumeEntityId: string, opts: BakeOpts?) -> BakeReport— bake a single probe volume, publish its field, and persist it. Spans many frames by design; raises with the cause when the field cannot be read back (okis only ever true).VolumeProbe.bakeAll(opts: BakeOpts?) -> { BakeAllEntry }— bake every entity with aVolumeProbecomponent, sharing scene-geometry + BVH caches, one container and one GPU scheduler across volumes.VolumeProbe.autoPlace(opts?) -> AutoPlaceResult— place volumes over a scope and bake them. A placed volume whose bake raised fails the call, which raises naming the volumes that failed and the first error; no light is marked baked and a held automatic re-bake stays held.VolumeProbe.checkOptions(opts)— raise naming the first bake option the bake cannot honour.VolumeProbe.restore(volumeEntityId: string) -> (boolean, string?)— restore + republish a volume's persisted field (the boot path; no re-bake).VolumeProbe.retire(volumeEntityId: string)— full teardown: retract the published field, destroy the SH buffer, remove the container entry, clear the ref.VolumeProbe.fieldLive(volumeEntityId: string) -> boolean— whether a field is already published under the volume's key this session.VolumeProbe.publishKey(volumeEntityId: string) -> string— the stable key the field publishes and persists under (fieldId, falling back to the entity id).VolumeProbe.findCovering(position: Vec3) -> (string?, boolean)— the volume covering a world position, plus whether the point is contained (false = nearest fallback).VolumeProbe.readIrradiance(volumeEntityId: string) -> { any }— read the baked field back ({ pos, color }per probe).VolumeProbe.bufferName(volumeEntityId: string) -> string— stable per-volume buffer name.
Types:
Vec3 = { x: number, y: number, z: number }ColorRGB = { r: number, g: number, b: number }BakeOpts = { samples?, bounces?, intensity?, rayMax?, sky?, baseColor?, rayBudget?, maxFramesInFlight?, bvhCache?, containerCache? }BakeReport = { ok, probeCount, resolution, bounds, ms, bufferName, invalidProbes, gpu }BakeAllEntry = { id: string, report: BakeReport }
Usage
local VolumeProbe = require("@builtin::systems.globalIllumination.volumeProbe")
local report = VolumeProbe.bake(volumeEntityId, { samples = 64 })
print(report.probeCount, "probes baked in", report.ms, "ms")
Notes
- Volume size + grid resolution come from the
VolumeProbecomponent on the volume entity (sizeX,sizeY,sizeZ,resX,resY,resZ).bakereads them each call so component edits propagate to the next bake without restart. rayMaxbounds every path segment; volumes that span large open scenes may want a larger value than the 100m default.bakeAllbuilds one BVH and shares it across every volume — adding or moving lights between invocations is fine, but adding new meshes requires a freshbakeAllfor the BVH cache to refresh.- The GPU work is split by
Baking.planChunksinto passes of samples over chunks of probes, each insiderayBudgetestimated rays (Baking.RAY_BUDGET_PER_FRAME), and issued a frame's worth at a time through aBaking.newGpuScheduler.probe_bakekeeps each probe's running sums in its record between passes (stage 1) and a last one-workgroup dispatch (stage 2) writes the completion flag, so a read that sees the flag sees every probe finished.samplesandbouncesare whole numbers (from 0 and from 1); anything else raises naming the option.