2
\$\begingroup\$

Hello I have a game using raycasting for the TI Nspire in Lua, and when the player faces a wall so that their degree they are facing is perpendicular to the length of the wall, I am having what is described as the fishbowl/fisheye effect.

How can I fix this? I have already looked here, and none of that makes sense to me. Here is my code:

map = {{0, 0, 50, 0},{50, 0, 50, 50},{50, 50, 0, 50},{0, 50, 0, 0}}

facing = 45 --"player rotation"
rays = {}
x, y = 25, 25

function raycast()
    for i,v in pairs(rays) do
        rays[i] = nil
    end

    rayf = facing + 30

    for i = 1, 320, 1 do --64, 5
        x_ray, y_ray = x, y
        ray()
        table.insert(rays, radius)
        rayf = rayf - (60/320)
    end
    platform.window:invalidate()
end

function ray()
    radius = 1
    for n = 1, 50, 1 do -- increase of testdot
        x_ray = math.floor(x + (radius * math.cos(rayf * 3.141592653/180)))
        y_ray = math.floor(y - (radius * math.sin(rayf * 3.141592653/180)))
        for k,v in pairs(map) do
            if (
                math.min(v[1],v[3]) <= x_ray and x_ray <= math.max(v[1],v[3])
            ) and (
                math.min(v[2],v[4]) <= y_ray and y_ray <= math.max(v[2],v[4])
            ) then
                --print("Collision")
                return true
            end
        end
        radius = n
    end
end

function on.paint(gc)
    gc:setColorRGB(0,200,255)
    gc:fillRect(0, 0, 320, 108)
    gc:setColorRGB(100,100,100)
    gc:fillRect(0, 109, 320, 109)
    gc:setColorRGB(100,0,100)
    for i,v in ipairs(rays) do
        gc:drawLine(i, 108.5 - ((100/v * 20)/2), i, 108.5 + ((100/v * 20)/2))
    end
end

function on.arrowKey(key)
    if key == 'left' then
        facing = facing + 3
    elseif key == 'right' then
        facing = facing - 3
    end
    if key == 'up' then
        y = y - 1
    elseif key == 'down' then
        y = y + 1
    end
    raycast()
end

I have also tried it like this:

map = {{0, 0, 50, 0},{50, 0, 50, 50},{50, 50, 0, 50},{0, 50, 0, 0}}

facing = 45 --"player rotation"
rays = {}
x, y = 25, 25

function raycast()
    for i,v in pairs(rays) do
        rays[i] = nil
    end

    rayf = facing + 30

    for i = 1, 320, 1 do --64, 5
        x_ray, y_ray = x, y
        ray()
        ----------------
        fisheyeradius = radius / (math.cos(rayf - facing))
        table.insert(rays, fisheyeradius)
        ----------------
        rayf = rayf - (60/320)
    end
    platform.window:invalidate()
end

function ray()
    radius = 1
    for n = 1, 50, 1 do -- increase of testdot
        x_ray = math.floor(x + (radius * math.cos(rayf * 3.141592653/180)))
        y_ray = math.floor(y - (radius * math.sin(rayf * 3.141592653/180)))
        for k,v in pairs(map) do
            if (
                math.min(v[1],v[3]) <= x_ray and x_ray <= math.max(v[1],v[3])
            ) and (
                math.min(v[2],v[4]) <= y_ray and y_ray <= math.max(v[2],v[4])
            ) then
                --print("Collision")
                return true
            end
        end
        radius = n
    end
end

function on.paint(gc)
    gc:setColorRGB(0,200,255)
    gc:fillRect(0, 0, 320, 106)
    gc:setColorRGB(100,100,100)
    gc:fillRect(0, 106, 320, 106)
    gc:setColorRGB(100,0,100)
    for i,v in ipairs(rays) do
        gc:drawLine(i, 108.5 - ((100/v * 20)/2), i, 108.5 + ((100/v * 20)/2))
    end
end

function on.arrowKey(key)
    if key == 'left' then
        facing = facing + 3
    elseif key == 'right' then
        facing = facing - 3
    end
    if key == 'up' then
        y = y - 1
    elseif key == 'down' then
        y = y + 1
    end
    raycast()
end

The wall originally looked like this:

wall

But I have made changes around the ---------------, and now the wall looks like this:

img

I this supposed to occur?

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

I actually had to solve this some time ago. The easiest fix is to simply divide by cos(angle of ray - viewAngle) and use that as the distance

\$\endgroup\$
11
  • \$\begingroup\$ the viewAngle is the angle of the player? \$\endgroup\$ Commented Mar 26, 2018 at 0:36
  • \$\begingroup\$ And is it the distance of a ray that I divide by cos(angle of ray - viewAngle)? \$\endgroup\$ Commented Mar 26, 2018 at 0:37
  • \$\begingroup\$ @crazicrafter1 Yes and yes \$\endgroup\$
    – Bálint
    Commented Mar 26, 2018 at 6:00
  • \$\begingroup\$ It looks like in the code above, you forgot to convert from degrees to radians, which is why you're getting that wrap-around/repeating effect. You're spinning around the whole circle more times than you intend. \$\endgroup\$
    – DMGregory
    Commented Mar 26, 2018 at 11:59
  • \$\begingroup\$ rayf * 3.141592653/180 is the part that converts to degrees to radians \$\endgroup\$ Commented Mar 27, 2018 at 1:11
4
\$\begingroup\$

It looks like you're firing out your rays at evenly spaced angular intervals.

rayf = facing + 30
...
x_ray = math.floor(x + (radius * math.cos(rayf * 3.141592653/180)))
y_ray = math.floor(y - (radius * math.sin(rayf * 3.141592653/180)))
...
rayf = rayf - (60/320)

This makes a kind of intuitive sense - you want to sweep rays across the field of view - but if you're mapping each ray to (a column of) pixels on a flat screen, then it's a mistake.

When the player looks at the grid of pixels on a flat screen, the angles from their eye to each column of pixels from left to right are not evenly spaced. They're spread apart widest in the middle of the screen, and bunch together toward the sides as the screen slopes away from perpendicular to the view ray, foreshortening in the player's view.

Diagram showing how angles between rays are not the same when firing at evenly spaced grid points.

When you use rays fired off at even angular intervals, you're effectively stating that the rendering intent is to be displayed on a cylindrical or spherical screen that wraps around the player's head. And for such a screen, this pinched lens shape is indeed the correct appearance for a flat wall.

Diagram showing a spherical screen displaying a lens-shaped wall.

So, to ensure your rendering looks good on a flat screen, fire your rays based on the flat screen. Imagine you have an image plane hovering in front of the player, pick evenly spaced points on that plane, and fire your rays toward those points.

For example...

forward_x = cos(facing * pi/180)
forward_y = -sin(facing * pi/180)  -- following the convention you use for rays

projection_plane_width = 2 * tan(horizontal_field_of_view * pi/180);

right_x = -forward_y * projection_plane_width
right_y = forward_x * projection_plane_width

for i = 1, 320, 1 do
    -- step from -0.5 to +0.5 in equal increments 
    progress = (i - 1)/319 - 0.5

    -- aim at a position ahead of the player and slightly to the left or right
    step_x = (forward_x + progress * right_x) * radius
    step_y = (forward_y + progress * right_y) * radius

For more info on projections for spherical vs flat screens, see this earlier answer.

\$\endgroup\$
3
  • \$\begingroup\$ What is the forward_x and forward_y variables for? The right_x, right_y? Where do I put this in the code? Sorry if I'm asking for too much \$\endgroup\$ Commented Mar 27, 2018 at 2:05
  • \$\begingroup\$ forward x & y are the components of a unit vector pointing in the direction the player is looking. Right x & y are the components of a unit vector pointing 90 degrees to the player's right. We use this coordinate basis to pick the points to fire rays toward. \$\endgroup\$
    – DMGregory
    Commented Mar 27, 2018 at 2:09
  • \$\begingroup\$ This later answer elaborates on this approach with a more complete example. \$\endgroup\$
    – DMGregory
    Commented Apr 12, 2022 at 15:06

You must log in to answer this question.

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