Builtin Library
readme.md
imageBasedLighting system
The sky as a light source — metal reflects what is actually above it, and matte surfaces pick up the sky's colour in the directions they face.
A surface lit only by direct lights has nothing to show where no light points. Polished metal is the extreme case: almost all of what it shows is reflected surroundings, so without an environment it renders near black with a pinpoint highlight, which is why untextured metal reads as plastic.
Using it
Environment lighting is a component — author it once and it persists:
local id = entity.spawn("environment").id
entity(id).component.add("EnvironmentLighting", {
diffuse = 1.0, -- sky-lit diffuse on matte surfaces
specular = 1.0, -- environment reflection on glossy surfaces
})
The sky's colours and the sun come from the scene, so recolouring the sky recolours the reflections with nothing on the component moving.
For code that changes it at runtime — moving indoors, a cutscene, a quality slider — reach for the module:
local ibl = require("@builtin::systems.imageBasedLighting.ibl")
ibl.set({ diffuse = 0.4, specular = 1.0 }) -- any subset; the rest is kept
ibl.follow() -- resume tracking the scene's sky
ibl.refresh() -- carry the scene's sky and sun in
ibl.get()
ibl.active()
ibl.clear()
A set from code holds against an EnvironmentLighting component in the same
scene: the component pushes on the frames one of its own fields moves, and such
a move puts all five of its values back in force at once. The sky and sun keep
following the scene through either of them, until a set pins one of them.
What the knobs do
| field | effect |
|---|---|
diffuse | How much sky-lit diffuse reaches matte surfaces. |
specular | How much environment reflection reaches glossy surfaces. |
skyIntensity | Scales the environment's radiance before either term. |
horizonSharpness | How tightly the gradient hugs the horizon. Higher keeps zenith colour further down the sky. |
sunSize | Angular size of the sun in the environment. Larger spreads its highlight over a wider part of a reflection. |
diffuse and specular are separate because an interior usually wants the
sky's diffuse contribution pulled down while its reflections stay, and an
exterior often wants the reverse.
Passing a colour or a sun direction explicitly pins it and takes the scene off
automatic tracking for that value. follow() puts it back.
How it renders
Four compute passes after lighting: the sky evaluated over the sphere into an equirectangular map, that map convolved into a diffuse irradiance map and into GGX-prefiltered specular levels, then both applied from the G-buffer.
Prefiltering is why this is more than one pass. A rough surface gathers light from a wide cone, and gathering it per pixel per frame would mean hundreds of samples per pixel; convolving once into a small map and reading one texel per pixel is what makes it affordable.
The maps are small enough that rebuilding them each frame costs less than a full-screen pass, which is what lets the sky change continuously with nothing to invalidate.
Files
imageBasedLighting.package/
EnvironmentLighting.component/ -- the scene's environment lighting
ibl.module/ -- ibl.set / .follow / .refresh / .get / .clear / .active
environmentLighting.renderFeature/ -- the environment, convolution and apply passes