Glow sprites
A small glowing thing vanishes from far: a 5 cm run of resin 100 m away is a third of a pixel across, and the frame draws it in some frames and not in others. A glow sprite keeps it in sight. Once the thing is smaller on the screen than minPixels, a soft sprite that many pixels wide stands in for it, as bright per pixel as the thing up close. The engine draws every glow sprite of the world in one instanced draw.
Register a point with registerGlowSprite(world, options) from a view, with the world useWorld returns, and call the Disposer it returns to take the point off. The options are a GlowSpriteOptions record you keep. The engine reads it each frame, so move a point by writing its position in a useFrame, and change it by writing its other values. Set intensity to 0 to put a point out for a while, such as a window at dawn, and take it off when it is gone for good. A value written later is checked as it lands, in development, as the registration checked the first.
For a point on an object a view holds, such as a lantern’s flame, mount <GlowSprite> under the object instead: it rides the object, and it goes when the view unmounts.
The sprite covers the distance alone. Up close, the game draws the thing itself, such as a window’s glowing mesh, and the sprite takes over as that mesh shrinks to a few pixels.
The values
Section titled “The values”registerGlowSprite takes these, and <GlowSprite> takes them as props:
position: where it glows, in metres, aVector3you move in place or any{ x, y, z }. On<GlowSprite>it is an[x, y, z]offset from the object it is mounted under,[0, 0, 0]when left out.color: a CSS colour string, a number such as0xffb04a, or aColoryou may change in place. A warm amber,#ffb04a, when left out.intensity: as an emissive surface’s intensity, 2 when left out. The sprite’s middle is the colour times this, so give the near thing’s own emissive values. Past the look’s bloomthreshold, 0.5 in the engine’s night look, it blooms, where the quality level draws bloom: Medium and High.size: the metres across the thing that glows, 0.2 when left out, such as 0.05 for a run of resin or 0.3 for a lantern’s glass.minPixels: the drawing-buffer pixels the sprite never shrinks below, 3 when left out. At a pixel ratio of 2, as High draws, 3 of them are 1.5 CSS pixels. Its full-bright middle is about this wide, with a soft edge past it.
The sprite fades in as the thing shrinks from twice minPixels across to minPixels, and draws nothing while the thing is larger: the thing’s own glowing mesh carries it there. A 30 cm lantern under a 55° view on a 720 pixel tall canvas is 6 pixels across at about 35 m, so its sprite starts there.
What it keeps from the frame
Section titled “What it keeps from the frame”- Blending. The sprite adds its light to the frame behind it, as a glow does, so it shows on a dark wall and barely on a bright sky.
- Brightness. The sprite holds its brightness per pixel at the floor, so a glow 300 m away is as bright at its middle as the same glow 40 m away, and never brighter than the thing up close.
- Fog. The scene’s plain fog dims it by the fog at its own distance: past the fog’s
farnothing of it is left, and the engine’s night look’s fog is full at 90 m. Give the night a fog that keeps the distance you want seen, such as afarof 700 m for lights seen 300 m away. A light mask’s thicker air in the dark dims it as it dims the ground there, so a glow in the dark fades sooner than one in the light. The height fog of Medium and High dims it with the frame behind it. - What stands in front. The ground and anything solid hide it: a hill, a trunk, a wall. The sprite stands nearer the camera than its point by its
sizeand 25 cm, at any distance, so the bark or the glass it sits on never cuts it. Something solid closer in front than that, such as a sill just in front of a window, does not hide it. - Leaves. A leaf or needle card in front hides the pixels it covers, as anything solid does. The sprite’s soft edge reaches past its middle, so a gap between needles lets part of it through rather than all or none.
Sample
Section titled “Sample”The engine’s tests run this file: every run of resin across a forest, and a lantern’s view with its glass and a sprite at its middle. Keep the list of runs the same array between renders, as a memo or a store keeps it: a new array takes every point off and registers it again.
import { useLayoutEffect } from "react";import { GlowSprite, registerGlowSprite, useWorld } from "@spawnite/engine";
/** A run of resin on a trunk: the point on the bark it glows from, in * metres. */export interface ResinRun { x: number; y: number; z: number;}
/** Every run of resin across the forest as a glowing point from far: one * registration a run, all taken off and made again when the list * changes. Mount it in a view. */export function ResinGlows({ runs }: { runs: readonly ResinRun[] }) { const world = useWorld(); useLayoutEffect(() => { const removals = runs.map((run) => registerGlowSprite(world, { position: run, color: "#ffb04a", intensity: 2, size: 0.05, }), ); return () => { for (const remove of removals) remove(); }; }, [world, runs]); return null;}
/** A lantern's view: its glass, glowing, and a glow sprite at its middle * that keeps it in sight once the glass is a few pixels across. */export function LanternView() { return ( <group> <mesh position={[0, 0.9, 0]}> <boxGeometry args={[0.25, 0.35, 0.25]} /> <meshStandardMaterial color="#ffcf7a" emissive="#ffcf7a" emissiveIntensity={3} /> </mesh> <GlowSprite position={[0, 0.9, 0]} color="#ffcf7a" intensity={3} size={0.3} /> </group> );}What it costs
Section titled “What it costs”Every sprite of the world draws in one instanced draw, and a world with none draws nothing. Each frame the engine reads each record and uploads only what changed, so a still forest uploads nothing. A sprite costs the few pixels it covers: at most a square about 11 drawing-buffer pixels a side with the default minPixels, as it fades out.
Measured on 2026-10-16 from a lookout over 300 sprites, and over 3,000 to see past the noise, on a desktop RTX 4070 SUPER shared with other work, with the sprites on and off from the same camera:
- At Low, 1,280 by 720 pixels, 3,000 sprites added about 0.24 ms of GPU time a frame, so 300 cost about 0.02 ms, below what a frame’s own spread shows.
- At High, 2,560 by 1,440 pixels, the frame’s spread from run to run, 11 to 18 ms, hid any difference at either count.
- Bloom costs nothing more for them: the level’s bloom draws the whole frame whether sprites are in it or not.
A phone or a laptop at Low was not measured.
What the other engines do
Section titled “What the other engines do”Unity’s Lens Flare (SRP) hangs one flare on one light, and its particles clamp a sprite’s screen size with minParticleSize. Unreal’s Niagara sprite renderer keeps a sprite under a pixel with PixelCoverageMode, which adjusts its colour for its coverage. Godot’s Sprite3D has fixed_size, Roblox’s BillboardGui a size in pixels, and PlayCanvas’s particles a scaleGraph. Each draws a sprite at a fixed or chosen size; none holds a floor under a size in metres. This engine takes the sprite they all draw and Unity’s minimum size, and holds the brightness per pixel where Niagara adjusts it, so a far glow never outshines a near one.
Where it runs
Section titled “Where it runs”Client. A room keeps a registration and draws nothing, and <GlowSprite> renders nothing there. Under WebGPU no sprite draws, as WebGPU lists.