GRAPHICS ENGINE internals SERIES
Read on Dev.to
Principal Graphics Engineering Deep Dive

Developing a 3D Raycasting Engine from Scratch: Vector Math and Rendering

A mathematical deep dive exploring digital differential analysis, camera vector projections, texture column mappings, and shading algorithms.

EA
Ebenezer AkinseindeSoftware Developer & AI Automations Engineer
Published Jun 202628 min readWeb Engineering

01.Interactive Raycaster Canvas Laboratory

Explore the live raycasting renderer below. The left half displays the **2D Minimap** showing player coordinates and visual rays cast in real time. The right half projects the **3D Viewport** with simulated wall height shading.

RAYCASTING RENDERER

2D Minimap vs. 3D Perspective

FOV:Depth Shade:
ENGINE NAVIGATION CONTROLS:

02.Raycasting Fundamentals & Wolfenstein 3D

In 1992, ID Software released *Wolfenstein 3D*, introducing players to a pseudo-3D game world. Because 1990s hardware lacked the floating-point capabilities to render true polygon-based 3D models, John Carmack implemented a technique called Raycasting.

Rather than projecting a full 3D polygon mesh, raycasting projects a 2D map grid into a pseudo-3D viewport. The engine casts individual rays from the player's perspective 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.

03.Vector Camera Planes & Projection

To render the scene, the engine tracks the player's coordinate vectors:

  • Position Vector (pos): Represents 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.

By shifting along the camera plane vector, the engine calculates the starting coordinates and direction vector for every ray cast across the screen width.

04.The DDA (Digital Differential Analysis) 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:

"Rather than 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."

05.Solving 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.

06.TypeScript Raycasting Implementation

Below is a clean TypeScript class showing how to set up the camera vectors, cast rays, and calculate perpendicular distances:

raycaster-core.ts
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 };
  }
}

07.Production Rendering Takeaways

Raycasting remains a foundational graphics technique used in retro engines, map editors, and lightweight mobile displays.

  • 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.
EA

Ebenezer Akinseinde

I engineer highly secure distributed consensus state machines, horizontal WebSocket relay networks, and performant AI vector databases. Let's build resilient systems together.