Skip to content

Tutorials · Particle Effects · Chapter 14

Light, glow and your own look

30 minutes Uses: ParticleEmitter, Trail, Light, AmbientLight, PostVolume, ShaderMaterial, Sprite, move_to, wait_frames, anneal_by, vec2, wait

By the end

  • An ambient light and a Light in each street lamp: the town dark, with a warm pool round each lamp that lights the smoke and the rain
  • Fire, sparks and fireflies that glow of their own (Unshaded), moths seen only in the lamp’s light (Light Only), and a Post Volume that gives the bright things a halo
  • spark-hue.wgsl and Custom Data: every spark of a firework in a colour of its own, twinkling
  • bat.rgs and a Trail: a lantern-bat and its ribbon, which breaks at the leap home
On this page

The lamps light the town

Until now the whole town has been drawn at full brightness. Lights light a particle as they light a sprite, so this chapter gives the town a night: a dim, cold ambient light everywhere, and a warm pool round each of the two street lamps. Light and Shadow teaches lights to the last control; this section sets only what the town needs.

  1. Select Town, press + Add node, and choose Ambient Light. Rename it ambient. Set its Position to 8 and 8. Click Color; if the button at the top left of the picker reads U8, press it so it reads F, as in chapter 2. Set Color to 0.3, 0.34, 0.42, alpha 1: a cold blue-grey.
  2. Select lamp_1, press Add component, and choose LightComponent. Leave Kind on Point. Set Radius to 150, Intensity to 2.4 and Color to 1, 0.78, 0.48, alpha 1: a warm orange.
  3. Select lamp_2 and do the same.
  4. Tick each lamp’s Sprite’s Unshaded. The lamp’s own picture is where the light comes from, so it keeps its painted brightness, and the next section says what Unshaded does for particles.
  5. Press Ctrl+S.
lamp_1 selected, its Inspector narrowed with the search box, lightcomponent, to its Light: Enabled ticked, Kind Point, Radius 150.00, Intensity 2.4, an orange Color swatch, Direction 0 and 1, Angle 1.2, Sort Layer Default 0, Channel 0, Softness 0.00, then a Sprite group and a Freeform group; the Viewport shows the town through the game camera, dim, with a large circle round each lamp, the lamp’s Radius, and a smaller circle high at the left round the lantern-bat, which you add below, and three thin orange circles, the particle attractors from chapter 10, over the fountain, the second lamp and the well
Color of the Ambient Light
The light the whole town has before any lamp adds its own, three numbers from 0 to 1. A cold 0.3, 0.34, 0.42 makes the night read blue. At 1, 1, 1 nothing is dimmed, which is how the town looked until now.
Radius and Intensity of a Light
How far a Point light reaches, in world units, and how bright it is at its middle. 150 and 2.4 make a pool about as wide as a house round each lamp. Light and Shadow’s A lantern shows them at several values.
Color of a Light
The colour a light lights with, a warm orange here. Everything the light reaches is tinted by it.
A strip of the town from the roofs down to the street, as chapter 13 left it: the forge’s fire at the left, a row of houses, two street lamps, the fountain’s stones, the well at the right, and fog along the paving, all at their painted brightness
For comparison, the town as chapter 13 left it, from the roofs to the street: every picture at its own brightness.
The same strip with the lamps lit and the glow, which comes later in this chapter, still off: the walls and street round each lamp lighter and warmer, and the rest of the strip darker and bluer, the forge’s fire and the fireflies as bright as before
With the lamps lit: a warm pool round each, and the rest of the town darker. The fire, sparks and fireflies are already Unshaded in this picture, as the next section makes them.

The smoke, the fog and the rain are lit too, since they are particles drawn in the town: where a lamp’s pool reaches them they are warm, and elsewhere they are dark and blue. The fire and the fireflies are lit too, which is not what a fire should be, and the next section sets it right.

What glows, what shows only in the light

A lit particle is its colour times the light that falls where it is. In a dark town that is right for smoke and rain, and wrong for a fire, which is itself a source of light. Two ticks decide it: Unshaded for a particle that makes its own light, and Light Only for one that is nothing but the light that falls on it.

  1. Tick Unshaded on forge_fire, forge_sparks, fireflies, well_ring, shooting_star and fireworks: the things in the town that are themselves light.
  2. Select Town, press + Add node, and choose Particles. Rename it moths and set its Position to 242.5 and 248, round the first lamp.
  3. Set Rate to 6, Lifetime to 4, Speed to 10, Spread to 7 and Gravity to 0.
  4. Set Start Color to 0.9, 0.9, 0.85, alpha 0.9, and End Color to 0.9, 0.9, 0.85, alpha 0. Set Start Size to 2; End Size is already 2.
  5. Set Capacity to 24, Prewarm to 4 and Sort Layer to 6. Tick Light Only, and set Texture to soft-dot.png.
  6. In Emission Shape, set Shape to Box, and Size to 56 and 36.
  7. Tick Noise and set Strength to 20, Frequency to 0.08, Scroll to 0 and 0, and Seed to 3: a slow, wandering drift. Press Ctrl+S.

Shape, Noise and Prewarm are chapters 4, 8 and 9’s: a box of moths round the lamp, wandering, and already there when the town opens. The two new controls are the ticks.

Unshaded
Draws the particles at full brightness, whatever the lights do, so they are neither darkened by the ambient light nor lit by the lamps. For things that are themselves the light: fire, sparks, fireflies, a magic ring. A sprite has it too, and it is what the lamps’ pictures use. It costs next to nothing.
Light Only
Draws the particles only where light falls, lit by it and by nothing else: in a lamp’s pool they show, and outside it they are gone. For things you see only by the light on them, such as moths, dust, rain in a beam. If Unshaded is ticked as well, Unshaded wins. It costs next to nothing.
Close up of the forge’s mouth with its fire shaded: orange flames in the dark arch, their hottest cores a cool blue-grey, a few orange sparks above, and no halo round any of them
Close up, for comparison, the fire and sparks not Unshaded: the hottest cores come out a cool grey, with no halo.
The same close up with the fire Unshaded: the same flames, but their hottest cores burn white and yellow and give off a soft halo, and the sparks above are brighter
Close up, Unshaded: the cores burn white, and spread a halo of their own.
moths selected, its Inspector narrowed with the search box, light, to the Particle Emitter’s rows that mention light: Unshaded unticked and Light Only ticked, then the Sprite group with its Stretch tick, unticked; in the Viewport a circle round each lamp and another high at the left, three thin orange circles for the attractors, and the node’s move arrows at the first lamp inside a violet rectangle, the Box, with a small blue square on each corner and a violet line running up from the node to another

Inside a lamp’s pool a moth is lit whether or not it is Light Only, and the moths’ box sits well inside the first lamp’s 150, so ticking and unticking it there changes nothing. The pictures below switch the lamp’s Light off instead, and look at the same moths in the dark.

Close up of the first street lamp between two houses: a dozen warm yellow specks, the moths, in the air round the lamp, with fog drifting at the bottom
Close up, five seconds in: the moths in the first lamp’s light.
The same close up with the first lamp’s Light unticked: the houses and the lamp dark and blue, and no moths at all in the air round the lamp
For comparison, the lamp’s Enabled unticked: Light Only moths are gone, since no light falls on them.
The same close up with the lamp off and Light Only unticked: a few dim grey specks in the air round the lamp, the moths drawn at the ambient’s brightness
For comparison, the lamp off and Light Only unticked: the moths are drawn like any particle, a few dim specks in the dark.

A glow over the town

A Post Volume changes the whole picture after it is drawn. Its bloom takes the brightest parts of the picture and spreads them into a halo, so the unshaded fire, the sparks and the fireflies, which are the brightest things in the town, begin to glow.

  1. Select Town, press + Add node, and choose Post Volume. Rename it glow, set its Position to 320 and 180, and its Area to Global.
  2. Open Profile. Set Exposure to 1, Tint to 1, 1, 1, Saturation to 1 and Vignette to 0.35.
  3. Keep the bloom’s Enabled ticked, and set Threshold to 0.55, Intensity to 1.4, Blur Pairs to 2 and Spread to 3. Keep Aberration and Grain at 0 and Grain Rolloff at 0.85.
  4. Press Ctrl+S.
Area
Where the volume applies. Global is the whole picture, wherever the camera looks, which is what a glow over a whole town needs.
Exposure, Tint and Saturation
The picture’s overall brightness, its colour cast and how strong its colours are. 1, white and 1 leave it as it was drawn.
Vignette
How much the corners of the picture are darkened, from 0, none. 0.35 is a light touch, which keeps the eye in the middle of the town.
Threshold, Intensity, Spread and Blur Pairs
The bloom. Threshold is how bright a pixel must be before it gives any light to the halo: 0.55, so only the fire, the sparks, the lamps and the fireflies do. Intensity is how strong the halo is, 1.4. Spread is how far it reaches, 3, and Blur Pairs is how smooth, 2; each pair is two more passes over the picture.
Aberration, Grain and Grain Rolloff
A coloured fringe at the corners, film grain, and how much grain the brightest parts keep. This town uses none, so 0, 0 and 0.85 leave them off. Light and Shadow’s Glow teaches every field of a Post Volume.
Close up, with the glow off, of the second street lamp, the fireflies in the air above the wall and round the lamp, and the well with its stone blocks at the right: the lamp’s globes, the windows and the fireflies are plain dots and shapes with hard edges
Close up, for comparison, without the glow.
The same close up with the glow on: the fireflies, the lamp’s two globes and the windows have a soft halo round them, and the corners of the picture are a little darker
With it: a halo round the fireflies, the lamp and the lit windows, and a vignette in the corners.

Every spark its own colour

Chapter 12’s firework bursts in one warm colour. A material is a shader on a node: on an emitter’s node it draws every particle the emitter makes. The emitter’s Custom Data hands each particle four numbers of its own, for the shader to read. Together they give a burst a colour for every spark.

  1. In Assets, click Assets in the path bar, so the shader is written beside project.rengy, then press Create, choose New shader, type spark-hue and press Enter. A shader is written in the editor, as in Light and Shadow’s chapter 12.
  2. Double-click spark-hue.wgsl to open it in the Code panel, replace everything in it with the text below, and press Ctrl+S.
  3. In Assets, open artifacts and double-click firework.artifact. It opens as a scene of its own, with one node, firework, which is the burst each shell makes.
wgsl spark-hue.wgsl
//! param twinkle: float = 9.0 range 0 30

// Each spark of a firework in a colour of its own. The emitter's Custom Data
// hands every spark four numbers, which arrive here as frag.custom:
//   x  its hue, 0 to 1 round the colour wheel, drawn at random at birth;
//   y  how brightly it burns, a curve over its life;
//   z  how much of its hue shows, the same for every spark;
//   w  where it is in its twinkle, drawn at random so no two twinkle together.

// A fully saturated colour at hue h, 0 to 1 round the wheel.
fn hue(h: f32) -> vec3<f32> {
    let k = vec3<f32>(0.0, 2.0 / 3.0, 1.0 / 3.0);
    return clamp(abs(fract(vec3<f32>(h) + k) * 6.0 - 3.0) - 1.0, vec3<f32>(0.0), vec3<f32>(1.0));
}

fn shade(frag: Fragment) -> vec4<f32> {
    let base = texel(frag.uv) * frag.color;
    let colour = mix(vec3<f32>(1.0), hue(frag.custom.x), frag.custom.z);
    let flicker = 0.75 + 0.25 * sin(time() * twinkle() + frag.custom.w);
    let glow = frag.custom.y * flicker;
    return vec4<f32>(base.rgb * colour * glow, base.a * glow);
}

The first line is a param: it makes twinkle a value the Inspector can set, 9 by default and from 0 to 30. shade runs once for every pixel of every spark, and frag.custom holds that spark’s four numbers. hue(frag.custom.x) turns the first into a fully coloured shade round the colour wheel, and mix then pulls the colour from white toward that shade by frag.custom.z: at 0 every spark is white, and at 1 it shows its full colour. How brightly it burns is frag.custom.y, which the curve lowers over the spark’s life, times a flicker of between 0.5 and 1, from sin(time() * twinkle() + frag.custom.w). Every spark twinkles at the same speed, and w starts each at a different point of its twinkle, so they do not flash together.

  1. Select firework and tick Unshaded, so that the town’s lights do not dim the burst.
  2. In the Custom Data group, tick Custom Data. Set X to Random, with Min 0 and Max 1. Set Y to Over Life, and open its Curve in the curve editor, as in chapter 5. It has two points, the left one at x 0 and y 1: click the right one and set its y to 0.7. Set Z to Fixed, with an Amount of 0.85. Set W to Random, with Min 0 and Max 6.28.
  3. Press Add component and choose ShaderMaterialComponent. Click its Shader slot and choose spark-hue.wgsl, or drag the file onto the slot. Under Values is a row for the shader’s one param, twinkle, reading unset. Press Set the 1 missing parameter(s): the material holds it at the file’s own 9.0, and the row reads = default. Typing 9 into its box would hold nothing: the box takes only a change.
  4. Press Ctrl+S. Then open scenes/town.ron again from Assets, to go back to the town.
The Inspector for the firework node, narrowed with the search box, shader, to its ShaderMaterialComponent: Shader reading spark-hue.wgsl with a small cube button at its edge, a Values list of one item, and a twinkle slider at 9.0 marked = default
The Inspector for the firework node, narrowed with the search box, custom, to its Custom Data: the Custom Data tick ticked, X Random with Min 0.00 and Max 1.00, Y OverLife with a Curve box showing a line that slopes gently down, Z Fixed with Amount 0.85, and W Random with Min 0.00 and Max 6.28
Custom Data
Four numbers that each particle carries, for a material to read as frag.custom. Ticked, every particle is born with its own four, set by the channels below. Unticked, no particle carries any, and a material reads zeros. It is the one way a shader can tell two particles of one emitter apart.
X, Y, Z and W
The four channels, each set one of three ways. Fixed gives every particle one Amount. Random draws a number once, at a particle’s birth, between Min and Max. Over Life reads a number from a Curve every step, along the particle’s life from birth, at the left, to death, at the right.
X
Here the hue, 0 to 1 round the colour wheel, drawn at random: every spark a colour of its own.
Y
Here how brightly the spark burns, 1 at birth falling to 0.7 at death. A lower end makes the sparks fade as they fall.
Z
Here how much of its hue a spark shows, 0.85 for every spark. 1 is the full colour, 0.5 to 0.9 keeps a warm look, and 0 is white.
W
Here where the spark is in its twinkle, 0 to 6.28, a whole turn of a circle in radians, drawn at random so that no two sparks twinkle together.
Shader
The .wgsl file the node is drawn through: spark-hue.wgsl. On an emitter’s node it draws every one of the emitter’s particles; on a sprite’s node it draws the sprite. Left empty, the node draws as it always did.
twinkle
The shader’s own value, the one param its first line declares: how fast the sparks twinkle, 9. From 5 to 15 is a shimmer, and higher is a flutter.
Close up of three bursts of sparks in the night sky, with the lantern-bat’s glow at the lower left edge: each burst a ring of dots, and every dot the same warm cream
Close up, for comparison, Z at 0: every spark the same warm white.
The same three bursts with the colours on: each ring made of pink, green, violet, orange and yellow dots, bright against the sky, each dot a colour of its own
Close up, as you set it: every spark its own colour, twinkling out of step.

A lantern-bat and its ribbon

A Trail is a component of its own, apart from an emitter: a ribbon drawn behind any node that moves. It makes no particles. It records where its node has been and draws a strip along those places. This one goes on a lantern-bat, a sprite that a script flits across the roofs.

  1. Make bat.rgs the way you made the others, with the script below, and press Ctrl+S.
  2. Select Town, press + Add node, and choose Sprite. Rename it bat. Set its Position to 150 and 150, its Texture to bat.png, its Size to 12 and 8, and its Sort Layer to 7.
  3. Press Add component and choose TrailComponent. Leave Emitting ticked and Tail Width at 0. Set Head Width to 3, Color to 1, 0.8, 0.45, alpha 0.8, Lifetime to 0.8, Max Length to 90, Min Distance to 3, Break Speed to 1200 and Sort Layer to 6.
  4. Press Add component again and choose LightComponent. Leave Kind on Point, and set Radius to 36, Intensity to 1.2 and Color to 1, 0.75, 0.4, alpha 1: the bat carries a lantern.
  5. Press Add component once more and choose Bat. Set Home to 150 and 150. Swoop starts at the script’s own 0.9: leave it.
  6. Press Ctrl+S.
rgs bat.rgs
/// A lantern-bat's round over the rooftops. It flits right across the town in
/// three swoops, rests, then darts back to `home` in one jump, too fast for
/// its ribbon to follow, so the ribbon breaks instead of drawing a line back
/// across the sky.
component Bat {
    /// Where each round starts.
    home: Vec2,
    /// Seconds for one swoop.
    #[suffix("s")]
    swoop: f32 = 0.9,

    fn loop() {
        move_to(home);
        await wait_frames(1);
        await anneal_by(vec2(90.0, -24.0), swoop, .InOutSine);
        await anneal_by(vec2(90.0, 30.0), swoop, .InOutSine);
        await anneal_by(vec2(90.0, -30.0), swoop, .InOutSine);
        await wait(0.6);
    }
}
The Code panel with bat.rgs open: a comment on a lantern-bat’s round over the rooftops, then component Bat with its fields home and swoop, and a loop that calls move_to home, waits a frame, makes three anneal_by swoops of 90 units right and up, down and up again, and waits 0.6 seconds; the Scripts list at the left names bat.rgs, cough.rgs, fireworks.rgs, shooting_star.rgs and wish.rgs

loop is one round. move_to(home) puts the bat at its start in a single move, and await wait_frames(1) lets one frame pass. The three anneal_by calls carry it by 90 units to the right, and a little up, then down, then up again, each over swoop seconds along .InOutSine, which eases in and out. It rests for 0.6 seconds, and the next round begins with move_to(home): a jump of about 270 units, in one frame.

bat selected, its Inspector narrowed with the search box, trail, to its TrailComponent: Emitting ticked, Head Width 3.00, Tail Width 0.00, a pale orange Color swatch, Lifetime 0.80 s, Max Length 90.00 u, Min Distance 3.00 u, Break Speed 1200.00 u/s and Sort Layer Custom 6; in the Viewport the bat’s circle of light, highlighted, and its move arrows at the upper left of the town
Emitting
Whether the ribbon records new points. This is the Trail’s own, apart from the emitter’s from chapter 1. Unticked, no point is added, and the ribbon shortens as its old points run out, which is how a ribbon can be ended gently. Leave it ticked.
Head Width
The ribbon’s width at the node, in world units: 3 here, about a third of the bat. 2 to 6 suits a streak behind a small thing.
Tail Width
Its width at its far end, in world units. 0 tapers it to a point, as a comet’s tail does.
Color
The ribbon’s one colour: three numbers and an alpha. A warm 1, 0.8, 0.45, with an alpha of 0.8 that lets the sky show through.
Lifetime
Seconds a recorded point lasts before it drops off the ribbon’s tail: 0.8. 0 means no time limit. A longer life makes a longer ribbon, up to Max Length.
Max Length
The longest the ribbon may be, in world units: 90. 0 means no length limit. With both this and Lifetime at 0 the engine caps the ribbon, so it cannot grow for ever.
Min Distance
How far the node must move before another point is recorded, in world units: 3. Smaller gives a smoother curve, and more points to draw.
Break Speed
Above this speed, in world units a second, a move counts as a jump: the ribbon breaks there and starts again, instead of drawing a line across the jump. 1200 here. 0 switches the guard off.
Sort Layer
Where the ribbon is drawn among the town’s layers, like the emitter’s: lower first, so further back. 6 puts it behind the bat’s own 7.
anneal_by and wait_frames
anneal_by carries the node by an amount over a time, along an easing: here .InOutSine, which starts and ends gently. wait_frames waits that many frames, where wait waits seconds.
Home and Swoop
Where each round starts, 150, 150, and the seconds one swoop takes, 0.9. A swoop of 0.6 to 1.2 seconds reads as a bat’s flit.
The sky over the roofs a second and a half into a round: the small bat with its glowing lantern at the middle of the picture, and behind it a warm orange ribbon, thick at the bat and thinning to a point, stretching up and to the left; a dark smudge of soot to its right, chimney smoke below and rain falling
The bat in its second swoop, its ribbon following.
The same sky in the third swoop: the bat at the upper right of the picture, and its ribbon, a warm orange line thinning to a point, stretching behind it down to the left
In its third swoop.
The same sky at the end of the round, 3.5 seconds in: the small bat back at the left of the picture with a faint glow round it, and no ribbon behind it; rain, chimney smoke and fireflies below
The leap home: the ribbon has broken, and nothing is drawn back across the sky.
The same moment with Break Speed 0: the bat back at the left, and from it a long straight orange line running to the right for about a fifth of the width of the picture, the ribbon drawn back along the leap
For comparison, Break Speed 0, the guard off: the jump home drawn as a line across the sky.

The recipes

The lamps

FieldValue
ambient, an Ambient LightPosition 8, 8, Color 0.3, 0.34, 0.42, alpha 1
lamp_1 and lamp_2, LightComponentKind Point, Radius 150, Intensity 2.4, Color 1, 0.78, 0.48, alpha 1
lamp_1 and lamp_2, SpriteUnshaded ticked

What glows

NodeValue
forge_fire, forge_sparks, fireflies, well_ring, shooting_star, fireworksUnshaded ticked
firework, in firework.artifactUnshaded ticked

The moths

FieldValue
Nodemoths, a Particles node, Position 242.5, 248
Rate6
Lifetime4
Speed10
Spread7
Gravity0
Start Color0.9, 0.9, 0.85, alpha 0.9
End Color0.9, 0.9, 0.85, alpha 0
Start Size2
Capacity24
Prewarm4
Sort Layer6
Light Onlyticked
Texturesoft-dot.png
ShapeBox, Size 56, 36
NoiseStrength 20, Frequency 0.08, Scroll 0, 0, Seed 3

The glow

FieldValue
Nodeglow, a Post Volume, Position 320, 180
AreaGlobal
Exposure1
Tint1, 1, 1
Saturation1
Vignette0.35
Bloom Enabledticked
Threshold0.55
Intensity1.4
Blur Pairs2
Spread3
Aberration, Grain, Grain Rolloff0, 0, 0.85

The sparks’ colours

FieldValue
Nodefirework, in firework.artifact
Lifetime1.4
Speed90
Spread7
Gravity40
Start Color1, 0.9, 0.7, alpha 1
End Color1, 0.6, 0.3, alpha 0
Start Size5
One Shotticked, Burst 48, Capacity 48
Sort Layer-22
BlendAdd
Unshadedticked
Texturesoft-dot.png
Drag0.8
Inherit Velocity0.5
Custom Dataticked: X Random 0 to 1; Y Over Life, a curve from 1 at the left to 0.7 at the right; Z Fixed 0.85; W Random 0 to 6.28
ShaderMaterialComponentShader spark-hue.wgsl, twinkle 9

The lantern-bat

FieldValue
Nodebat, a Sprite, Position 150, 150
Texture, Size, Sort Layerbat.png, 12, 8, 7
TrailComponentEmitting ticked, Head Width 3, Tail Width 0, Color 1, 0.8, 0.45, alpha 0.8, Lifetime 0.8, Max Length 90, Min Distance 3, Break Speed 1200, Sort Layer 6
LightComponentKind Point, Radius 36, Intensity 1.2, Color 1, 0.75, 0.4, alpha 1
BatHome 150, 150, Swoop 0.9

Check it works