3

I am writing a raycaster using SDL's C API. I have spent weeks trying to fix the notorious fisheye effect to no avail. According to this source, I can multiply my calculated distance by the cosine of half of the FOV to fix it. That has not worked for me. I still have the cosine correction in my code nonetheless.

Here are two images demonstrating the distortion:

Image 1

Image 2

I think a core problem of my code may be that my angle increment is constant, while the increment should be smaller as I'm closer to the screen borders. Unfortunately, I don't know how to start implementing this.

If possible, could anyone please take a look at my code and give me a tip on how to remove the fisheye? To move in any direction, use the arrow keys. Use the 'a' and 's' keys to turn left and right respectively.

This is how I'm compiling: clang `pkg-config --cflags --libs sdl2` raycaster.c

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

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,
    ray_theta_step = 0.4,
    ray_dist_step = 0.8,
    darkening = 1.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, 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, 1, 1, 1, 1},
    {1, 0, 4, 3, 2, 0, 0, 0, 2, 0, 0, 1},
    {1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1},
    {1, 0, 0, 0, 4, 3, 2, 1, 4, 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;
}

float distance(float x0, float y0, float x1, float y1) {
    return sqrt(((x1 - x0) * (x1 - x0)) + ((y1 - y0) * (y1 - y0)));
}

void shade(int* color, int darkener) {
    int darkened = *color - darkener;
    *color = darkened < 0 ? 0 : darkened;
}

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

void raycast(Player player) {
    float relative_x = player.x * width_ratio;
    float relative_y = player.y * height_ratio;
    float half_fov = player.fov / 2;
    float width_fov_ratio = (screen_width / player.fov) / 2;
    float distort_adjust = cos(to_radians(half_fov));

    // the core of my problem may be in the constant increment of my angle
    for (float theta = player.angle - half_fov, screen_x = 0;
        theta < player.angle + half_fov;
        theta += ray_theta_step, screen_x += width_fov_ratio) {

        float radian_theta = to_radians(theta);
        float cos_theta = cos(radian_theta), sin_theta = sin(radian_theta);

        float d = 0, new_x, new_y;
        while (d += ray_dist_step) {
            new_x = cos_theta * d + relative_x;
            new_y = sin_theta * d + relative_y;

            int map_x = new_x / width_ratio, map_y = new_y / height_ratio;
            int map_point = map[map_y][map_x];

            if (map_point) {
                int dist_wall = distance(relative_x, relative_y, new_x, new_y) * distort_adjust;
                int twice_dist_wall = 2 * dist_wall;
                if (twice_dist_wall >= screen_height) break;
                else if (map_point) { // succeeds when a wall is present
                    int r, g, b;
                    switch (map_point) {
                        case 1: r = 255, g = 255, b = 0; break;
                        case 2: r = 0, g = 128, b = 128; break;
                        case 3: r = 255, g = 165, b = 0; break;
                        case 4: r = 255, g = 0, b = 0; break;
                    }

                    int color_decr = dist_wall / darkening;
                    shade(&r, color_decr);
                    shade(&g, color_decr);
                    shade(&b, color_decr);

                    SDL_Rect vertical_line = {
                        screen_x, dist_wall,
                        width_fov_ratio + 1,
                        screen_height - twice_dist_wall
                    };

                    draw_rectangle(vertical_line, r, g, b);
                    break;
                }
            }
        }
    }
}

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, 210, 180, 140);

        raycast(player);

        SDL_RenderPresent(renderer);
        SDL_UpdateWindowSurface(window);
    }
}

After help from RandomDavis, the distortion is lessened. Here is the new result. Nonetheless, some warping remains:

new 1

new 2

new 3

NOTE: To anyone who is still struggling with this problem, I solved it here: How do I fix warped walls in my raycaster?

Caspian Ahlberg
  • 934
  • 10
  • 19
  • I've seen this exact issue before, to avoid the fishbowl effect you have to do some kind of trigonometric operation on the height of each column before you display it. I'll see if I can find out exactly what it is. – Random Davis Mar 11 '21 at 22:05
  • @RandomDavis Thank you, that would be super helpful. – Caspian Ahlberg Mar 11 '21 at 22:06
  • not sure if it would help but perhaps write `degrees * M_PI / 180;` as `degrees * M_PI_DEGREES` and add `#define M_PI_DEGREES (M_PI / 180.0f)` – Antonin GAVREL Mar 11 '21 at 22:16
  • @AntoninGAVREL I tried that - it didn't seem to make a difference: `20 * 3.14 / 180.0 == 20 * (3.14 / 180.0)` in a Python prompt yielded True. But thank you anyways! – Caspian Ahlberg Mar 11 '21 at 22:20
  • See also [How do I fix warped walls in my raycaster?](https://stackoverflow.com/questions/66644579/how-do-i-fix-warped-walls-in-my-raycaster) and [What is the reason for perspective being warped in my raycaster?](https://stackoverflow.com/questions/66424604/what-is-the-reason-for-perspective-being-warped-in-my-raycaster) from the same OP – ggorlen May 06 '21 at 20:17

1 Answers1

0

Okay I found a guide which talks about this exact issue.

Before drawing the wall, there is one problem that must be taken care of. This problem is known as the "fishbowl effect." Fishbowl effect happens because ray-casting implementation mixes polar coordinate and Cartesian coordinate together. Therefore, using the above formula on wall slices that are not directly in front of the viewer will gives a longer distance. This is not what we want because it will cause a viewing distortion such as illustrated below. Blockquote

enter image description here

Thus to remove the viewing distortion, the resulting distance obtained from equations in Figure 17 must be multiplied by cos(BETA); where BETA is the angle of the ray that is being cast relative to the viewing angle. On the figure above, the viewing angle (ALPHA) is 90 degrees because the player is facing straight upward. Because we have 60 degrees field of view, BETA is 30 degrees for the leftmost ray and it is -30 degrees for the rightmost ray.

Random Davis
  • 6,662
  • 4
  • 14
  • 24
  • I'm sorry to say that this method didn't work for me. I linked that guide at the top of my post, citing how it didn't affect the distortion. `float distort_adjust = cos(to_radians(half_fov));` might be wrong? – Caspian Ahlberg Mar 11 '21 at 22:14
  • 1
    @CaspianAhlberg `half_fov` isn't the correct value. The BETA value, and thus `distort_adjust` changes with each column; it's the angle between the camera and the column. So, if the player angle was 90 degrees and your FOV was 30, BETA would be -30 degrees to start with, and end up at 30 degrees. So I believe in your code it'd be `theta - player.angle` but you'll have to make sure that's right. That is to say, `distort_adjust` would be set to `cos(to_radians(theta - player_angle))`, I think. But your current attempt is wrong for sure, especially since it's not updating for each value of theta. – Random Davis Mar 11 '21 at 22:51
  • Your idea for `theta - player.angle` gives me a range from -15 to 15 degrees. That is consistent with the FOV, but there is still a little bit of distortion. Is it -15 to 15, or something else? – Caspian Ahlberg Mar 11 '21 at 23:28
  • You might have been thinking of the example from the tutorial when you said -30 to 30. – Caspian Ahlberg Mar 11 '21 at 23:35
  • @CaspianAhlberg yeah I was just giving an example. Also I meant if your FOV was 60 degrees. Sorry. Also maybe you can show how the distortion looks now. – Random Davis Mar 11 '21 at 23:44
  • @CaspianAhlberg yeah looks like there could be some other issue. I'm out of my depth at this point, I think you just have to carefully review your code and compare it to a working example. If you can't figure it out you might have to ask a new question if this one gets no other responses. – Random Davis Mar 12 '21 at 00:00
  • Got it. I might switch to a matrix-transformation approach eventually, or something not oriented towards trig as much. I'm not sure. I've worked on this so long that it might just be better to start over. – Caspian Ahlberg Mar 12 '21 at 00:09
  • 1
    @CaspianAhlberg ah well even if you start over, it'll probably be a lot easier. It sometimes really helps to start over from scratch. – Random Davis Mar 12 '21 at 16:04
  • One can (possibly) fix some of the distortion by height adjustment, but the main problem is that sampling at equal angles will distort the image in the horizontal dimension. Thus, one should also contract the image horizontally e.g. by resampling. – Aki Suihkonen Mar 16 '21 at 06:06