2
\$\begingroup\$

I have a misunderstanding of the raycasting formulas on flat screen because my rendering is not what I expected, this is some parts of the code to show my projection formulas that I wrote by myself

This is the code for the function intersection

int         intersection(t_wolf3d *w, int i, t_vec2f *dir, int iter){
int             mapx;
int             mapy;
unsigned int    length;


length = -1;
while (++length < w->cam->raylength)
{
    mapx = (int)(w->cam->position.x + (length * dir->x));
    mapy = (int)(w->cam->position.y + (length * dir->y));
    if ((mapy / BLOC_SIZE < w->map->h && w->map->board[mapy / BLOC_SIZE]
                    [mapx / BLOC_SIZE]) || (i == 1 && mapy >= HEIGHT_MM))
    {
        w->cam->intersection.x = mapx;
        w->cam->intersection.y = mapy;
        return (1);
    }
}
return (0);

}

Code used for the raycasting algorithm:

void                    raycasting(t_wolf3d *w, int limit){
int         i;
int         hit;
t_vec2f     dir;
t_vec2f     forward;
t_vec2f     right;
double      halfwidth;
double      offset;


i = -1;
forward.x = tcos(w->cam->angle);
forward.y = tsin(w->cam->angle);
right.x = forward.y;
right.y = -forward.x;
halfwidth = ttan(w->cam->fov / 2.0);
while (++i < limit && !(hit = 0))
{
    w->cam->rays[i].startpoint = w->cam->position;
    w->cam->rays[i].startpoint.color = 0xff0000;
    offset = ((i * 2.0 / (limit - 1.0)) - 1.0) * halfwidth;
    dir.x = forward.x + offset * right.x;
    dir.y = forward.y + offset * right.y;
    if ((hit = intersection(w, limit, &dir)))
        w->cam->rays[i].endpoint = w->cam->intersection;
    else
    {
        w->cam->rays[i].endpoint.x = w->cam->position.x + w->cam->raylength
            * dir.x;
        w->cam->rays[i].endpoint.y = w->cam->position.y + w->cam->raylength
            * dir.y;
    }
    limit == w->width ? render(w, i, hit) : 0;
}

}

For non-straight walls, I thought it was my variable types, it seems like not, I changed them from integers to double and i still got the same result, this is a look of my structures :

typedef struct      s_point{
int             x;
int             y;
int             color;}                 t_point;

typedef struct      s_vec2f{
double          x;
double          y;}                 t_vec2f;
typedef struct      s_camera{
double          radius;
double          angle;
double          fov;
double          raylength;
double          speedmove;
double          speedangle;
t_point         position;
t_point         intersection;
t_point         rays[WIDTH];}                   t_camera;
typedef struct      s_map{
int             **board;
int             w;
int             h;}                 t_map;
typedef struct      s_wolf3d{
t_map           *map;
t_camera        *cam;
int             hit;
t_image         *img;
void            *mlx_ptr;
void            *win_ptr;
char            texture;
t_image         *textures[3];
int             colors[3];
int             mini_h;
int             mini_w;
int             width;
int             height;}                    t_wolf3d;
\$\endgroup\$
5
  • \$\begingroup\$ I added my structures, which contain the type of each variable \$\endgroup\$
    – Num Lock
    Commented Apr 1, 2019 at 13:09
  • \$\begingroup\$ Try logging the list of points where your rays detect an intersection when looking at a straight wall. I wonder whether your raymarching routine might be tunneling past the first hit in some cases. \$\endgroup\$
    – DMGregory
    Commented Apr 2, 2019 at 11:24
  • \$\begingroup\$ I don't understand what u mean by a "minimal complete verifiable example", I just want to be clear that when i say straight walls, i am talking of the 2 parallels lines (top and bottom) of the walls which are not straights but look like small stairs. My expression doesn't look comprehensible and I sorry for that. \$\endgroup\$
    – Num Lock
    Commented Apr 2, 2019 at 11:25
  • \$\begingroup\$ So you searched the term and read the very first search result, right? Don't hesitate to include an image illustrating the problem so there's no ambiguity in your meaning when describing the artifacts you observe. \$\endgroup\$
    – DMGregory
    Commented Apr 2, 2019 at 11:28
  • \$\begingroup\$ I got you! I think the problem with my walls, is that i don't check the real distance from the player to the wall (i don't really know where the ray hits the wall in x or in y), i will try to implement that and show you some snapshots, tell me if I am wrong \$\endgroup\$
    – Num Lock
    Commented Apr 2, 2019 at 11:54

1 Answer 1

3
\$\begingroup\$

The trick here is to think of your image plane as though it was actually a flat object sitting in front of the camera, perpendicular to its view direction. This parallels the way the player's screen is a flat object in front of their eyes, perpendicular to their gaze - and this similarity is what makes the optical illusion of linear perspective work.

Diagram of raycasting setup

Following this line of thinking, each column of pixels on your screen corresponds to a specific point on this image plane in your game space.

Because the pixels on our screen are spaced out evenly, we need to space out these samples evenly in space along the image plane - I've visualized that with the line of little squares in the diagram above. So we need to find a direction to shoot each ray so it first "through" one of these evenly-spaced pixel points.

If we sweep our rays using equal angular increments like you do in this code:

angle = (w->cam->angle + (w->cam->fov / 2) - (i * w->cam->fov / WIDTH_MM));

then we don't get evenly-spaced points on our image plane - they'll bunch up in the middle and spread out at the sides, breaking our illusion. It makes straight edges of walls start looking bent.

In reality, the angular gap between adjacent pixels changes as we sweep our ray across the screen - with smaller angular spread at the sides and bigger strides in the middle:

Diagram of angles

So, working in angles can be a bit of a trap, and it helps to get out of angular space and into cartesian coordinates & vectors early. As a bonus, this will also save us most of our trig calculations! :)

First we'll need a unit vector in the direction the camera is facing, our forward vector. We'll use this a lot, so it's worth saving rather than looking up the trig values every time:

 forward.x = tcos(w->cam->angle);
 forward.y = tsin(w->cam->angle);

Next we need a vector pointing parallel to the image plane, to the camera's right. We can use a little trick to compute this from forward with no more trig:

right.x = forward.y;
right.y = -forward.x;

And we'll need to now how wide to sweep from the center of our view to one edge.

halfWidth = ttan(w->cam->fov / 2)

Now the ray through the leftmost pixel in our image plane points along forward - halfWidth * right, and the ray through the rightpmost pixel points along forward + halfWidth * right.

We can calculate the direction the ith ray should travel like so:

offset = ((i * 2.0 / (WIDTH_MM - 1.0)) - 1.0) * halfWidth;

rayDirection.x = forward.x + offset * right.x;
rayDirection.y = forward.y + offset * right.y;

You can optionally normalize or scale this direction vector as needed to suit your ray intersection routines.


To demonstrate that this works, I built a test scene in Unity that would render a series of cubes with the standard rasterization pipeline, then also fire rays and overlay a greyscale version calculated raycasting-style. You can see from this cross-fade between the two, the raycasting approach accurately reproduces the same linear perspective, with no erroneous curving of the walls:

Cross-fade between rasterized & raycast scene

Here's the code I use to fire my rays, and save the depths into a buffer to use in drawing the raycast version:

void Update()
{
    // Compute worldspace width of the projection plane (at a depth of 1)
    // using the camera's field of view (which in Unity is a vertical FoV).
    float halfHeight = Mathf.Tan(_camera.fieldOfView * Mathf.Deg2Rad / 2.0f);
    float halfWidth = _camera.aspect * halfHeight;   

    // For each pixel column in our raycasting buffer...
    for(int i = 0; i < _depths.Length; i++) {
        // Compute a ray firing through that point on the image plane.
        float offset = (2.0f * i) / (_depths.Length - 1.0f) - 1.0f;
        var ray = new Ray(
            transform.position, 
            transform.forward + transform.right * halfWidth * offset
        );

        // If it hits something, get its depth along the camera forward vector.
        RaycastHit hit;
        float depth = float.PositiveInfinity;
        if(Physics.Raycast(ray, out hit)) {
            depth = Vector3.Dot(hit.point - transform.position, transform.forward);                
        }
        _depths[i] = new Color(depth, depth, depth, depth);
    }

    _depthRibbon.SetPixels(_depths);
    _depthRibbon.Apply(false, false);
}

And inside the shader:

fixed4 frag (v2f i) : SV_Target
{
    // Sample depth from the buffer populated by our rays.
    float depth = tex2D(_MainTex, i.uv).r;

    // Wall height is our master height divided by depth.       
    float height = _WallScale / depth;

    // Default floor / ceiling to black.
    fixed4 col = fixed4(0, 0, 0, _Alpha);

    // If the pixel we're shading is closer to the middle of the screen
    // than the edge of the wall, shade it as a wall.
    if (abs(i.uv.y - 0.5f) < height) {
        col = lerp(fixed4(0, 0, 0, _Alpha), fixed4(1, 1, 1, _Alpha), exp(-depth * 0.3f + 0.3f));
    }
    
    return col;
}
\$\endgroup\$
5
  • \$\begingroup\$ Ty! For the explanation, why do u compute offset like that? \$\endgroup\$
    – Num Lock
    Commented Mar 31, 2019 at 19:19
  • \$\begingroup\$ Because half the field of view has to go on the left, and half on the right. So (i * 2.0 / screen width) gives us a value ranging from -1 to 1, multiplied by halfWidth gives us that half field of view on the left, half on the right for one full field of view in-between. \$\endgroup\$
    – DMGregory
    Commented Mar 31, 2019 at 19:25
  • \$\begingroup\$ @NumLock To double-check this, I built a test in Unity using the formulas I've described. No unnaturally curved walls in the result - it lines up with the rasterized reference render as expected. So, I think there must be a bug in how you've implemented this. Can you show us the code you use to choose the ray directions after trying to implement this answer? \$\endgroup\$
    – DMGregory
    Commented Apr 2, 2019 at 2:42
  • \$\begingroup\$ I understand and I thank you for that! Do you think there is a way to correct the difference of heights from rays near the perpendicular ray and the rays far to the perpendicular ray? Cause I have no straight walls as u shown in your example when look from side. \$\endgroup\$
    – Num Lock
    Commented Apr 2, 2019 at 10:59
  • \$\begingroup\$ The example above demonstrates that if you use this method, you will get straight walls, no additional correction is needed to the heights beyond what's shown above. I still don't have a minimal complete verifiable example of the problem you observe when implementing the method I've shown here, so without that I can't help you diagnose what's wrong. \$\endgroup\$
    – DMGregory
    Commented Apr 2, 2019 at 11:09

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .