# What is a good method for 2D isometric lighting in Pygame?

I'm making my first isometric game in Python using Pygame. It's a 2D tile-based dungeon crawler, but I want to create a nice lighting system. I've made a random level generator that creates the rooms and joins them with corridors. My current lighting method uses a tile-based shadow-casting algorithm to determine line-of-sight from the player. Each tile has an alpha mask with the alpha value determined by the algorithm.

I want something that looks a bit more natural, and instead of the lighting moving 1 tile at a time as the player crosses the tile boundary, something that can move per-pixel if that's indeed possible? The player walks smoothly around, but my lighting "jumps" by tile.

Here is some of my code to show how the shadow-casting works:

# Set multipliers for transforming coords to octants.
MULTI = [[1, 0, 0, -1, -1, 0, 0, 1],
[0, 1, -1, 0, 0, -1, 1, 0],
[0, 1, 1, 0, 0, -1, -1, 0],
[1, 0, 0, 1, -1, 0, 0, -1]]

OCTANTS = [[1, 0, 0, 1], [-1, 0, 0, 1], [0, -1, 1, 0], [0, -1, -1, 0],
[-1, 0, 0, -1], [1, 0, 0, -1], [0, 1, -1, 0], [0, 1, 1, 0]]

def render(self):
screen.surface.fill(BLACK)

# Calculate the radius around the player.
x, y, r = player.x, player.y, player.light_radius
x1 = 1 if x - r < 1 else x - r
y1 = 1 if y - r < 1 else y - r
x2 = self.width - 1 if x + r > self.width - 1 else x + r
y2 = self.height - 1 if y + r > self.height - 1 else y + r

# Set the alpha level of the tiles in range.
for row in range(y1, y2 + 1):
for col in range(x1, x2 + 1):
if self.map[row][col].is_seen:
self.map[row][col].alpha_layer.set_alpha(ALPHA_SEEN)
else:
self.map[row][col].alpha_layer.set_alpha(ALPHA_UNSEEN)
self.map[player.y][player.x].alpha_layer.set_alpha(0)

# Apply shadow casting to the area.
for octant in range(8):
self.MULTI[0][octant], self.MULTI[1][octant],
self.MULTI[2][octant], self.MULTI[3][octant])

# Draw all the tiles in the area.
for row in range(y1, y2 + 1):
for col in range(x1, x2 + 1):
tile = self.map[row][col]
if tile.name != 'void':
x, y = convert_to_iso(tile.x, tile.y)
if tile.name == 'wall_tile':
y -= TILE_H
screen.surface.blit(tile.image, (x, y))
screen.surface.blit(tile.alpha_layer, (x, y))

def shadow_cast(self, row, start, end, xx, xy, yx, yy):
if start < end:
return

new_start = 0
for depth in range(row, player.light_radius + 1):
dx, dy = -depth - 1, -depth
blocked = False

while dx <= 0:
dx += 1
cell_x = player.x + dx * xx + dy * xy
cell_y = player.y + dx * yx + dy * yy
l_slope = (dx - 0.5) / (dy + 0.5)
r_slope = (dx + 0.5) / (dy - 0.5)

if start < r_slope:
continue
elif end > l_slope:
break
else:
if dx * dx + dy * dy < radius2:
self.set_visible(cell_x, cell_y, depth)
if blocked:
if self.blocked(cell_x, cell_y):
new_start = r_slope
continue
else:
blocked = False
start = new_start
else:
if (self.blocked(cell_x, cell_y) and
blocked = True
depth + 1, start, l_slope, xx, xy, yx, yy)
new_start = r_slope
if blocked:
break

def set_visible(self, x, y, d):
if 0 <= x < self.width and 0 <= y < self.height:
self.map[y][x].is_seen = True
self.map[y][x].alpha_layer.set_alpha(
int(ALPHA_SEEN / (player.light_radius + 1)) * d)

def blocked(self, x, y):
return (x < 0 or y < 0 or x >= self.width or
y >= self.height or self.map[y][x].is_solid)


I haven't added the player in yet, but he is currently the source of the light at the centre. I really want to do something with the lighting that looks a lot better. I don't really want to get into OpenGL or anything like that, I'm currently using Python 3.10, Pygame 2.1.2 and Pycharm Community Edition as my IDE.

• It sounds like you might be interested in a 2D Visibility Algorithm. Feb 13, 2022 at 17:50
• My game is in 2.5D - isometric space - so ray casting might be a bit tough! Feb 13, 2022 at 17:58
• You can cast your rays in the 2D plane of the floor, dealing only with the square footprints of your tiles. Mapping the result to the tilted isometric perspective is then just a simple affine transformation from the flat 2D source space. Feb 13, 2022 at 18:00