Follow-up to #47: lighting tiers 3-4 — normal maps, soft shadows, spot/cone, falloff curves, day/night #49

Closed
opened 2026-08-31 14:50:11 +02:00 by orkun · 1 comment
Owner

Follow-up to #47, which shipped the ECS-native shape the lighting proposal named as the headline — the item blocked on "an engine-owned system over user components" (3679ce1). A torch is now just an entity carrying Light2D { x, y, radius, color, intensity }, a wall an entity carrying Occluder { x, y, w, h }, and one Ambient { color } entity sets the night tint; the engine runs the whole deterministic light pass at the end of the Render phase (ambient modulate → carve occluder shadows → accumulate each additive radial light) and presents, with no Light.* calls wired. Positions come from a Position component when present, else the light's own x/y. Colored lights already work (per-Light2D color). See runtime/native/systems_light.ludic and examples/library/light_ecs.ludic.

This tracks the remaining render-quality tiers from the original proposal, none of which need new ECS plumbing — they extend the accumulation core (runtime/native/light.ludic):

  • Tier 3 — normal-mapped sprites: a per-sprite normal buffer so a Light2D shades surfaces by angle, not just distance.
  • Tier 4 — soft shadows / penumbra: area lights and a blurred occluder edge instead of the current hard cut.
  • Spot / cone light: a directional Light2D variant (add angle + spread fields, consumed by the same system).
  • Falloff curve options: the proposal's falloff exponent on Light2D (today falloff is linear).
  • Light cookies / gels and a day/night helper (Light.time_of_day(t) animating ambient + a global directional light).

Related: #4 (the accumulation core), #47 (the shipped ECS-native shape), #43 (the engine-owned-system hook both consume).

Follow-up to #47, which shipped the **ECS-native shape** the lighting proposal named as the headline — the item blocked on "an engine-owned system over user components" (3679ce1). A torch is now just an entity carrying `Light2D { x, y, radius, color, intensity }`, a wall an entity carrying `Occluder { x, y, w, h }`, and one `Ambient { color }` entity sets the night tint; the engine runs the whole deterministic light pass at the end of the Render phase (ambient modulate → carve occluder shadows → accumulate each additive radial light) and presents, with no `Light.*` calls wired. Positions come from a `Position` component when present, else the light's own x/y. Colored lights already work (per-`Light2D` `color`). See `runtime/native/systems_light.ludic` and `examples/library/light_ecs.ludic`. This tracks the remaining render-quality tiers from the original proposal, none of which need new ECS plumbing — they extend the accumulation core (`runtime/native/light.ludic`): - [ ] **Tier 3 — normal-mapped sprites**: a per-sprite normal buffer so a `Light2D` shades surfaces by angle, not just distance. - [ ] **Tier 4 — soft shadows / penumbra**: area lights and a blurred occluder edge instead of the current hard cut. - [ ] **Spot / cone light**: a directional `Light2D` variant (add `angle` + `spread` fields, consumed by the same system). - [ ] **Falloff curve options**: the proposal's `falloff` exponent on `Light2D` (today falloff is linear). - [ ] **Light cookies / gels** and a **day/night helper** (`Light.time_of_day(t)` animating ambient + a global directional light). Related: #4 (the accumulation core), #47 (the shipped ECS-native shape), #43 (the engine-owned-system hook both consume).
Author
Owner

Shipped in 3828268 (feat(light): render-quality tiers 3-4, #49). All tiers extend the deterministic accumulation core (runtime/native/light.ludic) — no new ECS plumbing — and every one is integer + Q16.16 fixed, so scenes light identically on every run and headless.

  • Tier 3 — normal-mapped sprites: Light.normal(x, y, w, h, nx, ny) stamps a per-pixel surface normal into a G-buffer parallel to the framebuffer, and each light shades those pixels by N·L (facing), not distance alone. Light.height(h) sets the light's virtual z; Light.clear_normals() resets each frame. Unstamped pixels are unaffected (factor 1), so a scene with no normals lights exactly as before.
  • Tier 4 — soft shadows / penumbra: Light.soft(radius) treats the light as a small disk and averages visibility from a cross of sample points, so an Occluder edge fades through a penumbra instead of a hard cut. Light.soft(0) restores hard shadows.
  • Spot / cone light: Light.spot(x, y, radius, color, energy, direction, spread) — a directional wedge (degrees), with a self-contained integer atan2-in-degrees (no trig prelude pulled in) and a feathered edge.
  • Falloff curve options: Light.falloff(exponent) raises the brightness ramp to an integer power (1 linear, 2 quadratic, …) via repeated fixed multiply.
  • Light cookies / gels and a day/night helper: Light.gel(color) / Light.clear_gel() give a colour cookie (the light gels centre → rim), and Light.time_of_day(t) ramps a deep-blue night up to full daylight and back from a single 0..1 value.

The engine lighting system (systems_light.ludic) consumes matching optional Light2D fields — direction/spread/falloff/softness/gel — each defaulting off, so an older five-field Light2D is unchanged.

Worked example + regression: examples/library/light_tiers.ludic (1 1 1 1 1 1 1 1 1, verified by pixel readback). Nine new docs/language/light pages. Full suite 76 passed, self-host C-free fixpoint intact, no golden drift.

Shipped in 3828268 (feat(light): render-quality tiers 3-4, #49). All tiers extend the deterministic accumulation core (`runtime/native/light.ludic`) — no new ECS plumbing — and every one is integer + Q16.16 fixed, so scenes light identically on every run and headless. - [x] **Tier 3 — normal-mapped sprites**: `Light.normal(x, y, w, h, nx, ny)` stamps a per-pixel surface normal into a G-buffer parallel to the framebuffer, and each light shades those pixels by `N·L` (facing), not distance alone. `Light.height(h)` sets the light's virtual z; `Light.clear_normals()` resets each frame. Unstamped pixels are unaffected (factor 1), so a scene with no normals lights exactly as before. - [x] **Tier 4 — soft shadows / penumbra**: `Light.soft(radius)` treats the light as a small disk and averages visibility from a cross of sample points, so an `Occluder` edge fades through a penumbra instead of a hard cut. `Light.soft(0)` restores hard shadows. - [x] **Spot / cone light**: `Light.spot(x, y, radius, color, energy, direction, spread)` — a directional wedge (degrees), with a self-contained integer atan2-in-degrees (no trig prelude pulled in) and a feathered edge. - [x] **Falloff curve options**: `Light.falloff(exponent)` raises the brightness ramp to an integer power (1 linear, 2 quadratic, …) via repeated fixed multiply. - [x] **Light cookies / gels** and a **day/night helper**: `Light.gel(color)` / `Light.clear_gel()` give a colour cookie (the light gels centre → rim), and `Light.time_of_day(t)` ramps a deep-blue night up to full daylight and back from a single 0..1 value. The engine lighting system (`systems_light.ludic`) consumes matching optional `Light2D` fields — `direction`/`spread`/`falloff`/`softness`/`gel` — each defaulting off, so an older five-field `Light2D` is unchanged. Worked example + regression: `examples/library/light_tiers.ludic` (`1 1 1 1 1 1 1 1 1`, verified by pixel readback). Nine new `docs/language/light` pages. Full suite 76 passed, self-host C-free fixpoint intact, no golden drift.
orkun closed this issue 2026-08-31 15:36:24 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: workshopsoft/ludic#49
No description provided.