1

I am writing a raycaster using the SDL library with C. I have been dealing with the fisheye effect for many weeks now. For a field of view like 60 degrees, I multiply the distance to the wall by the cosine of the relative angle (ranging from -30 to 30), but still, I get the same fisheye. Here's what that looks like:

Example 1

I don't know what to do at this point, given that so many sources have recommended cosine correction and it just does not fix the distortion in my case.

  • I am compiling like this: clang `pkg-config --cflags --libs sdl2` raycaster.c
  • To go forward and back, press the up and down keys. Press left and right to strafe. You can use the a and s keys to turn left and right respectively.

My code is below if you want to take a look. If you manage to figure out why I am getting a warped perspective in my engine, please let me know.

#include <SDL2/SDL.h>
#include <math.h>

#define SET_COLOR(r, g, b) SDL_SetRenderDrawColor(renderer, r, g, b, SDL_ALPHA_OPAQUE)

typedef struct {
    float x, y, prev_x, prev_y, angle, fov;
} Player;

enum {
    map_width = 12, map_height = 15,
    screen_width = 800, screen_height = 500
};

const float
    move_speed_decr = 0.08,
    angle_turn = 2.0,
    theta_step = 0.05,
    dist_step = 0.8,
    width_ratio = (float) screen_width / map_width,
    height_ratio = (float) screen_height / map_height;

const unsigned char map[map_height][map_width] = {
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1},
    {1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1},
    {1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1},
    {1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
};

SDL_Window* window;
SDL_Renderer* renderer;

float to_radians(float degrees) {
    return degrees * (M_PI / 180.0f);
}

void draw_rectangle(SDL_Rect rectangle, int r, int g, int b) {
    SET_COLOR(r, g, b);
    SDL_RenderFillRect(renderer, &rectangle);
    SDL_RenderDrawRect(renderer, &rectangle);
}

void raycast(Player player) {
    SET_COLOR(210, 180, 140);

    float
        half_fov = player.fov / 2,
        rel_x = player.x * width_ratio, rel_y = player.y * height_ratio;

    float screen_x = 0, step_x = (screen_width / player.fov) * theta_step;

    for (float theta = player.angle - half_fov; theta < player.angle + half_fov; theta += theta_step) {
        float rad_theta = to_radians(theta);
        float cos_theta = cos(rad_theta), sin_theta = sin(rad_theta);

        float dist = 0;
        while (dist += dist_step) {
            float
                new_x = cos_theta * dist + rel_x,
                new_y = sin_theta * dist + rel_y;

            if (map[(int) (new_y / height_ratio)][(int) (new_x / width_ratio)]) {
                dist *= cos(to_radians(theta - player.angle));
                float double_dist = 2 * dist;

                if (double_dist >= screen_height) break;
                SDL_Rect column = {screen_x, dist, step_x + 1, screen_height - double_dist};

                SDL_RenderFillRect(renderer, &column);
                SDL_RenderDrawRect(renderer, &column);
                break;
            }
        }
        screen_x += step_x;
    }
}

void handle_input(const Uint8* keys, Player* player) {
    SDL_Event event;

    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) {
            SDL_DestroyWindow(window);
            SDL_DestroyRenderer(renderer);
            exit(0);
        }

        else if (event.type == SDL_KEYDOWN) {
            float radian_theta = to_radians(player -> angle);
            float move_x = cos(radian_theta) * move_speed_decr,
                move_y = sin(radian_theta) * move_speed_decr;

            // handle arrow keys
            if (keys[SDL_SCANCODE_UP]) player -> x += move_x, player -> y += move_y;
            if (keys[SDL_SCANCODE_DOWN]) player -> x -= move_x, player -> y -= move_y;
            if (keys[SDL_SCANCODE_LEFT]) player -> x += move_y, player -> y -= move_x;
            if (keys[SDL_SCANCODE_RIGHT]) player -> x -= move_y, player -> y += move_x;

            // handle 'a' and 's' for angle changes
            if (keys[SDL_SCANCODE_A]) player -> angle -= angle_turn;
            if (keys[SDL_SCANCODE_S]) player -> angle += angle_turn;

            // safeguards for invalid positions and angles
            if (player -> x < 0) player -> x = 0;
            else if (player -> x > screen_width) player -> x = screen_width;

            if (player -> y < 0) player -> y = 0;
            else if (player -> y > screen_height) player -> y = screen_height;

            // move the player to their previous coordinate if they're in a wall
            if (map[(int) player -> y][(int) player -> x])
                player -> y = player -> prev_y, player -> x = player -> prev_x;

            if (player -> angle > 360) player -> angle = 0;
            else if (player -> angle < 0) player -> angle = 360;

            player -> prev_y = player -> y, player -> prev_x = player -> x;
        }
    }
}

int main() {
    SDL_CreateWindowAndRenderer(screen_width, screen_height, 0, &window, &renderer);
    SDL_SetWindowTitle(window, "Raycaster");    

    Player player = {5, 5, 0, 0, 0, 60};
    SDL_Rect the_ceiling = {0, 0, screen_width, screen_height / 2};
    SDL_Rect the_floor = {0, screen_height / 2, screen_width, screen_height};
    const Uint8* keys = SDL_GetKeyboardState(NULL);

    while (1) {
        handle_input(keys, &player);

        draw_rectangle(the_ceiling, 96, 96, 96);
        draw_rectangle(the_floor, 255,69,0);

        raycast(player);

        SDL_RenderPresent(renderer);
        SDL_UpdateWindowSurface(window);
    }
}
Caspian Ahlberg
  • 934
  • 10
  • 19
  • 2
    It seems you already asked a similar question: https://stackoverflow.com/questions/66591163/how-do-i-fix-the-warped-perspective-in-my-raycaster that got an accepted answer. So, what did you incorporate from that answer and how is your current/newer code different? – Craig Estey Mar 15 '21 at 19:49
  • 1
    @CraigEstey This code is essentially the same as the previous code, but this time I worked to make it more minimal, with the cost of removing shaded and colored walls (for the sake of readability). If you read the bottom comment thread of the accepted answer, you'll see that the person who helped me said that the remaining distortion was beyond them. This question is for the distortion that remains. – Caspian Ahlberg Mar 15 '21 at 20:11
  • @CraigEstey From the previous question, I incorporated correct cosine correction (that still doesn't work fully): `float distort_adjust = cos(to_radians(theta - player.angle)); float dist = distance(rel_x, rel_y, new_x, new_y) * distort_adjust;` – Caspian Ahlberg Mar 15 '21 at 20:20
  • `dist_step = 0.8` is too large, you will get a lot of artifacts – tstanisl Mar 16 '21 at 14:45
  • @tstanisl I have tried a few different smaller values for `dist_step`, but they do not seem to make a difference. What size do you recommend? Additionally, the step is relative to the screen size, not the map size, so I'm thinking that 0.8 might actually be too small. What do you think? – Caspian Ahlberg Mar 16 '21 at 17:12

2 Answers2

1

You need to apply following diff:

diff --git a/so33.c b/so33.c
index e65cff8..b0f6d8a 100644
--- a/so33.c
+++ b/so33.c
@@ -56,7 +56,7 @@ void raycast(Player player) {
 
     float
         half_fov = player.fov / 2,
-        rel_x = player.x * width_ratio, rel_y = player.y * height_ratio;
+        rel_x = player.x, rel_y = player.y;
 
     float screen_x = 0, step_x = (screen_width / player.fov) * theta_step;
 
@@ -70,12 +70,12 @@ void raycast(Player player) {
                 new_x = cos_theta * dist + rel_x,
                 new_y = sin_theta * dist + rel_y;
 
-            if (map[(int) (new_y / height_ratio)][(int) (new_x / width_ratio)]) {
+            if (map[(int) (new_y)][(int) (new_x)]) {
                 dist *= cos(to_radians(theta - player.angle));
-                float double_dist = 2 * dist;
-
-                if (double_dist >= screen_height) break;
-                SDL_Rect column = {screen_x, dist, step_x + 1, screen_height - double_dist};
+               float wall_height = screen_height / dist;
+               if (wall_height > screen_height)
+                       wall_height = screen_height;
+                SDL_Rect column = {screen_x, screen_height/2 - wall_height/2, step_x + 1, wall_height};
 
                 SDL_RenderFillRect(renderer, &column);
                 SDL_RenderDrawRect(renderer, &column);

A few issues were identified.

  1. Coefficients width_ratio and height_ratio seems to mix coordinates in the map space with coordinates in the screen space. It is pointless. Moreover, it breaks navigation by moving faster along specific axis.

  2. After projecting dist to the ray cast through the center of the screen (dist *= cos(...) you have to apply simple perspective to compute height of the wall (variable wall_height)

  3. Finally, draw a rectangle of height wall_height around the middle horizontal line.

enter image description here

Edit. Set dist_step = 0.01

tstanisl
  • 13,520
  • 2
  • 25
  • 40
  • That is incredible! Thank you so much. I have been struggling with this for weeks, and you have made my problems go away. Bless your soul! – Caspian Ahlberg Mar 17 '21 at 02:40
0

There are two major problem in this code. The distance to the wall must be calculated for the intersection point of your ray to the wall instead of just relying that your endpoint lies inside the wall square.

Having a camera at C, the distortion is solved by casting rays from C through a sufficient number of points between A and B, just dividing this plane (your screen) to equally wide columns (pixels).

I'm fairly pessimistic about the cosine correction, since what I can tell, one should more likely adjust the drawn column width or position with it, than the column height.

Geometry of the problem

Aki Suihkonen
  • 19,144
  • 1
  • 36
  • 57
  • I am confused. Wouldn't the correct distance to the wall entail only that the endpoint lies within the wall square, and nothing more? – Caspian Ahlberg Mar 16 '21 at 04:49
  • @CaspianAhlberg: Oh no. There's a big difference if your nose just barely touches a wall or goes a few inches inside. Same with the ray. And if you look at the diagram closely you can notice e.g. how most rays might miss a square if the sampling points are too sparse. Thus one needs to find the intersecting squares analytically. (Or by some other method, such as Binary Space Partitioning.) – Aki Suihkonen Mar 16 '21 at 06:02
  • When you say 'analytically', can you name one such method? – Caspian Ahlberg Mar 16 '21 at 21:40
  • 1
    Convert the wall segment and ray to line equations and solve the intersection point. The squares are all axis aligned so it takes one division per intersection point. For generic cases graphics people typically use binary space partitioning trees. – Aki Suihkonen Mar 17 '21 at 06:01