Textured Raycaster [Multiple Levels! Door Covers! Jumping!]

Showcase your libraries, tools and other projects that help your fellow love users.
User avatar
Jasoco
Inner party member
Posts: 3727
Joined: Mon Jun 22, 2009 9:35 am
Location: Pennsylvania, USA
Contact:

Re: Textured Raycaster [Multiple Levels! Door Covers! Jumping!]

Post by Jasoco »

Hey, Davidobot, if I could change the subject back to raycasting for a bit, I'd appreciate if you could help me solve a few problems I'm having with my own raycaster.

Now the main problem is since my code is much different from yours, things are done differently to get the same outcome. You seem to have solved a few of my problems so maybe you can help me solve them over here. We'll start with a simple one I guess...

I'm having trouble trying to get sprites to sort properly with the wall strips. Obviously their "distance" from the camera is calculated differently. I tried looking at your code but you calculate both your wall segment and sprite distances differently from mine which are calculated completely differently from each other. Here's the problem I'm having:



As you see, it seems that the sprite distance isn't properly "corrected". I've been ripping my hair out trying to figure out a good way to do it. Of course it only shows its ugly head when the sprite is next to a wall.

Here's my code. It's a dang mess I know. Some stuff is commented out because I just pulled it from my old project and haven't gotten around to implementing some stuff like doors yet.

First up, the raycasting function:

Code: Select all

function castSingleRay(rayAngle, stripIdx, checkOnly)
	-- first make sure the angle is between 0 and 360 degrees
	rayAngle = rayAngle % twoPI
	-- if (rayAngle < 0) then rayAngle = rayAngle + twoPI end

	-- moving right/left? up/down? Determined by which quadrant the angle is in.
	local right = (rayAngle > twoPI * 0.75) or (rayAngle < twoPI * 0.25)
	local up = (rayAngle < 0) or (rayAngle > pi)

	-- only do these once
	local angleSin = math.sin(rayAngle)
	local angleCos = math.cos(rayAngle)

	local vHit = false
	local dist = 0	-- the distance to the block we hit
	local xHit = 0 	-- the x and y coord of where the ray hit the block
	local yHit = 0

	local textureX	-- the x-coord on the texture of the block, ie. what part of the texture are we going to render
	local wallX	-- the (x,y) map coords of the block
	local wallY

	-- first check against the vertical map/wall lines
	-- we do this by moving to the right or left edge of the block we're standing in
	-- and then moving in 1 map unit steps horizontally. The amount we have to move vertically
	-- is determined by the slope of the ray, which is simply defined as sin(angle) / cos(angle).
	local tileNum = nil
	local tileNum_u = nil
	local hitDoor = false

	local slope = angleSin / angleCos 	-- the slope of the straight line made by the ray

	-- we move either 1 map unit to the left or right
	local dX
	if right then dX = 1 else dX=-1 end

	local dY = dX * slope 		-- how much to move up or down

	-- starting horizontal position, at one of the edges of the current map block
	local x
	if right then x = math.ceil(camera.x) else x = math.floor(camera.x) end
	-- starting vertical position. We add the small horizontal step we just made, multiplied by the slope.
	local y = camera.y + (x - camera.x) * slope

	local floorTileName

	while (x >= 0 and x < mapWidth and y >= 0 and y < mapHeight) do
		local wallX
		if right then wallX = math.floor(x) else wallX = math.floor(x -1) end
		local wallY = math.floor(y)
		local inside = wallX > 0 and wallX <= mapWidth and wallY > 0 and wallY <= mapHeight

		floorTileName = "fb" .. wallX+1 .. "-" .. wallY+1

		if not floorBoxList[floorTileName] then
			floorBoxList[floorTileName] = { wallX+1, wallY+1 }
		end

        if inside then
            -- if doorMap[wallX+1][wallY+1].kind ~= "" and doorMap[wallX+1][wallY+1].orientation == "v" then
            --     visibleDoors[doorMap[wallX+1][wallY+1].id] = { active = true, orientation = "v" }
            -- end
        end

        -- is this point inside a wall block?
        local k = 1+(math.floor(wallY))*mapWidth+math.floor(wallX)
        if map[k] > 0 then
			wallBoxList["wb" .. wallX+1 .. "-" .. wallY+1] = { wallX+1, wallY+1 }
			floorBoxList["fb" .. wallX+1 .. "-" .. wallY+1] = nil

            tileNum = map[k]
            -- tileNum_u = map_upper[k]

            local distX = x - camera.x
            local distY = y - camera.y
            dist = distX*distX + distY*distY	-- the distance from the player to this point, squared.

            -- where exactly are we on the wall? textureX is the x coordinate on the texture that we'll use when texturing the wall.
            textureX = y % 1

            -- if we're looking to the left side of the map, the texture should be reversed
            if (not right) then textureX = 1 - textureX end

            xHit = math.floor(x)
            yHit = math.floor(y)
            ix = x
            iy = y
            vHit = true

            if right then
                -- if doorMap[wallX][wallY+1].kind ~= "" and (xHit == wallX and yHit == wallY) and doorMap[wallX][wallY+1].orientation == "h" then
                --     tileNum = 12
                -- end
            elseif not right then
                -- if doorMap[wallX+2][wallY+1].kind ~= "" and (xHit == wallX+1 and yHit == wallY) and doorMap[wallX+2][wallY+1].orientation == "h" then
                --     tileNum = 12
                -- end
            end

            break
        end

        if inside then
            -- if spriteMap[wallX][wallY] > -1 then visibleSprites[spriteMap[wallX][wallY]] = true end
        end

        x = x+dX
        y = y+dY
	end

	-- now check against horizontal lines. It's basically the same, just "turned around".
	-- the only difference here is that once we hit a map block,
	-- we check if there we also found one in the earlier, vertical run. We'll know that if dist != 0.
	-- If so, we only register this hit if this distance is smaller.
	local slope = angleCos / angleSin
	local dY = up and -1 or 1

	local dX = dY * slope

	local y

	if up then y = math.floor(camera.y) else y = math.ceil(camera.y) end

	local x = camera.x + (y - camera.y) * slope
	while (x >= 0 and x < mapWidth and y >= 0 and y < mapHeight) do
		local wallY
		if up then wallY = math.floor(y - 1) else wallY = math.floor(y) end
		local wallX = math.floor(x)
		local inside = wallX > 0 and wallX <= mapWidth and wallY > 0 and wallY <= mapHeight

		floorTileName = "fb" .. wallX+1 .. "-" .. wallY+1

		if not floorBoxList[floorTileName] then
			floorBoxList[floorTileName] = { wallX+1, wallY+1 }
		end

        if inside then
            -- if doorMap[wallX+1][wallY+1].kind ~= "" and doorMap[wallX+1][wallY+1].orientation == "h" then
            --     visibleDoors[doorMap[wallX+1][wallY+1].id] = { active = true, orientation = "h" }
            -- end
        end

		local k=1+(math.floor(wallY))*mapWidth+math.floor(wallX)
		if map[k] > 0 then
			wallBoxList["wb" .. wallX+1 .. "-" .. wallY+1] = { wallX+1, wallY+1 }
			floorBoxList[floorTileName] = nil
			local distX = x - camera.x
			local distY = y - camera.y
			local blockDist = distX*distX + distY*distY

			if (dist==0 or blockDist < dist) then
				dist = blockDist
				tileNum = map[k]
				-- tileNum_u = map_upper[k]

				xHit = math.floor(x)
				yHit = math.floor(y)
				ix = x
				iy = y
				vHit = false

				textureX = x % 1

				if (not up) then textureX = 1 - textureX end
			end

			-- if up then
			-- 	if doorMap[wallX+1][wallY+2].kind ~= "" and (xHit == wallX and yHit == wallY+1) and doorMap[wallX][wallY+1].orientation == "v" then
			-- 		tileNum = 12
			-- 	end
			-- elseif not up then
			-- 	if doorMap[wallX+1][wallY].kind ~= "" and (xHit == wallX and yHit == wallY) and doorMap[wallX][wallY+1].orientation == "v" then
			-- 		tileNum = 12
			-- 	end
			-- end

			break
		end

  --       if inside then
		-- 	if spriteMap[wallX][wallY] > -1 then visibleSprites[spriteMap[wallX][wallY]] = true end
		-- end

		x = x+dX
		y = y+dY
	end

	if checkOnly then
		return xHit, yHit, stripIdx, height, vHit, textureX, dist, tileNum
	else
		if (dist>0) then
			dist = math.sqrt(dist)
			dist = dist * math.cos(camera.rot - rayAngle)
			local height = (viewDist / dist)-- * ceilingHeight

			createStrip(stripIdx, height, vHit, textureX, dist, tileNum)
		end
	end
end
Which calls this function to actually create the strip itself: (Note I have to invert the "layer" which is the equivalent to the "z" value since I use my own drawPool library which sorts differently from the original. Just ignore it.)

Code: Select all

function createStrip(stripIdx, height, vHit, textureX, dist, tileNum)
	tileNum = tileNum or 4
	darkness = 200 / dist

	textureX = math.floor(textureX * tileSize)

	local scale = ((RENDERHEIGHT+height)*0.5 - (RENDERHEIGHT-height)*0.5) / tileSize

	-- z = -math.floor(dist*(10000))

	local qu, ql = nil, nil
	if tileNum > 0 then
		ql = wallQuad[tileNum][textureX]
	end

	local lum = 1-(dist/fog_dist)
	if lum < 0 then lum = 0 end
	if vHit == false then lum = lum * .7 end
	lum = lum * ambient_light

	local c = {ambient_color[1] * lum, ambient_color[2] * lum, ambient_color[3] * lum}
	local cz = false
	if stripIdx == stateStack.mouseX and DEBUGMODE then
		mouseStrip = dist
		c = {255,0,0}
		cz = true
	end

	if dist < fog_dist then
		if DRAWWALLS then
			fpsState.drawPool:push {
				kind = "image",
				image = wallImage,
				quad = ql,
				x = stripIdx,
				y = (RENDERHEIGHT-height) * camera.eyeHeight,
				sx = 1,
				sy = scale,
				layer = 100 - dist,
				colorize = cz,
				color = c,
				ioy = wallSegmentOffset
			}
		else
			local c = randColor[tileNum]
			fpsState.drawPool:push {
				kind = "rectangle",
				fill = "fill",
				x = stripIdx,
				y = (RENDERHEIGHT-height) * camera.eyeHeight,
				w = 1,
				h = scale * tileSize,
				layer = 100 - dist,
				color = {c[1]*lum,c[2]*lum,c[3]*lum}
			}
		end

		if DRAWDEBUGRAYLINES then fpsState.drawPool:push { kind = "rect", x = stripIdx, y = dist * 5, w = 1, h = 1, color = {255,255,255}, layer = 100000 } end
	end

	if furthestDist < dist then
		furthestDist = dist
		furthestStrip = stripIdx
		furthestTop = math.floor((RENDERHEIGHT-height) * camera.eyeHeight)
		furthestBottom = math.floor(furthestTop + scale * tileSize)
	end
end
And this code determines where sprites should render. This is probably where the problem is: (Note I use camera instead of player.)

Code: Select all

function getScreen2DPosition(e)
	local dx = e.x - camera.x
	local dy = e.y - camera.y

	-- local dist = math.sqrt(dx*dx + dy*dy)
	local dist = math.sqrt((camera.x - e.x)^2 + (camera.y - e.y)^2)

	local spriteAngle = math.atan2(dy, dx) - camera.rot

	local size = viewDist / (math.cos(spriteAngle) * dist)

	local x = math.tan(spriteAngle) * viewDist
	local left = (RENDERWIDTH/2 + x)
	local top = ((RENDERHEIGHT-size)*camera.eyeHeight)

	local dbx = e.x - camera.x
	local dby = e.y - camera.y

	local blockDist = dbx*dbx + dby*dby
	local z = -math.floor((dist-.5)*10000)

	local lum = 1-(dist/fog_dist)
	if lum < 0 then lum = 0 end
	lum = lum * ambient_light

	local scale = size / tileSize

	-- dist = math.sqrt(dist)
	-- dist = dist * math.cos(camera.rot - spriteAngle)

	return left, top + (tileSize * scale), scale > 0, dist, scale, lum
end
Before you ask, I've tried uncommenting each of those last two commented lines and neither of them fix it, instead making it worse. I've also tried uncommenting that other commented dist line.

Also note RENDERWIDTH/HEIGHT are the dimensions of my rendered screen since I am rendering to a lower resolution than native resolution for reasons™.

Any help would make me so happy. I've tried everything except completely rip your code and put it in my version. I can probably provide the actual project itself if I need to.
User avatar
Davidobot
Party member
Posts: 1226
Joined: Sat Mar 31, 2012 5:18 am
Location: Oxford, UK
Contact:

Re: Textured Raycaster [Multiple Levels! Door Covers! Jumping!]

Post by Davidobot »

Jasoco wrote: Thu Apr 06, 2017 5:41 am Hey, Davidobot, if I could change the subject back to raycasting for a bit, I'd appreciate if you could help me solve a few problems I'm having with my own raycaster.
...
Any help would make me so happy. I've tried everything except completely rip your code and put it in my version. I can probably provide the actual project itself if I need to.
I'm always happy to help. I do feel like this is going a bit off-topic in this particular thread. Would you like to maybe continue these and similar discussions on discord or something similar? If you'd like to add me, you can find me under "RinaLovesRobots [Davidobot]#9863" (without quotations marks of course)

Now, onto your code. The "getScreen2DPosition(e)" function just tells me how you get the distance to a point on your map. I don't think it shows how you actually draw or sort your sprites. In my project, I draw the sprites strip-by-strip. The distance to the center of the sprite is used to:
- Scale the sprite correctly and sort the sprites between one another.
- For each strip of the sprite, to check if that one strip is behind a wall or not, which is done by having a global zBuffer table with the closest wall distance at each point. So I check the appropriate strip against this buffer with zBuffer[strip] and check if that's less than the distance of the sprite (one distance per sprite, from the center of it). If the distance is greater than that of the buffer, I drop that one strip and continue.

I'm not sure if you render your sprite all at once, or strip-by-strip. If you have the former, try the latter. Otherwise, there is nothing in the code that instantly jumps out to me as wrong. I would gladly poke around in your whole project and help you find the error if you provide it. <- Ignore that, upon closer inspection of the video, it is obvious that you do it strip-by-strip. Since that's the case, look at the below recommendation.

Actually, I see that you're adjusting your wall distance to make it perpendicular to the camera with

Code: Select all

dist = dist * math.cos(camera.rot - rayAngle)
However, I do not see you doing the same thing to your sprite distance, only for your sprite size. This perpendicular distance is what prevents a "fish-eye" effect of the sprites. It could be that it does cause a visual distortion because you adjust the sprite size for it, but it could be causing a distance fish-eye effect of sorts, which could be your problem. Try changing that part of getScreen2DPosition(e) to:

Code: Select all

local spriteAngle = math.atan2(dy, dx) - camera.rot
local dist = math.sqrt((camera.x - e.x)^2 + (camera.y - e.y)^2) * math.cos(spriteAngle)
local size = viewDist /  dist
Also, you seem to calculate your angles differently in the raycasting and sprite distance ("camera.rot - rayAngle" vs "math.atan2(dy, dx) - camera.rot"). Can't tell if that's a problem without trying out your project.
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
Jasoco
Inner party member
Posts: 3727
Joined: Mon Jun 22, 2009 9:35 am
Location: Pennsylvania, USA
Contact:

Re: Textured Raycaster [Multiple Levels! Door Covers! Jumping!]

Post by Jasoco »

Success! It worked! One problem solved. I knew it was a "correction" problem I just couldn't figure out how to properly do it.

For the record, no I don't render sprites column by column like you do. I didn't feel it really necessary and never thought of it. I saw you do it in your code and wonder if there's a reason? Does it have any benefits? I'd think it would just add extra unnecessary slowdown to an already slow process? Maybe you know something I don't. lol

Anyway, my next problem is with determining what floor/ceiling tiles to render. You might notice some odd code in my example above. Lines that begin with something like floorBoxList["fb" .. wallX+1 .. "-" .. wallY+1]. This is my hacky way of "marking" tiles as "visible" so I can run through them and draw floor and ceiling tiles, or "flats" as I refer to them (Borrowing a term from DOOM. I'll also refer to sprites as "things" in the future.) but it seems to be buggy as it consistently marks non-visible tiles as visible.

For example:


In the video you see all the flats that are being rendered. Including some that are hidden behind walls and should not be seen at all. This adds a lot of unnecessary render time since the texture shader is already slightly slow.

How would I properly mark only visible flats?

Note: I haven't even looked at your code to figure it out yet.
User avatar
Davidobot
Party member
Posts: 1226
Joined: Sat Mar 31, 2012 5:18 am
Location: Oxford, UK
Contact:

Re: Textured Raycaster [Multiple Levels! Door Covers! Jumping!]

Post by Davidobot »

Jasoco wrote: Thu Apr 06, 2017 4:00 pm For the record, no I don't render sprites column by column like you do. I didn't feel it really necessary and never thought of it. I saw you do it in your code and wonder if there's a reason? Does it have any benefits? I'd think it would just add extra unnecessary slowdown to an already slow process? Maybe you know something I don't. lol
Well, my original version didn't really sort the sprites and the walls. It just drew everything that should be there. So it would discard unneeded strips and just draw everything. Hence, eliminating the need for sorting. Though, I think I have some sort of sorting now tho so I guess it's just a relic. :crazy:
Jasoco wrote: Thu Apr 06, 2017 4:00 pm In the video you see all the flats that are being rendered. Including some that are hidden behind walls and should not be seen at all. This adds a lot of unnecessary render time since the texture shader is already slightly slow.
How would I properly mark only visible flats?
Right then. I'll first explain my raycasting procedure -
- Increment the position of the ray on a tile-by-tile basis.
- If there is no wall, add the floor tile to a table and continue to increment.
- If there is a wall, add to the the wall strips and break the while loop.

Something odd about your code is that you loop through the system twice? I don't really know why you need to do that for "vertical" and "horizontal" lines separately. Also, you seem to add things and then remove them, I would recommend you just add the floor tile if there is no wall there.

Code: Select all

if map[k] > 0 then
	-- add wall
else
	-- add floor
end
It also bothers me a bit that line 69, doesn't just use

Code: Select all

floorBoxList[floorTileName] = nil
instead of

Code: Select all

floorBoxList["fb" .. wallX+1 .. "-" .. wallY+1] = nil
If that doesn't solve the problem, there are multiple other possibilities:
- You don't clear floorBoxList after finishing your draws and hence tiles stay selected, unless they are "nilled" out by the code.
- Try adding floor only on one of the runs. As in you add them on both the vertical and horizontal ones, but try just adding them on the vertical, or vice versa.
- Your code flags up tiles visible on either of the runs, but not ones visible on both of them, as it should. So maybe make sure that the tile you're flagging is visible in both of your runs.

If I had to take a guess, your problem would be the last of the "other possibilities", as in that the tile needs to be visible from both runs.
User avatar
Jasoco
Inner party member
Posts: 3727
Joined: Mon Jun 22, 2009 9:35 am
Location: Pennsylvania, USA
Contact:

Re: Textured Raycaster [Multiple Levels! Door Covers! Jumping!]

Post by Jasoco »

If you have a better way of doing the raycasting loop, let me know. The one I have is also a relic of the original converted code I got from another member long ago. It was originally a JavaScript engine. I don't know why it goes through twice. It seems if I disable one, then it only renders opposite corners of the map but leaves the others empty. So who knows. I don't really understand much about how it works, just that it works.

Also, floorTileName is just shorthand for the "fb" .. wallX+1 .. "-" .. wallY+1 part. I just didn't replace it completely.

I'd really love to figure out a better way to do it to make sure the flats are optimized. I hate wasting time rendering unseen tiles.

I also need to figure out a way to do transparent walls like you do. The code I have doesn't account for them. But yours does. No idea how you did it. I'd like to have fences and grates you can see through. In my experience trying to modify the code I have, it wouldn't work and would just stop rendering when it hit a transparent wall like a normal wall.

I also need to reimplement doors properly. My old method had "problems" with broken seams. I never did understand how to do them and the JavaScript code didn't go far enough to talk about doors. (Though it didn't do flats either. Hence why I needed to come up with my own method.)

My original floor rendering method was much different. It was two canvases. One with the whole floor. One for rendering. It would rotate and draw the floor onto the second canvas and render that canvas as horizontal strips. But it looked bad. It worked, but it looked bad as rotating pixels would look weird and wonky. The new shader method works much better, but is much slower. I just need to optimize it.
User avatar
Davidobot
Party member
Posts: 1226
Joined: Sat Mar 31, 2012 5:18 am
Location: Oxford, UK
Contact:

Re: Textured Raycaster [Multiple Levels! Door Covers! Jumping!]

Post by Davidobot »

Jasoco wrote: Thu Apr 06, 2017 7:05 pm If you have a better way of doing the raycasting loop, let me know.
Mine is quite similar to yours. I just calculate which way the ray is generally going (left or right; up or down) and then I have "delta" values for both the X and Y directions that get incremented every step, by the respective cos/sin of the angle. If the delta for X exceeds 1, then it's time to step left or right, which is does and then resets the delta value. Same thing for delta of Y.
This way, I am stepping one tile every loop of the while loop. When this happens, I check if the tile the ray is in, if it is:
- empty: add the tile to the floor list.
- a normal wall: add strip to the drawing list and terminate.
- a door or fence: add strip to drawing list but do not terminate.
Jasoco wrote: Thu Apr 06, 2017 7:05 pm I'd really love to figure out a better way to do it to make sure the flats are optimized. I hate wasting time rendering unseen tiles.
So I assume that checking if both of the vertical and horizontal passes return the same tile did not work?
Jasoco wrote: Thu Apr 06, 2017 7:05 pm I also need to figure out a way to do transparent walls like you do. The code I have doesn't account for them. But yours does. No idea how you did it. I'd like to have fences and grates you can see through. In my experience trying to modify the code I have, it wouldn't work and would just stop rendering when it hit a transparent wall like a normal wall.
I think this will be easier to do with a better-written raycasting loop.

Would you like to send me the project file so that I can try and fix the floor bug?
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
Jasoco
Inner party member
Posts: 3727
Joined: Mon Jun 22, 2009 9:35 am
Location: Pennsylvania, USA
Contact:

Re: Textured Raycaster [Multiple Levels! Door Covers! Jumping!]

Post by Jasoco »

I tried it last night. With the way I do it (The dual checks against horizontal and vertical walls separately) it's not going to work the way I thought. I might need to just borrow your method and modify it to work with my code. Did you create yours from scratch or use another source initially? Because mine was obviously a really old school method. We'll figure it out I'm sure. I'll look into modifying your raycast loop to see if it works for me. I think as long as it returns the strips in the right format with the right "distance" for sorting that matches the format of the sprite distance, it'll be fine. And if it helps with floor/ceiling culling, even better.

I whipped up a .love project for you. It's all the relevant code for the raycasting engine. All the code you need to see is in states/fps.lua. The rest is just the game wrapping, state stack, libraries, etc. I had to actually do a bunch of work to transplant it from my working project (Where it was just a separate state I was toying with) but it works.

Press F1 to show the performance graph. Blue is draw time, green (Which you can't see any because all the stuff is currently done in draw for performance reasons) is update, the darker blue is the texture draw time. You'll note that it takes up half the total draw time depending on how big a room is.

F3 shows the debug grid marking all the flats that are being rendered including unseen ones.

Press Tab to toggle mouse look on and off if you don't like using the A and D keys for turning. (Arrows are for movement and strafing. Standard FPS controls.)

Press = (equals) to change the wall height between 32, 48 and 64 pixels. (I am partial to the 48 height.) I'm going a different direction than you with higher walls. (The GunGodz direction)

Press 0 (zero) to toggle ambient occlusion on the flats.

Press F (Flats), W (Walls) or T (Things) to toggle between textures and no textures. Press / (slash) to toggle all at once. It's just for debugging. But it does help show that drawing a wall with an image or a rectangle is the same and has no impact on rendering. Textures on flats of course does. Try pressing these keys when the performance graph is open)

3d.love.zip
(147.47 KiB) Downloaded 507 times
(Sorry it's a .love file in a .zip file because for some reason I can't upload a .love anymore?)

Thanks for helping! It's my dream to make a GunGodz style tribute. Basically it has everything I want to do myself. Especially transparent "fence" style walls which you do yourself, but GunGodz also has a second type where the fence is halfway between tiles instead of just on the edge. But I guess if you make a "door" that doesn't actually open, it'll do the same thing. The taller ceilings are great too which is why I implemented them in mine.
User avatar
Davidobot
Party member
Posts: 1226
Joined: Sat Mar 31, 2012 5:18 am
Location: Oxford, UK
Contact:

Re: Textured Raycaster [Multiple Levels! Door Covers! Jumping!]

Post by Davidobot »

Jasoco wrote: Fri Apr 07, 2017 6:52 pm I tried it last night. With the way I do it (The dual checks against horizontal and vertical walls separately) it's not going to work the way I thought. I might need to just borrow your method and modify it to work with my code. Did you create yours from scratch or use another source initially? Because mine was obviously a really old school method. We'll figure it out I'm sure. I'll look into modifying your raycast loop to see if it works for me. I think as long as it returns the strips in the right format with the right "distance" for sorting that matches the format of the sprite distance, it'll be fine. And if it helps with floor/ceiling culling, even better.
I looked into your code. I think you'll have to change the raycasting loop to mine or something else, because I cannot, for the life of me, figure out how to make it work properly. I nearly got it to work, but if a floor tile if between two walls, like in the picture below, it won't render.
Image
Jasoco wrote: Fri Apr 07, 2017 6:52 pm Thanks for helping! It's my dream to make a GunGodz style tribute. Basically it has everything I want to do myself. Especially transparent "fence" style walls which you do yourself, but GunGodz also has a second type where the fence is halfway between tiles instead of just on the edge. But I guess if you make a "door" that doesn't actually open, it'll do the same thing. The taller ceilings are great too which is why I implemented them in mine.
I wish you luck! I want to make a polished small game using my engine too, eventuallyTM! :awesome:

So about your next step, I would recommend you look into my "single-run" raycast loop and try to understand it and adapt it to suit your engine. Also man, I really like the Ambient Occlusion, just sayin'.
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
Jasoco
Inner party member
Posts: 3727
Joined: Mon Jun 22, 2009 9:35 am
Location: Pennsylvania, USA
Contact:

Re: Textured Raycaster [Multiple Levels! Door Covers! Jumping!]

Post by Jasoco »

Yeah, I definitely need to use your loop. Mine is so old school. And with it doing the horizontal and vertical walls separately, it makes it a lot harder to implement other stuff. I'll see what I can do. It shouldn't be too hard.

Maybe.

Edit: Yeah it's a bit harder than I thought. You have a lot of different methods than I do. I have no equivalent for your camera.dirX/Y or camera.planeX/Y variables. My camera "dir" is a radian direction and I don't know how to properly convert that to the required dirX/Y values. And I don't even know what planeX/Y does. Your rayCast function accepts a single value of x to get its information which seems to be equivalent to whatever column of the screen it's scanning while mine accepts the angle of the camera. Yours also seems to only render in 4:3 instead of dynamically resizing to the aspect of the screen like mine. There's a lot of stuff I'm going to have to figure out.

Edit 2: Well it's a start but I got something working. It's not 100% though. I need to figure out a lot of things about your code. But hopefully I can make it work. Here's what I have so far with your raycasting overlaid on mine:


It.... does not work. Like at all. There's no "perspective". I am sure it's because I don't know how to properly translate some of the values you use that I don't have equivalents for. Here's the code I have so far:

Code: Select all

local zBuffer = {}
function castRay(x)
	local w = RENDERWIDTH
	local h = RENDERHEIGHT
	local tilt = 0
	local textureWidth = tileSize
	local textureHeight = tileSize
	local cameraX = 2 * x / w - 1
	-- print(cameraX)
	local rayPosX = camera.x
	local rayPosY = camera.y
	local rayDirX = math.cos(camera.rot) + 0 * cameraX
	local rayDirY = math.sin(camera.rot) + 0.66 * cameraX

	local mapX = math.floor(rayPosX)
	local mapY = math.floor(rayPosY)

	local sideDistX
	local sideDistY

	local deltaDistX = math.sqrt(1 + (rayDirY ^ 2) / (rayDirX ^ 2))
	local deltaDistY = math.sqrt(1 + (rayDirX ^ 2) / (rayDirY ^ 2))
	local perWallDist

	local stepX
	local stepY

	local hit = 0
	local side = 0

	if (rayDirX < 0) then
	  	stepX = -1
	  	sideDistX = (rayPosX - mapX) * deltaDistX
	else
	  	stepX = 1
	  	sideDistX = (mapX + 1.0 - rayPosX) * deltaDistX
	end
	if (rayDirY < 0) then
	  	stepY = -1
	  	sideDistY = (rayPosY - mapY) * deltaDistY
	else
	  	stepY = 1
	  	sideDistY = (mapY + 1.0 - rayPosY) * deltaDistY
	end

	local numSteps = 0
	while (hit == 0 and numSteps < 100) do
		-- local kk = {
		-- 	x = mapX,
		-- 	y = mapY,
		-- }
		-- if not checkMatch(floorTable, kk) then
		-- 	table.insert(floorTable, kk)
		-- end
		-- if not checkMatch(ceilingTable, kk) then
		-- 	table.insert(ceilingTable, kk)
		-- end

	  	if (sideDistX < sideDistY) then
			sideDistX = sideDistX + deltaDistX
			mapX = mapX + stepX
			side = 0
	  	else
			sideDistY = sideDistY + deltaDistY
			mapY = mapY + stepY
			side = 1
	  	end

	  	if mapX > 0 and mapX <= mapWidth and mapY > 0 and mapY <= mapHeight then
		  	if (level[mapX][mapY] > 0) or level[mapX][mapY] == -8 or level[mapX][mapY] == -7 then
		  		-- Checking if hit a fence or door. If so, continue casting and create a fence line
		  		if level[mapX][mapY] == 9 or level[mapX][mapY] == -8 or level[mapX][mapY] == -7 then
		  			if (side == 0)then
						perpWallDist = math.abs((mapX - rayPosX + (1 - stepX) / 2) / rayDirX)
					else
						perpWallDist = math.abs((mapY - rayPosY + (1 - stepY) / 2) / rayDirY)
					end

					local lineHeight = ((h + camera.eyeHeight) / perpWallDist)
					local normalLineHeight = ((h - camera.eyeHeight) / perpWallDist)

					drawStart = -lineHeight / 2 + ((h / 2) + tilt)
					drawEnd = normalLineHeight / 2 + ((h / 2) + tilt)

					-- Texturing calculations
					local texNum = 0
					if mapX > 0 and mapX <= mapWidth and mapY > 0 and mapY <= mapHeight then
						if level[mapX][mapY] == -8 or level[mapX][mapY] == -7 then
							texNum = ceiling[mapX][mapY]
						else
							texNum = level[mapX][mapY]
						end
					end

					if texNum < 0 then texNum = 0 end

					-- Calculate value of wallX
					local wallX -- Where exactly the wall was hit
					if (side == 1) then
						wallX = rayPosX + ((mapY - rayPosY + (1 - stepY) / 2) / rayDirY) * rayDirX
					else
						wallX = rayPosY + ((mapX - rayPosX + (1 - stepX) / 2) / rayDirX) * rayDirY
					end
					wallX = wallX - math.floor(wallX)

					-- X coordinate on the texture
					local texX = math.floor(wallX * textureWidth)
					if(side == 0 and rayDirX > 0) then texX = textureWidth - texX - 1 end
					if(side == 1 and rayDirY < 0) then texX = textureWidth - texX - 1 end

					-- Color
					local deathColor = 1 - (perpWallDist / fog_dist)
					if deathColor < 0 then deathColor = 0 end

					local tempColor = {}
					if (side == 1) then
						tempColor = {127 * deathColor, 127 * deathColor, 127 * deathColor}
					else
						tempColor = {255 * deathColor, 255 * deathColor, 255 * deathColor}
					end

						local i = {
							x = x,
							hor = texX,
							start = drawStart,
							scale = (drawEnd - drawStart) / textureHeight,
							texture = texNum,
							color = tempColor,
							z = perpWallDist,
							type = "wall",
							onlyTop = (level[mapX][mapY] == -8 or level[mapX][mapY] == -7),
						}
						-- table.insert(spritePool, i)
		  		else
					hit = 1

					-- local kk = {
					-- 	x = mapX,
					-- 	y = mapY,
					-- }
					-- if not checkMatch(floorTable, kk) then
					-- 	table.insert(floorTable, kk)
					-- end
					-- if not checkMatch(ceilingTable, kk) then
					-- 	table.insert(ceilingTable, kk)
					-- end
				end
		  	end
		end

		numSteps = numSteps + 1
	end

	if (side == 0)then
		perpWallDist = math.abs((mapX - rayPosX + (1 - stepX) / 2) / rayDirX)
	else
		perpWallDist = math.abs((mapY - rayPosY + (1 - stepY) / 2) / rayDirY)
	end

	local lineHeight = ((RENDERHEIGHT + camera.eyeHeight) / perpWallDist)
	local normalLineHeight = ((RENDERHEIGHT - camera.eyeHeight) / perpWallDist)

	local drawStart = -lineHeight / 2 + ((RENDERHEIGHT / 2) + tilt)

	local drawEnd = normalLineHeight / 2 + ((RENDERHEIGHT / 2) + tilt)

	-- Texturing calculations
	local texNum = 0
	if mapX > 0 and mapX <= mapWidth and mapY > 0 and mapY <= mapHeight then
		texNum = level[mapX][mapY]
	end

	if texNum < 0 then texNum = 0 end

	-- Calculate value of wallX
	local wallX -- Where exactly the wall was hit
	if (side == 1) then
		wallX = rayPosX + ((mapY - rayPosY + (1 - stepY) / 2) / rayDirY) * rayDirX
	else
		wallX = rayPosY + ((mapX - rayPosX + (1 - stepX) / 2) / rayDirX) * rayDirY
	end
	wallX = wallX - math.floor(wallX)

	-- X coordinate on the texture
	local texX = math.floor(wallX * textureWidth)
	if(side == 0 and rayDirX > 0) then texX = textureWidth - texX - 1 end
	if(side == 1 and rayDirY < 0) then texX = textureWidth - texX - 1 end

	local i = {
		x = x,
		hor = texX,
		start = drawStart,
		scale = (drawEnd - drawStart) / textureHeight,
		texture = texNum,
		z = perpWallDist,
		type = "wall"
	}

	-- Color
	local deathColor = 1 - (perpWallDist / fog_dist)
	if deathColor < 0 then deathColor = 0 end

	if (side == 1) then
		i.color = {127 * deathColor, 127 * deathColor, 127 * deathColor}
	else
		i.color = {255 * deathColor, 255 * deathColor, 255 * deathColor}
	end

	-- print(texX, drawStart, (drawEnd - drawStart) / textureHeight)
	if texNum > 0 then
		fpsState.drawPool:push { kind = "image", image = wallImage, quad = wallQuad[texNum][texX], x = x, y = drawStart, sx = 1, sy = (drawEnd - drawStart) / textureHeight, color = i.color, layer = 100000 }
	end

	-- table.insert(wallPool, i)

	-- zBuffer for sprite casting
	zBuffer[x] = perpWallDist -- Perpendicular distance is used
end
I had to patch some code holes and fix some stuff just to get it to run.
User avatar
Davidobot
Party member
Posts: 1226
Joined: Sat Mar 31, 2012 5:18 am
Location: Oxford, UK
Contact:

Re: Textured Raycaster [Multiple Levels! Door Covers! Jumping!]

Post by Davidobot »

Jasoco wrote: Sun Apr 09, 2017 3:08 am Edit 2: Well it's a start but I got something working. It's not 100% though. I need to figure out a lot of things about your code. But hopefully I can make it work. Here's what I have so far with your raycasting overlaid on mine:
Oh yeah, you definitely need to know a few things about my raycasting method before you start tinkering.
dirX and dirY do indeed control the angle orientation of the camera.
However, planeX and planeY are also the angle orientation.. Kind-of.

Code: Select all

if player.dx ~= 0 then
	local turnSpeed = rotSpeed * player.dx

	local oldDirX = player.dirX
	player.dirX = player.dirX * math.cos(turnSpeed) - player.dirY * math.sin(turnSpeed)
	player.dirY = oldDirX * math.sin(turnSpeed) + player.dirY * math.cos(turnSpeed)
	local oldPlaneX = player.planeX
	player.planeX = player.planeX * math.cos(turnSpeed) - player.planeY * math.sin(turnSpeed)
	player.planeY = oldPlaneX * math.sin(turnSpeed) + player.planeY * math.cos(turnSpeed)
end
This is the code to rotate the orientation, where dx is the amount to rotate. That cos/sin mess is just a rotation matrix.

The aspect ration is actually determined by the starting value of planeY, which is also the FOV. The 0.66 value there is because I use a 4:3 ratio. The formula for finding the aspect ratio from planeY and vice versa is:

Code: Select all

W:H (aspect ratio) = planeY * 2 (so if planeY is 2/3, the W:H is 4:3. if planeY is 8/9, then W:H is 16:9)
planeY = (aspect ration) * 0.5;

FOV (field of view value) = (2 * atan(planeY/1.0))
Also, you need to declare the dirX/dirY and planeX/planeY in the exact form they are now. If you want to start at a custom rotation (not math.pi) then start with the values I have and just rotate them using the rotation code.

EDIT: Btw, just so you know, after that fence/door comment, it's just a copy/paste of the strip-adding code from further down. I didn't move it into a separate function as I should have.

The eye-height is the player.zHeight value in lines 72, 73, 153, 154 of raycasting.lua; if you have a different eye-height, you need to change that. It's value in mine is 0 and that corresponds to eye-level being at exactly half the screen width. If you set zHeight to h, the player will be exactly level with the floor; -h and you'll be twice the current height. So if your eye-height is at 0.75 wall height, you need to set zHeight to -h/2.

Code: Select all

local lineHeight = ((h + player.zHeight) / perpWallDist)
local normalLineHeight = ((h - player.zHeight) / perpWallDist)
If it helps, the formula for determining the player/camera rotation is:

Code: Select all

function getPlayerRot()
	return math.atan2(player.dirY, player.dirX)
end
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
Post Reply

Who is online

Users browsing this forum: No registered users and 2 guests