This is an excerpt. The full article includes a live interactive 3D raycasting renderer โ control a 2D player model on a grid map using WASD/Arrow controls, inspect visual rays casting in real time, and watch the corresponding 3D viewport update with wall height calculations and shading. Read the full interactive version โ
The Birth of retro 3D: John Carmack and Wolfenstein 3D
In 1992, id Software released Wolfenstein 3D, introducing gamers to a fast-paced pseudo-3D world. Because early 90s personal computer hardware lacked the processing power to render true polygon-based 3D models in real time, John Carmack implemented a technique called Raycasting.
Rather than rendering 3D vertices, raycasting projects a simple 2D grid map into a 3D perspective viewport. The engine casts individual rays from the player's coordinate vectors across their Field of View (FOV).
When a ray hits a wall, the engine computes the distance, and uses that distance to determine how tall that segment of the wall should be rendered on screen. By casting one ray for every vertical column of the screen, the engine builds a convincing 3D perspective using basic 2D line rendering.
1. The Camera Vector Space
To calculate ray projections, the engine tracks three main coordinate vectors:
-
Position Vector (
pos): The player's coordinate location in the 2D grid space. -
Direction Vector (
dir): A unit vector representing the center line of the player's gaze. -
Camera Plane (
plane): A vector perpendicular to the direction vector, representing the screen viewport window.
Camera Plane (plane)
โโโโโโโโโโโโโฒโโโโโโโโโโโโ
โ โ โ
โ โ โ
โ โ (dir) โ
โโโโโโโโโโโโโผโโโโโโโโโโโโ
โ
โ
(player)
By shifting along the camera plane vector, the engine calculates the starting coordinates and direction vector for every ray cast across the screen width.
2. Fast Grid Traversal: The DDA Algorithm
Casting a ray by checking every tiny pixel increment (e.g. 0.01 step checks) is slow and can miss walls due to rounding errors.
To solve this, we use the DDA (Digital Differential Analysis) Algorithm. DDA is a fast grid traversal method that only checks the grid lines the ray crosses.
Instead of walking along the ray, DDA calculates the exact mathematical step distance to the next vertical (X) or horizontal (Y) grid boundary. By checking grid intersections sequentially, the algorithm determines wall hits using only basic additions and boundary compares.
Grid Space:
โโโโโโโโโฌโโโโโโโโฌโโโโโโโโ
โ โ โ Hit! โ
โ โ โ / โ
โโโโโโโโโผโโโโโโโโผโoโโโโโโค <-- Y boundary crossing
โ โ /โ โ
โ โ / โ โ
โโโโโโโโโผโโโโoโโโผโโโโโโโโค <-- X boundary crossing
โ (Player) / โ โ
โโโโโโโโโดโโโโโโโโดโโโโโโโโ
3. Correcting the Fish-Eye Distortion Effect
If we use the raw distance from the player to the wall, we get a Fish-eye Distortion effect, where flat walls appear curved.
This happens because rays cast at the outer edges of the FOV are longer than rays cast straight ahead.
To fix this, we project the ray distance onto the player's direction vector, calculating the perpendicular distance to the camera plane instead of the straight-line distance. This projection straightens the walls, ensuring they render correctly without distortion.
Curved Wall (Fish-eye): Corrected Wall:
โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
โ \ | / โ โ | | | โ
โ \ | / โ โ | | | โ
โ \ | / โ โ | | | โ
TypeScript Raycasting Implementation
Here is a clean TypeScript class showing how to set up the camera vectors, cast rays, and calculate perpendicular distances:
export interface Camera {
posX: number;
posY: number;
dirX: number; // gaze direction vector
dirY: number;
planeX: number; // perpendicular camera plane
planeY: number;
}
export class RaycasterEngine {
private map: number[][];
constructor(gridMap: number[][]) {
this.map = gridMap;
}
/**
* Casts a single ray for a column offset (-1 to 1)
* and calculates the perpendicular distance to the hit wall.
*/
public castRay(camera: Camera, cameraX: number): { distance: number; side: number } {
// Ray direction vector
const rayDirX = camera.dirX + camera.planeX * cameraX;
const rayDirY = camera.dirY + camera.planeY * cameraX;
// Grid coordinates
let mapX = Math.floor(camera.posX);
let mapY = Math.floor(camera.posY);
// Delta step distance along ray to cross one grid square boundary
const deltaDistX = Math.abs(1 / rayDirX);
const deltaDistY = Math.abs(1 / rayDirY);
let stepX = 0;
let stepY = 0;
let sideDistX = 0;
let sideDistY = 0;
// Calculate step directions and initial boundary distances
if (rayDirX < 0) {
stepX = -1;
sideDistX = (camera.posX - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0 - camera.posX) * deltaDistX;
}
if (rayDirY < 0) {
stepY = -1;
sideDistY = (camera.posY - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0 - camera.posY) * deltaDistY;
}
let hit = 0;
let side = 0; // 0 for X axis hit, 1 for Y axis hit
// DDA traversal loop
while (hit === 0) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
// Check collision
if (this.map[mapY][mapX] > 0) {
hit = 1;
}
}
// Calculate perpendicular wall distance (correcting fish-eye effect)
let perpWallDist = 0;
if (side === 0) {
perpWallDist = (mapX - camera.posX + (1 - stepX) / 2) / rayDirX;
} else {
perpWallDist = (mapY - camera.posY + (1 - stepY) / 2) / rayDirY;
}
return { distance: perpWallDist, side };
}
}
Engineering Takeaways
- High rendering efficiency: Raycasting reduces 3D rendering to 2D line drawing, allowing smooth performance on low-power hardware.
- Vector Math core: Clean camera and projection vector math is essential to avoid fish-eye distortion and layout glitches.
- Retro styling potential: Raycasters are a great choice for retro game aesthetics, mini-maps, and custom UI elements.
The full article features a live interactive raycasting sandbox โ toggle shading depth cues, adjust FOV parameters, and explore a retro 3D maze environment directly in your browser.
Written by Ebenezer Akinseinde โ Software Developer & AI Automations Engineer.











