7d3f394dcf
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
580 lines
15 KiB
C
580 lines
15 KiB
C
#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
|
|
|
|
#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();
|
|
}
|
|
|
|
void resize(int w, int h)
|
|
{
|
|
glutReshapeWindow(1024, 512);
|
|
}
|
|
|
|
// * 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);
|
|
glutInitWindowSize(1024, 510);
|
|
|
|
// Initialize the window to the center of the screen
|
|
glutInitWindowPosition(200, 200);
|
|
|
|
glutCreateWindow("Raycaster");
|
|
|
|
init();
|
|
|
|
// Send display parameters to the display function
|
|
glutDisplayFunc(display);
|
|
|
|
// Handle window resizes
|
|
glutReshapeFunc(resize);
|
|
|
|
// Send key presses to the keyboard function
|
|
glutKeyboardFunc(buttons);
|
|
|
|
glutMainLoop();
|
|
} |