Caspian Ahlberg
Caspian Ahlberg

Reputation: 976

How do I fix warped walls in my raycaster?

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.

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);
    }
}

Upvotes: 1

Views: 2166

Answers (2)

tstanisl
tstanisl

Reputation: 14157

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

Upvotes: 1

Aki Suihkonen
Aki Suihkonen

Reputation: 20037

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

Upvotes: 0

Related Questions