Computer Graphics — Interactive Tutorial

2.5D Raycasting
How Wolfenstein 3D worked

Wolfenstein 3D (1992) rendered a convincing 3D world on a 286 processor — with no GPU, no 3D models, and no floating-point unit. The secret was raycasting: a 2D grid, a handful of trigonometry, and one deeply clever perceptual trick. This page shows you exactly how it works.

Interactive demo — drag to move the player
ASCII render mode included
No libraries — vanilla JS + Canvas
The Core Idea

Three things, nothing more

The entire illusion rests on three observations about human vision. Internalize these and the code becomes obvious.

01

The world is a 2D grid

There is no 3D geometry. Every cell in a flat grid is either a wall or empty space. Building heights, floor types, and entity positions are just numbers stored in that grid. The renderer creates the illusion of a third dimension entirely.

02

One ray per screen column

From the player's position, rays are cast across the field of view — one ray per vertical column of pixels. Each ray travels through the grid until it strikes a wall, recording the distance to that wall. A 320-pixel-wide screen fires exactly 320 rays per frame.

03

Distance becomes wall height

This is the trick. A nearby wall hit → tall stripe on screen. A distant wall hit → short stripe. Your visual system interprets "tall = close, short = far" as genuine 3D depth. The renderer paints one vertical rectangle per column. That's the entire render loop.

Why was this revolutionary in 1992? True 3D rendering required processing every pixel in a full perspective projection — far too slow for home hardware. Raycasting cheats: instead of O(width × height) work, it does O(width) work. 320 rays instead of 76,800 pixels. The resulting look convinced millions of players they were inside a real 3D space.
Interactive Demo

See it in action

The left panel shows the true top-down map — the only data that exists. The right panel shows what the player sees. Click anywhere on the map to move the player. Adjust the sliders to understand how each parameter shapes the view.

FOV rays
Wall hit points
Player position (click map to move)
raycasting_demo.js — interactive
top-down map (what actually exists)
first-person view (rendered)
switch render mode ↑
FOV angle 66°
Ray count 40
Look direction 30°
Wall height scale 1.2×
Tip: reduce ray count to 4–8 to see the individual columns that form the entire render. Switch to ASCII mode to see how the city demo replaces pixel columns with characters.
The Math

How the distance becomes height

The formula is three lines. Understanding them is the entire course.

// 1. Cast a ray at angle θ from player position (px, py) dist = castRay(px, py, θ) // march along the ray until a wall cell is hit // 2. Fisheye correction — critical correctedDist = dist * Math.cos(θ - playerAngle) // Without this, walls curve at the edges like a fisheye lens // 3. Project wall height onto the screen wallHeight = screenHeight / correctedDist * heightScale // Paint a vertical stripe of this height, centered on the screen

The fisheye correction is the most misunderstood part. Without it, rays at the edge of the FOV travel a longer path to the same wall than rays at the center. The correction multiplies each distance by the cosine of the ray's offset from the camera's center, projecting onto a flat plane rather than a sphere. Try setting ray count to 8 and rotating slowly in the demo — without the correction, walls would visibly bow outward.

Why cosine? Imagine the player looking straight at a wall. The center ray hits it at distance d. A side ray at angle α off-center hits the same wall at distance d / cos(α) because it travels diagonally. Multiplying back by cos(α) recovers the true perpendicular distance to the wall plane — which is what determines apparent height.
ASCII Rendering

From pixel columns to characters

The ASCII city demo (Grow Now! Games, August 2026) uses the exact same raycasting engine. The only difference is the final rendering step: instead of painting a gray rectangle, the renderer picks a character from a lookup table based on the wall distance. Close walls get dense characters (#, ), far walls get sparse ones (., :, -). Your eye perceives the density gradient as shading and depth.

Dense = close

Characters like # @ M cover most of their cell. Clustered together, they read as a dark, solid surface — the visual weight of a nearby wall.

Sparse = distant

Characters like . , ' - have minimal coverage. Rows of them read as a pale, receding surface — the visual lightness of distance.

Color layer on top

The ASCII city adds a color channel — each character cell gets an RGBA tint based on wall face direction (north/south walls get one hue, east/west another) and distance. The characters provide luminance; the color provides atmosphere.

Switch the demo above to ASCII mode to see this in action. The same ray distances drive both renderers.

Limitations

What raycasting can't do

The technique imposes hard constraints that id Software famously worked around in creative ways, and that the ASCII city demo inherits today.

No looking up or down

Because each screen column maps to exactly one horizontal ray angle, true vertical pitch is impossible. Wolfenstein had no stairs, no ramps, and you couldn't look up or down. A y-shear trick (shifting the horizon line) fakes a limited tilt, which is what the ASCII city uses for its pitch effect.

All floors must be the same height

Every wall in the grid is full-height. You cannot have a half-wall, a window ledge, or a step. Doom (1993) partially solved this with sector-based height fields. True room-over-room geometry required waiting for Quake's BSP engine in 1996.

No diagonal walls

The grid is axis-aligned. Diagonal surfaces require either faking them with sprites or a more sophisticated DDA (Digital Differential Analysis) algorithm that can test off-axis geometry — which the ASCII city's engine actually implements for its building facades.