如何在我的光线投射器中修复扭曲的墙壁?

Posted

技术标签:

【中文标题】如何在我的光线投射器中修复扭曲的墙壁?【英文标题】:How do I fix warped walls in my raycaster? 【发布时间】:2021-03-15 19:36:06 【问题描述】:

我正在使用带有 C 的 SDL 库编写一个光线投射器。我已经处理鱼眼效果好几个星期了。对于像 60 度这样的视野,我将到墙壁的距离乘以相对角度的余弦(范围从 -30 到 30),但仍然得到相同的鱼眼。看起来是这样的:

鉴于很多来源都建议使用余弦校正,我现在不知道该怎么做,但它并不能解决我的情况下的失真问题。

我是这样编译的: clang `pkg-config --cflags --libs sdl2` raycaster.c 要前进和后退,请按向上和向下键。按左右键进行扫射。您可以使用as 键分别向左和向右转。

如果您想看一下,我的代码如下。如果您设法弄清楚为什么我的引擎会出现扭曲的视角,请告诉我。

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

【问题讨论】:

您似乎已经问过类似的问题:***.com/questions/66591163/… 得到了接受的答案。那么,您从该答案中融入了什么?您当前/较新的代码有何不同? @CraigEstey 这段代码与之前的代码基本相同,但这次我努力让它变得更简单,代价是移除了阴影和彩色墙壁(为了便于阅读)。如果您阅读已接受答案的底部评论线程,您会看到帮助我的人说剩余的失​​真超出了他们的范围。这个问题是针对剩余的失真。 @CraigEstey 从上一个问题中,我加入了正确的余弦校正(仍然不能完全工作):float distort_adjust = cos(to_radians(theta - player.angle)); float dist = distance(rel_x, rel_y, new_x, new_y) * distort_adjust; dist_step = 0.8 太大,会得到很多工件 @tstanisl 我已经为dist_step 尝试了一些不同的较小值,但它们似乎没有什么不同。你推荐什么尺寸?此外,步长与屏幕尺寸有关,而不是地图尺寸,所以我认为 0.8 实际上可能太小了。你怎么看? 【参考方案1】:

您需要应用以下差异:

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

发现了一些问题。

    系数width_ratioheight_ratio 似乎将地图空间中的坐标与屏幕空间中的坐标混合在一起。这是没有意义的。此外,它通过沿特定轴更快地移动来破坏导航。

    dist 投影到通过屏幕中心的光线投射后(dist *= cos(...) 你必须应用简单的透视来计算墙的高度(变量wall_height

    最后,围绕中间水平线画一个高度为wall_height的矩形。

编辑。放 dist_step = 0.01

【讨论】:

这太不可思议了!非常感谢。我已经为此苦苦挣扎了好几个星期,而你让我的问题消失了。祝福你的灵魂!【参考方案2】:

这段代码有两个主要问题。必须根据光线与墙壁的交点计算到墙壁的距离,而不是仅仅依靠端点位于墙壁正方形内。

C处有一个摄像头,通过将来自 C 的光线投射到 A 和 B 之间足够数量的点来解决失真,只需将此平面(您的屏幕)划分为等宽的列(像素)。

我对余弦校正相当悲观,因为据我所知,与列高相比,更可能调整绘制的列宽或位置。

【讨论】:

我很困惑。到墙壁的正确距离难道不是只需要端点位于墙壁正方形内,仅此而已吗? @CaspianAhlberg:哦,不。如果您的鼻子几乎不接触墙壁或向内几英寸,则会有很大的不同。与射线相同。如果您仔细查看图表,您会注意到例如如果采样点太稀疏,大多数光线可能会错过一个正方形。因此,需要分析地找到相交的正方形。 (或通过其他方法,例如二进制空间分区。) 当你说“分析”时,你能说出一种这样的方法吗? 将墙段和射线转换为线方程并求解交点。正方形都是轴对齐的,因此每个交点需要一个分区。对于一般情况下的图形,人们通常使用二进制空间分区树。

以上是关于如何在我的光线投射器中修复扭曲的墙壁?的主要内容,如果未能解决你的问题,请参考以下文章

THREE.JS : 带有光线投射器和透视相机的点击事件

如何使用javascript从精灵表中缩放/拉伸/倾斜精灵?

三个js raycasting OBJ

HTML5 Canvas fillRect 慢

使用光线投射防止破折号穿过墙壁

如何在threejs中通过raycaster从相交检查中排除辅助对象?