Weird Behavior with Rays in C and OpenGL

Joined
Feb 13, 2024
Messages
3
Reaction score
0
Hey there! I'm currently trying to learn more about game development outside of typical engines like Unity which is what I'm most used to. I decided to follow 3DSage's raycaster engine in C and OpenGL. I'm finally beginning to really understand what's going on behind the scenes, but I'm getting this really weird behavior for some reason. In this specific area of the map, rays just seem to skip over a certain segment, which is really weird. As you can see from the following two images, this specific segment seems to be the issue.


Everywhere else it seems to work just fine, like here:


Here is my code. I know it's a lot, but I have poured over this and 3DSage's source code for days and after hours of testing, I just can't figure out what the issue is. Would really appreciate some help!

C:
/*--HEADERS--*/
#include <stdio.h>
#include <stdlib.h>
#include <GL/glut.h>
#include <math.h>

/*--DEFINITIONS--*/
#define PI 3.141592653589793238
#define DR 0.0174533 //this is one degree in radians

/*--MAP--*/
int map_size_x = 8, map_size_y = 8, tile_size = 64; //defines the map as being 8 tiles horizontally and vertically, with each tile being 64 units

int map[] = //0's are empty, 1's are walls
{
    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,    
};

void DrawMap2D()
{
    int x, y; //for loop iterators for each tile, x and y, on t he mpa
    int x_offset, y_offset;
    
    for(y = 0; y < map_size_y; y++) //for loop continues for each tile on the y axis
    {
        for(x = 0; x < map_size_x; x++) //for loops goes through each x axis tile in each y column
        {
            if(map[y * map_size_x + x]    == 1) //a genius way of iterating through a 1 dimensional array as a 2 dimensional matrix. Hard to explain.
            {
                glColor3f(1, 1, 1); //if there's a wall, set the GL color to white
            }
            else
            {
                glColor3f(0, 0, 0); //if there's not a wall, set the GL color to black
            }
            /*
            To draw a 64 unit block, multiply the current x and y coords for that tile by the tile size, 64.
            This means that tile one will be drawn from units 0, 0; to units 64, 64;
            Tile two will be drawn from units 64, 64 to units 128, 128; etc. forming the grid.
            */
            
            x_offset = x * tile_size; y_offset = y * tile_size; //multiplies x and y by tile size to get the offset required
            glBegin(GL_QUADS); //begins GL in quadrilateral mode
            glVertex2i(x_offset+1,              y_offset+1             ); //draws point one at the offset (so for tile zero it will be (0, 0))
            glVertex2i(x_offset+1,              y_offset + tile_size-1); //draws point two at the offset plus tile size for y (so for tile zero it will be (0, 64))
            glVertex2i(x_offset-1 + tile_size, y_offset + tile_size-1); //draws point three at the offset plus tile size for both x and y (so for tile zero it will be (64, 64))
            glVertex2i(x_offset-1 + tile_size, y_offset+1             ); //draws point four at the offset plus tile size for x (so for tile zero it will be (64, 0))
            glEnd();
            /*
                This creates a quadrilateral with points    (0,  0)----------(0,  64)   for tile zero
                                                            |                        |
                                                            |                        |
                                                            |                        |
                                                            (64, 0)----------(64, 64)
                                                            
                This creates a quadrilateral with points    (64,  64)----------(64,  128)   for tile one
                                                            |                            |
                                                            |                            |
                                                            |                            |
                                                            (128, 64)----------(128, 128)
                                                            
                                                            and so on; 
                                                            
                These quads will be drawn regardless of if they are walls or not. If they are not walls, they will be
                drawn as black instead of white, but they are still drawn;
                
                The plus and minus one throughout is just to draw a border around the blocks for visibility
            */
        }
    }
}

/*--PLAYER--*/

float player_x, player_y; //player position
float player_delta_x, player_delta_y; //stores the amount of movement in the horizontal/vertical direction the player should move
float player_angle;

void DrawPlayer2D()
{
    glColor3f(1, 1, 0); //sets the current gl color to yellow
    glPointSize(8); //sets the point size to 8
    glBegin(GL_POINTS); //starts GL in point drawing mode
    glVertex2i(player_x, player_y); //uses the vertex integer draw function to draw a point at player's position
    glEnd(); //ends the current GL process
    
    glLineWidth(3);
    glBegin(GL_LINES);
    
    //draw a line from the players position to the players delta position, i.e. where they are facing
    glVertex2i(player_x, player_y);
    glVertex2i(player_x + player_delta_x * 5, player_y + player_delta_y * 5);
    glEnd();
}

/*---RAYS---*/

float Dist(float ax, float ay, float bx, float by, float ang)
{
    float horizontal_dist = bx - ax;
    float vertical_dist = by - ay;
    float horizontal_dist_sqr = horizontal_dist * horizontal_dist;
    float vertical_dist_sqr = vertical_dist * vertical_dist;
    float final_dist = sqrt(horizontal_dist_sqr + vertical_dist_sqr);
    return final_dist;
}

void DrawRays2D()
{
    int r, mx, my, mp, depth_of_field; //DoF == number of tiles to cast over, if more than DoF then just end the ray
    float ray_x, ray_y, ray_angle, x_offset, y_offset;
    float final_distance; //holds either horizontal or vertical distance, whicherver is shorter
    
    float horizontal_distance = 100000;
    float horizontal_x = player_x;
    float horizontal_y = player_y;
    
    ray_angle = player_angle - (DR * 30);
    if(ray_angle < 0)
    {
        ray_angle += 2*PI;
    }
    if(ray_angle > 2*PI)
    {
        ray_angle -= 2*PI;
    }
    
    for(r = 0; r < 60; r++)
    {
        // Check Horizontal Lines
        
        depth_of_field = 0;
        float aTan = -1 / tan(ray_angle);
        
        if(ray_angle > PI) //if the ray's angle is greater than Pi aka 180 degs, the player is looking up;
        {
            ray_y = ((((int)player_y) / tile_size) * tile_size) - 0.0001; //rounds the ray's y value to the nearest tile. Because each tile is 64 units, dividing and then multiplying by 64 rounds to the nearest multiple of 64, ensuring that the max y distance per depth_of_field iteration is a multiple of 64
            //the -0.0001 is for accuracy. Floating point numbers are inherently a bit inaccurate, meaning sometimes the rays will overshoot their collision and keep firing forward to the next wall.
            //Subtracting 0.0001 ensures that all rays undershoot by just a small amount, not enough to be perceptible but enough to prevent overshooting.
            //Overshooting causes the ray to go to the next wall, which is 64 blocks away. Undershooting by 0.0001 is better than overshooting by 64.
            
            /*---
            -----
            
            Tangent (Tan) is opposite over adjacent. We know the opposite line (the vertical one) is player_y - ray_y. 
            The Inverse Tangent (aTan) is adjacent over opposite. By multiplying opposite by aTan, we are multiplying
            opp * adj/opp, the opps cancel out and give us adj. We then add the player's x position to adj to figure out
            where on the map the ray's x vector is. 
            
            -----
            ---*/
            
            ray_x = (player_y - ray_y) * aTan + player_x; 
            
            /*---
            -----
            
            the offsets calculate the next hit position. y is the same, just up by 64 (GL renders with 0, 0 being top left, 
            and moving down increases y position). x is the negative of the y_offset (because x is rendered normally,
            left to right. If we shoot a ray and don't hit, we increment the ray's y value by subtracting 64, then use
            the aTan trick to find the proper x offset.
            
            -----
            ---*/
            y_offset = -64;
            x_offset = -y_offset * aTan;
        }
        
        if(ray_angle < PI) //if the ray's angle is less than Pi aka 180 degs, the player is looking down;
        {
            ray_y = ((((int)player_y) / tile_size) * tile_size) + tile_size; 
            //rounds the ray's y value to the nearest tile. Because each tile is 64 units, dividing and then multiplying 
            //by 64 rounds to the nearest multiple of 64, ensuring that the max y distance per depth_of_field iteration is a multiple 
            //of 64. By adding the tile_size (64), we ensure that the ray stops at the top of the tile rather than the bottom
                                                                            
            ray_x = (player_y - ray_y) * aTan + player_x; 
            
            y_offset = 64;
            x_offset = -y_offset * aTan;
        }
        
        if(ray_angle == 0 || ray_angle == PI) //player is looking straight left or right, the ray will never intersect
        {
            ray_x = player_x;
            ray_y = player_y;
            depth_of_field = 8; //Setting depth_of_field to 8 means that we don't need to continue casting rays
        }
        
        while(depth_of_field < 8) //if depth_of_field is less than 8, keep casting until it hits something or iterates 8 times
        {
            mx = (int)(ray_x) / tile_size; //the ray's x value divided by 64, effectively giving the tile position's x value
            my = (int)(ray_y)>>6; //the ray's y value divided by 64, effectively giving the tile position's y value
            mp = my * map_size_x + mx; //The ray's final point on the map
            
            if(mp > 0 && mp < map_size_x * map_size_y && map[mp] == 1) //if the ray's final point on the map is less than the size of the map (AKA not out of bounds) and is a 1, then the ray hit a wall. Set depth_of_field to 8 so that the while loop stops. The search is over.
            {
                horizontal_x = ray_x;
                horizontal_y = ray_y;
                horizontal_distance = Dist(player_x, player_y, horizontal_x, horizontal_y, ray_angle);
                
                depth_of_field = 8;
            }
            else
            {
                ray_x += x_offset;
                ray_y += y_offset;
                depth_of_field += 1;
            }
        }
        
        // Check Vertical Lines
        depth_of_field = 0;
        float nTan = -tan(ray_angle);
        
        float vertical_distance = 100000;
        float vertical_x = player_x;
        float vertical_y = player_y;
        
        if(ray_angle > (PI/2) && ray_angle < ((3*PI)/2)) //if the ray's angle is greater than PI/2 aka 90 degs, and less than 3PI/2 aka 270 degs, the player is looking left;
        {
            ray_x = ((((int)player_x) / tile_size) * tile_size) - 0.0001;
            ray_y = (player_x - ray_x) * nTan + player_y; 
            x_offset = -64;
            y_offset = -x_offset * nTan;
        }
        
        if(ray_angle < (PI/2) || ray_angle > ((3*PI)/2)) //if the ray's angle is less than PI/2 aka 90 or greater than 3PI/2 aka 270 degs, the player is looking right;
        {
            ray_x = ((((int)player_x) / tile_size) * tile_size) + tile_size;                                                                 
            ray_y = (player_x - ray_x) * nTan + player_y; 
            x_offset = 64;
            y_offset = -x_offset * nTan;
        }
        
        if(ray_angle == 0 || ray_angle == PI) //player is looking straight up or down, the ray will never intersect
        {    
            ray_x = player_x;
            ray_y = player_y;
            
            depth_of_field = 8;
        }
        
        while(depth_of_field < 8)
        {
            mx = (int)(ray_x) / tile_size;
            my = (int)(ray_y)>>6;
            mp = my * map_size_x + mx;
            
            if(mp > 0 && mp < map_size_x * map_size_y && map[mp] == 1)
            {
                vertical_x = ray_x;
                vertical_y = ray_y;
                vertical_distance = Dist(player_x, player_y, vertical_x, vertical_y, ray_angle);
                
                depth_of_field = 8;
            }
            else
            {
                ray_x += x_offset;
                ray_y += y_offset;
                depth_of_field += 1;
            }
        }
        
        if(horizontal_distance < vertical_distance)
        {
            ray_x = horizontal_x;
            ray_y = horizontal_y;
            final_distance = horizontal_distance;
        }
        
        if(horizontal_distance > vertical_distance)
        {
            ray_x = vertical_x;
            ray_y = vertical_y;
            final_distance = vertical_distance;
        }
        
        if(horizontal_distance == vertical_distance)
        {
            ray_x = horizontal_x;
            ray_y = horizontal_y;
            final_distance = horizontal_distance;
        }
        
        //printf("Horizontal X: %f\nHorizontal Y: %f\nHorizontal Dist: %f\n\nVertical X: %f\nVertical Y: %f\nVertical Dist: %f\n\nRay X: %f\n Ray Y: %f\nRayDist: &f\n\n", horizontal_x, horizontal_y, horizontal_distance, vertical_x, vertical_y, vertical_distance, ray_x, ray_y, (float)Dist(player_x, player_y, ray_x, ray_y, ray_angle));
        
        glColor3f(1, 1, 0);
        glLineWidth(3);
        glBegin(GL_LINES);
        glVertex2i(player_x, player_y);
        glVertex2i(ray_x, ray_y);
        glEnd();
        
        /*------------*\
        ----------------
        DRAWING 3D WALLS!
        ----------------
        \*------------*/
        
        float angle_offset = player_angle - ray_angle; //finds the distance between the player_angle (forward) and each ray
        if(angle_offset < 0)
        {
            angle_offset += 2*PI;
        }
        if(angle_offset > 2*PI)
        {
            angle_offset -= 2*PI;
        }
        
        final_distance = final_distance * cos(angle_offset); //finds the correct ratio needed for level walls
        
        float screen_height = 320; float screen_width = 530;
        float line_height = (tile_size * screen_height) / final_distance; //By dividing by final distance, further away things will have a higher final distance and thus will divide more, creating a smaller object
        if(line_height > 320)
        {
            line_height = 320; //ensures that we are not drawing lines offscreen;
        }
        
        float line_offset = screen_height - line_height / 2; //lowers the y value of each line so that it is centered on screen
        
        /*---
        -----
        Draws a line every 8 pixels (vertex x value is r iteration times 8) with a width of 8. It is then shifted
        530 pixels to the right so that it is not overlapping with the 2D scene;
        
        All raycast 3D games have a fisheye warping effect. This is because lines further away from the center ray must
        travel further than the center line, making their final_distance larger and thus the line smaller. To fix this, we
        can calculate the distance between the center ray and each off-angle ray, then use trigonometry to find the correct
        ratio to make all walls level;
        -----
        ---*/
        glLineWidth(8);
        glBegin(GL_LINES);
        glVertex2i(r*8+530, line_offset);
        glVertex2i(r*8+530, line_height+line_offset);
        glEnd();
        
        
        ray_angle += DR;
        if(ray_angle < 0)
        {
            ray_angle += 2*PI;
        }
        if(ray_angle > 2*PI)
        {
            ray_angle -= 2*PI;
        }
    }
}

void Display()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //clears the color and depth buffer before drawing
    
    DrawMap2D(); //map must be drawn before player is drawn to ensure that player is drawn afterwards and appears on top
    DrawPlayer2D();
    DrawRays2D();
    
    glutSwapBuffers(); //swaps the buffers. One buffer draws, the other buffer displays. The draw buffer draws without showing the user, preventing artifacting and other weird junk
}

void Keystrokes(unsigned char key, int x, int y) //detects keystrokes
{
    /*
    ---------
    If A or D are pressed, rotates the character by a small amount. We are moving by radians, not degrees, so 1 degree movement is 0.02 rads. 0.1 rads is 5.7 degrees
    
    Rotation works like this: You get the player's angle, then use cosine to determine x movement and sine to determine
    y movement. These functions divide the amount of x and y movement by the hypotenuse of the right triangle made by
    the angle of the player forwards. This is to normalize the value between 0 and 1. So if the player is facing all the
    way to the right and moves forward, the player would move in the x direction by 1 and in the y direction by 0. If the
    player is facing 45 degrees, then they would move 0.5 units up and 0.5 units right. This ensures proper movement
    direction. These numbers (delta_x and delta_y) are then multiplied by 5, which is the 'speed' of the character;
    
    If W or D are pressed, the delta_y and delta_x variables that we calculated are then applied, moving the character
    in the motion we want;
    ---------
    */
    if(key == 'w')
    {
        player_x += player_delta_x;
        player_y += player_delta_y;
    }
    
    if(key == 'a')
    {
        player_angle -= 0.1;
        
        if(player_angle < 0)
        {
            player_angle += 2*PI; //If player angle is negative, add 2*PI to ensure it is always between 0 and 360 degs
        }
        
        player_delta_x = cos(player_angle) * 5;
        player_delta_y = sin(player_angle) * 5;
    }
    
    if(key == 's')
    {
        player_x -= player_delta_x;
        player_y -= player_delta_y;
    }
    
    if(key == 'd')
    {
        player_angle += 0.1;
        
        if(player_angle > (2*PI))
        {
            player_angle -= 2*PI; //If player angle is above 360 degrees, subtract 2*PI to ensure it is always between 0 and 360 degs
        }
        
        player_delta_x = cos(player_angle) * 5;
        player_delta_y = sin(player_angle) * 5;
    }
    
    glutPostRedisplay(); //tells GL that the screen needs to be redrawn because the player moved
}

void Init()
{
    glClearColor(0.3, 0.3, 0.3, 0);
    gluOrtho2D(0, 1024, 512, 0);
    player_x = 300; player_y = 300; //sets the player's position
    player_delta_x = cos(player_angle) * 5; player_delta_y = sin(player_angle) * 5; //sets the delta_x and y to show the line when the program starts 
}

int main(int argc, char* argv[]) //BS parameters that don't do anything
{
    glutInit(&argc, argv); //BS parameters. Can allow passing commandline args, but I ain't doin' that shit brother
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA); //sets display mode to double buffer (uses buffer swap) and RGBA
    glutInitWindowSize(1024, 512);
    glutCreateWindow("My Game");
    Init();
    glutDisplayFunc(Display); //the function that displays aka clears, draws, and swaps buffers
    glutKeyboardFunc(Keystrokes); //this function is called whenever a key is pressed. It calls Keystrokes with the pressed key as a parameter
    glutMainLoop();
    return 0;
}
 
Joined
Sep 21, 2022
Messages
121
Reaction score
15
What was the player_angle that produced the first two images?

It looks to be about 110 degrees.

Have you changed the program since you made those pictures?
 
Joined
Feb 13, 2024
Messages
3
Reaction score
0
What was the player_angle that produced the first two images?

It looks to be about 110 degrees.

Have you changed the program since you made those pictures?
Thanks for the reply. I changed quite a bit on the program, but the general raycasting hasn't changed much. I'm shooting 240 rays instead of 60, but the problem appears regardless of the amount of rays. The weird part is that it doesn't seem to care about the player angle. Here are some picture at different angles (which you can see in the console). It also appears on the bottom right, not just the top left.

halp1.png
halp2.png
halp3.png
halp4.png


I'm pretty new to all this trigonometry stuff and I'm definitely learning a lot but I'm not sure if I know enough to even begin to diagnose the problem. I don't think its an issue with the angle math as it seems to happen at various angles, but idk. I'll put the new code here, but honestly not much has changed with the rays like I said. DrawRays2D is the raycast function name.

https://pastebin.com/AVHUD8BJ
 
Joined
Sep 21, 2022
Messages
121
Reaction score
15
Two problems are causing this.

(1) The top left square of the map is not being tested.

if(mp > 0 && mp < map_size_x * map_size_y && map[mp] == 1)

mp > 0 should be mp >= 0

(don't forget to change both lines where this expression occurs)

(2) horizontal_distance needs to be reset to 100000 inside the r for loop.

I had trouble reading your program because you're not using angles in the normal way.

Here's what's happening:

When the ray meets (64,128) the horizontal and vertical distances are the same.

As the ray angle increases (clockwise in your system) the vertical distance increases, but not the horizontal, because the horizontal test goes right thru the top left square.

Horizontal distance stays as it was, and the ray that should be drawn on the segment is drawn at (64,128)

That's my 2 cents worth.
 
Joined
Feb 13, 2024
Messages
3
Reaction score
0
Two problems are causing this.

(1) The top left square of the map is not being tested.

if(mp > 0 && mp < map_size_x * map_size_y && map[mp] == 1)

mp > 0 should be mp >= 0

(don't forget to change both lines where this expression occurs)

(2) horizontal_distance needs to be reset to 100000 inside the r for loop.

I had trouble reading your program because you're not using angles in the normal way.

Here's what's happening:

When the ray meets (64,128) the horizontal and vertical distances are the same.

As the ray angle increases (clockwise in your system) the vertical distance increases, but not the horizontal, because the horizontal test goes right thru the top left square.

Horizontal distance stays as it was, and the ray that should be drawn on the segment is drawn at (64,128)

That's my 2 cents worth.
Oh lord, I feel silly at such simple mistakes. I appreciate it so much.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,537
Members
45,022
Latest member
MaybelleMa

Latest Threads

Top