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

TypeKindDescription
BloomrecordThe bloom tuning a frame is drawn with.
BloomSpectypeWhat a caller passes to setBloom.
DropShadow2DstructCasts a stretched copy of the entity along the ground, away from light.
Occluder2DstructBlocks light from reaching what lies behind the entity.
PointLight2DstructRepresents a light the deferred resolve accumulates.
ShadowsrecordThe shadow tuning a frame is drawn with.
ShadowSpectypeWhat a caller passes to setShadows.

Functions

FunctionKindDescription
ambientfunctionReturns the ambient light every surface receives.
bloomfunctionReturns the bloom tuning a frame is drawn with.
resetfunctionForgets every setting and returns to the engine's own defaults.
setAmbientfunctionSets the light every surface receives before any light entity is counted.
setBloomfunctionEnables bloom and sets its tuning, or disables it.
setShadowsfunctionEnables shadows and sets their tuning, or disables them.
shadowsfunctionReturns the shadow tuning a frame is drawn with.

Values

ValueKindDescription
CAST_FANOUTvariableCast-list entries one caster occupies, whether or not that many lights reach it.
DropShadow2DComponentvariableThe process-wide DropShadow2D component definition.
LIGHT_FLOATSvariableFloats one light occupies on the wire: position and radius, then color and intensity.
LIGHT_STRIDEvariableBytes one light occupies on the wire.
MAX_LIGHTSvariableLights one frame resolves.
Occluder2DComponentvariableThe process-wide Occluder2D component definition.
PointLight2DComponentvariableThe process-wide PointLight2D component definition.
TILE_SLOTSvariableLights one tile holds.
TILESvariableTiles the view is divided into on each axis, so a fragment consults the lights that reach its tile rather than every...

Types#

Bloomrecord#

record Bloom
    threshold: number
    knee: number
    intensity: number
end

The bloom tuning a frame is drawn with.

Fields

threshold#
threshold: number

Read-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: number

Read-only. Reports the positive width of the threshold's soft knee, which is what keeps a surface crossing the threshold from popping.

intensity#
intensity: number

Read-only. Scales the blurred contribution the composite adds.

BloomSpectype#

type BloomSpec = {
    threshold: number?,
    knee: number?,
    intensity: number?
}

What a caller passes to setBloom. Every field is optional.

DropShadow2Dstruct#

struct DropShadow2D
    height: number
end
@derive(nupp.derive.Debug, nupp.derive.Serde)

Casts 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: number

Caller-writable. Sets how far lights throw the shadow, from zero to one of the world's configured shadow height.

Occluder2Dstruct#

struct Occluder2D
    height: number
end
@derive(nupp.derive.Debug, nupp.derive.Serde)

Blocks 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: number

Caller-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
end
@derive(nupp.derive.Debug, nupp.derive.Serde)

Represents a light the deferred resolve accumulates.

Fields

height#
height: number

Caller-writable. Sets the height above the surface plane in world units. At zero the Lambert term vanishes and the light contributes nothing.

radius#
radius: number

Caller-writable. Sets the light's reach in world units. Falloff is smooth and reaches exactly zero at the radius.

r#
r: number

Caller-writable. Sets the red channel from zero to one.

g#
g: number

Caller-writable. Sets the green channel from zero to one.

b#
b: number

Caller-writable. Sets the blue channel from zero to one.

intensity#
intensity: number

Caller-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
end

The shadow tuning a frame is drawn with.

Fields

steps#
steps: number

Read-only. Reports the samples a shadow march takes at full attenuation, which is also the ceiling the adaptive count is clamped to.

margin#
margin: number

Read-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: number

Read-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: number

Read-only. Reports how dark a drop shadow is where the light throwing it is at full strength, from zero to one.

dropLength#
dropLength: number

Read-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, number

Returns the ambient light every surface receives.

Returns

TypeDescription
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

TypeDescription
Bloom?

the module's own record, which a caller reads and must not write, or nil when bloom is disabled

resetfunction#

function reset(): nil

Forgets 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

TypeDescription
nil

setAmbientfunction#

function setAmbient(r: number, g: number, b: number): nil

Sets 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

NameTypeDescription
rnumber

the red channel, at or above zero

gnumber

the green channel, at or above zero

bnumber

the blue channel, at or above zero

Returns

TypeDescription
nil

Raises

  • when a channel is negative or not finite

setBloomfunction#

function setBloom(spec: BloomSpec?): nil

Enables bloom and sets its tuning, or disables it.

Arguments

NameTypeDescription
specBloomSpec?

the tuning, whose omitted fields take the defaults documented on Bloom, or nil to disable bloom

Returns

TypeDescription
nil

Raises

  • when a field is not finite or falls outside its range

setShadowsfunction#

function setShadows(spec: ShadowSpec?): nil

Enables 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

NameTypeDescription
specShadowSpec?

the tuning, whose omitted fields take the defaults documented on Shadows, or nil to disable shadows

Returns

TypeDescription
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

TypeDescription
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: integer

Cast-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: integer

Floats one light occupies on the wire: position and radius, then color and intensity.

LIGHT_STRIDEvariable#

const LIGHT_STRIDE: integer

Bytes one light occupies on the wire.

MAX_LIGHTSvariable#

const MAX_LIGHTS: integer

Lights 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: integer

Lights 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: integer

Tiles 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.