Raycasting engine rendering creating slight distortion increasing towards edges of screen

前端 未结 2 1454
心在旅途
心在旅途 2020-12-30 17:13

I\'m developing a basic raycasting engine for HTML5 canvas, of the variety used in games like Wolfenstein 3D and Doom, as a learning exercise / hobby project. I\'ve got to t

2条回答
  •  天涯浪人
    2020-12-30 18:08

    Having spent a couple of hours trying to solve this exact problem on a raycasting engine of my own, I would like to give more detailed mathematical background as to why this is the correct answer, since I wasn't entirely convinced at first. Especially since when doing perspective projection, you already have to correct some spherical distortion (fish bowl effect). The effect described here is a completely different effect.

    This is what I got in my engine: the camera is in a square room, looking at a corner, at an angle of roughly 45°, with a FOV of 90°. It seems to have a slight spherical distortion. The red lines have been added afterward, it is also much more obvious to see in motion, but making a GIF is a PITA:

    Spherical distortion

    Here is the same room, same location and angle, but with a FOV of 70°. It is not as noticeable (and again, easier to see in motion):

    Same room with FOV=70

    The first version of my raycasting engine emitted rays from -FOV/2+camera_angle to FOV/2+camera_angle, with each angle spaced by FOV/SCREEN_WIDTH degrees (in my case SCREEN_WIDTH was 640).

    Here is a top-view schema, with SCREEN_WIDTH = 9:

    Casting rays

    You can see the problem here : when we use a fixed angle, the only thing guaranteed to be constant are those arcs of circle between two rays. But what should be constant are the segments on the projection plane. We can see by using a fixed angle, that the segments get longer the farther from the center.

    To solve this, keep in the mind the following parameters :

    • FOV = field of view, 90° in this example.
    • DIST = distance from camera to projection plane. In my engine I initially choose 50, not knowing better, but it will need to be adjusted depending on the FOV actually.
    • SCREEN_WIDTH = width of the screen in pixels, 640 in my example

    Knowing this, we can compute what the length of the segments (SEG_LEN) on the projection plane should be, by using some trigonometry in the triangle ABC:

    tan(FOV/2) = SCREEN_HALFLEN / DIST

    SCREEN_HALFLEN = DIST * tan(FOV/2)

    SCREEN_HALFLEN is the length of screen projected on our imaginary plane, to get the SEG_LEN, simply do:

    SEG_LEN = SCREEN_HALFLEN / (SCREEN_WIDTH/2)

    Knowing the segment length, we can compute the real angles at which rays need to be emitted: given a column x going from 0 to SCREEN_WIDTH-1, the angle should be:

    ANGLES[x] = atan(((SEG_LEN * x - SCREEN_HALFLEN) / DIST)

    This is the more or less the same formula given by James Hill in his final example. Putting this all together in the engine, it indeed eliminates the spherical distortion:

    Spherical distortion corrected

    For fun, we can compute what are the differences between a fixed angle raycasting and fixed length raycasting, in the worst case at ray x = 97 where there is a 9 pixels difference:

    The angle for the fixed angle raycasting is = 97 * FOV/SCREEN_WIDTH - FOV/2 = -31.359375°

    With a fixed length raycasting, the angle is : atan(97 * SEG_LEN / DIST) = -34.871676373193203°

    So, up to an 11% error, using given parameters (FOV = 90, DIST = 50, SCREEN_WIDTH = 640).

    For reference, I would like to add more detail as to how I implemented this is my engine: for the better or the worse, I wanted to do everything using integer arithmetic (except initialization stuff). First I setup two tables to pre-compute sine and cosine values, using fixed point arithmetic (examples are in C language):

    #define FIXEDSHIFT     13
    #define FIXEDPRES      (1<

    I initially used these tables to also cast rays, which resulted in the first 2 screenshots. So I added the ANGLES table, split into cartesian coordinates:

    int16_t * XRay = malloc(SCREEN_WIDTH * sizeof *XRay);
    int16_t * YRay = malloc(SCREEN_WIDTH * sizeof *YRay);
    double    dist = (DIST * tan(FOV*M_PI/360)) / (HALF_WIDTH-1);
    
    for (i = 0; i < HALF_WIDTH; i ++)
    {
        #if 0
        /* for fun, this re-enables the spherical distortion */
        double angle = i * (2.0*M_PI / (MAX_TAB));
        #else
        double angle = atan((dist * i) / DIST);
        #endif
    
        XRay[HALF_WIDTH-i-1] =   XRay[HALF_WIDTH+i] = (int16_t)(cos(angle) * FIXEDPRES);
        YRay[HALF_WIDTH-i-1] = -(YRay[HALF_WIDTH+i] = (int16_t)(sin(angle) * FIXEDPRES));
    }
    

    Then in the raycasting engine, to get the correct rays, I used :

    int raycasting(int camera_angle)
    {
        int i;
        for (i = 0; i < SCREEN_WIDTH; i ++)
        {
            int dx = Cos[camera_angle];
            int dy = Sin[camera_angle];
    
            /* simply apply a rotation matrix with dx (cos) and dy (sin) */
            int xray = (XRay[i] * dx - YRay[i] * dy) >> FIXEDSHIFT;
            int yray = (XRay[i] * dy + YRay[i] * dx) >> FIXEDSHIFT;
    
            /* remember that xray and yray are respectively cosine and sine of the current ray */
    
            /* you will need those values to do perspective projection */
    
            /* ... */
        }
    }
    

提交回复
热议问题