I am making a quite simple portal based mostly software program renderer in PyGame that simply makes use of portals as hyperlinks to different sectors – like a window or doorway. I’ve have gotten considerably far, with texture mapping (thanks Plus-Mud), entities, and another neat issues. I am going forward with a portal based mostly SW renderer as a result of a depth buffer was apparently too troublesome for me so as to add beforehand and that I am not eager to do BSP in the intervening time because of restricted time, so I am not doing something fancy that makes a portal a correct portal – one which transforms a sector and places it infront of the portal as if it have been a doorway.
Forgive the truth that I could sound like an fool, however my drawback begins with the portal traversal. For instance, we’ll have 3 sectors. We’ve got sector A, B, and C. A goes into C, B goes into C and C goes each into A AND B. My drawback is within the type of the wrong sector being drawn, as C would generally overlap C. My portal rendering system works by first getting the portals within the present sector I’m in, then if the portal is seen, I then go to the sector linked by that sector and append the sector I used to be in earlier than to an array, so I be sure NOT to return there. In the course of the drawing course of, I test if there’s a pixel already assigned to the place I’m wanting to attract. If there is not, I draw my pixel.
I’ve tried making the linked sectors for the portals render throughout the portal bounds on the display, and I’ve tried appending my portals as a substitute of the sectors to an array so I do not in some way go backwards on accident, however that does not work both. Any recommendation on what I ought to do? Additionally, apologies for the upcoming spaghetti code. I will be glad to reply any questions requested.
engine.py;
from settings import *
import stage
import raster
import lessons
screenArray = blankScreenArray.copy()
sectors_traversed = []
def lerp(t, a, b):
return a + t * (b - a)
class Engine:
def __init__(self, participant, main_screen, logical_screen, textures):
## globals
international screenArray
screenArray=blankScreenArray.copy()
self.display=logical_screen
self.main_screen=main_screen
self.participant:lessons.Participant=participant
self.textures=textures
self.traversed_portals=[]
def renderWall(self, wx1, wy1, wx2, wy2, z0, z1, f, c, fillPortal, visPlane, ceiling_c, floor_c, t0, t1, wall_length, fov, yaw, wall_texture, floor_texture, ceiling_texture, floor_texture_scale, ceiling_texture_scale, ceiling_distance, floor_distance, wall_height, wall_is_sky, ceiling_is_sky, v_offset, lighting):
cx, cy = self.participant.x, self.participant.y
current_ceiling_lut = [0]*W
current_floor_lut = [H1]*W
sx1, sy1 = raster.transformToScreen(f, (wx1, wy1, z0))
sx2, sy2 = raster.transformToScreen(f, (wx2, wy2, z0))
sy3 = raster.transformYCoordToScreen(f, (wx1, wy1, z1))
sy4 = raster.transformYCoordToScreen(f, (wx2, wy2, z1))
current_ceiling_lut, current_floor_lut = raster.rasterizeTextured(screenArray, sx1, sx2, sy1, sy2, sy3, sy4, c, fillPortal, portal_buffer, current_ceiling_lut, current_floor_lut, wall_texture, wy1, wy2, t0, t1, wall_length, wall_height, wall_is_sky, self.participant.yaw, v_offset, lighting)
yaw = np.radians(yaw)
if visPlane == -1:
raster.rasterizeVisplane(screenArray, current_ceiling_lut, floor_texture, floor_texture_scale, fov, f, yaw, cx, cy, floor_distance, False, lighting, floor_c)
raster.rasterizeVisplane(screenArray, current_floor_lut, ceiling_texture, ceiling_texture_scale, fov, f, yaw, cx, cy, ceiling_distance, ceiling_is_sky, lighting, ceiling_c)
elif visPlane == 1:
raster.rasterizeVisplane(screenArray, current_ceiling_lut, floor_texture, floor_texture_scale, fov, f, yaw, cx, cy, floor_distance, False, lighting, floor_c)
elif visPlane == 2:
raster.rasterizeVisplane(screenArray, current_floor_lut, ceiling_texture, ceiling_texture_scale, fov, f, yaw, cx, cy, ceiling_distance, ceiling_is_sky, lighting, ceiling_c)
def renderSector(self, sector, yaw):
if sector in sectors_traversed:
return
cx, cy, cz = self.participant.x, self.participant.y, self.participant.z
cs, sn = cos[yaw], sin[yaw]
if sector == None:
print('No Reference Sector; Out of Bounds or Lacking Sector?')
return
sector_elevation = sector.e
sector_height = sector.h
sector_entities = sector.entities
ceiling_c = sector.cc
floor_c = sector.fc
f = self.participant.FocalLength
fov = self.participant.FOV
portal_buffer=blankScreenArray.copy()
portal_queue = []
cz = self.participant.z+self.participant.h
floor_distance = cz-sector.e
ceiling_distance = sector.e+sector.h-cz
# do entity rendering first
for entity in sector_entities:
wx0, wy0 = raster.transformVector((entity.x-cx, entity.y-cy), cs, sn)
if wy0 < 1:
proceed
sprite = self.textures[entity.sprite][0]
e=cz-entity.z-entity.h
sx0, sy0 = raster.transformToScreen(f, (wx0, wy0, e))
raster.rasterizeSprite(screenArray, sx0, sy0, sprite, wy0, entity.scale, f)
lighting = sector.lighting
# do wall rendering final
for wall in sector.partitions:
wall_p1 = wall.p1
wall_p2 = wall.p2
c=wall.c
rx1 = wall_p1[0]-cx
ry1 = wall_p1[1]-cy
rx2 = wall_p2[0]-cx
ry2 = wall_p2[1]-cy
def_01_is_portal = wall.def_1_portal
def_01_portal_link = wall.def_1_link
wall_texture=wall.wall_texture
floor_texture=wall.floor_texture
ceiling_texture=wall.ceiling_texture
floor_texture_scale=wall.floor_texture_scale
ceiling_texture_scale=wall.ceiling_texture_scale
wall_length = wall.size
wall_height = sector_height+sector_elevation
t0, t1 = 0, 1
wx1, wy1 = raster.transformVector((rx1, ry1), cs, sn)
wx2, wy2 = raster.transformVector((rx2, ry2), cs, sn)
if wy1 < 1:
wx1, wy1, t = raster.clip(wx1, wy1, wx2, wy2, 1, 1, W, 1)
t1 += t/self.participant.fovWidthAtY
elif wy2 < 1:
wx2, wy2, t = raster.clip(wx2, wy2, wx1, wy1, 1, 1, W, 1)
t0 -= t/self.participant.fovWidthAtY
## determine what facet we're at relative to the phase
wall_normal_x = wall_p2[1]-wall_p1[1]
wall_normal_y = -(wall_p2[0]-wall_p1[0])
facet = (wall_normal_x * rx1) + (wall_normal_y * ry1)
facet = facet > 0 and 1 or facet < 0 and -1 or 0
# 1 = Proper, -1 = Left, 0 = Colinear
# we discover out what facet is being drawn, test if we will draw a portal and alter how we rasterize the wall
portal_to_render = None
wall_is_sky = wall_texture[2] == 'sky'
ceiling_is_sky = ceiling_texture[2] == 'sky'
wall_texture = wall_texture[0]
floor_texture = floor_texture[0]
ceiling_texture = ceiling_texture[0]
if def_01_is_portal:
portal_to_render = def_01_portal_link
if facet == -1:
if portal_to_render == None:
if wy1 > 1 and wy2 > 1:
z0 = cz-sector_elevation
z1 = z0-sector_height
self.renderWall(wx1, wy1, wx2, wy2, z0, z1, f, c, False, -1, ceiling_c, floor_c, t0, t1, wall_length, fov, yaw, wall_texture, floor_texture, ceiling_texture, floor_texture_scale, ceiling_texture_scale, ceiling_distance, floor_distance, wall_height, wall_is_sky, ceiling_is_sky, 0, lighting)
else:
proceed
else:
portal_bottom = wall.portal_bottom
portal_top = wall.portal_top
# we've got a portal, so we're going to cut up the sector into two first. we will even test how excessive and the way low can the portal be?
wall_bottom = cz-sector_elevation # that is the underside of the sector
wall_portion_01 = wall_bottom-portal_bottom
#wall_height_bottom = portal_bottom
wall_top = wall_bottom-sector_height # that is the highest of the sector
wall_portion_02 = wall_top+portal_top
#wall_height_top = portal_top
if wy1 > 1 and wy2 > 1:
wall_height = portal_bottom
self.renderWall(wx1, wy1, wx2, wy2, wall_bottom, wall_portion_01, f, c, False, 1, ceiling_c, floor_c, t0, t1, wall_length, fov, yaw, wall_texture, floor_texture, ceiling_texture, floor_texture_scale, ceiling_texture_scale, ceiling_distance, floor_distance, wall_height, wall_is_sky, ceiling_is_sky, -40, lighting)
wall_height = portal_top
self.renderWall(wx1, wy1, wx2, wy2, wall_portion_02, wall_top, f, c, False, 2, ceiling_c, floor_c, t0, t1, wall_length, fov, yaw, wall_texture, floor_texture, ceiling_texture, floor_texture_scale, ceiling_texture_scale, ceiling_distance, floor_distance, wall_height, wall_is_sky, ceiling_is_sky, 0, lighting)
wall_height = sector_height+sector_elevation
# we now know the place we rendered the underside of the highest portion and the highest of the underside portion, now we will draw the portal
if not sector in portal_queue:
sx1, sy1 = raster.transformToScreen(f, (wx1, wy1, wall_portion_01))
sx2, sy2 = raster.transformToScreen(f, (wx2, wy2, wall_portion_01))
sy3 = raster.transformYCoordToScreen(f, (wx1, wy1, wall_portion_02))
sy4 = raster.transformYCoordToScreen(f, (wx2, wy2, wall_portion_02))
#portal_occluded = raster.is_portal_visible(screenArray, sx1, sx2, sy1, sy2, sy3, sy4)
portalSector = stage.sectors[portal_to_render]
portal_center = (wall.p1+wall.p2)/2
portal_array = [portalSector, sx1, sx2, sy1, sy2, sy3, sy4, wall]
portal_queue.append(portal_array)
## render portals
if len(portal_queue) > 0:
for portalArray in portal_queue:
portal_linked_sector, sx1, sx2, sy1, sy2, sy3, sy4, wall = portalArray
if not wall in self.traversed_portals:
self.traversed_portals.append(wall)
# we are going to visualize the portals first
obstructed = raster.rasterizePortal(screenArray, portal_buffer, sx1, sx2, sy1, sy2, sy3, sy4, PINK, 1)
if not obstructed:
move
self.renderSector(portal_linked_sector, yaw)
def replace(self, dt):
## globals
international screenArray, portal_buffer, sectors_traversed
sectors_traversed = []
portal_buffer = blank_portal_buffer.copy()
in_sector = self.find_sector_from_point(self.participant.x, self.participant.y)
if in_sector != None:
self.participant.currentSector = in_sector
if self.participant.currentSector != None:
self.participant.vz -= 9.82
self.participant.z += self.participant.vz/dt
if self.participant.z < self.participant.currentSector.e:
# they've hit the ground
self.participant.vz = 0
if self.participant.hasJumped:
if pygame.time.get_ticks()-self.participant.jumpTick >= 2/dt:
self.participant.hasJumped = False
# we're on the bottom and the cooldown is above 2
elif self.participant.z > self.participant.currentSector.e+self.participant.currentSector.h-self.participant.h:
self.participant.vz = 0
# they hit the ceiling
self.participant.z = min(max(self.participant.z, self.participant.currentSector.e), self.participant.currentSector.e+self.participant.currentSector.h-self.participant.h)
## replace all entities coordinates and put them within the appropriate sectors
# when the entity strikes, and has a velocity > 0, we are going to test whether it is inside a brand new sector and set it to the brand new sector in that case
for sector in stage.sectors:
for entity in sector.entities:
entity.replace(dt, self, sector, entity)
if entity.isTouchable:
entity.checkTouch(self.participant.x, self.participant.y, sector, entity)
## draw the present sector
yaw = int(self.participant.yaw)%360
self.renderSector(self.participant.currentSector, yaw)
self.traversed_portals = []
def draw(self):
## globals
international screenArray
pygame.surfarray.blit_array(self.display, screenArray)
scaled_surface = pygame.rework.scale(self.display, (640, 480))
self.main_screen.blit(scaled_surface, (0, 0))
pygame.show.replace()
screenArray = blankScreenArray.copy()
"""buffer = (ctypes.c_uint8 * (W * H * 3)).from_buffer(screenArray)
ctypes.memset(ctypes.addressof(buffer), 0, len(buffer))"""
def point_in_polygon(self, cx, cy, partitions):
# Forged a ray to the appropriate and rely intersections
intersections = 0
for wall in partitions:
p1 = wall.p1
p2 = wall.p2
x0, y0 = p1
x1, y1 = p2
# Test if the purpose is on an edge
if (min(y0, y1) <= cy < max(y0, y1)) and (cx < max(x0, x1)):
if (y1 - y0) != 0: # Keep away from division by zero
x_intersection = x0 + (cy - y0) * (x1 - x0) / (y1 - y0)
if cx < x_intersection:
intersections += 1
return intersections % 2 == 1
def find_sector_from_point(self, x, y):
for sector in stage.sectors:
sector_walls = sector.partitions
is_inside_sector = self.point_in_polygon(x, y, sector_walls)
if is_inside_sector:
return sector
return None
raster.py
from settings import *
@staticmethod
@njit
def transformVector(v, cs, sn):
x, y = v
tx = x*cs-y*sn
ty = y*cs+x*sn
return (tx, ty)
@staticmethod
@njit
def transformToScreen(f, v):
x, y, z = v
inv_y = 1 / y # Keep away from repeated division
return (x * inv_y * f + W2, z * inv_y * f + H2)
@staticmethod
@njit
def transformYCoordToScreen(f, v):
_, y, z = v
inv_y = 1 / y # Keep away from repeated division
return z * inv_y * f + H2
# Yuriy Georgiev
@staticmethod
@njit
def clip(ax, ay, bx, by, px1, py1, px2, py2):
a = (px1 - px2) * (ay - py2) - (py1 - py2) * (ax - px2)
b = (py1 - py2) * (ax - bx) - (px1 - px2) * (ay - by)
t = a / (b+1)
ax -= t * (bx - ax)
ay -= t * (by - ay)
return ax, ay, t
@staticmethod
@njit
def rasterizeVisplane(screenArray, lut, texture, texture_scale, fov, f, yaw, cx, cy, elevation, is_sky, lighting, c):
tanFOV=np.tan(fov/2)
for x in prange(1, W1):
Y = lut[x]
if not is_sky:
beta = x/W1*fov-fov/2
alpha = (yaw+beta)
cos_beta = np.cos(beta)
sin_alpha = np.sin(alpha)
cos_alpha = np.cos(alpha)
else:
angle = np.atan((x-W2)/W2*tanFOV)
adjusted_angle = np.levels(yaw + angle)
texX = int(adjusted_angle/360*texture.form[0])%texture.form[0]
path = Y > H2
if path:
for y in prange(Y, H1):
if not (x > W1 or x < 1 or y > H1 or y < 1):
colAvg = (screenArray[x, y][0]+screenArray[x, y][1]+screenArray[x, y][2])/3
isFilled = colAvg > 0
if not isFilled:
if not is_sky:
r = abs(y-H2)
straightDist = (elevation*f)/r
d = (straightDist/cos_beta)
wx = cx + (sin_alpha*d)
wy = cy + (cos_alpha*d)
texX = int(wx*texture_scale)%texture.form[0]
texY = int(wy*texture_scale)%texture.form[1]
texture_clr = texture[texX, texY]
screenArray[x, y] = (texture_clr[0], texture_clr[1], texture_clr[2])
else:
if y < texture.form[1]:
texY = int(y)%texture.form[1]
texture_col = texture[texX, texY]
texture_col = [
max(texture_col[0], 0),
max(texture_col[1], 0),
max(texture_col[2], 0)
]
screenArray[x, y] = texture_col
else:
for y in prange(1, Y):
if not (x > W1 or x < 1 or y > H1 or y < 1):
colAvg = (screenArray[x, y][0]+screenArray[x, y][1]+screenArray[x, y][2])/3
isFilled = colAvg > 0
if not isFilled:
if not is_sky:
r = abs(y-H2)
straightDist = (elevation*f)/r
d = (straightDist/cos_beta)
wx = cx + (sin_alpha*d)
wy = cy + (cos_alpha*d)
texX = int(wx*texture_scale)%texture.form[0]
texY = int(wy*texture_scale)%texture.form[1]
texture_clr = texture[texX, texY]
screenArray[x, y] = (texture_clr[0], texture_clr[1], texture_clr[2])
else:
if y < texture.form[1]:
texY = int(y)%texture.form[1]
texture_col = texture[texX, texY]
texture_col = [
max(texture_col[0], 1),
max(texture_col[1], 1),
max(texture_col[2], 1)
]
screenArray[x, y] = texture_col
@staticmethod
@njit
def rasterizeTextured(screenArray, x0, x1, y0, y1, y2, y3, c, fill_Portal, portal_buffer, ceiling_lut, floor_lut, texture, wy0, wy1, t0, t1, wall_length, wall_height, is_sky, yaw, v_offset, lighting):
def lerp(a, b, c):
return (1 - a) * b + a * c
dx = max(x1 - x0, 1)
sf = wall_length * 1 # Texture scaling issue
texLeft = lerp(t0, 0, sf)
texRight = lerp(t1, 0, sf)
X0 = max(min(int(x0), W1), 1)
X1 = max(min(int(x1), W1), 1)
z0 = 1 / wy0
z1 = 1 / wy1
for x in prange(int(X0), int(X1)):
# Interpolated Y coordinates
t = (x - x0) / dx
Y1 = int(lerp(t, y0, y1))
Y2 = int(lerp(t, y2, y3))
if is_sky:
angle = np.atan((x - W2) / W2 * 0.70020718322)
adjusted_angle = yaw + np.levels(angle)
texX = int(adjusted_angle / 360 * texture.form[0]) % texture.form[0]
else:
texX = int(lerp(1-t, texLeft / z0, texRight / z1) / lerp(1-t, wy0, wy1) * texture.form[0]) % texture.form[0]
# Replace ceiling and ground look-up tables
if Y2 - Y1 == 0:
ceiling_lut[x] = Y1
floor_lut[x] = Y2
for y in prange(int(Y2), int(Y1)):
if 1 <= x <= W1 and 1 <= y <= H1:
ceiling_lut[x] = Y1
floor_lut[x] = Y2
colAvg = sum(screenArray[x, y]) / 3
isFilled = colAvg > 0
if fill_Portal:
portal_buffer[x, y] = True
elif not isFilled:
if is_sky:
if y < texture.form[1]:
texY = y % texture.form[1]
texture_col = texture[texX, texY]
screenArray[x, y] = texture_col
else:
# Wall texture rendering
a = (y - Y2) / (Y1 - Y2)
v = lerp(a*wall_height*0.1, 0, 1)
texY = int(v * texture.form[1] + v_offset) % texture.form[1]
texture_col = texture[texX, texY]
screenArray[x, y] = (
texture_col[0],
texture_col[1],
texture_col[2],
)
return ceiling_lut, floor_lut
@staticmethod
@njit
def rasterizePortal(screenArray, portalArray, x0, x1, y0, y1, y2, y3, c, lighting):
dx = max(x1-x0, 1)
X0 = max(min(int(x0), W1), 1)
X1 = max(min(int(x1), W1), 1)
def lerp(a, b, c):
return (1 - a) * b + a * c
full = False
if abs(X1-X0) == 0:
X0 = 1
X1 = W1
full = True
for x in prange(int(X0), int(X1)):
if not full:
t = (x - x0) / dx
Y1 = int(lerp(t, y0, y1))
Y2 = int(lerp(t, y2, y3))
else:
Y2 = 1
Y1 = H1
for y in prange(int(Y2), int(Y1)):
if not (x > W1 or x < 1 or y > H1 or y < 1):
isFilled = screenArray[x, y][0] > 0 or screenArray[x, y][1] > 0 or screenArray[x, y][2] > 0
if not isFilled:
return False
return True
@staticmethod
@njit
def rasterizeSprite(screenArray, entity_x, entity_y, sprite, entity_depth, entity_scale, f):
sprite_width = sprite.form[0]
sprite_height = sprite.form[1]
sprite_width_offset = sprite_width/2
sprite_height_offset = sprite_height/2
xStart = int(entity_x)
xEnd = int(entity_x+(sprite_width/entity_depth*entity_scale))
yStart = int(entity_y)
yEnd = int(entity_y+(sprite_height/entity_depth*entity_scale))
for x in prange(xStart, xEnd):
for y in prange(yStart, yEnd):
X = int(x - sprite_width_offset/entity_depth*entity_scale)
Y = int(y - sprite_height_offset/entity_depth*entity_scale)
if X > 1 and X < W1 and Y > 1 and Y < H1:
pixel_col = (255, 0, 0)
colAvg = (screenArray[X, Y][0]+screenArray[X, Y][1]+screenArray[X, Y][2])/3
isFilled = colAvg > 0
if not isFilled:
texX = int((x-xStart)*entity_depth/entity_scale)
texY = int((y-yStart)*entity_depth/entity_scale)
pixel_col = sprite[texX, texY]
screenArray[X, Y] = pixel_col
@staticmethod
@njit(parallel=True, fastmath=True, cache=True)
def resetScreenArray(screenArray):
for x in prange(screenArray.form[0]):
for y in prange(screenArray.form[1]):
screenArray[x, y] = (0,0,0)
return screenArray
@staticmethod
@njit
def get_segment_normal(x0, y0, x1, y1):
dx = x1-x0
dy = y1-y0
dist = 1+np.hypot(dx, dy)
return dy/dist, -dx/dist