Skip to content

Corg-Labs/caster

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 

DOOM-Style Raycaster in C

A real-time raycasting engine — walk through a 2‑D grid map with WASD controls, rendered as distance-shaded wall slices via SDL2.

One ray per screen column steps through the map until it hits a wall; the distance determines the projected wall height and its brightness. No GPU, no textures, no Z-buffer — just ray-intersection math and a framebuffer.


Features

  • Raycasting with DDA-style linear stepping
  • Fisheye correction via view-plane projection
  • Per-column wall slice rendering with distance-based shading
  • Solid ceiling and floor
  • WASD movement and rotation
  • 160 rays at 6 px per column — 960 × 640 window
  • True 24‑bit color rendering via SDL2 framebuffer
  • Keyboard and Ctrl-C quit handling
  • Written entirely in C

How It Works

For each of 160 screen columns, a ray is cast from the player's position at an angle spread evenly across the 1.0-radian field of view. The ray steps forward in 0.04-unit increments until it hits a wall cell on the 16 × 12 tile map. The straight-line distance is corrected for fisheye by multiplying by cos(ray_angle − view_dir), then projected to a wall height of WIN_H / dist. The wall slice is drawn as a coloured column with brightness linearly decreasing from full (close) to dark (at max range). The ceiling is navy and the floor is dark green.


Tutorial / Rendering Pipeline

1. The Tile Map

The world is a 2‑D character grid:

static const char *MAP[] = {
    "################",
    "#..............#",
    "#..##....####..#",
    /* ... */
    "################"
};
#define MW 16
#define MH 12

# is a solid wall; . is open space. The wall() helper checks if a world-space coordinate falls on a wall:

static int wall(double x, double y) {
    int ix = (int)x, iy = (int)y;
    if (ix < 0 || iy < 0 || ix >= MW || iy >= MH) return 1;
    return MAP[iy][ix] == '#';
}

Out-of-bounds coordinates are treated as walls.


2. Player State

The player has a position and a viewing direction:

double px = 2.5, py = 2.5, dir = 0;

W/S move along the current direction; A/D rotate. Movement checks each axis independently so the player slides along walls:

case SDLK_w: {
    double nx = px + cos(dir) * 0.2;
    double ny = py + sin(dir) * 0.2;
    if (!wall(nx, py)) px = nx;
    if (!wall(px, ny)) py = ny;
    break;
}

3. Raycasting Loop

One ray per screen column, spread across the field of view:

for (int col = 0; col < W; col++) {
    double ra = dir - FOV / 2.0 + FOV * col / (double)W;
    double sx = cos(ra), sy = sin(ra);

    double dist = 0;
    while (dist < MAX_DIST) {
        dist += STEP;
        if (wall(px + sx * dist, py + sy * dist)) break;
    }

Each ray steps in 0.04-unit increments along its direction until a wall or the 20-unit max range. A finer step gives better accuracy but costs more iterations.


4. Fisheye Correction

Naive ray distance produces a bulging effect because peripheral rays travel farther. Projecting onto the view plane corrects it:

dist *= cos(ra - dir);

Multiplying by the cosine of the angular offset converts slant (radial) distance to perpendicular (planar) distance.


5. Wall Projection and Shading

Corrected distance is inversely proportional to wall height:

int wall_h = (int)(WIN_H / dist);
if (wall_h > WIN_H) wall_h = WIN_H;

int ceiling = (WIN_H - wall_h) / 2;
int base = ceiling + wall_h;

The brightness of the wall slice decreases linearly with distance:

float shade = 1.0f - fminf((float)dist / MAX_DIST, 1.0f);
Uint8 c = (Uint8)(shade * 230 + 25);
Uint32 col_wall = SDL_MapRGB(fmt, c, (Uint8)(c * 0.7f), (Uint8)(c * 0.5f));
Distance Shade RGB (approx) Visual
0 1.0 255, 178, 128 Bright
10 0.5 140, 98, 70 Mid
20 0.0 25, 17, 12 Dark

6. Column Rendering

Each column of COL_W pixels is filled with the appropriate colour — ceiling, wall, or floor:

for (int y = 0; y < WIN_H; y++) {
    Uint32 color;
    if (y < ceiling)       color = col_ceil;
    else if (y >= base)    color = col_floor;
    else                   color = col_wall;
    for (int px = x_start; px < x_end; px++)
        pixels[y * pitch + px] = color;
}

The ceiling is a solid navy (30, 30, 50); the floor is dark green (15, 25, 15).


7. Controls and Frame Timing

Input is polled via SDL events. The frame rate is capped at ~40 fps:

Uint32 elapsed = SDL_GetTicks() - frame_start;
if (elapsed < 25) SDL_Delay(25 - elapsed);

Build

git clone <this-repo>
cd Doom-Style-Raycaster
make

Or manually:

gcc caster.c -o caster -lm $(sdl2-config --cflags --libs)

Dependencies: SDL2 and a C compiler (gcc/clang).

Install SDL2 via Homebrew:

brew install sdl2

Or apt:

sudo apt install libsdl2-dev

Run

./caster

Controls:

  • W — move forward
  • S — move backward
  • A — turn left
  • D — turn right
  • ESC or Q — quit
  • Ctrl-C — quit (fallback)

Customizing

Edit constants at the top of caster.c:

  • W — number of rays (higher = smoother, slower)
  • COL_W — pixel width per column
  • FOV — field of view in radians
  • STEP — ray marching increment (smaller = more accurate)
  • MAX_DIST — maximum render distance
  • Map — edit MAP[] to design your own level
  • Ceiling, floor, and wall colours

Concepts Practiced

  • Ray-casting with linear stepping against a 2‑D grid
  • View-plane projection for fisheye correction
  • Inverse-distance wall height projection
  • Distance-based shading (linear brightness falloff)
  • Axis-independent collision detection for wall sliding
  • SDL2 surface/pixel-buffer graphics
  • Real-time animation with frame-rate capping
  • Event-driven keyboard input

Dependencies

  • SDL.h — window management, pixel buffer, input
  • stdio.h — stderr diagnostics
  • stdlib.hNULL
  • string.hmemset
  • math.hcos, sin, fmin
  • signal.hSIGINT handler

About

► DOOM-style raycaster rendered as ASCII, in C.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors