A minimal Wolfenstein 3D raycasting engine

ibobev1 pts0 comments

A minimal Wolfenstein 3D raycasting engine Skip to content<br>Wolfenstein 3D (id Software, 1992)[04] rendered first-person corridors on 286/386-class hardware with no GPU, no z-buffer, and no polygons at all. The trick that made it possible is raycasting: the world is stored as a flat 2D grid, the camera never leaves the plane of that grid, and the renderer does only as much work as there are columns of pixels on screen. One ray per column, marched until it hits a wall, is enough to reconstruct a fully textured, moving 3D scene [01] [02].<br>This post builds a minimal version of that engine in plain JavaScript on top of Canvas2D. All of the art: walls, sprites, and weapons, is generated procedurally at runtime; nothing is copied from the original game’s released source[06]. It is an homage to the technique, not a clone of the original game and assets.<br>Here is the finished engine:<br>Loading

The world is a grid<br>The map is authored as an array of strings for readability, one character per tile, and then parsed once at startup into a flat Uint8Array:<br>const MAP_ROWS = [<br>'1111111111122222222222',<br>'1.........1..........2',<br>'1.........1..........2',<br>'1..11D11..1...22D22..2',<br>'1..1...1..1...2...2..2',<br>'1..1...1..1...2...2..2',<br>'1..11111..1...22222..2',<br>'1.........1..........2',<br>'11111D11111..........2',<br>'3.........3333D3333332',<br>'3....................3',<br>'3..33333333..44444...3',<br>'3..3......3..4...4...3',<br>'3..3......D..4...D...3',<br>'3..3......3..4...4...3',<br>'3..33333333..44444...3',<br>'3....................3',<br>'3..4444444444444.....3',<br>'3..4.............4...3',<br>'3..44444444444444....3',<br>'3....................3',<br>'3333333333333333333333',<br>];<br>const MAP_W = 22, MAP_H = MAP_ROWS.length;<br>if (MAP_ROWS.some((r) => r.length !== MAP_W)) throw new Error('bad map row');<br>const map = new Uint8Array(MAP_W * MAP_H);<br>for (let y = 0; y MAP_H; y++) {<br>for (let x = 0; x MAP_W; x++) {<br>const c = MAP_ROWS[y][x];<br>map[y * MAP_W + x] = c === '.' ? 0 : c === 'D' ? 5 : c.charCodeAt(0) - 48;

A . is empty floor, digits 1-4 select which procedural texture a wall tile uses, and D marks a sliding door (handled separately, see the doors section below). Every row must have exactly MAP_W characters or the map fails to load; there is no implicit padding.<br>The player is a small bundle of vectors, not a matrix or a camera object:<br>const player = { x: 2.5, y: 2.5, dirX: 1, dirY: 0, planeX: 0, planeY: 0.66 };

dirX, dirY is the unit-length view direction. planeX, planeY is the camera plane: a vector perpendicular to the view direction whose length sets the field of view. For a screen column x out of FB_W columns, the raycaster maps that column to a position on the camera plane:<br>\[\vec{r} = \vec{d} + cameraX \cdot \vec{p}\]where cameraX = 2x/W - 1 runs from -1 at the left edge of the screen to +1 at the right edge, d is the direction vector, and p is the plane vector. With |p|/|d| = 0.66, the half-angle to the screen edge is atan(0.66), so the full horizontal field of view is 2 atan(0.66), about 66.8 degrees, matching the classic Wolf3D FOV.<br>Finding the wall: DDA step by step<br>Given a ray direction for a screen column, the renderer needs the exact distance to the nearest wall along that ray. A naive approach would march the ray forward in small fixed steps and check the tile at each point, but that either skips thin features like corners when the step is too large, or wastes enormous amounts of work re-checking the same tile when the step is small enough to be safe.<br>The grid structure gives a better way. A ray only ever changes which cell it occupies at a grid line, a vertical or horizontal boundary between tiles. So instead of marching in fixed steps, the ray can jump directly from boundary to boundary, doing exactly one tile lookup per crossing. This is the DDA (digital differential analyzer) algorithm described by Amanatides and Woo[03], also written up for raycasting engines specifically by Lodev[01].<br>The quantities involved are:<br>\[\Delta_x = \left| \frac{1}{r_x} \right|, \quad \Delta_y = \left| \frac{1}{r_y} \right|\]deltaDistX and deltaDistY are the distance traveled along the ray between one vertical grid line and the next, and between one horizontal grid line and the next, respectively. Because the ray direction is fixed, these are constants for the whole cast; only the reciprocal of each ray component is needed. The same stepping idea, advancing along whichever axis is closer, appears generally as the digital differential analyzer algorithm for line drawing[05].<br>sideDistX and sideDistY start out as the distance from the player to the first boundary crossing on each axis, then get extended by deltaDistX/deltaDistY every time that axis is stepped. The loop invariant is simple: at any point, sideDistX and sideDistY hold the total ray distance at which the ray will next cross an x boundary and a y boundary. Whichever of the two is smaller tells you which boundary the ray reaches first, so each iteration advances that one and steps the corresponding map...

grid boundary map_w screen tile const

Related Articles