Log In  

I've been working on my first Raycaster and I ran into a problem I can't seem to fix.
I think it has to do with the CAMX variable, and the Ray Hit section of the code (Specifically CAMX's implementation in the RDIRX and RDIRY values).

Depending on the direction I'm facing I get lines that go straight through everything (With virtually no line-height).

I've also noticed that this bug tends to only happen in the reverse SIDE as the wall it's supposed to hit,
Front wall (SIDE 0) has a line that has the color of a SIDE 1 wall.

Here is how it looks:

Here's the code:

-- Based off of: http://lodev.org/cgtutor/raycasting.html
local w,h = 127, 90

-- starting x and y
posx = 15
posy = 11
-- direction vector (looking direction)
dirx = 0  -- angle size of rays
diry = 0  -- left/right offset of rays 
-- up/down
dirz = 0
-- 2d angles in which rays enter the world at
planex = 0       -- (angle rotation)
planey = 0.66    -- (angle size)
-- field of vision
-- this takes the viewport "width" (planey) and the 
-- max ray angles (dirx), and divides them to find the fov
-- 1:1 = 90
-- some simple algebra is used to change this to be fov
-- variable dependant, hence why dirx isn't defined.
-- dirx / planey
fov = 100

-- misc
rotspd = 0.06
movspd = 0.20
lookspd = 0.8

running = true
linebug = 0
debug = false

world = {
  {1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1},
  {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,2,2,2,2,2,0,0,0,0,3,0,3,0,3,0,0,0,1},
  {1,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,2,0,0,0,2,0,0,0,0,3,0,0,0,3,0,0,0,1},
  {1,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,2,2,0,2,2,0,0,0,0,3,0,3,0,3,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,4,0,4,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,4,0,0,0,0,5,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,4,0,4,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,4,0,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
}

function rad(n)
   return n * (3.14159/180) + (0.001)
end

function msin(n) return sin(n*0.159) end
function mcos(n) return cos(n*0.159) end
function msqrt(n)
 local i = 0
 while(i*i <= n) do 
  i += 1
 end
 i -= 1
 local d = n-i*i
 local p = d/(2*i)
 local a = i+p
 return a -(p*p)/(2*a)
end

function mcut(n,degree)
  if(abs(n) > 1/degree)return n
  return flr(n * degree)/degree
end

function updatefov()
-- dirx*-1 / planey = n*90 = fov
-- planey = -dirx * (fov/90)
   dirx   = -(planey / (fov/90))
end

function wmap(x,y)
   x = flr(x)
   y = flr(y)
   return world[y][x]
end

function _init()
  cls()
end

function drawhud()
   rectfill(0,h,128,128,5)
   if(debug)then
    print("rayx: " .. raydx,0,h+8,7)
    print("rayy: " .. raydy,0,h+16,7)
    print("checkerr: " .. linebug,0,h+24,7)
   end
end

function _draw()
   updatefov()
   rectfill(0,0,w,(h+dirz)/2,15)
   rectfill(0,(h+dirz)/2,w,h,4)
    for x = 0, w do
      -- camera offset relative to middle
      -- -n = left, 0 = middle, +n = right
      local camx  = (2 * x) / w -1

      -- min/max ranges of ray directions
      local rdirx = dirx + planex * camx
      local rdiry = diry + planey * camx
      local mapx = flr(posx)
      local mapy = flr(posy)

      local sidex, sidey, stepx, stepy, wall

      -- length of ray from one side to another
      raydx = sqrt(1 + (rdiry * rdiry) / (rdirx * rdirx) )
      raydy = sqrt(1 + (rdirx * rdirx) / (rdiry * rdiry) )

      -- check if wall was hit
      local hit = 0
      -- was a north-south or east-west wall hit?
      local side

      -- find which direction to step in

      if(rdirx <= 0)then
        stepx = -1
        sidex = (posx - mapx) * raydx
      else
        stepx = 1
        sidex = (mapx + 1.0 - posx) * raydx
      end

      if(rdiry <= 0)then
        stepy = -1
        sidey = (posy - mapy) * raydy
      else
        stepy = 1
        sidey = (mapy + 1.0 - posy) * raydy
      end
      -- draw lines
      while (hit == 0) do
        if(sidex < sidey)then
          sidex  += raydx
          mapx += stepx
          side = 0
        else
          sidey += raydy
          mapy += stepy
          -- bug lies here, when rendering the y portions
          side = 1
        end
        -- check if ray has hit a wall
        if(world[mapy][mapx] > 0) hit = 1
      end

      -- calculate distance to project on camera
      if(side == 0)then
         wall = (mapx - posx + (1 - stepx) /2 ) / rdirx
      else
         wall = (mapy - posy + (1 - stepy) /2 ) / rdiry
      end

      -- calculate height of line
      local lineh = -flr( -(h / wall) )

      if(lineh <= 1)linebug = x

      -- calculate lowest/highest pixel to fill stripe
      drawstart =       -lineh /2 + (h+dirz)/2
      drawend   =        lineh /2 + (h+dirz)/2
      if(drawstart < 0) drawstart = 0
      if(drawend >= h ) drawend   = h - 1

      -- get color of wall
      box = wmap(mapx,mapy)
      if(side == 0)then -- x sides
            if(box == 1)then boxcol = 8     -- red
        elseif(box == 2)then boxcol = 11    -- green
        elseif(box == 3)then boxcol = 12    -- blue
        elseif(box == 4)then boxcol = 7     -- white
        elseif(box == 5)then boxcol = 10    -- yellow
        else boxcol = 7 end            -- white
      elseif(side == 1)then -- y sides
            if(box == 1)then boxcol = 2    -- violet
        elseif(box == 2)then boxcol = 3     -- dark-green
        elseif(box == 3)then boxcol = 1     -- dark-blue
        elseif(box == 4)then boxcol = 6     -- gray
        elseif(box == 5)then boxcol = 9     -- white
        else boxcol = 7 end  -- orange  
      end

      -- draw striped line
      line(x, drawstart, x, drawend, boxcol)
   end
   if(debug)then
     print("x: " .. posx, 0,8,0)
     print("y: " .. posy, 0,16,0)
     print("rang: " .. dirx, 0,24,0)
     print("roff: " .. diry, 0,32,0)
     print("prot: " .. planex, 0,40,0)
     print("pwid: " .. planey, 0,48,0)
   end
   drawhud()
end

function rotate(rdir)
  local oldx = dirx
  local oldpx = planex
  dirx   = dirx   * mcos(rdir) - diry   * msin(rdir)
  diry   = oldx   * msin(rdir) + diry   * mcos(rdir)
  planex = planex * mcos(rdir) - planey * msin(rdir)
  planey = oldpx  * msin(rdir) + planey * mcos(rdir)
end

local pmx = 0
local pmy = 0

function _update60()
   --poke(0x5f2d, 1)
   local mx = 0--stat(32) -- x coord
   local my = 0--stat(33) -- y coord

   -- player input!
   if(btn(2))then -- up
        -- collision check, move forwards
      if(wmap(posx + dirx * movspd, posy) == 0)then
         posx += dirx * movspd
      end
      if(wmap(posx, posy + diry * movspd) == 0)then
         posy += diry * movspd
      end
   end
   if(btn(0,1))then -- strafe left
      -- If anyone could help with these it would be great too xD
   end
   if(btn(3))then -- down
      -- collision check, move backwards
      if(wmap(posx - dirx * movspd, posy) == 0)then
         posx -= dirx * movspd
      end
      if(wmap(posx,posy - diry * movspd) == 0)then
         posy -= diry * movspd
      end
   end

   if(btn(2,1))then -- look up
      dirz += lookspd
   end

   if(btn(3,1))then -- look down
      dirz -= lookspd
   end

   if(btn(1,1))then -- strafe right
       -- If anyone could help with these it would be great too xD
   end
   -- rotation!
   if(btn(1) or mx > pmx)then -- right
      rotate(rotspd)
   end
   if(btn(0) or mx < pmx)then -- left
      rotate(-rotspd)
   end

   --pmx = mx
   --pmy = my
end

I'm soo close to getting this working nice and smoothly. Any help is appreciated, thanks!

P#43674 2017-08-27 08:19 ( Edited 2018-01-09 23:52)

I noticed the gaps only appear along cardinal directions (axes) relative to the viewpoint, so I figured it was probably an over/underflow when some vector's X or Y is too close to 0.

Looking in the code, I found this:

      -- length of ray from one side to another
      raydx = sqrt(1 + (rdiry * rdiry) / (rdirx * rdirx) )
      raydy = sqrt(1 + (rdirx * rdirx) / (rdiry * rdiry) )

Pico-8's numbers are fixed-point 16.16, so if a value is less than 1/256 and you square it, that's less than 1/65536, which is the smallest number Pico can represent. Similar for squaring 256. Do that and, in this case, you're suddenly dividing by zero, which on Pico gives you the closest value to infinity, 32768.

Your rdirx,y values don't seem to go >= 256, but they do go < 1/256, so the optimization being used in that code will not work well without true floats. Technically it can underflow on a true float as well, but only extremely rarely.

If you expand the optimized code to what it really represents, which is the scale you want to apply to the eye ray to turn it into steps, then this is what it means to do:

      -- length of ray from one side to another
      local rdirl=sqrt(rdirx * rdirx + rdiry * rdiry)
      raydx = rdirl/abs(rdirx)
      raydy = rdirl/abs(rdiry)

With that, it works without gaps. Note that raydx,y are meant to be positive scales for a separate vector, so I had to use abs() to replace the implicit abs() we originally got by squaring the rdirx,y denominators when they were inside the sqrt().

I think the optimization was probably adapted from other code in which it worked better. It looks like it was designed to reduce the number of operations, though honestly I think the thread got lost somehow and the optimized code actually ended up doing more work by calling sqrt twice. So the replacement shouldn't hurt you at all. It may even be faster.

P#43679 2017-08-27 11:03 ( Edited 2017-08-27 15:03)
1

I think there may be an unrelated issue somewhere else, also related to Pico-8's precision. Sometimes if I'm basically right on a wall, I get a cluster of gaps in front of me. I'm sure this is to do with the lack of precision when stepping along rays, where your steps can't quite be represented correctly with Pico's number system because the distance between you and the wall is too tiny.

I think it's probably unavoidable, though. The solution is just to keep the camera somewhat away from walls. In general you want to avoid that kind of extremely-small-number math anyway, even with floats.

P#43680 2017-08-27 11:09 ( Edited 2017-08-27 15:11)

Works perfectly!
Thanks for the help!

This code was adapted from the C++ raycaster which of course has more capable doubles/floats.

I noticed when debugging that I was getting some very large numbers, I just didn't think it could be from the values being negative.

I originally thought this was an issue with Pico-8's SQRT function, so I found another one, which fixed another problem, but introduced another as well.

Thanks for the well-explained answer as well!

P#43681 2017-08-27 11:12 ( Edited 2017-08-27 15:12)
1

I should clarify. When I said they were less than 1/256, the issue wasn't that they went negative. it was because they were between 0 and 1/256. If you square a number like that, it'll underflow to 0 on Pico.

Anyway, you're welcome. Have fun with Pico!

P#43683 2017-08-27 11:24 ( Edited 2017-08-27 15:24)

@Lewrish did you post the fixed code? i'm trying to find some resources on raycasting for pico8

P#48005 2018-01-09 18:52 ( Edited 2018-01-09 23:52)

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2024-03-28 10:18:27 | 0.015s | Q:18