graphics programming – How do portals in Duke Nukem 3D traverse into different sectors correctly whereas preserving issues rendering usually?


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.


Sector 3
Sector 3

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

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles