Raycaster: fish eye effect past 60 degrees

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Aidymouse
Prole
Posts: 6
Joined: Mon Aug 13, 2018 10:09 am
Location: New Zealand

Raycaster: fish eye effect past 60 degrees

I'm trying to make a dungeon crawler type game and I thought a nice way to do the game screen would be raycasting.
I've been following this awesome tutorial: https://permadi.com/1996/05/ray-casting ... f-contents
Unfortunately, the raycaster I have built only works at an FOV of 60 degrees of less, while most dungeon crawlers show more than that.
After some experimentation, I found that an FOV of 120 degrees includes everything I want on the screen.

There's just one thing. Even with fish eye correction, the screen begins warping at anything higher than 60 degrees.

Code: Select all

-- raycaster.lua
sin = math.sin
cos = math.cos
tan = math.tan

Newcaster = {}

Newcaster.map = {{1, 1, 1, 1, 1},
{1, 0, 0, 0, 1},
{1, 0, 1, 0, 1},
{1, 0, 0, 0, 1},
{1, 1, 1, 1, 1}}

Newcaster.player = {angle = 360, x = 2, y = 2}
Newcaster.highlightRay = 1
Newcaster.displayRay = true -- horizontal intercepts
--[[ ANGLES

/- 090 -\
/    |    \
180 --+-- 360
\    |    /
\- 270 -/

360 is EAST
360 -> 1 instead of 359 -> 0

Blocks are 100 wide
]]

-- center of projection plane: 250, 150
screenWidth = 500
screenHeight = 300

FOV = 120

function Newcaster:raycast()
distToPlane = math.floor((screenWidth / 2) / tan(rad( FOV / 2 )))
subAngle = FOV / screenWidth -- angle of subsequent rays

rayAngle = self.player.angle + FOV / 2
rayAngle = rayAngle + subAngle -- Because we subtract from it once the loop starts

if self.player.angle > 360 then self.player.angle = 1 end
if self.player.angle < 1 then self.player.angle = 360 end

if self.highlightRay > 500 then self.highlightRay = 1 end
if self.highlightRay < 1 then self.highlightRay = 500 end

for ray=1, screenWidth do
rayAngle = rayAngle - subAngle
if rayAngle < 1 then rayAngle = rayAngle + 360 end
if rayAngle > 360 then rayAngle = rayAngle - 360 end

startCoords = {x = (self.player.x-1)*100+50,
y = (self.player.y-1)*100+50}

dx = 100 / tan(rad(rayAngle)) -- The amount that x changes for every horizontal intercept
dy = 100 * tan(rad(rayAngle)) -- The amount that y changes for every vertical intercept

-- If the ray shoots straight up or straight down, dx will be infinite
if rayAngle == 360 or rayAngle == 180 then
dx = math.huge
-- If the ray shoots directly left or right, dy will be infinite
elseif rayAngle == 90 or rayAngle == 270 then
dy = math.huge
end

-- Find Horizontal Intercepts ------------------------------
local upOrDown = 100

if rayAngle < 180 then upOrDown = -100 end

if rayAngle > 180 and rayAngle < 360 then
dx = -dx
end

curPointHoriz = {x = startCoords.x + dx/2,
y = startCoords.y + upOrDown/2}

local findHorizIntercepts = true

for intercept=1, 7 do -- Range of 7 blocks
testRow = curPointHoriz.y / 100
testCol = 1 + ((curPointHoriz.x - (curPointHoriz.x % 100)) / 100)

if curPointHoriz.y <= 0 or curPointHoriz.y >= #self.map*100 or
curPointHoriz.x <= 0 or curPointHoriz.x >= #self.map[1]*100 then -- It's outta da map!
findHorizIntercepts = false
end

if findHorizIntercepts then
if self.map[testRow][testCol] > 0 then
findHorizIntercepts = false
elseif self.map[testRow+1][testCol] > 0 then
findHorizIntercepts = false
else -- Increment them, we didn't hit nothin'!
curPointHoriz.x = curPointHoriz.x + dx
curPointHoriz.y = curPointHoriz.y + upOrDown
end
end

end

-- Find Vertical Intercepts ------------------------------
local leftOrRight = 100
if rayAngle < 270 and rayAngle > 90 then leftOrRight = -100 end

if rayAngle > 270 or rayAngle < 90 then dy = -dy end

curPointVert = {x = startCoords.x + leftOrRight/2,
y = startCoords.y + dy/2 }

local findVertIntercepts = true

for intercept=1, 7 do -- Range of 7 blocks
testRow = 1 + ((curPointVert.y - (curPointVert.y % 100)) / 100)
testCol = curPointVert.x / 100

if curPointVert.y <= 0 or curPointVert.y >= #self.map*100 or
curPointVert.x <= 0 or curPointVert.x >= #self.map[1]*100 then -- It's outta da map!
findVertIntercepts = false
end

if findVertIntercepts then
if self.map[testRow][testCol] > 0 then
findVertIntercepts = false
elseif self.map[testRow][testCol+1] > 0 then
findVertIntercepts = false
else -- Increment them, we didn't hit nothin' (again)!
curPointVert.x = curPointVert.x + leftOrRight
curPointVert.y = curPointVert.y + dy
end
end

end

-- Find the shortest line
horizLineDist = math.sqrt( (startCoords.x - curPointHoriz.x)^2 + (startCoords.y - curPointHoriz.y)^2 )
vertLineDist = math.sqrt( (startCoords.x - curPointVert.x)^2 + (startCoords.y - curPointVert.y)^2 )

drawAngle = self.player.angle - rayAngle
if horizLineDist < vertLineDist then
else
end

local drawHeight = math.floor((100 / correctedDistance * distToPlane))
love.graphics.setColor(1, 1, 1)
if self.highlightRay == ray then
if love.keyboard.isDown('l') then
print(drawAngle)
end
love.graphics.setColor(0, 1, 0)
end

love.graphics.rectangle('fill', ray+50, love.graphics.getHeight()/2 - drawHeight/2, 1, drawHeight)

--[[ Draw the rays, infinitely helpful for debugging ------------------------------]
love.graphics.setColor(1, 0, 0) -- Horizontal Intercept Lines
if self.highlightRay == ray then highlightCoords = {x = curPointHoriz.x, y = curPointHoriz.y} end
love.graphics.line(startCoords.x, startCoords.y, curPointHoriz.x, curPointHoriz.y)

love.graphics.setColor(0, 0, 1)
if self.highlightRay == ray then highlightCoords = {x = curPointVert.x, y = curPointVert.y} end
love.graphics.line(startCoords.x, startCoords.y, curPointVert.x, curPointVert.y)
--]]

if self.highlightRay == ray then
if love.keyboard.isDown('p') then
print('Ray: '..ray..'\tRayangle: '..rayAngle)
end
end

end
-- Print some easy debug information
love.graphics.setColor(0, 1, 0)
love.graphics.print(self.highlightRay, 0, 12)
love.graphics.setColor(1, 1, 1)
love.graphics.print(self.player.angle, 0, 0)

end

return Newcaster
If I had to guess, I would say the code causing the malfunction is here, as that's where I start doing calculations to actually draw the walls.

Code: Select all

--raycaster.lua (subsection)

drawAngle = self.player.angle - rayAngle
if horizLineDist < vertLineDist then
else
end

local drawHeight = math.floor((100 / correctedDistance * distToPlane))

love.graphics.setColor(1, 1, 1)
love.graphics.rectangle('fill', ray+50, love.graphics.getHeight()/2 - drawHeight/2, 1, drawHeight) 
Thank you for any help! A readme is included that lists the controls.
Attachments
raydungeon.love

pgimeno
Party member
Posts: 1495
Joined: Sun Oct 18, 2015 2:58 pm

Re: Raycaster: fish eye effect past 60 degrees

Hi Aidymouse, welcome to the forums.
Aidymouse wrote:
Mon Aug 20, 2018 6:50 am
After some experimentation, I found that an FOV of 120 degrees includes everything I want on the screen.

There's just one thing. Even with fish eye correction, the screen begins warping at anything higher than 60 degrees.

[...]

If I had to guess, I would say the code causing the malfunction is here, as that's where I start doing calculations to actually draw the walls.
Edited to remove my previous text because I misunderstood. I thought you were worried about the natural distortion in rectilinear projection, and you were trying to compensate for it.

Now I think you want rectilinear but you're trying to compensate for the "natural" barrel distortion that happens in raycast and that's what doesn't work so well.

I think the problem is before that section. The angle increases by a constant increment of 0.12 degrees for every ray, and that can't be right. It should not be constant. You should calculate the angle corresponding to each horizontal screen pixel. The angles for the pixels near the borders are smaller than near the centre.

Edit: The tutorial makes the same mistake. The JS demos have curved walls. Here's a screenshot from the first demo, with a straight line drawn with GIMP, which shows the curvature of the walls:

Last edited by pgimeno on Mon Aug 20, 2018 8:13 pm, edited 5 times in total.
Thrust II Reloaded - GifLoad for Löve - GSpöt GUI - My NotABug.org repositories - portland (mobile orientation)
The MS-Github repositories I had have been closed after the acquisition announcement and will be removed in the near future.

Davidobot
Party member
Posts: 1144
Joined: Sat Mar 31, 2012 5:18 am
Location: Game-Dev. Land
Contact:

Re: Raycaster: fish eye effect past 60 degrees

For my own raycaster, I followed this great tutorial - https://lodev.org/cgtutor/raycasting.html

If you Ctrl+F "fisheye" it will point you to some code that will act as fisheye-correction.
After the DDA is done, we have to calculate the distance of the ray to the wall, so that we can calculate how high the wall has to be drawn after this. We don't use the Euclidean distance however, but instead only the distance perpendicular to the camera plane (projected on the camera direction), to avoid the fisheye effect. The fisheye effect is an effect you see if you use the real distance, where all the walls become rounded, and can make you sick if you rotate.
My library:
LovelyMoon

Check out my current project:
Raycaster

Oh hey, I have a website now!

Aidymouse
Prole
Posts: 6
Joined: Mon Aug 13, 2018 10:09 am
Location: New Zealand

Re: Raycaster: fish eye effect past 60 degrees

Hi, thanks for the quick reply!
pgimeno wrote:
Mon Aug 20, 2018 3:19 pm
I think the problem is before that section. The angle increases by a constant increment of 0.12 degrees for every ray, and that can't be right. It should not be constant. You should calculate the angle corresponding to each horizontal screen pixel. The angles for the pixels near the borders are smaller than near the centre.
I'm just a bit confused on what this part means, namely the bit about calculating the angle corresponding to each horizontal pixel.

Following from the tutorial, it says to take the FOV and divide it by width of the screen (500 pixels in my case). Using this, we can find the angle between each ray so that each column of pixels on the screen has a ray no matter the FOV + the width of the drawn environment stays constant.

Code: Select all

subAngle = FOV / screenWidth -- angle of subsequent rays

rayAngle = self.player.angle + FOV / 2
rayAngle = rayAngle + subAngle -- Because we subtract from it once the loop starts

One thing I could try is finding the ray angle relative to the player. At the moment, the players angle is used to find the ray angle which is consistent with the environment instead of being relative to the playe-
You know what, I'll just draw a picture.

Excuse the innacuracy of the image. Ah... ms-paint.

EDIT: The angle of the ray is based off of global angle positioning rather than being relative to the player. This might be a bit confusing but I don't think making the values relative to the player wouldn't fix the issue I'm having.

I'm not sure if that illuminates anything. I don't quite understand finding the angle of the ray based on the horizontal screen position.
Thanks a lot for your help! I'll keep experimenting and see if I can figure anything else out.
pgimeno wrote:
Mon Aug 20, 2018 3:19 pm
The angles for the pixels near the borders are smaller than near the centre.
Just a note on this section. Is this what I should be aiming for, or is this what I'm currently doing which is causing problems.

Thanks!

pgimeno
Party member
Posts: 1495
Joined: Sun Oct 18, 2015 2:58 pm

Re: Raycaster: fish eye effect past 60 degrees

Allow me to start from the bottom of your post, to clear up a confusion, as it's very important to the point.
Aidymouse wrote:
Tue Aug 21, 2018 12:09 am
pgimeno wrote:
Mon Aug 20, 2018 3:19 pm
The angles for the pixels near the borders are smaller than near the centre.
Just a note on this section. Is this what I should be aiming for, or is this what I'm currently doing which is causing problems.
I should have said "should be", rather than "are". It's what you should be aiming for. Also, I meant the increments between angles, sorry for not making that clear. I'll try to explain why.

Aidymouse wrote:
Tue Aug 21, 2018 12:09 am
Following from the tutorial, it says to take the FOV and divide it by width of the screen (500 pixels in my case). Using this, we can find the angle between each ray so that each column of pixels on the screen has a ray no matter the FOV + the width of the drawn environment stays constant.
Doing that results in projecting the image on a cylinder, not on a plane. The result does not preserve the straightness of lines, that's why you get deformations. You can do it on a plane and still fill every horizontal pixel of the screen.

Let me see if I can make you see why.

Not sure if you know how projection to the screen works. The principle is the same as in the photo cameras, more particularly the "camera obscura", except the image is formed on a plane in front of the point of view, rather than behind, which avoids inversion of the image. Here's an illustration of a camera obscura from Wikipedia (small because I'm stealing bandwidth from them):

So, this is the goal roughly:

(bear with me about the size and number of the pixels - that's a low resolution screen )

The rays that you should be casting should be from the POV to each pixel (ideally to the centre of each pixel, but it won't be noticeable if you choose a border), like this:

Now what I said about the angle increment being smaller near the edges should be more evident. By choosing a constant angle increment, what you're effectively doing is this:

That's what I meant about projecting the image on a cylinder. That causes horizontal distortion, even with vertical correction. I don't think you can preserve straight lines that way. With a lower angle of view, the cylinder is closer to a plane, and the distortion is less noticeable, but it's there, even in the demos, as I showed.

Now let's go for the math to make it look right. You first need to define the length from the POV to the projection plane. Let's define it as 1 unit of distance. Now, you want to determine how large is the screen, in distance units, with the given angle of view (AOV, which is in my opinion a more proper term for the angle that you call FOV). Since the distance to the screen is 1, that happens to be tan(AOV/2)*2 (we're calculating one half first and then duplicating it). I can make a diagram if you need more explanations.

That's the segment that you need to divide by the number of pixels, 500 in your case. Remember to keep half of them above and half below zero. Now, to cast the ray, the angle you need is the angle of the vector that goes from the POV to each pixel, i.e. to each subdivision. To calculate that angle, you can use atan(X coordinate of the subdivision). Again, I can make a diagram if you need to see why.

All these calculations can probably be avoided by throwing away angles and using coordinates directly. I haven't looked in detail into the tutorial that Davidobot has linked, but it seems to be much simpler calculation-wise.

I have not considered the player orientation. You'll have to add that angle before actually casting the ray. That should be the easy part.
Thrust II Reloaded - GifLoad for Löve - GSpöt GUI - My NotABug.org repositories - portland (mobile orientation)
The MS-Github repositories I had have been closed after the acquisition announcement and will be removed in the near future.

pgimeno
Party member
Posts: 1495
Joined: Sun Oct 18, 2015 2:58 pm

Re: Raycaster: fish eye effect past 60 degrees

Since you already had a variable for the distance to the plane of projection, I used math.atan2 instead of math.atan to modify your code. I just changed this:

Code: Select all

		rayAngle = rayAngle - subAngle

to this:

Code: Select all

		rayAngle = math.deg(math.atan2(screenWidth/2 - (ray - 0.5), distToPlane)) + self.player.angle

The result:

Before:

After:

Before:

After:
Thrust II Reloaded - GifLoad for Löve - GSpöt GUI - My NotABug.org repositories - portland (mobile orientation)
The MS-Github repositories I had have been closed after the acquisition announcement and will be removed in the near future.

Who is online

Users browsing this forum: keharriso, yoki and 8 guests