Skip to content

Tutorials · Light and Shadow · Chapter 11

Living light

30 minutes Uses: Light, AmbientLight, pulse_number, anneal_number, random, wait, anneal_colour, set_flag

By the end

  • flicker.rgs, and a Flicker on each lantern
  • lightning.rgs, and a Lightning on the ambient and on the moonlight, flashing together
  • sputter.rgs, a script that switches the second lantern off and on: set_flag
  • What a pulse, an anneal and a wait do, and why each light’s Calm is its own colour
On this page

Light that moves

So far the hall holds still. A script can change any number on a light while the game runs, and that is all a flame or a flash needs. This is the tutorial’s first Rengy Script: three short files that the editor runs as they are, with nothing to install and nothing to build.

A flame that flickers

  1. In Assets, click Assets in the path bar over the tree, so the script is written at the project’s top, beside art, scenes and window.ron, then press Create, choose New script, type flicker, and press Enter.
  2. In the Code tab, replace everything in flicker.rgs with the script below, and press Ctrl+S.
rgs flicker.rgs
/// A lantern's flame. Its brightness wavers at random between `low` and
/// `high`, toward a new aim every `gap` seconds, and its reach breathes out to
/// `reach` and back, `breath` seconds each way.
component Flicker {
    /// The dimmest the flame gets.
    #[range(0..4)]
    low: f32 = 1.0,
    /// The brightest the flame gets.
    #[range(0..4)]
    high: f32 = 1.3,
    /// Seconds from one aim to the next.
    #[suffix("s")]
    gap: f32 = 0.12,
    /// How far the light reaches at the top of a breath.
    #[suffix("u")]
    reach: f32 = 270.0,
    /// Seconds to breathe out, and as long to breathe back in.
    #[suffix("s")]
    breath: f32 = 1.6,
    /// Seconds until the next aim.
    #[hidden]
    next: f32 = 0.0,

    fn on_create() {
        // There and back for ever: from the radius the light has, out to
        // `reach`, and back.
        pulse_number("LightComponent", "radius", reach, breath, .InOutSine);
    }

    fn loop(dt: f32) {
        next -= dt;
        if next <= 0.0 {
            // A new aim somewhere between the dimmest and the brightest,
            // reached by the time the next one is chosen.
            anneal_number("LightComponent", "intensity", random(low, high), gap, .InOutSine);
            next = gap;
        }
    }
}
The Code panel with flicker.rgs open at its first lines: the comment on a lantern’s flame, then component Flicker with its fields low, high, gap, reach, breath and a hidden next, each with a doc comment, and the start of on_create; the Scripts list at the left names flicker.rgs, lightning.rgs and sputter.rgs

on_create runs once, when the node is made, and starts a pulse on the Light’s Radius: out to reach and back, over breath seconds each way, for ever. loop runs every step, with dt, the seconds since the last. It counts next down, and when it reaches 0 it picks a new aim for the Light’s Intensity, somewhere between low and high, and eases toward it over gap seconds, which is the time until the next aim.

pulse_number
Takes a number field of another component on the node to a value and back, for ever. Here the Light’s Radius, out to reach and back, breath seconds each way, along .InOutSine, which starts and ends slowly.
anneal_number
Takes a number field to a value over a time, along a curve. Here the Light’s Intensity, to its new aim, in gap seconds.
random
A number from low up to but not including high, new at every call and different every time you press Play, so a flame never repeats itself.
  1. Select lantern_1, press Add component, and choose Flicker. Set Low to 1.7, High to 2.7, Gap to 0.12, Reach to 270 and Breath to 1.6.
  2. Select lantern_2, add Flicker, and set 1.4, 2.4, 0.15, 260 and 1.9.
  3. Select lantern_3, add Flicker, and set 1.0, 1.8, 0.1, 198 and 1.3.
  4. Press Ctrl+S.
lantern_1 selected, its Inspector narrowed to its Flicker: Low 1.70 and High 2.70 on sliders, Gap 0.12 s, Reach 270.00 u and Breath 1.60 s; in the Viewport the Light’s circle round the first lantern
Low, High
The dimmest and the brightest Intensity the flame aims for, from 0 to 4. Put the lantern’s own Intensity between them, as the three lanterns do: 1.7 and 2.7 round the first lantern’s 2.2, so the first aim does not jump.
Gap
Seconds from one aim to the next. 0.12 is a quick flame; 0.5 would be a lazy one.
Reach
The Radius at the top of a breath, in world units. A little over the Light’s own: 270 on the first lantern’s 260, so the pool swells by ten units and falls back.
Breath
Seconds to breathe out, and as long to breathe back in: 1.6 here. The three lanterns breathe at different paces, 1.6, 1.9 and 1.3, so they never beat together.
The first lantern and its bricks half a second in: a warm pool of bricks round the lamp, palest nearest it, the left pillar’s cap at the bottom right and a small rune at the bottom left
Half a second in.
The same lantern a second later: the pool a touch different in brightness and reach, the bricks, the pillar and the rune as before
A second later: the same lantern, its brightness and reach a little changed.

The two pictures are alike on purpose: a flicker is small. Intensity wavers within about a fifth of the lantern’s own, and the Radius by ten units. In motion it reads as flame; in a still it is a shade.

A storm beyond the window

  1. In Assets, press Create, choose New script, type lightning, and press Enter.
  2. In the Code tab, replace everything in lightning.rgs with the script below, and press Ctrl+S.
rgs lightning.rgs
/// A storm beyond the window. Every `every` seconds the colour of this node's
/// light flashes to `flash` and fades back to `calm`, then flashes again, more
/// briefly, and fades for longer. `target` names which light: the
/// AmbientLightComponent on the room's ambient, the LightComponent on the
/// moonlight. Two nodes with the same `every` flash together.
component Lightning {
    /// The component whose colour flashes.
    target: String = "LightComponent",
    /// The colour at the height of a flash.
    #[color]
    flash: Vec4,
    /// The colour between flashes: the light's own.
    #[color]
    calm: Vec4,
    /// Seconds from one storm to the next.
    #[suffix("s")]
    every: f32 = 7.0,

    fn loop() {
        await wait(every);
        await anneal_colour(target, "color", flash, 0.05, .Linear);
        await anneal_colour(target, "color", calm, 0.4, .OutQuad);
        await wait(0.12);
        await anneal_colour(target, "color", flash, 0.04, .Linear);
        await anneal_colour(target, "color", calm, 0.8, .OutQuad);
    }
}
The Code panel with lightning.rgs open: the comment on a storm beyond the window, then component Lightning with its fields target, flash, calm and every, and fn loop with its first lines, await wait(every) and the first anneal_colour

loop here waits. await wait(every) holds this hook for every seconds and carries on from that line. Then the light’s Color is annealed to flash in 0.05 seconds, a snap, and the next line waits for it to finish: back to calm in 0.4, which starts fast and slows. A pause of 0.12, a second snap to flash in 0.04, a longer fade of 0.8, and loop starts again, to wait every seconds more.

  1. Select ambient, press Add component, and choose Lightning. Set Target to AmbientLightComponent, Flash to 0.45, 0.5, 0.7, alpha 1, Calm to 0.08, 0.09, 0.16, alpha 1, and Every to 7.
  2. Select moonlight, add Lightning, and set Target to LightComponent, Flash to 1, 1, 1, alpha 1, Calm to 0.5, 0.6, 1.0, alpha 1, and Every to 7.
  3. Press Ctrl+S.
ambient selected, its Inspector narrowed to its Lightning: Target AmbientLightCompo, cut off by the box, Flash a pale blue swatch, Calm a dark blue swatch, and Every 7.00 s; in the Viewport the node’s small square at the top left of the view
Target
The name of the component whose Color flashes, on this node: AmbientLightComponent on the ambient, LightComponent on the moonlight.
Flash
The colour at the height of a flash. The ambient’s is 0.45, 0.5, 0.7: a cold pale blue, lifting the whole hall. The moonlight’s is 1, 1, 1, white.
Calm
The colour between flashes, which is the light’s own: the ambient’s 0.08, 0.09, 0.16 and the moonlight’s 0.5, 0.6, 1.0.
Every
Seconds from one storm to the next: 7. The first flash comes seven seconds after the game starts.
wait
Suspends the hook for that many seconds, then carries on from the same line. It is awaited: await wait(every).
anneal_colour
Takes a colour field of another component on the node to a colour over a time, along a curve. Awaited here, so each line starts when the one before it has finished.
The whole hall just under seven seconds in, calm: the first lantern’s bricks warm, the window white, a green wash round the third lantern, the stained glass on the floor, a timber leaning on the right pillar, a pale shaft in the doorway, and the rest of the hall dark and cold blue
Just under seven seconds in: the calm before the first flash.
The same hall seven seconds in, in the first flash: the dark and the cold blue lifted to a pale blue-white wash that shows the walls, the arches and the floor, with the lanterns, the window, the timber and the doorway’s shaft as before
Seven seconds in: the first flash.

A lantern that sputters

A script can switch a light off and on as well as change its numbers. A lantern whose wick is drowning sputters: now and then it goes out, comes back, and goes out once more, before it settles.

  1. In Assets, press Create, choose New script, type sputter, and press Enter.
  2. In the Code tab, replace everything in sputter.rgs with the script below, and press Ctrl+S.
rgs sputter.rgs
/// A lantern that sputters. Most of the time it burns steadily; every few
/// seconds its wick gutters, and the light goes out and comes back twice in
/// quick succession, as if it were drowning in its oil.
component Sputter {
    /// The shortest steady spell between two sputters, in seconds.
    #[suffix("s")]
    calm_min: f32 = 2.5,
    /// The longest steady spell between two sputters, in seconds.
    #[suffix("s")]
    calm_max: f32 = 6.0,
    /// How long the light stays out each time it gutters, in seconds.
    #[suffix("s")]
    blink: f32 = 0.08,

    fn loop() {
        await wait(random(calm_min, calm_max));
        set_flag("LightComponent", "enabled", false);
        await wait(blink);
        set_flag("LightComponent", "enabled", true);
        await wait(blink * 2.0);
        set_flag("LightComponent", "enabled", false);
        await wait(blink);
        set_flag("LightComponent", "enabled", true);
    }
}
The Code panel with sputter.rgs open: the comment on a lantern that sputters, then component Sputter with its fields calm_min, calm_max and blink, each with a doc comment and a suffix of s, and fn loop with its first lines, await wait(random(calm_min, calm_max)) and the first set_flag("LightComponent", "enabled", false); the Scripts list at the left names flicker.rgs, lightning.rgs and sputter.rgs

loop waits a steady spell, a random number of seconds between Calm Min and Calm Max. Then set_flag writes the Light’s Enabled to false, loop waits one Blink, and set_flag writes it back to true. It waits two blinks, goes out and back once more, and starts again.

set_flag
Writes a true or false onto a field of another component on this node, as set_number writes a number. Here the Light’s Enabled: false switches the light off and true on.
Calm Min, Calm Max
The shortest and the longest steady spell between two sputters, in seconds. The script waits a random time between them, so no two spells are alike: 2.5 and 6.
Blink
How long the light stays out each time, in seconds. 0.08 is about five frames; the pause between the two blinks is twice that.
  1. Select lantern_2, press Add component, and choose Sputter. Set Calm Min to 2.5, Calm Max to 6 and Blink to 0.08.
  2. Press Ctrl+S.
lantern_2 selected, its Inspector narrowed to its Sputter: Calm Min 2.50 s, Calm Max 6.00 s and Blink 0.08 s; in the Viewport the Light’s circle round the second lantern, in its niche at the right
The second lantern in its niche, lit: warm bricks under the arch and a magenta ring behind the lantern, the right pillar’s cap and the leaning timber at the bottom, and the doorway’s glow
In one run, about four seconds in: the second lantern lit.
The same view a tenth of a second later: the second lantern’s light out, the bricks under the arch and the niche’s stones dark, only the lantern’s glass still bright
A tenth of a second later: its light out and its niche dark.

What it costs

A script costs the GPU nothing: the lights it changes are drawn as they always were, at their new values. A pulse or an anneal, once started, runs by itself every step, so the scripts here do almost no work of their own. A light switched off is not drawn at all, with no draw and no shadow, so a sputter costs nothing while the lantern is out.

The flicker, the lightning and the sputter

The flicker

Fieldlantern_1lantern_2lantern_3
Low1.71.41.0
High2.72.41.8
Gap0.120.150.1
Reach270260198
Breath1.61.91.3

The lightning

LightTargetFlashCalmEvery
ambientAmbientLightComponent0.45, 0.5, 0.7, alpha 10.08, 0.09, 0.16, alpha 17
moonlightLightComponent1, 1, 1, alpha 10.5, 0.6, 1.0, alpha 17

The sputter

FieldValue
Nodelantern_2, beside its Flicker
Calm Min2.5
Calm Max6
Blink0.08

Check it works