The remaining lighting tiers from the original proposal, all extending the deterministic accumulation core (light.ludic) — no new ECS plumbing: - Light.spot: cone / flashlight lights (direction + spread degrees), with a self-contained integer atan2-in-degrees and a feathered edge. - Light.falloff: a brightness-ramp exponent (1 linear, 2 quadratic, …) via repeated fixed multiply. - Light.soft: soft shadows — an area-sampled light so an occluder edge fades through a penumbra instead of a hard cut. - Light.gel + Light.clear_gel: colour cookies — a light gels from its centre colour to a rim colour. - Light.normal + Light.clear_normals + Light.height: a normal G-buffer so surfaces shade by facing (N·L), not distance alone (tier 3). - Light.time_of_day: a day/night ambient ramp 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 lights exactly as before. Every tier is integer + Q16.16 fixed, so scenes light identically on every run and headless. Worked example + regression: examples/library/light_tiers.ludic (1 1 1 1 1 1 1 1 1). Nine new docs/language/light pages. Full suite 76 passed, self-host C-free fixpoint intact, no golden drift. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
4.5 KiB
Text
91 lines
4.5 KiB
Text
# light_tiers.ludic — the lighting render-quality tiers from #49, all riding on
|
|
# the same deterministic accumulation core (integer + Q16.16 fixed):
|
|
#
|
|
# * Light.spot — a cone / flashlight (direction + spread degrees)
|
|
# * Light.falloff — the brightness-ramp exponent (linear → quadratic → …)
|
|
# * Light.soft — soft shadows: an occluder edge fades through a penumbra
|
|
# * Light.gel — a colour cookie: the light gels centre → rim
|
|
# * Light.normal — normal-mapped surfaces shade by facing (N·L), not distance
|
|
# * Light.time_of_day — a day/night ambient ramp from one 0..1 value
|
|
#
|
|
# Additive blocks light a *black* scene (so brightness is the light itself, not a
|
|
# saturated white); the day/night block modulates a *white* scene (ambient is
|
|
# multiplicative). Deterministic, so:
|
|
# bin/ludic examples/library/light_tiers.ludic -> 1 1 1 1 1 1 1 1 1
|
|
program LightTiers {
|
|
property Marker { n: int = 0 } # a component makes this a game (links the runtime)
|
|
model M { Marker }
|
|
|
|
function dark() -> void { Screen.clear(0) }
|
|
function white() -> void { Screen.fill_rectangle(0, 0, 64, 64, 16777215) }
|
|
function bit(b: bool) -> int { if b { return 1 }; return 0 }
|
|
function lum(x: int, y: int) -> int { return Screen.pixel(x, y) & 255 }
|
|
|
|
entry {
|
|
# --- 1. spot / cone: aimed right (0°), a narrow 25° cone -----------------
|
|
# a pixel to the right is inside the cone (lit); one straight up is outside.
|
|
dark()
|
|
Light.spot(32, 32, 24, 16777215, 1.0, 0, 25)
|
|
print(bit(lum(44, 32) > 0)) # 1 — right of centre, inside the cone
|
|
print(bit(lum(32, 14) == 0)) # 1 — above centre, outside the cone (dark)
|
|
|
|
# --- 2. falloff exponent: quadratic dims the mid-radius vs linear ---------
|
|
dark()
|
|
Light.falloff(1)
|
|
Light.point(32, 32, 24, 16777215, 1.0)
|
|
let lin = lum(44, 32) # linear brightness a little off-centre
|
|
dark()
|
|
Light.falloff(2)
|
|
Light.point(32, 32, 24, 16777215, 1.0)
|
|
let quad = lum(44, 32) # quadratic: same spot, dimmer
|
|
Light.falloff(1) # reset for later blocks
|
|
print(bit(quad < lin)) # 1 — a steeper curve is darker mid-radius
|
|
|
|
# --- 3. soft shadows: a penumbra pixel is partly lit where a hard edge cuts
|
|
dark()
|
|
Light.clear_occluders()
|
|
Light.occlude(38, 22, 3, 12) # a pillar right of the torch
|
|
Light.point(32, 32, 28, 16777215, 1.0)
|
|
let hard = lum(46, 34) # hard shadow at this pixel → fully dark
|
|
dark()
|
|
Light.soft(5) # 5px penumbra
|
|
Light.point(32, 32, 28, 16777215, 1.0)
|
|
let soft = lum(46, 34) # same pixel, now inside the penumbra
|
|
Light.soft(0) # back to hard shadows
|
|
Light.clear_occluders()
|
|
print(bit(hard == 0)) # 1 — a hard shadow cuts the ray fully
|
|
print(bit(soft > 0)) # 1 — the penumbra lets partial light through
|
|
|
|
# --- 4. gel / colour cookie: rim tints toward the outer colour -----------
|
|
dark()
|
|
Light.gel(255) # rim gels to pure blue (0x0000FF)
|
|
Light.point(32, 32, 24, 16711680, 1.0) # inner is pure red (0xFF0000)
|
|
let center_b = Screen.pixel(34, 32) & 255 # blue near the centre → low
|
|
let rim_b = Screen.pixel(50, 32) & 255 # blue near the rim → high
|
|
Light.clear_gel()
|
|
print(bit(rim_b > center_b)) # 1 — the gel tints the rim bluer than the core
|
|
|
|
# --- 5. normal mapping: a face tilted toward the light is brighter --------
|
|
dark()
|
|
Light.clear_normals()
|
|
Light.normal(18, 30, 8, 8, 0.7, 0.0) # this patch faces right (toward the light)
|
|
Light.normal(46, 30, 8, 8, 0.0 - 0.7, 0.0) # this patch faces left (away)
|
|
Light.point(32, 34, 44, 16777215, 1.0) # a light centred between the patches
|
|
let toward = lum(22, 34) # patch facing the light
|
|
let away = lum(48, 34) # patch facing away
|
|
Light.clear_normals()
|
|
print(bit(toward > away)) # 1 — N·L brightens the facing surface
|
|
|
|
# --- 6. time of day: midnight is dark, noon is bright (ambient modulate) --
|
|
white()
|
|
Light.time_of_day(0.0) # midnight
|
|
let night = lum(10, 10)
|
|
white()
|
|
Light.time_of_day(0.5) # noon
|
|
let noon = lum(10, 10)
|
|
print(bit(noon > night)) # 1 — daylight lifts the ambient
|
|
print(bit(night < 60)) # 1 — midnight stays dim
|
|
|
|
quit()
|
|
}
|
|
}
|