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

Post-processing

<PostProcessing> mounts a stack of screen-space passes in a scene, inside World, not as a child of Game: Game’s children are DOM, and the stack draws over the canvas. No scene mounts one means no composer and no cost, so a game pays for the stack only where it uses it. The stack’s libraries load as a chunk of their own when a scene first mounts one, and the scene draws without the stack until they arrive.

Its props are in PostProcessingProps.

A prop the scene leaves out follows the player’s quality level, the one Game’s quality store keeps, and changes with it while the game runs. A prop the scene passes always wins over the level.

Level multisampling Bloom draws AmbientOcclusion draws AmbientOcclusion quality halfResolution
Minimum 0 no no “performance” true
Low 2 no no “performance” true
Medium 2 yes yes “performance” true
High 4 yes yes “medium” true

<Bloom> draws at a level whose bloom is on, and <AmbientOcclusion> at one whose occlusion is on, in a scene’s own stack as in a look’s, so the player’s rows mean the same in every scene. A game that wants either at another level turns that one value on for it, as Graphics quality shows. Every other pass, the vignette and a library pass included, draws in a scene’s own stack at every level. Automatic draws the row of the level the engine picked for the device, as Automatic describes, and the player’s Post-processing, Ambient occlusion and Anti-aliasing rows can each take another level’s row. A game changes a level’s row with Game’s quality prop. Ambient occlusion stays at half resolution at every level: at full resolution it cost about 2 ms more of the GPU’s frame on a desktop card, a quarter of the frame’s budget. Unity and Unreal scale post effects per level from their quality settings; here the level the player picks decides, the same one that sets the pixel ratio.

Where the level draws at a pixel ratio of 2 or more, as on a retina screen at High, a stack that names no multisampling draws none. The ratio is the level’s and the player’s render scale’s, before dynamic resolution’s share, so a resolution step never turns samples on and rebuilds the composer. A jagged edge is half a CSS pixel wide there and hard to see, and 4 samples of the half-float frame at that size cost more of the GPU’s frame than bloom does. Godot ships with multisampling off; Unity, Unreal and Roblox tie it to a quality level and not to the screen’s density. PlayCanvas draws at a pixel ratio of 1 unless a game asks for the screen’s own.

A stack takes one of each pass: two of a kind share one set of settings. To turn a library pass on and off, set it from state the stack’s parent holds, so the stack renders again and picks up the change; the engine’s own passes can also be toggled from state inside a child. Ambient occlusion goes first among the children, so bloom and the vignette draw over the darkened contact rather than under it. Tone mapping always runs last, after every other pass, because a pass such as bloom reads the frame’s true brightness rather than the compressed screen curve. The daylight rig’s exposure still applies underneath it: the rig sets renderer.toneMappingExposure from the hour of day, and the composer’s tone-mapping curve reads that same exposure when it draws.

A glow around whatever is brighter than the threshold: the sun, a lamp, an emissive material.

Its props are in BloomProps.

Darkens where surfaces meet: under a tree, at the foot of a wall.

Its props are in AmbientOcclusionProps.

A see-through object, such as a glow, smoke or water, stays unshaded: the pass draws the scene’s transparent objects twice more each frame to find where they cover the occlusion. The engine draws those two targets with its own pass rather than the library’s. The library’s walked the whole scene five times a frame, and each of its two draws updated every matrix and drew the shadow maps again; with 60 skinned monsters on screen that cost 3.7 ms of a 12 ms frame. The engine’s pass gives the same two targets from one walk, and skips the draw of a target no object shows in. Each extra draw still costs every transparent object two more draw calls, so a crowd’s glows and shadows are cheapest as one instanced mesh each.

Darker corners, which pull the eye to the middle of the screen.

Its props are in VignetteProps.

Roblox offers bloom, blur and colour correction as post effects under Lighting. Unity and Unreal build the same passes into a volume-based post stack, and Godot’s WorldEnvironment resource does the same. The engine mounts them as components instead, so a pass turns on or off with the rest of a scene’s JSX rather than a separate settings object, and an agent reads its cost the same way it reads any other child of the scene.

The stack is a PostProcessing row in the devtools tree, with a leaf per pass: Bloom, AmbientOcclusion and Vignette. Each leaf shows the pass’s number props as sliders, and an edit shows on the next frame. A prop the scene changes replaces the edit, and Reset goes back to the props, as for a behaviour. Tone mapping and multisampling are not in the inspector: they are set where the stack mounts. The row cannot be deleted, because the stack’s component owns it.

A devtools capture, an entity’s screenshot or capture(view), draws through the mounted stack from the capture’s own camera, so it shows the frame the player sees, passes and tone mapping included.

Ambient occlusion, bloom and a vignette over the meadow, tone mapped with ACES:

import {
AmbientOcclusion,
Bloom,
PostProcessing,
Vignette,
World,
} from "@spawnite/engine";
export function MeadowScene() {
return (
<World map="meadow">
<PostProcessing>
<AmbientOcclusion />
<Bloom intensity={0.8} />
<Vignette darkness={0.6} />
</PostProcessing>
</World>
);
}

A stack takes any effect or pass from the postprocessing library, one it ships or one you write. Build it once with useMemo and mount it as <primitive object={effect} /> among the stack’s children, where it draws in its place in the order. The library ships depth of field, outlines, pixelation, chromatic aberration, SMAA and a lookup-table grade, among others. To write your own, extend its Effect class with a GLSL mainImage function, which runs once for each pixel of the finished frame. This file turns the frame to night-vision green, and the engine’s tests mount it:

packages/engine/test/outside/NightVision.tsx
import { useEffect, useMemo } from "react";
import { PostProcessing } from "@spawnite/engine";
import { Effect } from "postprocessing";
// Runs once per pixel, after the scene draws: the pixel's brightness in
// goggle green.
const fragment = /* glsl */ `
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
float light = dot(inputColor.rgb, vec3(0.299, 0.587, 0.114));
outputColor = vec4(vec3(0.2, 1.0, 0.3) * light, inputColor.a);
}`;
export class NightVisionEffect extends Effect {
constructor() {
super("NightVisionEffect", fragment);
}
}
export function NightVision() {
const effect = useMemo(() => new NightVisionEffect(), []);
useEffect(() => () => effect.dispose(), [effect]);
return (
<PostProcessing>
<primitive object={effect} />
</PostProcessing>
);
}

Add postprocessing to your game’s dependencies at the version @spawnite/engine names as its peer. A published page then loads the engine’s own copy through the release, as Publishing says, so your effect is one the stack can use. The stack keeps only effects and passes from that copy. A child it cannot draw, such as an effect from another version of the library, throws in development, naming the child and the fix. Outside development, the stack draws without that child and logs the same message once.

Unity takes a custom pass as a renderer feature, Unreal as a post-process material, Godot as a compositor effect and PlayCanvas as a post effect. Here the pass is a class of the same library the engine’s own passes come from, so it mixes with them in one stack.

Client. The stack changes nothing in the world; each player’s browser draws its own frame through it.