# `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.
```nupp
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.
## Types
### `Bloom` _record_
```nupp
record Bloom
threshold: number
knee: number
intensity: number
end
```
The bloom tuning a frame is drawn with.
#### Fields
##### `threshold`
```nupp
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`
```nupp
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`
```nupp
intensity: number
```
Read-only. Scales the blurred contribution the composite adds.
### `BloomSpec` _type_
```nupp
type BloomSpec = {
threshold: number?,
knee: number?,
intensity: number?
}
```
What a caller passes to `setBloom`. Every field is optional.
### `DropShadow2D` _struct_
```nupp
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`
```nupp
height: number
```
Caller-writable. Sets how far lights throw the shadow, from zero to one
of the world's configured shadow height.
### `Occluder2D` _struct_
```nupp
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`
```nupp
height: number
```
Caller-writable. Sets the occluder height from zero to one of the
world's configured shadow height.
### `PointLight2D` _struct_
```nupp
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`
```nupp
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`
```nupp
radius: number
```
Caller-writable. Sets the light's reach in world units. Falloff is
smooth and reaches exactly zero at the radius.
##### `r`
```nupp
r: number
```
Caller-writable. Sets the red channel from zero to one.
##### `g`
```nupp
g: number
```
Caller-writable. Sets the green channel from zero to one.
##### `b`
```nupp
b: number
```
Caller-writable. Sets the blue channel from zero to one.
##### `intensity`
```nupp
intensity: number
```
Caller-writable. Scales the light's contribution. Values above one are
meaningful, because the resolve writes a wider-than-eight-bit target.
### `Shadows` _record_
```nupp
record Shadows
steps: number
margin: number
height: number
dropOpacity: number
dropLength: number
end
```
The shadow tuning a frame is drawn with.
#### Fields
##### `steps`
```nupp
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`
```nupp
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`
```nupp
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`
```nupp
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`
```nupp
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.
### `ShadowSpec` _type_
```nupp
type ShadowSpec = {
steps: number?,
margin: number?,
height: number?,
dropOpacity: number?,
dropLength: number?
}
```
What a caller passes to `setShadows`. Every field is optional.
## Functions
### `ambient` _function_
```nupp
function ambient(): number, number, number
```
Returns the ambient light every surface receives.
#### Returns
| Type | Description |
| --- | --- |
| `number` | the red channel, then the green, then the blue |
| `number` | |
| `number` | |
### `bloom` _function_
```nupp
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 |
### `reset` _function_
```nupp
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
| Type | Description |
| --- | --- |
| `nil` | |
### `setAmbient` _function_
```nupp
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
| 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
### `setBloom` _function_
```nupp
function setBloom(spec: BloomSpec?): nil
```
Enables bloom and sets its tuning, or disables it.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `spec` | `BloomSpec?` | the tuning, whose omitted fields take the defaults documented on `Bloom`, or nil to disable bloom |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Raises
- when a field is not finite or falls outside its range
### `setShadows` _function_
```nupp
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
| Name | Type | Description |
| --- | --- | --- |
| `spec` | `ShadowSpec?` | the tuning, whose omitted fields take the defaults documented on `Shadows`, or nil to disable shadows |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Raises
- when a field is not finite or falls outside its range
### `shadows` _function_
```nupp
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_FANOUT` _variable_
```nupp
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.
### `DropShadow2DComponent` _variable_
```nupp
const DropShadow2DComponent: components.FFIComponent
```
The process-wide `DropShadow2D` component definition.
### `LIGHT_FLOATS` _variable_
```nupp
const LIGHT_FLOATS: integer
```
Floats one light occupies on the wire: position and radius, then color and
intensity.
### `LIGHT_STRIDE` _variable_
```nupp
const LIGHT_STRIDE: integer
```
Bytes one light occupies on the wire.
### `MAX_LIGHTS` _variable_
```nupp
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.
### `Occluder2DComponent` _variable_
```nupp
const Occluder2DComponent: components.FFIComponent
```
The process-wide `Occluder2D` component definition.
### `PointLight2DComponent` _variable_
```nupp
const PointLight2DComponent: components.FFIComponent
```
The process-wide `PointLight2D` component definition.
### `TILE_SLOTS` _variable_
```nupp
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.
### `TILES` _variable_
```nupp
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.