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

The 2D frame

A 2D game plays in the engine’s 3D world: y up, the ground on x and z, metres as the unit. Three values settle how its art sits in that world, and every 2D piece reads the same three:

  • The plane the game plays on, Plane2D.
  • Pixels per metre, how many of the art’s pixels make one metre.
  • Exact colours, from a nearest-filtered image loader and the pixel look.

Unity, Unreal and PlayCanvas run 2D the same way, inside their 3D engine: a sprite is a quad in the 3D scene. Camera2D holds the frame and draws it.

The 2D layer is an entry of its own, @spawnite/engine/2d, so a 3D game downloads none of it. Camera2D, the sprites, the tile maps and the parallax layers come from it. The frame they share stays in the root entry, @spawnite/engine: Plane2D, Frame2DTrait, useSpriteTexture and the pixel look’s entry, @spawnite/engine/looks/pixel. A 2D game imports from both:

import { Plane2D } from "@spawnite/engine";
import { Camera2D, defineSpriteSheet, Sprite } from "@spawnite/engine/2d";

Plane2D names the two planes a 2D game plays on:

Plane Axes The camera looks Screen right Screen up For
Plane2D.Ground x, z down, along -y +x -z A top-down game
Plane2D.Side x, y along -z +x +y A side-scroller

An image’s top row lies toward screen up on either plane. A top-down game keeps the engine’s ground, so its characters walk on the map with the engine’s controller, its NPCs navigate it, and its bodies fall onto it, as in a 3D game.

Pixels per metre is how many of the art’s pixels make one metre of the world: Unity’s and PlayCanvas’s pixels per unit. The engine’s value is 16, the tile most pixel art packs draw at, so a 16 by 16 tile is one metre square and a 32 by 16 sign is 2 m by 1 m.

Every sheet, tile set and camera in a scene shares one value. Art drawn at a different value from the camera’s lands off the camera’s pixel grid and shimmers as it moves, so a development build throws an error naming both values, and a built game logs it. A value of 0, below it, or not a finite number is refused the same way.

useSpriteTexture(image, options) loads a sprite’s or a tile set’s image for a material’s map. It loads the image in sRGB with no mipmaps and nearest filtering, so each pixel of the art draws as a square. Every view that names the same file shares one texture, and the texture is freed when the last of them unmounts. It suspends while it loads, so mount it under a Suspense.

The image is a URL or a painted definePixelTexture. A painted one is painted on first use from the game’s texture seed, its colour and coverage alone, as a sheet painted in code is. The following sample loads a URL:

packages/engine/test/outside/PixelSign.tsx
import { useSpriteTexture } from "@spawnite/engine";
// A 32 by 16 pixel sign at the engine's 16 pixels a metre: 2 m by 1 m,
// nearest-filtered, cut out where the art is transparent.
export function PixelSign() {
const texture = useSpriteTexture("/assets/sign.png");
return (
<mesh position-y={0.5}>
<planeGeometry args={[2, 1]} />
<meshBasicMaterial map={texture} alphaTest={0.5} fog={false} />
</mesh>
);
}

For art that is not pixel art, pass { filter: TextureFilter.Linear }, Godot’s name: each screen pixel then blends the four nearest pixels of the art. A file that does not load throws an error naming it. A failed file stays failed until the page loads again.

The sign’s material is the one a sprite draws with:

  • Unlit, MeshBasicMaterial, so the art’s colours are not shaded.
  • Cut out at half alpha, alphaTest={0.5}, so it writes depth and the depth buffer sorts it.
  • Unfogged, fog={false}, since a 2D camera’s height above the ground is no distance.

The pixel look draws an unlit pixel as the file holds it by day, and darkens it with the exposure at night. Import it from @spawnite/engine/looks/pixel and hand it to a Landscape’s look, as any look.

It maps the frame with no tone curve, ToneMappingMode.LINEAR, and turns off fog, bloom, ambient occlusion, the vignette, the character light and the sun’s shadow. It keeps the exposure and the grade. Unity’s 2D renderer and Godot’s canvas draw sprites the same way by default. Pick it for any 2D game, pixel art or smooth.

A 3D look, such as noon, maps the whole frame with ACES, sprites included, so it moves every colour in the art. A material’s toneMapped: false does not stop that: the look’s post-processing maps the frame after every material has drawn.

Three things still change a pixel under pixel:

  • A grade other than none, which a look you base on pixel may set.
  • The quality level’s screen-space anti-aliasing, at High, which smooths the steps along an edge in the art. A pixel-perfect Camera2D takes none.
  • Cloud shadows, when a look you base on pixel turns them on: { base: pixel, cloudShadows: {} } drifts soft shade over the ground and the sprites.