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

Your renderer and your own three.js

Game draws one canvas with three’s WebGLRenderer, made the way React Three Fiber makes it. Three props on Game change that renderer, and fiber’s useThree hands it to any component for what can change while the game runs. Each prop takes the shape fiber’s own <Canvas> takes, so a game that knows fiber knows them.

Prop What it sets
gl The settings three’s WebGLRenderer is made with, over fiber’s defaults, or a factory that makes the renderer itself and may return a promise. A factory may return a WebGPURenderer.
shadows How the shadow map filters a shadow’s edge: "basic", "percentage" (the default) or "variance", or false for no shadow map at all.
onCreated A function Game calls once the canvas has made its renderer, scene and camera, before its first frame.

Game reads all three once, when it mounts, as it reads room and quality. A later render that passes other values changes nothing, so a game that switches renderers reads its choice before Game mounts, such as from the page’s address. gl takes either form, and a game may pick one at run time: gl={useWebGPU ? factory : { stencil: true }}. A scene mounted headless, as a room’s server and spawnite simulate mount it, reads none of them: its useThree((state) => state.gl) is a stand-in that draws nothing.

Some settings belong to the WebGL context, and the browser fixes them when it makes the canvas’s context. Pass them in gl:

Setting Use it for
stencil A stencil buffer, which a portal or a mirror masks the world with.
logarithmicDepthBuffer Depth precision across a planet’s scale, where near and far are kilometres apart.
reversedDepthBuffer The same precision on a GPU that supports it, at less cost than a logarithmic buffer.
alpha false clears the canvas opaque. Fiber’s default, true, clears it clear, so the page shows where nothing draws. The HUD draws in the page over the canvas either way.
preserveDrawingBuffer A canvas that keeps its last frame, for a page that reads it with toDataURL. It can cost a copy of the frame each frame on some GPUs, and the engine’s captures do not need it.
powerPreference Fiber asks for "high-performance". Pass "low-power" for a game that should spare a laptop’s battery.
antialias Fiber turns it on. The look’s post-processing stack draws off the canvas and takes its samples from the player’s quality level instead.

The following game asks for a stencil buffer and blurred shadow edges:

packages/engine/test/outside/StencilGame.tsx
import { Game, Scene, ShadowType, World } from "@spawnite/engine";
import { noon } from "@spawnite/engine/looks/noon";
// A game whose portals mask the world behind them with the stencil buffer,
// and whose shadows blur at the edge.
function Lobby() {
return <World map="grid" look={noon} />;
}
export function App() {
return (
<Game
name="portals"
start="lobby"
gl={{ stencil: true }}
shadows={ShadowType.Variance}
>
<Scene name="lobby" component={Lobby} />
</Game>
);
}

shadows takes ShadowType.Basic, ShadowType.Percentage or ShadowType.Variance, or the same words as strings. Fiber’s true and "soft" are not offered: they ask for three’s soft filter, which three r186 removed, so three would warn and fall back to the percentage filter. A variance map blurs each light’s shadow by its shadow.radius and shadow.blurSamples, at the cost of one blur pass per light; the sun of a look sets its own radius. Fiber puts the shadow type back each time the canvas renders again, as a resize, the devtools’ pause and the player’s frame rate cap each make it, so set the type with this prop rather than on the renderer.

To make the renderer yourself, pass a factory. Game hands it a RendererDefaults: the canvas, powerPreference, antialias and alpha. RendererDefaults, RendererFactory, RendererOptions, GameRenderer and ShadowType are exports of @spawnite/engine. The canvas mounts nothing until the factory’s promise resolves, and the loading screen stays up until the first frame draws. A factory that throws or rejects stops the game: a built game shows the engine’s error screen, and the dev server shows its overlay.

PlayCanvas takes the same context settings in createGraphicsDevice(canvas, options), and Unity and Godot hold the depth, stencil and shadow choices in a project’s settings. Here they belong to the game’s Game, read once, so one page can mount two games with two renderers.

The renderer’s other settings, such as toneMapping, localClippingEnabled or outputColorSpace, can change at any time. Read the renderer with useThree((state) => state.gl) in any component under a scene, and set them in an effect. It is the same renderer gl made, and onCreated hands it over once at the start.

Two settings belong to the engine while its parts run:

  • The daylight rig writes toneMappingExposure every frame from the hour of day, under either renderer. To scale it and keep the rig, set the look’s exposure, which multiplies the rig’s: look={{ base: noon, exposure: 0.6 }}. lighting={false} on the World hands you the exposure, but it also takes away the look’s sun, sky, fill, fog and stack, so you light the scene yourself. The look’s stack tone maps with the same exposure, so the value reaches the screen either way.
  • The player’s quality level sets the pixel ratio. Change a level’s ratio with Game’s quality prop, as Graphics quality shows.

Each frame runs fiber’s useFrame callbacks in order of their priority, lowest first:

Priority What runs
Below 0 Your callbacks that must run before the world steps.
0 The engine’s step, which mounts first, then your callbacks at the default priority, which read the world as this frame’s step left it.
1 and above Whatever renders the frame. The look’s post-processing stack renders at 1. Any callback at 1 or above tells fiber not to render the scene itself.

To draw the frame yourself, such as a second camera’s view or a pipeline of your own, render in a useFrame at priority 1 or above, and pass postProcessing: false to the look so its stack stands aside. The look’s stack also steps aside on its own when a scene mounts its own <PostProcessing>, as Post-processing says.

three r186 can draw a TSL node material under WebGLRenderer: call setNodesHandler(new WebGLNodesHandler()) on the renderer from three/examples/jsm/tsl/WebGLNodesHandler.js, in an effect, before the first node material draws. Lights, percentage shadows, fog, instancing and skinning draw on the first build. Treat it as a preview, not a shader language to build a game on:

  • At most 12 distinct node materials draw. Each takes 2 of the GPU’s 24 uniform buffer slots, and the 13th draws black. 24 is WebGL2’s minimum, so phones sit at or near it.
  • A program that builds again, such as when the sun’s shadow turns on or off with a quality change, uses slots it never frees, and every node material then draws black.
  • The handler draws no variance shadow, so leave shadows at "percentage" beside it.
  • It loads three/webgpu, about 193 KiB gzipped more for a page that uses it.

These numbers were measured on three r186 in Chromium on a desktop GPU. For a shader that works today, use Shader. To run on WebGPU itself, see WebGPU (experimental).