Article 6

Lovingly reproduced from the Game Engine Black Book: Wolfenstein 3D by Fabien Sanglard.


How Wolfenstein 3D Draws Its World

Lovingly reproduced from the Game Engine Black Book: Wolfenstein 3D by Fabien Sanglard.


Each frame of Wolfenstein 3D goes through five stages: clear the screen with ceiling and floor colors, draw the walls, draw the sprites, draw the weapon, and flip to the next framebuffer. That's it. Simple in concept. Wildly complicated in practice.

Fixed-Point: Faking Fractions Without Floating Point

Before the engine can draw anything in 3D, it needs to handle trigonometry. Sine, cosine, tangent: these are the foundations of 3D math, and they deal in fractions. But the 386 can only do integer arithmetic at useful speeds.

The solution is called fixed-point arithmetic. Instead of using the float type (which is accurate but impossibly slow on a 386 without a math co-processor), the engine repurposes integers by treating some of the bits as a fractional part. A 16:16 fixed-point number uses the upper 16 bits as the integer part and the lower 16 bits as the fraction. The CPU operates on it exactly like any integer: addition and subtraction work identically, and multiply/divide just require an extra shift. The CPU never knows it's dealing with fractions. Speed is preserved, precision is good enough.

The player's position in the world is stored as a 16:16 fixed-point value. Column heights are stored as 29:3.

The Square World

Wolfenstein 3D's maps are grids of 64x64 blocks. Every wall is aligned to this grid. Every wall is the same height. There are no slopes, no stairs, no ceilings of varying height. These constraints look like limitations, and they are, but they are also the key to everything.

Because the world is a grid of axis-aligned blocks, finding where a ray (cast from the player's viewpoint) hits a wall becomes a deterministic, cheap operation. Instead of checking every surface in the world, the engine only needs to check where the ray crosses grid lines.

This algorithm is called DDA (Digital Differential Analyzer). Once you know where a ray first crosses a horizontal or vertical grid line, every subsequent crossing in the same direction is just two additions away. The engine casts one ray per pixel column on screen (320 rays for a full-width view), and each ray finds its wall intersection through a series of cheap additions and lookups.

As Carmack himself noted: "The resulting code was small and very regular compared to the hairball of my wall span renderers, and it did deliver the rock-solid feel I wanted." Earlier games (Hovertank One and Catacomb 3-D) had used a different approach that produced occasional graphical glitches. Raycasting eliminated those entirely.

The DDA is implemented in 740 lines of handwritten assembly. It checks both horizontal and vertical grid intersections per ray, ping-ponging between two loops via goto statements. On a modern CPU with deep pipelines and instruction caches, this would be disastrous. On the 386 with its shallow pipeline and no cache, it's fine.

The Fisheye Problem

Once a ray hits a wall, the engine calculates how tall the wall column should appear on screen. The naive formula is simple: column height = scale factor / distance. But which distance?

If you use the direct geometric distance from the player to the wall intersection, you get a fisheye effect. Walls on the sides of the screen appear smaller than walls directly in front, even if they're the same distance away. This creates a disturbing barrel-lens distortion that makes straight walls look curved.

The correct approach uses the projected distance: how far away the wall is as measured along the camera's forward axis, not along the ray. This is calculated using the same SOH-CAH-TOA triangle math taught in high school. The player's view angle is used to project the raw ray distance onto the forward axis, and the result gives correct, undistorted perspective.

Drawing Walls: Two Speed Tricks

With column height calculated, drawing a textured wall column should be simple: loop through the pixels, sample the texture, write to the screen. For a 386, this is still too slow.

The engine uses two tricks to make it fast enough.

Compiled scalers. Scaling a 64-texel tall texture to any height requires a loop with a step accumulator. Loops are expensive because of branch instructions. The engine's solution: eliminate the loop entirely. At startup, the engine generates machine code on the fly, creating 136 custom scaling functions covering every relevant column height. These functions are just linear sequences of read-this-texel-write-this-pixel instructions, with no branches, no accumulator, no overhead. A scaler that writes a 4-pixel column takes 25 instructions. A scaler for 128 pixels takes 705 instructions. There is no faster way to do it.

These precompiled scalers take up about 83KB of RAM.

Deferred column rendering. The VGA's bank structure, which seems like a curse, can become a performance advantage. Because consecutive columns on screen are stored in consecutive banks, if multiple adjacent columns share the same wall texture and the same approximate height, they can be drawn with a single pass using the VGA mask register (which writes to multiple banks simultaneously). The engine groups up to 8 similar columns together and writes them in as few as one pass, with "free" columns drawn without any extra write operations. In close-up scenes with large magnified walls, this cuts the number of write operations by around 50%.

The Light Trick

Wall textures were drawn twice by Adrian Carmack: once lit and once dark. At runtime, if a ray hits a vertical wall (aligned to the Y axis), the lit version is used. If it hits a horizontal wall (X axis), the darker version is used. This cheap directional lighting, baked entirely into the assets, makes scenes look dramatically more three-dimensional than a flat-shaded equivalent. It costs almost nothing at runtime.

Drawing Sprites

After walls are done, the engine draws sprites: enemies, items, decorations. Three steps: figure out which sprites are visible, figure out which parts of each sprite aren't hidden behind a wall, and draw what remains.

Visibility is tracked during raycasting. As each ray travels toward a wall, it marks every grid tile it passes through in a 64x64 boolean array. Any sprite on a marked tile is potentially visible.

Sprites are drawn back-to-front (furthest first) using a simple O(n²) sort. Since the maximum number of visible sprites is capped at 50, this is fast enough in practice.

Sprite columns use the same compiled scalers as walls, but with a modification: sprites have transparent pixels (color 0xFF in the palette, always skipped). To handle this, sprites are stored as a list of commands per column, each command specifying an offset, a length, and a payload of texels. The engine patches the compiled scaler with an early-return instruction before calling it, so the scaler stops when the opaque region of the sprite column ends. The patch is undone immediately after.

For clipping (determining what part of a sprite is hidden behind a wall), the engine keeps an array of wall heights for each screen column, written during raycasting. If a wall column is taller than the sprite column at the same screen position, the sprite is behind the wall and that column is skipped.

The "Call Apogee" Easter Egg

In map E2M8, deep inside a forest of push walls and past several bosses, there's a sign that reads "Call Apogee and say Aardwolf." It was meant to be the start of a contest: the first person to find it would win a prize. The contest was cancelled almost immediately after launch, when players released cheat programs and level editors that made finding the sign trivial. The sign was left in anyway. Apogee continued receiving letters about it for years.

What is Aardwolf? A maned mammal of southern and eastern Africa related to the hyena. It was the first image file in the NeXT computer dictionary on all of id's workstations. That's why it was chosen.


To return to the articles section, click here. 

Scroll to top
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.