How do I fix wall warping in my raycaster?

Question:

So I have made a raycaster in python using pygame. I cast my rays little bit by bit and once they’ve hit a wall, I calculate the distance and use that base the height of my walls. The problem is that my walls are curved.

Sadly my photos cannot upload but I will try my best to explain.

I had thought I had fixed this problem by adding this

            angle = math.radians(self.view - degree)
            dist *= math.cos(angle)

But with that the walls to the left or right of me would still be curved. I’ve tried for several hours to try to fix this by I cannot seem to find the correct answer. If you have an idea please comment!

Here is the full code or you can play it here!

#Import modules
import pygame
import math

#Set up window
pygame.init()

winWidth, winHeight = (1024, 512)
window = pygame.display.set_mode((winWidth, winHeight))

#Make player object
class Player:
    def __init__(self, x, y, tm, fov, view):
        self.x = x
        self.y = y
        self.tm = tm #tilemap
        self.fov = fov #field of view
        self.view = view #players angle
        self.movements = {'w': False, 'a':False, 's':False, 'd':False}
        self.distances = []
        self.img = pygame.image.load("tile_test.png").convert_alpha()
        self.img = pygame.transform.scale(self.img, (64, 64))
    
    def update(self): #Player movements
        radAngle = math.radians(self.view)
        if self.movements['w'] == True and self.tm[int(self.y+(math.sin(radAngle)*0.05))][int(self.x+(math.cos(radAngle)*0.05))] == 0:
            self.x += math.cos(radAngle)*0.05
            self.y += math.sin(radAngle)*0.05
        if self.movements['a'] == True:
            self.view -= 3
        if self.movements['d'] == True:
            self.view += 3
            
    def draw(self, window):
        #---TopDown View---
        #Map
        for y, row in enumerate(self.tm):
            for x, tile in enumerate(row):
                if tile == 1:
                    pygame.draw.rect(window, (255,255,255), (x*64, y*64, 64, 64))
                    pygame.draw.rect(window, (0,0,0), (x*64, y*64, 64, 64), 1)
                else:
                    pygame.draw.rect(window, (0,0,0), (x*64, y*64, 64, 64))
                    pygame.draw.rect(window, (255,255,255), (x*64, y*64, 64, 64), 1)
        #player
        pygame.draw.circle(window, (255,255,0), (self.x*64, self.y*64), 8)
        #Rays :D
        self.distances = []
        for degree in range(int(self.view-(self.fov/2)), int(self.view+(self.fov/2))):
            radAngle = math.radians(degree)
            rayx = self.x
            rayy = self.y
            
            stop = False
            while self.tm[int(rayy)][int(rayx)] == 0 and stop == False:
                rayx += math.cos(radAngle)*0.01
                rayy += math.sin(radAngle)*0.01

            #Calculate ray distance
            dist = math.sqrt(((rayx-self.x)*(rayx-self.x)+(rayy-self.y)*(rayy-self.y)))
            #Draw the ray
            pygame.draw.line(window, (0,255,0), (self.x*64, self.y*64), (rayx*64, rayy*64))
            

            #Decide if colides horizontally or vertically (To help with drawing tiles)
            rx = round(rayx - int(rayx), 5)
            ry = round(rayy - int(rayy), 5)
            h_col = False
            if rx > .5:
                if ry > .5 - (rx - .5) and ry < .5 + (rx - .5):
                    h_col = True
                else:
                    h_col = False
            elif rx <= .5:
                if ry > .5 - (.5 - rx) and ry < .5 + (.5 - rx):
                    h_col = True
                else:
                    h_col = False
            
            if h_col == True:
                num = ry
            else:
                num = rx

            #Attempt at fixing curved walls. Works somewhat but not really
            angle = math.radians(self.view - degree)
            dist *= math.cos(angle)
            
            self.distances.append((dist, num))
        #draw player view ray
        pygame.draw.line(window, (255,0,0), (self.x*64, self.y*64), ((self.x+math.cos(math.radians(self.view)))*64, (self.y+math.sin(math.radians(self.view)))*64))
        #---3D View---
        for x, line in enumerate(self.distances):
            height = 256 - round(line[0], 1)*42
            if height <= .5:
                height = .5
            w, h = self.img.get_width(), self.img.get_height()

            img_x = int(line[1]*w)
            if img_x > 63:
                img_x = 63
            elif img_x < 0:
                img_x = 0
            img = self.img.subsurface(img_x, 0, 1, h)
            img = pygame.transform.scale(img, (8, height*2))

            window.blit(img, (512+(x*8), 256-height))

        
class Control:
    def __init__(self):
        self.run = True
        self.clock = pygame.time.Clock()
    
    def update(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.run = False
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_w:
                    player.movements["w"] = True
                if event.key == pygame.K_a:
                    player.movements["a"] = True
                if event.key == pygame.K_s:
                    player.movements["s"] = True
                if event.key == pygame.K_d:
                    player.movements["d"] = True
                if event.key == pygame.K_ESCAPE:
                    self.run = False
            elif event.type == pygame.KEYUP:
                if event.key == pygame.K_w:
                    player.movements["w"] = False
                if event.key == pygame.K_a:
                    player.movements["a"] = False
                if event.key == pygame.K_s:
                    player.movements["s"] = False
                if event.key == pygame.K_d:
                    player.movements["d"] = False

game = Control()
tm = [
    [1,1,1,1,1,1,1,1],
    [1,0,1,0,0,0,0,1],
    [1,0,1,1,0,1,0,1],
    [1,0,0,0,0,1,0,1],
    [1,1,1,1,0,1,0,1],
    [1,0,1,1,0,0,0,1],
    [1,0,0,0,0,0,0,1],
    [1,1,1,1,1,1,1,1]
]

player = Player(3.5, 3.5, tm, 64, 0)

while game.run:
    window.fill((50,50,50))
    player.draw(window)

    pygame.display.update()
    game.update()
    player.update()

    game.clock.tick(30)

pygame.quit()
Asked By: Richard Holt

||

Answers:

Your attempt to project the distance to the line of sight is almost correct:

dist *= math.cos(math.radians(degree - self.view))

However, the calculation of the height is wrong:

height = 256 - round(line[0], 1)*42

height = round(256 / line[0])

Also calculate the angle from a projection to a plane:

class Player:
    # [...]

    def draw(self, window):
        # [...]

        for dy in range(-self.fov//2, self.fov//2):
            degree = self.view + math.degrees(math.atan2(dy, 50))
            radAngle = math.radians(degree)

            # [...]

            # project distance
            dist *= math.cos(math.radians(degree - self.view))

        # [...]

        for x, line in enumerate(self.distances):

            # calculate height
            height = round(256 / line[0])

            # [...]


Optimized minimal example:

import pygame
import math

pygame.init()

tile_size, map_size = 50, 8
board = [
    '########',
    '#   #  #',
    '#   # ##',
    '#  ##  #',
    '#      #',
    '###  ###',
    '#      #',
    '########']

def cast_rays(sx, sy, angle):
    rx = math.cos(angle)
    ry = math.sin(angle)
    map_x = sx // tile_size
    map_y = sy // tile_size

    t_max_x = sx/tile_size - map_x
    if rx > 0:
        t_max_x = 1 - t_max_x
    t_max_y = sy/tile_size - map_y
    if ry > 0:
        t_max_y = 1 - t_max_y

    while True:
        if ry == 0 or t_max_x < t_max_y * abs(rx / ry):
            side = 'x'
            map_x += 1 if rx > 0 else -1
            t_max_x += 1
            if map_x < 0 or map_x >= map_size:
                break
        else:
            side = 'y'
            map_y += 1 if ry > 0 else -1
            t_max_y += 1
            if map_x < 0 or map_y >= map_size:
                break
        if board[int(map_y)][int(map_x)] == "#":
            break

    if side == 'x':
        x = (map_x + (1 if rx < 0 else 0)) * tile_size
        y = player_y + (x - player_x) * ry / rx
        direction = 'r' if x >= player_x else 'l'
    else:
        y = (map_y + (1 if ry < 0 else 0)) * tile_size
        x = player_x + (y - player_y) * rx / ry
        direction = 'd' if y >= player_y else 'u'
    return (x, y), math.hypot(x - sx, y - sy), direction   

def cast_fov(sx, sy, angle, fov, no_ofrays):
    max_d = math.tan(math.radians(fov/2))
    step = max_d * 2 / no_ofrays
    rays = []
    for i in range(no_ofrays):
        d = -max_d + (i + 0.5) * step
        ray_angle = math.atan2(d, 1)
        pos, dist, direction = cast_rays(sx, sy, angle + ray_angle)
        rays.append((pos, dist, dist * math.cos(ray_angle), direction))
    return rays

window = pygame.display.set_mode((tile_size*map_size*2, tile_size*map_size))
clock = pygame.time.Clock()

board_surf = pygame.Surface((tile_size*map_size, tile_size*map_size))
for row in range(8):
    for col in range(8):
        color = (192, 192, 192) if board[row][col] == '#' else (96, 96, 96)
        pygame.draw.rect(board_surf, color, (col * tile_size, row * tile_size, tile_size - 2, tile_size - 2))

player_x, player_y = round(tile_size * 4.5) + 0.5, round(tile_size * 4.5) + 0.5
player_angle = 0
max_speed = 3
colors = {'r' : (196, 128, 64), 'l' : (128, 128, 64), 'd' : (128, 196, 64), 'u' : (64, 196, 64)}

run = True
while run:
    clock.tick(30)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False    
    
    keys = pygame.key.get_pressed()
    hit_pos_front, dist_front, side_front = cast_rays(player_x, player_y, player_angle)
    hit_pos_back, dist_back, side_back = cast_rays(player_x, player_y, player_angle + math.pi)
    player_angle += (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * 0.1
    speed = ((0 if dist_front <= max_speed else keys[pygame.K_UP]) - (0 if dist_back <= max_speed else keys[pygame.K_DOWN])) * max_speed
    player_x += math.cos(player_angle) * speed
    player_y += math.sin(player_angle) * speed
    rays = cast_fov(player_x, player_y, player_angle, 60, 40)

    window.blit(board_surf, (0, 0))
    for ray in rays:
        pygame.draw.line(window, (0, 255, 0), (player_x, player_y), ray[0])
    pygame.draw.line(window, (255, 0, 0), (player_x, player_y), hit_pos_front)
    pygame.draw.circle(window, (255, 0, 0), (player_x, player_y), 8)

    pygame.draw.rect(window, (128, 128, 255), (400, 0, 400, 200))
    pygame.draw.rect(window, (128, 128, 128), (400, 200, 400, 200))
    for i, ray in enumerate(rays):
        height = round(256 / (ray[2]/50))
        color = pygame.Color((0, 0, 0)).lerp(colors[ray[3]], min(height/256, 1))
        rect = pygame.Rect(400 + i*10, 200-height//2, 10, height)
        pygame.draw.rect(window, color, rect)
    pygame.display.flip()

pygame.quit()
exit()

Also see PyGameExamplesAndAnswers – Raycasting

Answered By: Rabbid76