# `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.