Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit61a33237e0

README.md

lightCookies

Projects an authored image through a spot light's cone — a gobo, window blinds, a headlight cutoff, a projector pattern.

How it divides up

The renderer already projects a texture through a spot's frustum. At each shaded point it reads zero_feature_texture(uv, cookieLayer), with uv from that point's position in the light's own clip space, and multiplies the cone by what it finds. SpotLight.cookieLayer = -1 means no cookie.

What the renderer does not decide is what goes in the layer. That is this package: resample an authored image into one full layer of the shared feature-texture array, and point a spot at that layer.

Using it

local lightCookies = require("@builtin::systems/lightCookies.package/lightCookies")

-- Size the array once for every layer the scene will use.
lightCookies.configure({ resolution = 256, layers = 4 })

lightCookies.project(lampId, "/textures/cookies/window_blinds.png", { layer = 0 })

Or attach a LightCookie component next to the SpotLight and author texture / layer / gain / tint on it.

lightCookies.clear(lampId) stops the projection; the layer keeps its contents.

When a fill lands

fill and project queue the fill and return its layer at once. The module's own loop writes it over the frames that follow: the rasterize is issued on the next frame and the copy into the layer on the frame after, and the fill is written once renderer.featureTexture.state().fills reports the copy landed in its layer, one fill at a time in the order they were asked for. The loop belongs to the module, so a fill lands even when the code that asked for it (a one-shot script, a task scope, a component that is destroyed) has ended.

lightCookies.status(layer) reports where the last fill of a layer stands:

local s = lightCookies.status(0)
-- s.state:  "queued" | "rasterizing" | "copying" | "written" | "refused"
-- s.reason: why a refused fill was refused, in the engine's words
-- s.filled: whether renderer.featureTexture.state().filled lists the layer

status reads a fill "written" from the first reading of the array that reports its copy landed, so s.state and s.filled agree whichever of the module's loop and the reader runs first in a frame.

A fill names a layer below the count configure sized the array to; one past it is refused at the call, which returns nil and the reason. A fill that cannot land once it is on its way (the GPU refuses its rasterize because its image was given back first, a later configure left the array without its layer, or its copy never reached the array) reads "refused" with the reason, and is also logged as a warning.

A LightCookie component whose fill is refused at the call, such as one authored before the scene's configure has sized the array, asks again each time configure sizes it. lightCookies.observe().configurations counts those calls for code of its own that does the same. lightCookies.observe() reports how many fills are waiting, the one in progress, and the loop's own counters.

An empty layer puts the light out

The cone is multiplied by what it reads from its layer, so a spot pointed at a layer nothing has filled renders black rather than plain. project fills before it points the light, which is the order that keeps the cone lit; if you set SpotLight.cookieLayer by hand, fill that layer first.

The array is shared, and sizing it is explicit

The feature-texture array is one resource that every feature using zero_feature_texture draws layers from, and it has no allocator: sizing it zeroes every layer, including layers another feature owns. So this package never sizes it as a side effect of setting a cookie, and layer is authored rather than assigned. lightCookies.configure is the one call that resizes, made once by whoever knows what else in the scene wants layers.

A scene that also bakes global illumination shares this array with the lightmap atlas. Size it for both, and give cookies layers above the ones the bake fills.

A cookie fills a whole layer

The cone's lookup runs over the layer's entire uv range, so an image is resampled to the layer's extent rather than placed in a corner of it. One layer carries one cookie.

Alpha

The source's alpha is how much of the pattern applies. At alpha 0 the cone passes through unmodulated, so a fully transparent image is a no-op rather than a black cone.