Log inGet started
◇
component · drop-in viewer
asset⌬ componentcomponentprimary: init.luau·originates fromworld 07158574-5…

Light

Adds a light source to an entity. The entity's world transform determines the light's position (point) or direction (directional). Point light positions are auto-synced from that world transform, so a light on a child entity burns where the entity stands — the position `entity.po…

byzero-proxy @ DESKTOP-DB3UJOJ·posted 2mo ago
What it does

Light

Adds a light source to an entity. The entity's world transform determines the light's position (point) or direction (directional). Point light positions are auto-synced from that world transform, so a light on a child entity burns where the entity stands — the position entity.position reports. A point light's intensity is on one scale with the SpotLight component's intensity/brightness: the same number at the same radius/range puts the same light on a surface either kind faces from the same place, and a spot spends it on the cone it opens on rather than all around itself.

Kinds: "point", "spot", "directional" (the scene's sun), "ambient", "distant" (a parallel light beside the sun).

Public fields: kind, colorR/G/B, intensity, radius, directionX/Y/Z, castsShadows, lightChannels, mobility. range, color and direction are aliases accepting the composite/renamed forms.

Methods: :setColor(color) (color: {r, g, b} array or {r=, g=, b=} map), :setIntensity(i), :setRadius(r), :setDirection(dir), :setKind(lightKind).

kind = "spot" opens a cone along the entity's forward axis, at the engine's default 30° outer and 20° inner half-angles; the SpotLight component is the one that carries the cone angles and the face axis as fields. "directional" and "distant" are parallel lights: they arrive from the same direction at every point in the world and no distance attenuates them, so their intensity reads against the sun's rather than against a point light's. The SpotLight README carries the rest of how to balance a mixed point-and-spot rig.

entity(id).component.add("Light", { kind = "point", intensity = 2, radius = 10 })
entity(id).component.add("Light", { kind = "directional", direction = {-0.5, -1, -0.3} })

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

Light Component Adds a light source to an entity. The entity's world transform determines the light's position (point) or direction (directional). A point light's `intensity` is on one scale with the SpotLight component's `intensity`/`brightness`: the same number at the same `radius`/`range` puts the same light on a surface either kind faces from the same place, and a spot spends it on the cone it opens on rather than all around itself. Point light positions are auto-synced from that world transform by the engine — no manual position updates needed. A light on a child entity burns where the entity stands, at the position `entity.position` reports. Kinds: "point" — Emits light in all directions from entity position. "spot" — A cone along the entity's forward axis, at the engine's default 30° outer and 20° inner half-angles. The SpotLight component carries the cone angles and the `face` axis as fields. "directional" — Sets the scene directional light direction + color. "ambient" — Sets the scene ambient light color + intensity. "distant" — A parallel light beside the sun, one row of the scene's light buffer. It arrives from the same direction at every point in the world and no distance attenuates it, so its `intensity` reads against the sun's rather than against a point light's. Lights cast shadows by default (`castsShadows`); set it `false` to keep a light purely additive. Directional lights drive the scene's cascaded shadow map. Point lights cast omnidirectional (cube) shadows on the surrounding geometry; the capacity is capped, so past the cap a light stays lit but unshadowed. Config is the native Light ECS component, driven through the typed ecs.Light API. Color is stored as the `colorR`/`colorG`/`colorB` channels; `radius`, `kind`, and `directionX/Y/Z` hold the rest. The `color`, `range`, and `direction` aliases accept the natural composite/renamed forms at `component.add` time and route to those fields. Usage: entity.find("lamp").component.add("Light", { kind = "point", color = {1, 0.8, 0.5}, intensity = 2, range = 10 }) entity.find("sun").component.add("Light", { kind = "directional", direction = {-0.5, -1, -0.3} }) entity.find("scene").component.add("Light", { kind = "ambient", intensity = 0.3 })

isBakedState( ) → void

capitalize(s: ?) → void

argtypedescription
s?

contributionWithheld( ) → void

A baked static light contributes nothing live: the baked artifacts already carry all of it, so realtime light would double it and its shadow map would re-render every frame for nothing. A mixed light keeps its direct light and shadows — only its bounce is baked.

lightValue( ) → void

Build the typed ecs.Light value from the public fields. lightType takes the capitalized LightType variant ("Point" / "Directional" / "Ambient" / "Distant"). A withheld light keeps its row and zeroes what the row carries. The row is how the scene states that it has a light of this kind at all, and other systems read that: with the row gone, a procedural sky takes the absent sun as its cue to drive the scene's directional light itself.

componentIsLive( ) → boolean

Bring the live row to match the component. Idempotent, so re-entrant property notifications can't double-insert. Whether this component is running: its own switch is on, and the entity carrying it is active in the hierarchy. A component that is not running holds no row — `onDisable` and the active cascade each take it out — so a write that lands while it is off changes the authored value and nothing else, and `onEnable` builds the row back from whatever the fields hold by then. Read guarded: an entity mid-teardown answers nothing.

refreshRow( ) → void

applyAuthored( ) → void

Apply an authored change, un-baking first. Public setters route through this so editing a baked light restores its live contribution.

publishSunHolder( ) → void

Tell the lighting module whether THIS light is the scene's sun. A light is a component, so the component is what knows: nothing else should have to scan the scene to find the directional one, and the readers that ask every frame (atmosphere, fog, contact shadows, IBL, volumetrics) get a lookup instead.

retractSunHolder( ) → void

Give up the claim, but only if it is still ours: another directional light may have registered since, and clearing it then would blank a sun this component never held.

awake( ) → void

onEnable( ) → void

onDisable( ) → void

onPropertyChanged(key: ?, value: ?, oldValue: ?) → void

argtypedescription
key?
value?
oldValue?

onDestroy( ) → void

resolveMobility( ) → string

Resolve this light's mobility to `"static"`, `"mixed"` or `"dynamic"` — how GI baking treats it. An explicit `mobility` field wins; `"auto"` resolves to `"dynamic"` for a light something carries (non-world participation, an animated or physics-driven entity) and `"mixed"` for the rest. `"static"` bakes the light whole — direct light and bounce — and withholds its live contribution, so it costs nothing per frame and lights nothing that was not there at bake time. `"mixed"` bakes only its bounce and keeps its direct light and shadows live, so it still lights and shadows anything that moves. This is what `"auto"` picks, because a light that stands still still shines on characters walking under it. `"dynamic"` keeps the light out of the bake entirely.

examples

local mob = light:resolveMobility()

lightMobility( ) → string

How GI baking should treat this light, resolved to `"static"`, `"mixed"` or `"dynamic"`. Every component that puts a light in the scene answers this, which is how a bake finds the lights it has to account for without knowing what component authored them.

examples

if light:lightMobility() == "dynamic" then ... end

setColor(color: table) → void

Set the light color. Values >1 are auto-scaled from 0..255.

argtypedescription
colortable`{r, g, b}` array or `{r=, g=, b=}` map.

examples

light:setColor({1, 0.8, 0.5})
light:setColor({r = 255, g = 200, b = 128})

setIntensity(i: number) → void

Set the light intensity (0..N).

argtypedescription
inumberIntensity scalar.

examples

light:setIntensity(2.5)

setRadius(r: number) → void

Set the radius (point lights only).

argtypedescription
rnumberRadius in world units.

examples

light:setRadius(15)

setDirection(dirOrX: table | number, y: number?, z: number?) → void

Set the direction vector (directional lights only). Accepts three numbers or a single `{x, y, z}` / `{x=, y=, z=}` vector.

argtypedescription
dirOrXtable | numberEither the x component, or a `{x, y, z}` array / `{x=, y=, z=}` map.
ynumber?The y component when the first argument is a number.
znumber?The z component when the first argument is a number.

examples

light:setDirection(-0.5, -1, -0.3)
light:setDirection({-0.5, -1, -0.3})

setKind(lightKind: string) → void

Switch the light kind ("point" / "directional" / "ambient" / "distant"). `"directional"` aims the scene's sun, which is a single field: setting it replaces whatever the sun was. `"distant"` is parallel light held as a row of the scene's light buffer, so several coexist — `DirectionalLight` is the component that authors one.

argtypedescription
lightKindstringOne of `"point"`, `"directional"`, `"ambient"`, `"distant"`.

examples

light:setKind("directional")

setCastsShadows(b: boolean) → void

Enable or disable shadow casting. Point lights cast omnidirectional (cube) shadows; spot lights cast a single projected shadow. Capacity is capped per kind — past the cap the light stays lit but unshadowed.

argtypedescription
bboolean`true` to cast shadows, `false` to disable.

examples

light:setCastsShadows(true)

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

20items
·
other · born here
▤file
▲ 0↑ born
▣
module · born here
❒asset
# lighting Scene-lighting base capability: ambient + directional ("sun") light and procedural-sky setup with modify-or-spawn, read-merge-write semantics, plus clear color, and the per-frame reads of the resolved lights. Identity is the component, never the entity name: the sun is the `Light` the renderer's snapshot names as the directional holder, the ambient is the light-role component of kind "ambient", and the sky is the entity carrying a sky-role component. Name-based lookup, word resolution, and sky-material orchestration (presets, material swap) live in the `lighting` toolbox. `sun` and `ambient` configuration patch the scene's resolved sun / ambient light components, spawning a carrying entity when the scene has none. A partial update (e.g. only `intensity`) reads the component's current fields first and merges the given fields on top, so unspecified fields keep their authored values instead of resetting to a default. `sky` writes the sky entity's ProceduralSky component the same way — any of its fields, plus `enabled = false` to remove the sky component. ## Types - `SunOpts` — `{ direction?, color?, intensity?, castsShadows? }`. - `AmbientOpts` — `{ color?, intensity? }`. - `ProceduralSkyOpts` — ProceduralSky component fields, plus `enabled = false` to remove the sky. - `SetupOpts` — `{ sun?, ambient?, sky?, clearColor? }`. - `SetupResult` — `{ sun?, ambient?, sky? }`, the entity ids each provided section resolved to. - `Sun` — `{ direction, color, intensity, castsShadows, entityId? }`. - `Ambient` — `{ color, intensity, entityId? }`. - `LightRow` — one punctual light as the renderer resolved it. ## Exports Writes — each takes the opts above and answers with the entity the light resolved to: - `M.setSun(opts: SunOpts) -> string` — set the scene's sun, returning its entity id. - `M.setAmbient(opts: AmbientOpts) -> string` — set the scene's ambient term, returning its entity id. - `M.applySetup(opts: SetupOpts) -> SetupResult` — apply a lighting setup in one call. All fields optional — only provided fields change. `clearColor` is `{r, g, b}`. - `M.setProbeVolume(key: string, volume)` / `M.removeProbeVolume(key: string)` — publish or retract a baked irradiance probe volume. Reads — each takes no arguments, and answers from the resolved lighting state at a cost independent of scene size: - `M.sun() -> Sun` — the sun the frame is shaded by. - `M.ambient() -> Ambient` — the ambient term the frame is shaded by. - `M.lightRows() -> { LightRow }` — every punctual light the renderer resolved this frame. Identity: - `M.lightKind(componentType: string, component: any?) -> string?` — the kind a light-role component names. - `M.lightOfKind(entityId: string, kind: string) -> any` — the light-role component of that kind on one entity. ## Usage ```luau local lighting = require("@builtin::modules.api.engine.lighting") lighting.setSun({ direction = { 0.3, -1, 0.2 }, intensity = 2.0 }) lighting.setAmbient({ intensity = 0.3 }) lighting.applySetup({ sun = { direction = { 0.3, -1, 0.2 }, intensity = 2.0 }, ambient = { intensity = 0.3 }, sky = { timeOfDay = 14 }, }) local sun = lighting.sun() print(sun.intensity, sun.entityId) ``` A reader takes no arguments and a setter takes the opts table, so `lighting.sun(opts)` is refused with the name of the call that applies it.
▲ 0↑ born
◇
component · born here
❒asset
# ProceduralSky The editable atmospheric sky: a day/night gradient with a sun disc, glow, stars and a moon, all derived procedurally from the directional light's elevation. Add it to an entity like a light: ```lua entity.spawn("sky").component.add("ProceduralSky", { timeOfDay = 18.5 }) entity.spawn("sky").component.add("ProceduralSky", { preset = "sunset" }) entity.spawn("sky").component.add("ProceduralSky", { zenithColor = { r = 0.05, g = 0.1, b = 0.3 }, horizonColor = { r = 0.9, g = 0.4, b = 0.2 }, }) ``` A `ProceduralSky` owns its material outright: it registers a **runtime GPU material** of its own over the builtin procedural-sky shader and pushes every visual parameter (colours, sun, stars, moon, turbidity, exposure) straight to that record. The component's fields **are** the material definition — no `.material` asset backs it, so retuning a scene's sky changes that scene's sky and nothing else. The renderer reads the values from the material's group(1) uniform like any other material — there is no sky-specific render path. Those values also travel the native `Sky` bridge into the scene sky config, alongside the time-of-day / auto-cycle / sun-sync controls. That config is what `sky.get()` reports and what a saved scene records, so both name the sky being drawn. With `syncSunToLight` on (the default), the sky's `timeOfDay` is what the scene is lit by: the scene's directional light takes its direction, colour and intensity from the sun's position, so the sun in the sky and the sun the scene is lit by are the same sun through a whole day/night cycle. Turn it off for a light the sky leaves alone. The intensity that arrives is the day/night curve between night and noon, and `sunPeakIntensity` is the noon end of it — `1.0`, the sky's own daylight, unless a scene asks for more. A harder sun on water or snow is stated here; setting the light itself does not survive, because the sky writes over it every time the hour moves. Like the directional light, the sky is conceptually **singular per scene**. A `ProceduralSky` fully defines its material on `awake` (every parameter from its own fields), so switching scenes never inherits a previous scene's overrides. Day/night comes from the sun's elevation, so evening presets render dark — drive the look by `timeOfDay` (which orients the sun). ## Fields | Field | Type | Default | Meaning | |---|---|---|---| | `preset` | string | `""` | Named look applied before explicit fields: `clear_day`, `sunset`, `sunrise`, `overcast`, `night`. | | `timeOfDay` | number | `14.0` | 0..24 hours; orients the sun and the day/night gradient. | | `autoCycle` | bool | `false` | Advance `timeOfDay` each frame. | | `cycleSpeed` | number | `60.0` | Game-seconds per real-second for the auto cycle. | | `syncSunToLight` | bool | `true` | Drive the directional light from the sun. | | `sunPeakIntensity` | number | `1.0` | What the sun's light measures at noon; the day/night curve runs from night up to this. | | `zenithColor` / `horizonColor` / `groundColor` | color | — | Sky gradient colours. | | `sunSize` / `sunIntensity` | number | `0.02` / `20.0` | Sun disc size + brightness. | | `starsIntensity` | number | `1.0` | Night star-field intensity. | | `moonSize` | number | `0.03` | Moon disc size. | | `turbidity` | number | `4.0` | Atmospheric haziness. | | `exposure` | number | `1.0` | Sky exposure multiplier. | ## Methods | Method | Description | |---|---| | `sky:setTimeOfDay(hours)` | Set the time of day (0..24). | | `sky:applyPreset(name)` | Apply a named look. | | `sky:setZenithColor(c)` / `setHorizonColor(c)` / `setGroundColor(c)` | Set a gradient colour (`{r,g,b}` array or map; >1 auto-scales /255). |
▲ 0↑ born
◇
component · born here
❒asset
# Skybox The scene's sky as a single **material**. Add it to an entity and the engine renders that material across the sky behind everything else: ```lua entity.spawn("sky").component.add("Skybox", { material = "sky_cubemap" }) entity.spawn("sky").component.add("Skybox", { material = "my_custom_sky" }) entity.spawn("sky").component.add("Skybox", { kind = "none" }) -- explicit no sky ``` `Skybox` is the generic, material-driven sky: it points at **any** sky material — a builtin (`sky_procedural`, `sky_solid`, `sky_cubemap`, `sky_equirect`) or your own authored sky-domain material — and makes it the active sky. For the editable procedural atmosphere (day/night, sun, stars, moon), use the [`ProceduralSky`](../ProceduralSky.component/README.md) component instead — it is a `Skybox` over the `sky_procedural` material plus typed parameter controls. A Skybox **materialises its material's GPU handle** when it binds it. The sky's point-of-use is the sky pass, which never binds the material the way a `Model` does, so the component performs the Disk→CPU→GPU upload (`:handle()`) itself. The renderer binds the sky by the material's **identity** (the key it is resident under once materialised), so a material-mode sky renders correctly after boot and mode flips — no manual `:handle()` needed. Like the directional light, the sky is conceptually **singular per scene**: the engine copies the most recently authored sky into the scene-wide sky config the renderer reads each frame. A sky belongs to the scene that spawned it; removing the component reverts the scene to the engine fallback sky. State is bridged through the native `Sky` ECS component (sky type `material` / `none`), so there is no global sky singleton — two scenes can never clobber each other. ## Fields | Field | Type | Default | Meaning | |---|---|---|---| | `material` | string | `sky_procedural` | The sky material to render. Any registered material whose shader is a sky-domain shader. Defaults to the procedural sky so an empty `Skybox{}` is never a black void. | | `kind` | string | `material` | `"material"` renders `material`; `"none"` turns the sky pass off — the explicit authored form of "this scene has no sky". | ## Methods | Method | Description | |---|---| | `skybox:setMaterial(name)` | Point the sky at a different material (materialises its handle). | | `skybox:setNone()` | Turn the sky off explicitly. |
▲ 0↑ born
▣
module · born here
❒asset
# environment Module Environment / reflection capture — bake the scene into reflection-probe cube slots from world positions, persist them as `faces6` `.texture` assets, set per-probe blend data so surfaces reflect the probes covering them, and capture the sky into its own slot as the fallback under them. Public Luau surface over the `__environment` Internal FFI namespace, auto-injected as `_G.environment` via the prelude. ## Purpose The generic "render the scene into a cubemap from a point" capability the reflection-probe system is built on. Captures are queued for the render system (which owns the live scene); `captureSlotToAsset` additionally yields a few frames while the GPU readback completes. Persisted cubes are `faces6` `.texture` assets (px/nx/py/ny/pz/nz PNGs + a `cube.yaml` sidecar — see `docs/specs/cubemap-textures.md` §4 for the face convention). For probe authoring use the higher-level `reflectionProbe` module; reach for `environment` when you need the raw per-slot primitives. ## Usage ```luau -- Register probe blend data: index i maps to cube slot i. environment.setProbes({ { x = 0, y = 2, z = 0, radius = 12 } }) -- Bake slot 0 from a point (queued, next frame). environment.captureSlot(0, 0, 2, 0) -- Bake + persist to /source/probe_lobby.texture/ (yields; call from a -- task/coroutine/execute context). local path, err = environment.captureSlotToAsset("probe_lobby", 0, 0, 2, 0) -- Restore a persisted cube into a slot WITHOUT re-rendering. environment.loadSlotFromAsset("probe_lobby", 0) -- Capture the sky alone into the fallback slot: a surface no probe covers -- reflects the sky rather than black. environment.captureSky() ``` ## Exports - `environment.setProbes(probes) -> boolean` — set active probes' blend data; array of `{ x, y, z, radius, priority? }`, index i → cube slot i, gathered highest `priority` first - `environment.captureSky(x?, y?, z?) -> boolean` — render the sky alone into the fallback slot and arm it (queued) - `environment.setSkyFallback(active) -> boolean` — arm/disarm the fallback against the sky already captured (arming is refused while the slot holds none) - `environment.captureSlot(slot, x, y, z) -> boolean` — bake the scene into a slot from a point (queued) - `environment.captureSlotToAsset(name, slot, x, y, z, timeoutFrames?) -> (string?, string?)` — bake + persist as a `faces6` `.texture`; yields - `environment.loadSlotFromAsset(name, slot) -> (boolean, string?)` — upload a persisted cube into a slot without re-rendering Back-compat single-global-reflection helpers (slot 0 + one full-coverage probe): - `environment.capture(x, y, z) -> boolean` - `environment.captureToAsset(name, x, y, z) -> (string?, string?)` - `environment.loadFromAsset(name) -> (boolean, string?)`
▲ 0↑ born
✦
shader · born here
❒asset
# procedural_sky The editable atmospheric sky: a day/night gradient with a sun disc and glow, a procedural star field, and a moon opposite the sun, authored entirely from material properties — no texture backs it. Day, night and the dawn/dusk blend all derive from the directional light's elevation, so the sky follows whatever drives the scene's sun; there is no time-of-day uniform. The gradient's zenith, horizon and ground colours, the sun's size and intensity, the stars, the moon, turbidity and exposure come from `properties.yaml`. Output is radiance scaled by `exposure` — the scene's own tone-mapping does the range compression, the same contract as every sky-domain shader. The `ProceduralSky` component owns the runtime material over this shader (`__procedural_sky`) and pushes its fields here; `sky.get()` reports the resulting configuration.
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.