commit 27b1881701a09bdeeb8d34c903f09ef943c9ae24 Author: Hans Kokx Date: Sun Aug 30 13:26:02 2026 +0200 A fully-functioning raycaster in C with OpenGL diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35ccbdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +!.vscode +**/*.dSYM +raycaster \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ae5a50f --- /dev/null +++ b/.vscode/launch.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..152c0d5 --- /dev/null +++ b/.vscode/tasks.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/raycaster.c b/raycaster.c new file mode 100644 index 0000000..c8d3e43 --- /dev/null +++ b/raycaster.c @@ -0,0 +1,414 @@ +#include +#include +#include +#include +#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(); +} \ No newline at end of file