tecs.gfx.lighting
Deferred two-dimensional lighting, the shadows it casts, and optional bloom.
The lighting resolve reads the G-buffer the geometry pass wrote and adds one contribution per light that reaches a pixel, over an ambient term. A light is an entity: tecs.ecs.Transform2D places it and PointLight2D gives it a height, a reach, a color, and a strength.
tecs.gfx.lighting.setAmbient(0.12, 0.14, 0.2)
world:spawn(tecs.ecs.Transform2D(320, 180), tecs.gfx.PointLight2D(64, 220, 1.0, 0.85, 0.6, 1.4))
world:spawn(
tecs.ecs.Transform2D(280, 200, 0, 1, 0, 48, 48),
tecs.gfx.Tint(0.7, 0.7, 0.75, 1),
tecs.gfx.Occluder2D(1.0),
tecs.gfx.Renderable2D
)Two shadow mechanisms, and they answer different questions. Occluder2D adds the entity's silhouette to one mask every light marches against, so one entity blocks every light for the cost of drawing itself once rather than once per light. It blocks each light's own contribution and cannot touch the ambient term. DropShadow2D throws a stretched copy of the entity along the ground away from the few nearest lights and darkens everything under it, ambient included, while blocking no light at all. An entity carrying both is an occluder, because dropping that half would silently unblock a light.
A translucent entity casts nothing. It never reaches the G-buffer, so a hard silhouette of it would be a lie.
setShadows(nil) and setBloom(nil) turn the two lanes off, and the frame then runs neither their passes nor their draws. Their render targets stay declared either way: tecs.gpu.passes declares one graph for the life of the process, so a game that placed a pass of its own beside bloomExtract keeps it across a change of tuning.
The target scales are fixed by that declaration and are not tuning. The occluder mask is drawn at half the frame on both axes because a nine-tap separable blur runs over it immediately, so a full-resolution mask would spend four times the bandwidth on detail the blur discards. A game that wants other scales declares its own targets and passes rather than reconfiguring these.
Module contents
Types
| Type | Kind | Description |
|---|---|---|
Bloom | record | The bloom tuning a frame is drawn with. |
BloomSpec | type | What a caller passes to setBloom. |
DropShadow2D | struct | Casts a stretched copy of the entity along the ground, away from light. |
Occluder2D | struct | Blocks light from reaching what lies behind the entity. |
PointLight2D | struct | Represents a light the deferred resolve accumulates. |
Shadows | record | The shadow tuning a frame is drawn with. |
ShadowSpec | type | What a caller passes to setShadows. |
Functions
| Function | Kind | Description |
|---|---|---|
ambient | function | Returns the ambient light every surface receives. |
bloom | function | Returns the bloom tuning a frame is drawn with. |
reset | function | Forgets every setting and returns to the engine's own defaults. |
setAmbient | function | Sets the light every surface receives before any light entity is counted. |
setBloom | function | Enables bloom and sets its tuning, or disables it. |
setShadows | function | Enables shadows and sets their tuning, or disables them. |
shadows | function | Returns the shadow tuning a frame is drawn with. |
Values
| Value | Kind | Description |
|---|---|---|
CAST_FANOUT | variable | Cast-list entries one caster occupies, whether or not that many lights reach it. |
DropShadow2DComponent | variable | The process-wide DropShadow2D component definition. |
LIGHT_FLOATS | variable | Floats one light occupies on the wire: position and radius, then color and intensity. |
LIGHT_STRIDE | variable | Bytes one light occupies on the wire. |
MAX_LIGHTS | variable | Lights one frame resolves. |
Occluder2DComponent | variable | The process-wide Occluder2D component definition. |
PointLight2DComponent | variable | The process-wide PointLight2D component definition. |
TILE_SLOTS | variable | Lights one tile holds. |
TILES | variable | Tiles the view is divided into on each axis, so a fragment consults the lights that reach its tile rather than every... |
Types#
Bloomrecord#
The bloom tuning a frame is drawn with.
Fields
threshold#
threshold: numberRead-only. Reports the non-negative brightness a pixel passes to be extracted. The lit target is wider than eight bits, so a value above one is meaningful here.
knee#
knee: numberRead-only. Reports the positive width of the threshold's soft knee, which is what keeps a surface crossing the threshold from popping.
BloomSpectype#
type BloomSpec = {
threshold: number?,
knee: number?,
intensity: number?
}What a caller passes to setBloom. Every field is optional.
DropShadow2Dstruct#
struct DropShadow2D
height: number
endCasts a stretched copy of the entity along the ground, away from light.
This darkens everything a light left, including ambient light, which Occluder2D cannot do. It blocks no light in return: a crowd of light-blocking silhouettes merges under the mask into one flat mat of darkness, so the thing that wants a contact shadow is exactly the thing that must not be an occluder. An entity carrying both is an occluder.
The nearest few lights by weight throw the copy. Adding a distant light does not move an established shadow.
Fields
height#
height: numberCaller-writable. Sets how far lights throw the shadow, from zero to one of the world's configured shadow height.
Occluder2Dstruct#
struct Occluder2D
height: number
endBlocks light from reaching what lies behind the entity.
The renderer adds the silhouette to the occluder mask every light samples, so one entity blocks every light at the cost of one drawing of itself rather than one per light. It blocks each light's contribution, not ambient light. DropShadow2D handles ambient darkening.
Coverage is the silhouette the material already decided, so a circle, a rounded box, or a glyph casts the shape it draws with no threshold of its own to set.
Fields
height#
height: numberCaller-writable. Sets the occluder height from zero to one of the world's configured shadow height.
PointLight2Dstruct#
struct PointLight2D
height: number
radius: number
r: number
g: number
b: number
intensity: number
endRepresents a light the deferred resolve accumulates.
Fields
height#
height: numberCaller-writable. Sets the height above the surface plane in world units. At zero the Lambert term vanishes and the light contributes nothing.
radius#
radius: numberCaller-writable. Sets the light's reach in world units. Falloff is smooth and reaches exactly zero at the radius.
intensity#
intensity: numberCaller-writable. Scales the light's contribution. Values above one are meaningful, because the resolve writes a wider-than-eight-bit target.
Shadowsrecord#
record Shadows
steps: number
margin: number
height: number
dropOpacity: number
dropLength: number
endThe shadow tuning a frame is drawn with.
Fields
steps#
steps: numberRead-only. Reports the samples a shadow march takes at full attenuation, which is also the ceiling the adaptive count is clamped to.
margin#
margin: numberRead-only. Reports how far outside the view, in world units, a caster is still kept. A wall just off the left edge throws a shadow that falls on screen, and this is what stops the cull dropping it. It also widens the occluder mask's projection by the same amount, so what is kept lands inside the mask rather than off its edge.
height#
height: numberRead-only. Reports the world height a caster at height one stands, which is the one number that puts a light's world height and a caster's zero-to-one height in the same space.
dropOpacity#
dropOpacity: numberRead-only. Reports how dark a drop shadow is where the light throwing it is at full strength, from zero to one.
dropLength#
dropLength: numberRead-only. Reports the longest a drop shadow may be, in world units. The projection that places the tip divides by the light's height above the caster's, so a light barely clearing a tall caster would otherwise throw a shadow to the horizon.
ShadowSpectype#
type ShadowSpec = {
steps: number?,
margin: number?,
height: number?,
dropOpacity: number?,
dropLength: number?
}What a caller passes to setShadows. Every field is optional.
Functions#
ambientfunction#
function ambient(): number, number, numberReturns the ambient light every surface receives.
Returns
| Type | Description |
|---|---|
number | the red channel, then the green, then the blue |
number | |
number |
bloomfunction#
function bloom(): Bloom?Returns the bloom tuning a frame is drawn with.
Returns
| Type | Description |
|---|---|
Bloom? | the module's own record, which a caller reads and must not write, or nil when bloom is disabled |
resetfunction#
function reset(): nilForgets every setting and returns to the engine's own defaults.
A test uses this so one suite's tuning does not reach the next.
Returns
| Type | Description |
|---|---|
nil |
setAmbientfunction#
function setAmbient(r: number, g: number, b: number): nilSets the light every surface receives before any light entity is counted.
Authored ambient occlusion reaches this term and nothing else: a point light is a directionally known contribution and is not hidden by a baked ambient value.
Arguments
| Name | Type | Description |
|---|---|---|
r | number | the red channel, at or above zero |
g | number | the green channel, at or above zero |
b | number | the blue channel, at or above zero |
Returns
| Type | Description |
|---|---|
nil |
Raises
when a channel is negative or not finite
setBloomfunction#
function setBloom(spec: BloomSpec?): nilEnables bloom and sets its tuning, or disables it.
Arguments
| Name | Type | Description |
|---|---|---|
spec | BloomSpec? | the tuning, whose omitted fields take the defaults documented on |
Returns
| Type | Description |
|---|---|
nil |
Raises
when a field is not finite or falls outside its range
setShadowsfunction#
function setShadows(spec: ShadowSpec?): nilEnables shadows and sets their tuning, or disables them.
Passing nil turns off the occluder mask, its blur, and the drop-shadow target, and the frame then runs neither those passes nor the draws that fill them.
Arguments
| Name | Type | Description |
|---|---|---|
spec | ShadowSpec? | the tuning, whose omitted fields take the defaults documented on |
Returns
| Type | Description |
|---|---|
nil |
Raises
when a field is not finite or falls outside its range
shadowsfunction#
function shadows(): Shadows?Returns the shadow tuning a frame is drawn with.
Returns
| Type | Description |
|---|---|
Shadows? | the module's own record, which a caller reads and must not write, or nil when shadows are disabled |
Values#
CAST_FANOUTvariable#
const CAST_FANOUT: integerCast-list entries one caster occupies, whether or not that many lights reach it.
One per light a drop shadow is thrown by: four lights is more than any one character stands in, and the fourth is already fainter than a viewer picks out. It is uniform across casters, including the occluders that throw no copy at all, which is what makes an entry's rank its position modulo this and saves carrying one.
DropShadow2DComponentvariable#
const DropShadow2DComponent: components.FFIComponent<DropShadow2D>The process-wide DropShadow2D component definition.
LIGHT_FLOATSvariable#
const LIGHT_FLOATS: integerFloats one light occupies on the wire: position and radius, then color and intensity.
LIGHT_STRIDEvariable#
const LIGHT_STRIDE: integerBytes one light occupies on the wire.
MAX_LIGHTSvariable#
const MAX_LIGHTS: integerLights one frame resolves. Past this the remainder is dropped rather than reported, because the alternative is a frame that raises over a light nobody would have picked out of the image.
Occluder2DComponentvariable#
const Occluder2DComponent: components.FFIComponent<Occluder2D>The process-wide Occluder2D component definition.
PointLight2DComponentvariable#
const PointLight2DComponent: components.FFIComponent<PointLight2D>The process-wide PointLight2D component definition.
TILE_SLOTSvariable#
const TILE_SLOTS: integerLights one tile holds. A tile reached by more than this keeps the ones earliest in the light buffer, which is a stated choice rather than whichever the device happened to write first.
TILESvariable#
const TILES: integerTiles the view is divided into on each axis, so a fragment consults the lights that reach its tile rather than every light in the scene.
A count rather than a size in pixels, so the two tile buffers are allocated once and never replaced while a frame in flight is reading them. The same number appears in assets/shaders/wgsl/lighting.wgsl and the pair only works while they agree.