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
Before you make a thread asking for help, read this.
Post Reply
User avatar
Aidymouse
Prole
Posts: 7
Joined: Mon Aug 13, 2018 10:09 am
Location: New Zealand

Raycaster: fish eye effect past 60 degrees

Post by Aidymouse »

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
rad = math.rad

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
			correctedDistance = math.abs(horizLineDist * cos(rad(drawAngle)))
		else
			correctedDistance = math.abs(vertLineDist * cos(rad(drawAngle)))
		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
			correctedDistance = math.abs(horizLineDist * cos(rad(drawAngle)))
		else
			correctedDistance = math.abs(vertLineDist * cos(rad(drawAngle)))
		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. :awesome:
Attachments
raydungeon.love
(2.85 KiB) Downloaded 216 times
User avatar
pgimeno
Party member
Posts: 3544
Joined: Sun Oct 18, 2015 2:58 pm

Re: Raycaster: fish eye effect past 60 degrees

Post by pgimeno »

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:

Image
Last edited by pgimeno on Mon Aug 20, 2018 8:13 pm, edited 5 times in total.
User avatar
Davidobot
Party member
Posts: 1226
Joined: Sat Mar 31, 2012 5:18 am
Location: Oxford, UK
Contact:

Re: Raycaster: fish eye effect past 60 degrees

Post by Davidobot »

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.
PM me on here or elsewhere if you'd like to discuss porting your game to Nintendo Switch via mazette!
personal page and a raycaster
User avatar
Aidymouse
Prole
Posts: 7
Joined: Mon Aug 13, 2018 10:09 am
Location: New Zealand

Re: Raycaster: fish eye effect past 60 degrees

Post by Aidymouse »

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.
Image
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! :awesome: :awesome:
User avatar
pgimeno
Party member
Posts: 3544
Joined: Sun Oct 18, 2015 2:58 pm

Re: Raycaster: fish eye effect past 60 degrees

Post by pgimeno »

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):
Image

So, this is the goal roughly:

Image

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

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:

Image

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:

Image

And then straightening the screen (sorry about the hand-made divisions).

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.
User avatar
pgimeno
Party member
Posts: 3544
Joined: Sun Oct 18, 2015 2:58 pm

Re: Raycaster: fish eye effect past 60 degrees

Post by pgimeno »

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:
Image

After:
Image


Before:
Image

After:
Image
Post Reply

Who is online

Users browsing this forum: Google [Bot] and 34 guests