Builtin Library
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.