A fully-functioning raycaster in C with OpenGL

This commit is contained in:
2026-08-30 13:26:02 +02:00
commit 27b1881701
4 changed files with 467 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
!.vscode
**/*.dSYM
raycaster
+18
View File
@@ -0,0 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "clang - Build and debug active file",
"type": "cppdbg",
"request": "launch",
"program": "${fileDirname}/${fileBasenameNoExtension}",
"args": [],
"stopAtEntry": false,
"cwd": "${fileDirname}",
"environment": [],
"externalConsole": false,
"MIMode": "lldb",
"preLaunchTask": "C/C++: clang build active file"
}
]
}
+32
View File
@@ -0,0 +1,32 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: clang build active file",
"command": "/usr/bin/clang",
"args": [
"-g",
"${file}",
"-o",
"${fileDirname}/${fileBasenameNoExtension}",
"-framework",
"OpenGL",
"-framework",
"GLUT",
"-Wno-deprecated-declarations"
],
"options": {
"cwd": "${fileDirname}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Compiler task for OpenGL/GLUT on macOS"
}
]
}
+414
View File
@@ -0,0 +1,414 @@
#include <stdio.h>
#include <stdlib.h>
#include <GLUT/glut.h>
#include <math.h>
#define PI 3.1415926535f
#define PI2 PI / 2
#define PI3 3 * PI / 2
#define DR 0.0174533f // One degree in radians
// 0° => 0 radians (RIGHT)
// 90° => π/2 radians (TOP)
// 180° => π radians (LEFT)
// 270° => 3π/2 radians (BOTTOM)
// 360° => 2π radians (RIGHT)
// * Player position (px, py), delta (pdx, pdy), and angle (pa)
float px, py, pdx, pdy, pa;
// * Draw the player on the screen
void drawPlayer()
{
// Draws the player square
glColor3f(1, 1, 0);
glPointSize(8);
glBegin(GL_POINTS);
glVertex2i(px, py);
glEnd();
// Draws the player direction
glLineWidth(3);
glBegin(GL_LINES);
glVertex2i(px, py);
glVertex2i(px + pdx * 5, py + pdy * 5);
glEnd();
}
// * Define the world
int mapX = 8, mapY = 8, mapS = 64;
// clang-format off
int map[] =
{
1,1,1,1,1,1,1,1,
1,0,1,0,0,0,0,1,
1,0,1,0,0,0,0,1,
1,0,1,0,0,0,0,1,
1,0,0,0,0,0,0,1,
1,0,0,0,0,1,0,1,
1,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,
};
// clang-format on
// * Draws the top-down map with white alls and black floors
void drawMap2D()
{
int x, y, xo, yo;
for (y = 0; y < mapY; y++)
{
for (x = 0; x < mapX; x++)
{
// Check if it's a wall (e.g., 1)
if (map[y * mapX + x] == 1)
{
// Color walls (white)
glColor3f(1, 1, 1);
}
else
{
// Color floors (black)
glColor3f(0, 0, 0);
}
xo = x * mapS;
yo = y * mapS;
glBegin(GL_QUADS);
// Draw each block; the +-1 creates the outline
glVertex2i(xo + 1, yo + 1);
glVertex2i(xo + 1, yo + mapS - 1);
glVertex2i(xo + mapS - 1, yo + mapS - 1);
glVertex2i(xo + mapS - 1, yo + 1);
glEnd();
}
}
}
// * Returns the distance between the player and the ray's endpoint
// That's the hypotenuse, so we can use the Pythagorean theorem.
float dist(float ax, float ay, float bx, float by, float ang)
{
// SOH - CAH - TOA
return (sqrt((bx - ax) * (bx - ax) + (by - ay) * (by - ay)));
}
// * The actual raycaster
void drawRays3D()
{
int r, mx, my, mp, dof;
// Ray x, y, ray angle
float rx, ry, ra, xo, yo;
// Let's initialize the ray angle n degrees from the player's facing position
// ra = ray angle; pa = player angle; DR = degree radian; n = degrees to use
ra = pa - DR * 30;
// Make sure to limit the next angle
if (ra < 0)
{
ra += 2 * PI;
}
if (ra > 2 * PI)
{
ra -= 2 * PI;
}
// * Cast n rays
for (r = 0; r < 360; r++)
{
// * --- Check Horizontal Lines ---
dof = 0;
// We need to get the shortest ray cast in either the vertical or horizontal
// distance. So, we'll set a default that's way higher than we'll ever
// calculate, and save the horizontal ray's X and Y position.
float disH = 1000000, hx = px, hy = py;
// Negative inverse of tangent
float aTan = -1.0f / tanf(ra);
// Check if the ray is looking up or down.
// If it's less than π (e.g., 180°), we're looking down.
// ? Looking up (ray angle > π)
if (ra > PI)
{
// Round the ray's Y position to the nearest 64th value.
// We can do this by bit-shifting down by 6, then bit-shifting back up by
// 6. Then, we subtract a small amount for accuracy.
ry = (((int)py >> 6) << 6) - 0.0001f;
// The ray's X value is the distance between the player and the ray's Y
// position, times the inverse of tangent, plus the player's X position.
rx = (py - ry) * aTan + px;
// Now that we have the ray's first grid intersection position, we need
// the next X and Y offset for the second grid intersection point.
yo = -64;
xo = -yo * aTan;
}
// ? Looking down
if (ra < PI)
{
// This is almost exactly the same as looking up, but we add one grid
// unit, instead of subtracting a tiny amount.
ry = (((int)py >> 6) << 6) + 64;
rx = (py - ry) * aTan + px;
// This is almost exactly the same as looking up, but we add instead of
// subtract.
yo = 64;
xo = -yo * aTan;
}
// ? Looking straight left or right
if (ra == 0.0f || ra == PI)
{
// It's impossible to ever hit a horizontal line
rx = px;
ry = py;
}
// ? We don't want to get caught in an infinite loop, so use the
// depth-of-field to limit our check to a max of 8.
while (dof < 8)
{
// We know the coordinates where the ray will hit the wall, but we need
// to know where that is in the map array.
// Let's take the ray hit position (rx,ry) and divide it by 64 (bit-shift
// 6 places), and use that to find the position in the map's array.
mx = (int)(rx) >> 6;
my = (int)(ry) >> 6;
mp = my * mapX + mx;
// If the map position is less than the array's size, then we can check it
// inside the map. If the value is 1, there's a wall. If we hit a wall,
// we're done!
if (mp > 0 && mp < mapX * mapY && map[mp] == 1)
{
// We hit a wall!
// Save the values and calculate the distance from the player
hx = rx;
hy = ry;
disH = dist(px, py, hx, hy, ra);
dof = 8; // Break the loop - no need to calculate more.
}
else
{
// If we didn't hit a wall, we check the next horizontal line by adding
// the x and y offset.
rx += xo;
ry += yo;
dof += 1;
}
}
// * --- Check Vertical Lines ---
dof = 0;
float disV = 1000000, vx = px, vy = py;
// Negative of tangent
float nTan = -tanf(ra);
// ? Looking left
if (ra > PI2 && ra < PI3)
{
rx = (((int)px >> 6) << 6) - 0.0001f;
ry = (px - rx) * nTan + py;
xo = -64;
yo = -xo * nTan;
}
// ? Looking right
if (ra < PI2 || ra > PI3)
{
rx = (((int)px >> 6) << 6) + 64;
ry = (px - rx) * nTan + py;
xo = 64;
yo = -xo * nTan;
}
// ? Looking straight up or down
if (ra == 0.0f || ra == PI)
{
rx = px;
ry = py;
}
while (dof < 8)
{
// We know the coordinates where the ray will hit the wall, but we need
// to know where that is in the map array.
// Let's take the ray hit position (rx,ry) and divide it by 64 (bit-shift
// 6 places), and use that to find the position in the map's array.
mx = (int)(rx) >> 6;
my = (int)(ry) >> 6;
mp = my * mapX + mx;
// If the map position is less than the array's size, then we can check it
// inside the map. If the value is 1, there's a wall. If we hit a wall,
// we're done!
if (mp > 0 && mp < mapX * mapY && map[mp] == 1)
{
// We hit a wall!
// Save the values and calculate the distance from the player
vx = rx;
vy = ry;
disV = dist(px, py, vx, vy, ra);
dof = 8; // Break the loop - no need to calculate more.
}
else
{
// If we didn't hit a wall, we check the next horizontal line by adding
// the x and y offset.
rx += xo;
ry += yo;
dof += 1;
}
}
// * Compare shortest horizontal and vertical line, then use the shortest
// This is what actually determines our ray that hits a wall!
if (disV < disH)
{
rx = vx;
ry = vy;
}
if (disH < disV)
{
rx = hx;
ry = hy;
}
// * Draw the shortest line
glColor3f(1, 0, 0);
glLineWidth(3);
glBegin(GL_LINES);
glVertex2i(px, py);
glVertex2i(rx, ry);
glEnd();
// * Increase the ray angle to draw the next ray
ra += DR;
if (ra < 0)
{
ra += 2 * PI;
}
if (ra > 2 * PI)
{
ra -= 2 * PI;
}
}
}
// * Main rendering
void display()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawMap2D();
drawRays3D();
drawPlayer();
glutSwapBuffers();
}
// * Check for key presses and move the "player" accordingly
void buttons(unsigned char key, int x, int y)
{
// ? Move left
if (key == 'a')
{
// We change the angle by a small value because we're not working with
// degrees. In C, we instead use radians, which is 0 -> 2π.
//
// 0° -> 360° ≈ 0 -> ~6.28
// Change player angle slightly (using radians)
pa -= 0.1;
// If player angle drops below 0, let's reset back to 2π
if (pa < 0)
{
pa += 2 * PI;
}
// The values returned from sin() and cos() are very small, so we'll just
// artificialy inflate them by multilying by 5. This is our actual movement.
pdx = cos(pa) * 5.0f;
pdy = sin(pa) * 5.0f;
}
// ? Move right
if (key == 'd')
{
pa += 0.1;
if (pa > 2 * PI)
{
pa -= 2 * PI;
}
pdx = cos(pa) * 5.0f;
pdy = sin(pa) * 5.0f;
}
// ? Move forward
if (key == 'w')
{
// We change the X and Y values based on our delta, which is calculated
// during our rotation.
px += pdx;
py += pdy;
}
// ? Move backwards
if (key == 's')
{
px -= pdx;
py -= pdy;
}
glutPostRedisplay();
}
void init()
{
// Sets the screen to the given color (grey)
glClearColor(0.3, 0.3, 0.3, 0);
// Sets the window to be 2D at a given size
gluOrtho2D(0, 1024, 512, 0);
// Set the initial player location
px = 300;
py = 300;
// Calculate the initial player deltaX and deltaY
pdx = cos(pa) * 5.0f;
pdy = sin(pa) * 5.0f;
}
int main(int argc, char *argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowSize(1024, 512);
glutCreateWindow("Raycaster");
init();
// Send display parameters to the display function
glutDisplayFunc(display);
// Send key presses to the keyboard function
glutKeyboardFunc(buttons);
glutMainLoop();
}