Как исправить искаженную перспективу в моем рейкастере?

Я пишу raycaster, используя C API SDL. Я потратил недели, пытаясь исправить пресловутый эффект «рыбий глаз», но безрезультатно. Согласно этому источнику, я могу умножить рассчитанное расстояние на косинус половины FOV, чтобы исправить это. Это не сработало для меня. Тем не менее, у меня все еще есть коррекция косинуса в моем коде.

Вот два изображения, демонстрирующие искажение:

Изображение 1

Изображение 2

Я думаю, что основная проблема моего кода может заключаться в том, что мой приращение угла является постоянным, в то время как приращение должно быть меньше, чем я ближе к границам экрана. К сожалению, я не знаю, как начать реализовывать это.

Если возможно, может ли кто-нибудь взглянуть на мой код и дать мне совет, как удалить рыбий глаз? Чтобы двигаться в любом направлении, используйте клавиши со стрелками. Используйте клавиши «a» и «s», чтобы повернуть влево и вправо соответственно.

Вот как я компилирую: 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);
    }
}

После помощи RandomDavis искажение уменьшилось. Вот новый результат. Тем не менее, некоторая деформация остается:

новый 1

новый 2

новый 3

ПРИМЕЧАНИЕ. Всем, кто все еще борется с этой проблемой, я решил ее здесь: Как исправить деформированные стены в моем raycaster?


person Caspian Ahlberg    schedule 11.03.2021    source источник
comment
Я видел эту точную проблему раньше, чтобы избежать эффекта аквариума, вам нужно выполнить какую-то тригонометрическую операцию над высотой каждого столбца, прежде чем отображать его. Я посмотрю, смогу ли я точно узнать, что это такое.   -  person Random Davis    schedule 12.03.2021
comment
@RandomDavis Спасибо, это было бы очень полезно.   -  person Caspian Ahlberg    schedule 12.03.2021
comment
не уверен, что это поможет, но, возможно, напишите degrees * M_PI / 180; как degrees * M_PI_DEGREES и добавьте #define M_PI_DEGREES (M_PI / 180.0f)   -  person Antonin GAVREL    schedule 12.03.2021
comment
@AntoninGAVREL Я пробовал это - похоже, это не имело значения: 20 * 3.14 / 180.0 == 20 * (3.14 / 180.0) в приглашении Python дало True. Но все равно спасибо!   -  person Caspian Ahlberg    schedule 12.03.2021


Ответы (1)


Хорошо, я нашел руководство, в котором говорится именно об этой проблеме.

Перед рисованием стены необходимо решить одну проблему. Эта проблема известна как эффект аквариума. Эффект аквариума возникает из-за того, что реализация raycasting смешивает полярную координату и декартову координату. Следовательно, использование приведенной выше формулы на срезах стен, которые не находятся непосредственно перед зрителем, даст большее расстояние. Это не то, что нам нужно, потому что это приведет к искажению изображения, как показано ниже. Цитата

введите здесь описание изображения

Таким образом, чтобы устранить искажение при просмотре, результирующее расстояние, полученное из уравнений на рисунке 17, необходимо умножить на cos(BETA); где БЕТА — угол падающего луча относительно угла обзора. На рисунке выше угол обзора (альфа) составляет 90 градусов, потому что игрок смотрит прямо вверх. Поскольку у нас поле зрения 60 градусов, БЕТА составляет 30 градусов для крайнего левого луча и -30 градусов для крайнего правого луча.

person Random Davis    schedule 11.03.2021
comment
Сожалею, что этот метод не сработал для меня. Я связал это руководство вверху своего сообщения, сославшись на то, что оно не повлияло на искажение. float distort_adjust = cos(to_radians(half_fov)); может ошибаться? - person Caspian Ahlberg; 12.03.2021
comment
@CaspianAhlberg half_fov не является правильным значением. Значение БЕТА и, следовательно, distort_adjust меняется с каждым столбцом; это угол между камерой и колонной. Таким образом, если бы угол игрока был 90 градусов, а ваше поле зрения было 30, БЕТА сначала была бы -30 градусов, а закончилась бы 30 градусами. Поэтому я считаю, что в вашем коде это будет theta - player.angle, но вам нужно убедиться, что это правильно. То есть, я думаю, distort_adjust будет установлено на cos(to_radians(theta - player_angle)). Но ваша текущая попытка точно неверна, тем более что она не обновляется для каждого значения тета. - person Random Davis; 12.03.2021
comment
Ваша идея для theta - player.angle дает мне диапазон от -15 до 15 градусов. Это согласуется с FOV, но все еще есть небольшое искажение. Это от -15 до 15 или что-то другое? - person Caspian Ahlberg; 12.03.2021
comment
Возможно, вы подумали о примере из учебника, когда вы сказали от -30 до 30. - person Caspian Ahlberg; 12.03.2021
comment
@CaspianAhlberg да, я просто привел пример. Также я имел в виду, если бы ваш FOV был 60 градусов. Извини. Также, возможно, вы можете показать, как теперь выглядит искажение. - person Random Davis; 12.03.2021
comment
Я обновил пост с некоторыми фотографиями. - person Caspian Ahlberg; 12.03.2021
comment
@CaspianAhlberg да, похоже, может быть какая-то другая проблема. На данный момент я не в себе, я думаю, вам просто нужно внимательно просмотреть свой код и сравнить его с рабочим примером. Если вы не можете понять это, вам, возможно, придется задать новый вопрос, если на этот вопрос не будет других ответов. - person Random Davis; 12.03.2021
comment
Понятно. В конце концов я мог бы переключиться на подход с матричным преобразованием или на что-то, не так ориентированное на триггер. Я не уверен. Я работал над этим так долго, что, возможно, было бы лучше начать сначала. - person Caspian Ahlberg; 12.03.2021
comment
@CaspianAhlberg ну что ж, даже если вы начнете сначала, это, вероятно, будет намного проще. Иногда очень помогает начать с нуля. - person Random Davis; 12.03.2021
comment
Можно (возможно) исправить некоторые искажения с помощью регулировки высоты, но основная проблема заключается в том, что выборка под одинаковыми углами будет искажать изображение в горизонтальном измерении. Таким образом, следует также сжать изображение по горизонтали, например. путем повторной выборки. - person Aki Suihkonen; 16.03.2021