# Dynamic fire light `scripts/fire_light.gd` replaces the old single light with sinusoidal flicker. One noise signal now drives the light rig **and** the flame, so brightness, colour, shadow sway and flame height pulse together. ## Research Perplexity answered this time (it had been unavailable in earlier sessions), and the official documentation was checked in parallel: - Godot renderer comparison table — Compatibility is the only method available for the web target. It supports 8 OmniLights per mesh by default, glow, depth/height fog and SSAO, and it does **not** support volumetric fog, light projector textures, PCSS, SSIL, SDFGI, decals or CompositorEffects post-processing. Lights **with** shadows are drawn in extra passes and blended in sRGB, so they differ in appearance and dominate the cost: https://docs.godotengine.org/en/stable/3d/overview_of_renderers.html - Faking global illumination — for positional lights, add a second OmniLight3D as a child of the main one with +25/50% range, shadows off, energy 10-40% and specular 0: https://docs.godotengine.org/en/stable/tutorials/3d/global_illumination/faking_global_illumination.html - FastNoiseLite (`get_noise_1d`, Perlin, seed): https://docs.godotengine.org/en/stable/classes/class_fastnoiselite.html - Key advice from Perplexity: continuous noise instead of per-frame randomness, fixed `omni_range`, frame-rate independent smoothing, amplitude 15-30%, colour variation smaller than brightness, `omni_attenuation` ~2 for a physical falloff, one shadowed light only, and profile illumination and shadows separately. - Community: hard on/off flicker makes the lit area visibly "expand" after each switch, so the light is never toggled. Particles are lit inconsistently by a light placed inside the same particle system, which is why the flame stays unshaded and luminous on its own (the light only lights the room). ## Implementation | Element | Value | Reason | | --- | --- | --- | | Main light | energy 1.9-2.8, range **fixed** 10, attenuation 1.6, shadows **on** (cube) | the only shadow caster; animating the range makes the boundary breathe | | Noise | FastNoiseLite Perlin, seed 20261011, 0.8 Hz (weight 0.75) + 7 Hz (0.25), gain 1.6, response 15 | smooth, deterministic, frame-rate independent, no strobing | | Colour drift | green 0.36-0.42, blue 0.07-0.12 | deliberately smaller than the brightness swing | | Sway | ±4 cm at ~1.2 Hz, main light only | makes the shadows move; kept subtle | | Bounce light | child, range 14 (=+40%), energy 20%, shadows off, specular 0, attenuation 1.5 | documented fake-GI recipe; no indirect lighting exists in Compatibility | | Core light | range 3.5, energy 45%, shadows off, 45 cm below the main light | hot glow at the flame | | Embers | 8 unshaded additive particles, 1.8 s | cheapest large realism gain | | Flame | quad height ±8%, material brightness ±25% | visibly breathes with the light | | Glow | enabled, intensity 0.45, blend additive, `hdr_threshold` **1.3**, levels 2-4 | stands in for the missing volumetric fog; a high threshold keeps the amber HUD from blooming | Total: 6 omni lights in the room (2 desk lamps, 3 fire, 1 muzzle flash), only one with shadows, against the default limit of 8 per mesh. ## Measured cost (desktop Radeon 780M, GL Compatibility, 1280x720, vsync off) | State | FPS | Frame time | | --- | --- | --- | | Fire rig active | 262.6 | 3.8 ms | | Fire lights hidden | 250.7 | 4.0 ms | | Shadows off | 588.4 | 1.7 ms | The three fire lights are inside the measurement noise; the single shadow-casting light costs about 2.1 ms, i.e. more than half the frame. That matches the documentation and is the intended trade: the shadow is what makes the fire feel present. On a phone the same ratio is expected to be heavier, and this workstation cannot measure it. To shed the cost on weak devices, in this order: `main_light.shadow_enabled = false`, glow off, then `fire_rig.main_light.visible = false`. ## Pitfalls found while implementing 1. **Dual-paraboloid shadows do not exist in Compatibility.** Setting `OmniLight3D.SHADOW_DUAL_PARABOLOID` logs `Dual paraboloid shadow mode not supported in the Compatibility renderer` and the light loses its shadows. Only `SHADOW_CUBE` works, even though the class reference describes the other mode as faster. Advice found online ("try dual paraboloid first") is wrong for this renderer. 2. Keep `omni_range` constant; animating it makes the light edge visibly expand. 3. Never switch the light off to flicker: the lit area visibly pops afterwards. 4. Keep fire particles unshaded and additive; the light must only light the room. 5. Raise the glow threshold before trusting it: at the default 1.0 the amber HUD panels bloom. 6. When taking verification screenshots, stop wave spawning first: a zombie that reached the camera looks like a rendering bug but is just a silhouette in front of the lens. ## Verification ```sh "$G" --headless --path . --script res://tools/test_fire_light.gd "$G" --display-driver x11 --rendering-method gl_compatibility --path . --script res://tools/shot_fire.gd ``` `test_fire_light.gd` simulates 60 s at 60 Hz on the pure noise signal and checks: energy stays inside 1.9-2.8 (measured 1.96-2.76), deviation above 0.05 (0.135, so the fire is alive), maximum per-frame step below 0.05 (0.036, so no strobing), the three lights move together, the range stays fixed, bounce and core have no shadows and no specular, the bounce is a child at +40% range, only the main light casts shadows, the sway stays inside ±4 cm (measured 3.5 cm) while actually moving, the flame height varies, embers exist, and two rigs with the same seed produce identical signals. `test_office_atmosphere.gd` was updated to the new light budget. All eleven suites pass. Images `/tmp/zombie-fire-{basso,alto,alto-scarto,insieme}.png` compare the dim and bright phases, the same brightness with a different sway phase, and a wide view: the wall and ceiling illumination, the floor shadow and the flame halo all change together, and the HUD stays crisp.