Skip to content

Light, Sound & Vision

Light, sound, and vision as mechanics — how exposed you are, what you can see, and who can hear you past walls. The engine samples; your game owns the policy — there is deliberately no stealth, HP, or detection rule in the engine, only the primitives you read and threshold yourself (the same boundary as combat’s opaque payload). Frozen contract, ADR 0067. See the LSV demo, whose HUD meters exposure + noise like a real game.

All three share one occluder layer — bodies that should block line-of-sight, light, audio, and fog set this layer, and every occlusion query defaults its mask to it (pass 0 for the default).

Illumination.illuminanceAt(scene, x, y, z, mask)

Section titled “Illumination.illuminanceAt(scene, x, y, z, mask)”

Returns: Num — a continuous 0..1 “how lit is this point”: the engine sums every scene light’s falloff (matching the render, so reads-lit == looks-lit) with a masked line-of-sight occlusion test on mask. 0 = fully hidden, 1 = fully lit. Bidirectional by nature — the lamp that lets you see also lets things see you. The engine returns the raw value; a hard “spottable” stat is just illuminanceAt(...) > threshold, or use it as a soft exposure meter or a × detectionRange multiplier into Ai.nearestVisibleTarget.

import "engine" for Illumination
var exposure = Illumination.illuminanceAt(scene, px, 0.5, pz, 0) // 0 hidden … 1 lit
var spottable = exposure > 0.35 // the GAME decides the threshold

A world-XZ visibility field (Unknown / Explored / Visible) for a top-down co-op ARPG. configure bakes an occluder grid from the scene’s static bodies (call after the level’s statics + a physics step). Each frame: beginFrame greys stale vision to Explored (memory persists, even when a light dies), then reveal once per viewer. Both team-shared and per-player vision are supported (the core is per-player-keyed).

FogOfWar.configure(scene, originX, originZ, cellSize, width, height)

Section titled “FogOfWar.configure(scene, originX, originZ, cellSize, width, height)”

Build the field + bake the occluder grid. origin is the world corner of cell (0,0); the grid is width×height cells of cellSize.

Grey stale Visible cells to Explored. Call once at the start of a frame’s reveal pass.

Reveal line-of-sight cells from a viewer at (x, z) out to radius. Pass a radius already scaled by the viewer’s illuminance — darkness shrinks your view (3 + illuminance * 7, say). A wall stops the reveal.

Returns: Num0 Unknown, 1 Explored (remembered), 2 Visible (in view now).

FogOfWar.mark(scene, x, z, ttlSeconds) · tick(scene, dt) · isRevealed(scene, x, z, radius)

Section titled “FogOfWar.mark(scene, x, z, ttlSeconds) · tick(scene, dt) · isRevealed(scene, x, z, radius)”

The wall-penetrating sonar reveal-tag channel — the inverse of line-of-sight. mark reveals a position for ttlSeconds; tick ages the tags; isRevealed tests any live tag within radius. Range is a game input (scale it by stillness for a “listen” ability).

noiseBus.emitOccluded(scene, x, y, z, radius, mask)

Section titled “noiseBus.emitOccluded(scene, x, y, z, radius, mask)”

Returns: List of heard listener ids — like NoiseBus.emit, but a wall on mask between the sound and a listener blocks it. The same wall that muffles the audio shrinks what the AI hears. Needs the scene’s physics (a masked raycast per listener).

A pure-Wren helper unifying the audible side (a Source you play) with the audible-to-AI side (a NoiseBus stimulus), so they stay consistent — a silenced weapon makes no noise; a silent pressure-plate can still make noise. Auto-emit (any playing source auto-raising noise) is deliberately not offered.

Returns: SoundEmittersource may be null for a noise-only emitter; bus is a NoiseBus.

emitter.position(x, y, z) · emitter.radius=(r)

Section titled “emitter.position(x, y, z) · emitter.radius=(r)”

Set the emitter’s world position and loudness radius.

emitter.emit() · emitter.emitOccluded(scene, mask)

Section titled “emitter.emit() · emitter.emitOccluded(scene, mask)”

Play the clip (if any) and raise a NoiseBus stimulus at the emitter; emitOccluded blocks listeners a wall on mask occludes. Returns: List of heard listener ids.

The audible half of emitOccluded: a wall between the listener and this Source muffles it. gain (broadband) drops the volume; gainHF (0..1) drives a per-source low-pass — the muffle. Both 1 = unoccluded. Compute them from a masked listener→source ray (a Raycast on the occluder layer). Pairs with NoiseBus.emitOccluded so the same wall muffles the audio and shrinks what the AI hears. (Degrades to volume-only where the OpenAL EFX filter is unavailable.)