Raycasting engine rendering creating slight distortion increasing towards edges of screen

前端 未结 2 1397
心在旅途
心在旅途 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<<FIXEDSHIFT)
    #define DIST           50
    #define FOV            90
    #define SCREEN_WIDTH   640
    #define SCREEN_HEIGHT  480
    #define HALF_WIDTH     (SCREEN_WIDTH/2)
    
    int i;
    int size = 360.0 / ((double)FOV / SCREEN_WIDTH)));
    int16_t * Cos = malloc(size * sizeof *Cos);
    int16_t * Sin = malloc(size * sizeof *Sin);
    
    for (i = 0; i < size; i ++)
    {
        double angle = i * (2.0*M_PI / size);
        Cos[i] = (int16_t)(cos(angle) * FIXEDPRES);
        Sin[i] = (int16_t)(sin(angle) * FIXEDPRES);
    }
    

    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 */
    
            /* ... */
        }
    }
    
    0 讨论(0)
  • 2020-12-30 18:16

    I solved this problem properly quite a while ago, but haven't got round to updating the answer until now. I have removed my previous answer which was incorrect (it gave almost correct results but by an indirect method, thanks to my lack of understanding of the root cause of the problem).

    As Sam mentioned in his earlier comment, the root cause of the issue is that fixed angle increments are actually not correct if you want to achieve equally-spaced columns (which are necessary for the rendered result to look undistorted). This was mentioned in a forum post here, but although I found this I didn't fully understand why this was the case, or how to remedy the problem, until much later.

    To achieve equally spaced columns on the screen, it stands to reason that each ray must travel from the point of view and pass through a pixel which is equally spaced along the projection surface, which means that as the rays move further from the central pixel of the screen, the increment by which the angle from the look direction increases gets gradually smaller. This is illustrated by the following picture (apologies, it isn't exactly a work of art):

    With small fields of view the problem is not very noticeable, but becomes more problematic as the field of view increases (in my diagram the field of view is quite large to clearly illustrate the problem). To correctly calculate the ray angle increments, the following process must be used:

    Where:

    ang = ray angle from the look direction, whose ray passes through the central x coordinate of the screen;
    opp = opposite side (equivalent to the distance of the screen X coordinate through which the ray passes from the screen X coordinate of the central pixel);
    adj = adjacent side (equivalent to the distance from the point of view to the projection surface, which will be predetermined in code somewhere);
    

    We can use the following formula (derivation included for clarity):

    tan( ang ) = opp / adj
    ang = atan( opp / adj )
    ang = atan( ( pixel x coord - half screen width ) / dist to projection surface )
    

    Javascript code example from my engine:

    for( var x = 0; x < canvasSizeX; x++ ){
    
        var xAng = _atan( ( x - canvasSizeHalfX ) / m_DistToProjSurf );
        xRayAngles.push( xAng );
    }
    

    Due to the somewhat scarce nature of information on raycasting engines which is available online, and also due to the fact this particular issue isn't explicitly covered in any of the main tutorials which are out there, I wanted to update this post with the correct information in case anyone else has the same problem I did and doesn't understand why. Hopefully this will help someone.

    0 讨论(0)
提交回复
热议问题