Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

Point lights

<PointLight> is a light that shines all round from a point, as three’s PointLight is, with three’s units: intensity in candela and distance in metres. It draws through the Game’s light pool. Every lit program draws a fixed count of lights, the quality level’s maxPointLights, and the pool gives them to the lights in view nearest the camera’s focus. A lamp that mounts compiles nothing, however many stand in the scene.

// Excerpt of packages/engine/test/outside/StreetLamp.tsx
<PointLight
ref={lamp}
position={[0, 2.75, 0]}
color="#ffb46b"
intensity={12}
distance={9}
flicker={0.2}
dayShare={0.05}
priority={2}
/>

A PointLight stands in the scene graph like any object, so it follows its parent: a torch mounted under a character’s hand moves with the hand. It casts no shadow. A room and a headless world draw none. It lights the Game’s own scene: a portal or a render texture, which holds a scene of its own, takes a raw <pointLight>.

Its props are in PointLightProps.

A lamp burns at its dayShare by day and whole at night, blended by the look’s night share, as the live daylight gives it. Give a lamp 0.05: it is dark by day and lit after sunset. A share under 0.45 rises toward 0.45 under overcast weather, so a lamp reads lit in the rain at noon.

A fire or a hearth burns all day: leave its dayShare at 1.

The ref holds the light, a PointLightObject, which you move and write in a useFrame as you would three’s light: intensity, color, distance, decay, flicker, dayShare and priority. It also reads flickerFactor, the factor its flicker multiplies it by this frame, and isDrawn, whether it holds a slot and lights the scene.

A light whose intensity is 0, or that is hidden or under a hidden parent, takes no slot, and goes dark in the frame it turns off. A light that mounts or turns on is ranked on the next frame and fades in over 0.35 s, except on a scene’s first frame, where the first lights draw whole, so a scene opens with its lamps lit.

useEmissive(material, { day, night, light }) drives a material’s emissiveIntensity from day by day to night at night. Windows glow at 1.6 and lantern glass at 2.4 in the diorama look, over its bloom threshold. night takes the material’s own emissiveIntensity when left out, and day takes 0. Unmounting gives the material its own intensity back. The hook runs in a view, under <Views>.

Name the lamp the glass belongs to with light, a ref to its PointLight. The glass then glows as its lamp burns, by the lamp’s dayShare and the weather, dark while the lamp is off or hidden. It takes 60% of the lamp’s flicker, whether or not the lamp holds a slot. A distant window keeps glowing after its light leaves the pool.

The material is any of three’s Lambert, Phong, Toon, Standard or Physical materials, with an emissive colour that is not black. Every mesh wearing the material glows as one, so one hook drives a material, and a second is refused in development. A lantern that flickers with its own lamp wears a material of its own.

Every 0.2 s the pool scores each light in view, and the lowest scores hold the slots. A light’s score is the sum of the following:

  • Its distance from the camera’s focus, with the height counted at half. Where the camera names no focus, as a free camera does, the camera itself.
  • 1.5 m for each step of priority.
  • 10 m times the share of its intensity it lacks at this hour, so a lamp dark by day ranks behind a hearth.
  • Less 2 m for a light that holds a slot already, so two lights at nearly the same score never trade a slot back and forth.

A light that loses its slot fades out over 0.35 s, and the light that won the slot then fades in. A free slot takes a light at once. A light out of the pool keeps its own state, so its glass glows and flickers on.

The devtools’ Frame panel shows the pool as “7 / 12 lights”, and lists each light that holds a slot with its place, its priority and how far it has faded in.

maxPointLights on the quality level sets the slots: 12 at High and Medium, 4 at Low and none at Minimum, where the emissives still glow. Change it with quality.levels, a whole number from 0 to 16. Each slot costs every lit pixel one more light. The diorama’s square at night at High, 1920 by 1080 on an RTX 3070, drew its GPU frame in 2.67 ms with 12 slots and 2.42 ms with none, measured on 2026-10-18.

A scene holds no slots until its first PointLight mounts, so a game with none pays nothing, and it keeps them until the scene changes. A change of the count, from a quality change or a scene’s first lamp, compiles ahead: the old count draws until every lit program for the new count has linked, then the new count lands in a frame that compiles nothing. A scene’s first lamp after its load stays dark until then.

A raw <pointLight> under a Game is outside the pool. It adds to every lit program’s light count, so mounting, unmounting or hiding it recompiles every lit program, and in development the pool warns of each one.

The engine’s tests run this file: a lamppost whose lamp burns at night, flickering a little, and whose glass glows and flickers with it.

packages/engine/test/outside/StreetLamp.tsx
import { useEffect, useMemo, useRef } from "react";
import { MeshLambertMaterial } from "three";
import {
PointLight,
useEmissive,
type PointLightObject,
} from "@spawnite/engine";
/** A lamppost whose lamp burns at night, flickering a little, and whose
* glass glows and flickers with it. */
export function StreetLamp({
position,
}: {
position: [number, number, number];
}) {
const lamp = useRef<PointLightObject>(null);
// Its own glass, so it flickers with its own lamp alone.
const glass = useMemo(
() =>
new MeshLambertMaterial({ color: "#ffe2b0", emissive: "#ffb46b" }),
[],
);
useEffect(() => () => glass.dispose(), [glass]);
useEmissive(glass, { night: 2.4, light: lamp });
return (
<group position={position}>
<mesh position-y={1.3}>
<boxGeometry args={[0.14, 2.6, 0.14]} />
<meshLambertMaterial color="#2c2622" />
</mesh>
<mesh position-y={2.75} material={glass}>
<boxGeometry args={[0.3, 0.36, 0.3]} />
</mesh>
<PointLight
ref={lamp}
position={[0, 2.75, 0]}
color="#ffb46b"
intensity={12}
distance={9}
flicker={0.2}
dayShare={0.05}
priority={2}
/>
</group>
);
}