#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 #define gameWidth 320 #define gameHeight 256 #define GAP 20 #define MAP_WIDTH (mapX * mapSize) #define MAP_HEIGHT (mapY * mapSize) #define TOTAL_WIDTH (MAP_WIDTH + GAP + gameWidth) #define TOTAL_HEIGHT (MAP_HEIGHT > gameHeight ? MAP_HEIGHT : gameHeight) // 0° => 0 radians (RIGHT) // 90° => π/2 radians (TOP) // 180° => π radians (LEFT) // 270° => 3π/2 radians (BOTTOM) // 360° => 2π radians (RIGHT) // * Define the world int mapX = 8, mapY = 8, mapSize = 32; // clang-format off int map[] = { 1,1,1,1,1,1,1,1, 1,0,0,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,2,0,1, 1,0,0,0,0,0,0,1, 1,1,1,1,1,1,1,1, }; // clang-format on // Function to check if a number is a power of 2 int isPowerOfTwo(int n) { return (n > 0) && ((n & (n - 1)) == 0); } // * Player position (playerX, playerY), delta (playerDeltaX, playerDeltaY), and angle (pa) float playerX, playerY, playerDeltaX, playerDeltaY, playerAngle; // * Draw the player on the screen void drawPlayer() { // Draws the player square glColor3f(1, 1, 0); glPointSize(8); glBegin(GL_POINTS); glVertex2i(playerX, playerY); glEnd(); // Draws the player direction glLineWidth(3); glBegin(GL_LINES); glVertex2i(playerX, playerY); glVertex2i(playerX + playerDeltaX * 5, playerY + playerDeltaY * 5); glEnd(); } // * Draws the top-down map with white alls and black floors void drawMap2D() { int gridMapSize = mapSize; int x, y, xo, yo; for (y = 0; y < mapY; y++) { for (x = 0; x < mapX; x++) { // Check if it's a wall if (map[y * mapX + x] > 0) { // Color walls (white) glColor3f(1, 1, 1); } else { // Color floors (black) glColor3f(0, 0, 0); } xo = x * gridMapSize; yo = y * gridMapSize; glBegin(GL_QUADS); // Draw each block; the +-1 creates the outline glVertex2i(xo + 1, yo + 1); glVertex2i(xo + 1, yo + gridMapSize - 1); glVertex2i(xo + gridMapSize - 1, yo + gridMapSize - 1); glVertex2i(xo + gridMapSize - 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, mapPoint, dof; int gridMapSize = mapSize; // Ensure mapSize is a power of 2, otherwise default or handle error if (!isPowerOfTwo(mapSize)) { fprintf(stderr, "Error: mapSize must be a power of 2 (e.g., 16, 32, 64, 128).\n"); exit(1); } int shiftSize = (int)(log2f((float)mapSize)); // Ray x, y, angle float rayX, rayY, rayAngle; float xo, yo; // Final distance float disT; // 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 rayAngle = playerAngle - DR * 30; // Make sure to limit the next angle if (rayAngle < 0) { rayAngle += 2 * PI; } if (rayAngle > 2 * PI) { rayAngle -= 2 * PI; } // * Cast n rays for (r = 0; r < 60; 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 distanceHor = 1000000, hx = playerX, hy = playerY; // Negative inverse of tangent float aTan = -1.0f / tanf(rayAngle); // 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 (rayAngle > PI) { // Round the ray's Y position to the nearest 64th value. // We can do this by bit-shifting down by `shiftSize`, then bit-shifting back up by // `shiftSize`. Then, we subtract a small amount for accuracy. rayY = (((int)playerY >> shiftSize) << shiftSize) - 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. rayX = (playerY - rayY) * aTan + playerX; // 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 = -gridMapSize; xo = -yo * aTan; } // ? Looking down if (rayAngle < PI) { // This is almost exactly the same as looking up, but we add one grid // unit, instead of subtracting a tiny amount. rayY = (((int)playerY >> shiftSize) << shiftSize) + gridMapSize; rayX = (playerY - rayY) * aTan + playerX; // This is almost exactly the same as looking up, but we add instead of // subtract. yo = gridMapSize; xo = -yo * aTan; } // ? Looking straight left or right if (rayAngle == 0.0f || rayAngle == PI) { // It's impossible to ever hit a horizontal line rayX = playerX; rayY = playerY; } int mv = 0, mh = 0; // ? 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)(rayX) >> shiftSize; my = (int)(rayY) >> shiftSize; mapPoint = 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 (mapPoint > 0 && mapPoint < mapX * mapY && map[mapPoint] > 0) { // We hit a wall! mh = map[mapPoint]; // Save the values and calculate the distance from the player hx = rayX; hy = rayY; distanceHor = dist(playerX, playerY, hx, hy, rayAngle); 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. rayX += xo; rayY += yo; dof += 1; } } // * --- Check Vertical Lines --- dof = 0; float distanceVert = 1000000, vx = playerX, vy = playerY; // Negative of tangent float nTan = -tanf(rayAngle); // ? Looking left if (rayAngle > PI2 && rayAngle < PI3) { rayX = (((int)playerX >> shiftSize) << shiftSize) - 0.0001f; rayY = (playerX - rayX) * nTan + playerY; xo = -gridMapSize; yo = -xo * nTan; } // ? Looking right if (rayAngle < PI2 || rayAngle > PI3) { rayX = (((int)playerX >> shiftSize) << shiftSize) + gridMapSize; rayY = (playerX - rayX) * nTan + playerY; xo = gridMapSize; yo = -xo * nTan; } // ? Looking straight up or down if (rayAngle == 0.0f || rayAngle == PI) { rayX = playerX; rayY = playerY; } 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)(rayX) >> shiftSize; my = (int)(rayY) >> shiftSize; mapPoint = 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 (mapPoint > 0 && mapPoint < mapX * mapY && map[mapPoint] > 0) { // We hit a wall! mv = map[mapPoint]; // Save the values and calculate the distance from the player vx = rayX; vy = rayY; distanceVert = dist(playerX, playerY, vx, vy, rayAngle); 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. rayX += xo; rayY += yo; dof += 1; } } // * Compare shortest horizontal and vertical line, then use the shortest // This is what actually determines our ray that hits a wall! // Vertical wall hit if (distanceVert < distanceHor) { rayX = vx; rayY = vy; disT = distanceVert; // Vertical walls are drawn a little brighter than horizontal walls glColor3f(0.9, 0, 0); if (mv == 2) { glColor3f(0, 0, 0.7); } } // Horizontal wall hit if (distanceHor < distanceVert) { rayX = hx; rayY = hy; disT = distanceHor; // Horizontal walls are drawn a little darker than vertical walls glColor3f(0.7, 0, 0); if (mh == 2) { glColor3f(0, 0, 0.4); } } // * Draw the ray glLineWidth(3); glBegin(GL_LINES); glVertex2i(playerX, playerY); glVertex2i(rayX, rayY); glEnd(); // * --- Draw 3D Walls --- // Correct the fish-eye effect float correctedAngle = playerAngle - rayAngle; if (correctedAngle < 0) { correctedAngle += 2 * PI; } if (correctedAngle > 2 * PI) { correctedAngle -= 2 * PI; } disT = disT * cos(correctedAngle); // The formula for each line height will be the cube size (64) times our // screen height, divided by the ray distance (disT). // The further away, the larger the distance and the shorter the final line // height will be. float lineHeight = (mapSize * gameHeight) / disT; if (lineHeight > gameHeight) { lineHeight = gameHeight; } // Let's make sure the line doesn't exceed our vertical space, so we cap it if (lineHeight > gameWidth) { lineHeight = gameWidth; } // We need to offset the line to the middle of the screen // That's the full window height, minus half the line height float lineOffset = (gameHeight / 2.0f) - (lineHeight / 2.0f); // Calculate dynamic column width and horizontal starting offset float rayWidth = (float)gameWidth / 60.0f; float rayX3D = MAP_WIDTH + GAP + (r * rayWidth); // * Draw the "wall" line // Draw solid wall column using QUADS instead of GL_LINES glBegin(GL_QUADS); glVertex2f(rayX3D, lineOffset); glVertex2f(rayX3D + rayWidth, lineOffset); glVertex2f(rayX3D + rayWidth, lineOffset + lineHeight); glVertex2f(rayX3D, lineOffset + lineHeight); glEnd(); // * Increase the ray angle to draw the next ray rayAngle += DR; if (rayAngle < 0) { rayAngle += 2 * PI; } if (rayAngle > 2 * PI) { rayAngle -= 2 * PI; } } } // * Draw ceiling void drawCeiling() { float halfHeight = (float)(gameHeight / 2); glBegin(GL_QUADS); for (float distance = 0; distance < halfHeight; distance += 1.0f) { // Normalize color from 0.0 to 1.0 based on distance, multiplied by relative intensity float colorIntensity = 0.5f + (distance / halfHeight); glColor3f(0.25 * colorIntensity, 0.25 * colorIntensity, 0.25 * colorIntensity); // Draw a horizontal strip at the current distance glVertex2f(MAP_WIDTH + GAP, distance); glVertex2f(MAP_WIDTH + GAP, distance + 1.0f); glVertex2f(TOTAL_WIDTH, distance + 1.0f); glVertex2f(TOTAL_WIDTH, distance); } glEnd(); } // * Draw floor void drawFloor() { float halfHeight = (float)(gameHeight / 2); glBegin(GL_QUADS); for (float distance = 0; distance < halfHeight; distance += 1.0f) { float colorIntensity = 0.5f + (distance / halfHeight); glColor3f(0.25 * colorIntensity, 0.25 * colorIntensity, 0.0f); float y = halfHeight + distance; glVertex2f(MAP_WIDTH + GAP, y); glVertex2f(MAP_WIDTH + GAP, y + 1.0f); glVertex2f(TOTAL_WIDTH, y + 1.0f); glVertex2f(TOTAL_WIDTH, y); } glEnd(); } // * Main rendering void display() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); drawCeiling(); drawFloor(); 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) playerAngle -= 0.1; // If player angle drops below 0, let's reset back to 2π if (playerAngle < 0) { playerAngle += 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. playerDeltaX = cos(playerAngle) * 5.0f; playerDeltaY = sin(playerAngle) * 5.0f; } // ? Move right if (key == 'd') { playerAngle += 0.1; if (playerAngle > 2 * PI) { playerAngle -= 2 * PI; } playerDeltaX = cos(playerAngle) * 5.0f; playerDeltaY = sin(playerAngle) * 5.0f; } // ? Move forward if (key == 'w') { // We change the X and Y values based on our delta, which is calculated // during our rotation. playerX += playerDeltaX; playerY += playerDeltaY; } // ? Move backwards if (key == 's') { playerX -= playerDeltaX; playerY -= playerDeltaY; } 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); gluOrtho2D(0, TOTAL_WIDTH, TOTAL_HEIGHT, 0); // Spawn player centered in grid tile (1, 1) playerX = 1.5f * mapSize; playerY = 1.5f * mapSize; playerAngle = 0; // Calculate the initial player deltaX and deltaY playerDeltaX = cos(playerAngle) * 5.0f; playerDeltaY = sin(playerAngle) * 5.0f; } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA); glutInitWindowSize(TOTAL_WIDTH, TOTAL_HEIGHT); glutCreateWindow("Raycaster"); init(); // Send display parameters to the display function glutDisplayFunc(display); // Send key presses to the keyboard function glutKeyboardFunc(buttons); glutMainLoop(); }