Fire material¶
Flames have a material of their own. Until 2026-10-05 they were drawn as any glowing tile: a cut-out with emission from its bright texels, which showed hard pixel edges, cast a sun shadow of itself, and had one flat brightness from base to tip. The game's own animated art is still the flame. Goanna swaps the frame a vanilla client shows and only changes how each texel is drawn.
What is a flame¶
src/goanna_flame.h decides, from the node definitions the server sends,
never from a list of games:
- every tile of a node with the
firelikedrawtype; - an animated tile of a glowing (
light_sourceabove 0) mesh, plantlike or nodebox node whose first image is named with the wordfire,flameorflames; - an upright sprite entity whose texture is named that way.
The word is a whole word between underscores, so campfires, firefly
and fireworks do not count. The animation and shape requirements keep
out fire coral, Ethereal's fire flower, Caverealms' fire vine and Kythen's
lamp faces, which are still images or cube faces. A torch is not a flame
here: its tile is the stick as well as the flame.
Checked against the node definition dumps of 2026-09-29
(goanna_audit/w_*/matdump.json), and in src/goanna_flame_test.cpp:
| Game | Taken | Needs nothing else |
|---|---|---|
| Minetest Game | fire:basic_flame, fire:permanent_flame (firelike) |
It has no campfire, candle or burning entity |
| Mineclonia | fire, eternal fire, soul fire (firelike); both campfires' flame tile; lit candles and candle cakes; the burning entity sprite | |
| VoxeLibre | fire, eternal fire, soul fire; both campfires; the burning entity (glow -1, still taken) | It has no candles |
| Asuna | MTG fire; Everness' three permanent flames (firelike); x_farming candles; Everness' candle skull and forsaken fire; Caverealms' constant flame (plantlike) | |
| Kythen | nothing | Its qulliq lamp's flame is a cube face and stays as it was |
Only Mineclonia has been looked at in a running client. The rest is the rule applied to the dumps.
A Mineclonia candle or campfire flame is a culled mesh tile, which Goanna
would otherwise draw from its animation array. A flame keeps its single
image instead (GoannaClient::keyForIrr), whose frames are swapped as
upstream swaps them, so it can take this material.
How it is drawn¶
project/shaders/flame.gdshader, then flame_glow.gdshader (the halo)
and flame_shimmer.gdshader (the heat shimmer) as next passes; all three
share flame_common.gdshaderinc.
- Edges. The flame is blended, not cut out. Inside it every texel is whole, so close up it is still the art's pixels. Each texel on the edge fades over its outer half, from four reads of the base level. Once a texel is smaller than a pixel the mip's own alpha takes over.
- Heat. Each texel's heat is its own brightness against the art's
luminance range, cooling towards the top of the frame. Hot texels are
pulled towards a whitened core colour and brightened up to 2.8 times;
cool ones towards the tip colour and dimmed. The core and tip colours
are the mean of the brightest sixth and darkest quarter of the art's
opaque texels over all its frames (
measureFlameRamp). Soul fire's art is cyan, so it ramps from deep teal to pale cyan without being named. The hot core is nearly opaque and the tips let a third of what is behind them through. - Animation. The game's frames and their timing are untouched. A flicker of three waves travelling up the flame modulates brightness and heat between frames, larger at the tips, with a phase per node. It runs on the node animation clock in whole cycles a minute, so it does not jump when the clock wraps, and pinning the clock pins it.
- Halo. The glow pass adds the core colour where a coarser mip of the art's alpha reaches past the sharp edge. It is never negative, so halos that overlap only add up to a brighter glow. On an entity's sprite sheet the coarse read also reaches the next frame up, so there the halo fades out over the five texels at the top and bottom of the cell; without that a burning mob had a flat glowing box over its head.
- Shimmer. The shimmer pass adds the difference between the opaque
scene at a displaced point and at the pixel, over the flame and above
it. A firelike quad's top edge is raised 0.6 nodes in that pass, with its
texture coordinate, to make room above the flame. The bend is a few
pixels at two nodes and fades out by 24. The difference is held to 0.08
a channel: the pixel it adds to already holds the flames, and a bend
from a bright face into a dark joint behind them cut thin black cracks
into the flame. It is off on Lowest and Low
(
render_fire_shimmer, render feature switches), where its quads collapse to a point and nothing is drawn.
Godot's screen texture is taken before the blended pass, so it holds no water and no glass. Drawing it would cut a window through a lake behind a fire. Added as a difference, the water or glass behind keeps its colour and only the opaque detail through it wavers.
A difference is only right once per pixel. Until 2026-10-06 the shimmer
was part of the glow pass and every quad added its own: a firelike node is
six quads, and a field of fire stacks dozens behind one pixel, so a pixel
moved several times as far as the bend, past black where the scene behind
was bright (a band of dark noise over a fire) and into cyan and green where
only its red channel went under (the far rows of a field). The shimmer pass
now uses the stencil buffer: a pixel takes the shimmer of the first layer
drawn there (stencil_mode read, write, compare_not_equal, 7), and pixels
the shimmer does not reach are discarded before they can claim it. Nothing
else in Goanna uses the stencil.
Fitting in¶
- Lighting. The flame is unshaded. The light it casts is still the node
lamp's, with its own flicker (
update_lights), which this does not touch. The two flickers are not in step. - Shadows and depth. Flames write no depth and cast no shadow, so the depth prepass, SSAO and the shadow maps do not see them. The cut-out flame used to cast a sun shadow of itself.
- Fog and water. Godot's fog applies to the flame pass as to any blended
surface. Seen from under water, both passes take the water's absorption
(
underwater.gdshaderinc); the glow and shimmer passes, which have Godot's fog off, also takes the fog's share. - Sorting. Flames go on a mesh instance of their own under each block
(
flames), never into regional batches, so they sort by their own bounds rather than the whole block's water and glass. Within a frame, Godot still sorts blended instances by their centres; see Limits. - Micro shadows, parallax. Neither touches a flame. The netherrack under a fire keeps its own.
- SDFGI. The cut-out flame fed its emission to SDFGI on Medium and above. A blended, unshaded surface does not; the lamp still lights the surroundings.
GoannaClient.set_flame_material(false), or GOANNA_FLAME_MATERIAL=0 at
launch, draws flames the old way, for comparison.
Review¶
First drawn on 2026-10-06 by the render service on the RTX 3090 (Godot
4.5.1, Luanti 5.17.0 server, Mineclonia release 38561, tier High), old
against new by day and at night. All three shaders compiled. The first
frames showed the stacked shimmer described above; the frames after the
fix, the cause and the timings are in
docs/perf/fire-shader-2026-10-05.
On the nine by nine field the material costs about 0.6 ms of GPU time more
than the old flame, with or without the fix; the shimmer is about 0.1 ms
of that. GOANNA_FLAME_MATERIAL=0 falls back to the old flame.
Looked at again on 2026-10-10, rebased on main, by the render service on
the same card, server and game, Godot 4.5.1-stable, tiers High and Low,
each launch on a fresh profile read back (on High mat_parallax 1,
mat_micro_shadow 1, mat_parallax_short 0, render_fire_shimmer 1,
view_range 12; on Low render_fire_shimmer 0 and view_range 6). What
was seen, frames and jobs in the same directory:
- Fire on netherrack, soul fire, the campfire, lit candles, a fire in front of water, fires beside and behind glass and the nine by nine field all draw on their nodes, by day and at night, with softer edges and a brighter core than the old flame, and without its blown out white over the field and the candles. No dark band and no speckle. Soul fire stays cyan. On Low the flames are the same and the shimmer is gone.
- They animate: three frames of one close pose taken ten seconds apart differ in 16 to 20 percent of their pixels.
- The burning zombie's flame drew nothing of its own. The upright sprite's cell rectangle is min and max corners and the flame passes read it as origin and size; once that was fixed the halo still drew a flat box above the head, from the frame above in the sheet. Both are fixed. The flame now shows as upstream draws it, one plane through the zombie's middle the zombie's height, so from the front and back it shows beside the legs and as a spark over the head, and edge on from the side not at all. Checked with the body hidden, through the control channel, where the whole flame shows.
- Close up, the shimmer cut thin black cracks into a fire on netherrack, gone with it off. The limit above removed them.
- The fixture's burn is unreliable: in the full run after the fixes no variant, old or new, had a flame on the zombie, and in the zombie job after it the old flame variant had none while both new ones did. A missing flame on the fixture's zombie is not by itself a client fault.
Not tried: any game but Mineclonia in a running client, a burning player, a live server other than the render service's fixture, and timings after the 2026-10-10 fixes. The owner has not yet judged the look.
What has been run, with the 2026-10-10 build:
cmake --build build --target check: every native test, includinggoanna_flame_test, the recognition rule on the names in the def dumps.project/tests/graphics_profiles.gdandproject/tests/render_features.gd, headless with a scratch profile, passed with the new gate.
The 2026-10-05 fixture in that directory was never run; both reviews used the render service's stage with the same set of fires.
Limits¶
- The halo and the shimmer are two more passes over every flame quad. At Lowest and Low the halo is still drawn and the shimmer's quads collapse, but the shimmer material still declares the screen texture, so Godot copies the screen whenever a flame is in view, as it does for water and glass.
- Where flames overlap on screen, a pixel shows the shimmer of the first one drawn there, which is the farthest, because blended passes are drawn back to front. A near flame does not bend what is seen through a farther flame's shimmer.
- Embers were not added.
- Godot sorts blended mesh instances by the centre of their bounds. A flame and a large water or glass instance that overlap on screen can still be drawn in the wrong order; see the review for what was seen.
- Kythen's qulliq flame, a cube face, is not recognised.