import math import copy import colorsys import random import imageio import numpy # functions # converts a coordinate from a coordinate with origin at the middle, to a coordinate with origin at the top left def conv_coord(points2d, size, scale): for i in range(len(points2d)): points2d[i][0] = (points2d[i][0] * scale) + size[0] / 2 points2d[i][1] = (points2d[i][1] * scale) + size[1] / 2 return points2d # projects a 3d point onto a 2d screen def project(points3d, fp_distance, distance): c_type = "perspective" if c_type == "perspective": points2d = [] for i in range(len(points3d)): points2d.append([0, 0]) # calculate x pos try: points2d[i][0] = ((fp_distance*points3d[i][0]) / (points3d[i][1] - distance)) # calculate y pos points2d[i][1] = ((fp_distance*points3d[i][2]) / (points3d[i][1] - distance)) except ZeroDivisionError: points2d[i] = [0, 0] return points2d elif c_type == "orthographic": points2d = [] for points in points3d: points2d.append([points[0], points[2]]) return points2d # rotates a 3d point def rotate(points3d, rx, ry, rz): rx = math.radians(rx) ry = math.radians(ry) rz = math.radians(rz) new_points3d = [] for i in range(len(points3d)): new_points3d.append([0, 0, 0]) new_points3d[i][0] = ((math.cos(rz))*points3d[i][0] + (-math.sin(rz))*points3d[i][1]) new_points3d[i][1] = ((math.sin(rz)*math.cos(rx))*points3d[i][0] + (math.cos(rz)*math.cos(rx))*points3d[i][1] + (-math.sin(rx)) * points3d[i][2]) new_points3d[i][2] = ((math.sin(rz)*math.sin(rx))*points3d[i][0] + (math.cos(rz)*math.sin(rx))*points3d[i][1] + (math.cos(rx))*points3d[i][2]) return new_points3d def reverse_rotate(points3d, ry, rx, rz): rx = math.radians(rx) ry = math.radians(ry) rz = math.radians(rz) new_points3d = [] # matrix z * y * x for p in points3d: new_points3d.append([((math.cos(rz)*math.cos(ry))*p[0] + (math.cos(rz)*math.sin(ry)*math.sin(rx) - math.sin(rz)*math.cos(ry))*p[1]) + (math.cos(rz)*math.sin(ry)*math.cos(rx) + math.sin(rz)+math.sin(ry))*p[2], (math.sin(rz)*math.cos(ry))*p[0] + (math.sin(rz)*math.sin(rx)*math.sin(rx) + math.cos(rz)*math.cos(rx))*p[1] + (math.sin(rz)*math.sin(ry)*math.sin(rx) - math.cos(rz)*math.cos(rx))*p[2], -(math.sin(ry)*p[0] + (math.cos(ry)*math.sin(rx))*p[1] + (math.cos(ry)*math.cos(rx))*p[2]) ]) return new_points3d def translate(points3d, t_vec): new_points3d = copy.deepcopy(points3d) for i in range(len(points3d)): new_points3d[i][0] += t_vec[0] new_points3d[i][1] += t_vec[1] new_points3d[i][2] += t_vec[2] return new_points3d # calculates the focal point distance given an FOV and screen size in units def fov_calc(fov, size): fp_distance = (size / (2 * (math.tan(math.radians(fov) / 2)))) return fp_distance # function that returns the angle of a face to the camera def face_angle(points3d, fp_distance, distance): # calculate cross product: vec1 = points3d[0][0], points3d[0][1]-(distance), points3d[0][2] vec2 = cross_product(sub_l(points3d[0], points3d[1]), sub_l(points3d[1], points3d[2])) return math.degrees(_2vec_angle(vec1, vec2)) # returns the cross product of two vectors def cross_product(vector1, vector2): product = [vector1[1]*vector2[2] - vector1[2]*vector2[1], vector1[2]*vector2[0] - vector1[0]*vector2[2], vector1[0]*vector2[1] - vector1[1]*vector2[0]] return product # returns the angle of 2 vectors in radians def _2vec_angle(vector1, vector2): result = math.acos( (vector1[0]*vector2[0] + vector1[1]*vector2[1] + vector1[2]*vector2[2]) / # ----------------------------------------------------------- (math.sqrt((vector1[0] ** 2) + (vector1[1] ** 2) + (vector1[2] ** 2)) * math.sqrt((vector2[0] ** 2) + (vector2[1] ** 2) + (vector2[2] ** 2))) ) return result def sub_l(list1, list2): result = list() for i1, i2 in zip(list1, list2): result.append(i1 - i2) return result # returns a new list of points, without ones that are behind the camera def fix_list_points_behind_camera(points3d, distance, face_list): new_points3d = [] invalid_points = [] for i in range(len(points3d)): if points3d[i][1] <= distance + .5: invalid_points.append(points3d[i]) # print(points3d[i][1], distance) for j in range(len(face_list)): try: face_list[j].remove(i) except ValueError: continue else: new_points3d.append(points3d[i]) return points3d def fix_points_behind_camera(points3d, distance): new_points3d = [] for _3point in points3d: if _3point[1] > distance + .5: new_points3d.append(_3point) return new_points3d # returns a new list of points, without ones that are behind the camera - for edges (only 2 points) def fix_points_behind_camera_edge(p1, p2, distance): if p2[1] <= distance + 0.01 and p1[1] <= distance + 0.01: print("aaa") return [] elif p1[1] <= distance + 0.01: diff = sub_l(p2, p1) percentage = (p2[1]-1) / (p2[1]-p1[1]) new_p1 = [p2[0] - (diff[0]*percentage), 1, p2[2] - (diff[2] * percentage)] return [new_p1, p2] elif p2[1] <= distance + 0.01: diff = sub_l(p1, p2) percentage = (p1[1]-1) / (p1[1]-p2[1]) new_p2 = [p1[0] - (diff[0]*percentage), 1, p1[2] - (diff[2] * percentage)] return [p1, new_p2] else: return[p1, p2] # new classes class Colour: r = 0 g = 0 b = 0 a = 0 def __init__(self, rgb): if len(rgb) == 3: self.r = rgb[0] self.g = rgb[1] self.b = rgb[2] elif len(rgb) == 4: self.r = rgb[0] self.g = rgb[1] self.b = rgb[2] self.a = rgb[3] def set_rgb(self, rgb: list[int]): self.r = rgb[0] self.g = rgb[1] self.b = rgb[2] def get_rgb(self): return [self.r, self.g, self.b] def set_hsv(self, hsv): rgb = [x * 256 for x in colorsys.hsv_to_rgb(hsv[0] / 256, hsv[1] / 256, hsv[2] / 256)] self.r = rgb[0] self.g = rgb[1] self.b = rgb[2] def get_hsv(self): hsv = [x * 256 for x in colorsys.hsv_to_rgb(self.r / 256, self.g / 256, self.b / 256)] return [self.r, self.g, self.b] def get_alpha(self): if self.a > 128: return True else: return False class Point2D: x: float y: float def __init__(self, x, y): self.x = x self.y = y def get(self): return [self.x, self.y] class Point3D: x: float y: float z: float def __init__(self, xyz : list[float]): self.x = xyz[0] self.y = xyz[1] self.z = xyz[2] def translate(self, t_vec): # but whyy self.x += t_vec.x self.y += t_vec.y self.z += t_vec.z def rotate(self, rot_matrix: numpy.array): point = numpy.array([self.x, self.y, self.z]) numpy.multiply(point, rot_matrix) def get_2d_point(self, fp_distance): # converts a 3d point into a 2d point distance = 0 # calculate x pos try: x = ((fp_distance * self.x) / (self.y - distance)) # calculate y pos y = ((fp_distance * self.z) / (self.y - distance)) except ZeroDivisionError: x = 0 y = 0 return Point2D(x, y) def to_list(self): return [self.x, self.y, self.z] class Texture: # Y -> X -> Colour pixels: list[list[Colour]] = [] def __init__(self, path: str): self.pixels = [] image = imageio.imread(path) for i, image_y in enumerate(image): self.pixels.append([]) for image_x in image_y: self.pixels[i].append(Colour(image_x)) def get(self): return self.pixels def get_pixel(self, x, y): return self.pixels[y][x] def get_pixel_alpha(self, x, y): return self.pixels[y][x].get_alpha() class Face: # The face class stores a face with N points, a colour, information about edges and faces with nothing else points: list[Point3D] colour: Colour has_faces: bool has_edges: bool def __init__(self, points: list[Point3D], colour: Colour, has_edges: bool, has_faces: bool): self.hasEdges = has_edges # whether the main face has edges self.colour = colour self.points = points self.hasFaces = has_faces def get_points(self): # returns the positions of points (not rotated, not relative to the player) return self.points class TexturedFace(Face): texture: Texture # This class inherits from Face, and instead of a base colour it has a texture def set_texture(self, texture: Texture): # sets the texture of the textured Face def get_sub_faces(self): # new functions def generateRotMatrix(Rx: int, Ry: int, Rz: int): # generates a rotation matrix for the current camera rotations # should only be run once per frame (hopefully a performance improvement?) def drawFace(face: Face, rot_matrix: numpy.array, playerPos: Point3D): # has face to draw and precalculated camera rotation matrix, along with player position as input # the face has the global coordinates here x = 0 face.points[x].translate(playerPos.to_list()).rotate(rot_matrix) def drawTexturedFace(face: Face, transform_matrix: numpy.array): # has face and a camera rotation + position matrix as input, draws the textured face onto the screen # old classes # class Face: # def __init__(self, points: list[list[float]], colour: Colour, has_edges, has_faces: bool): # self.hasEdges = has_edges # whether the main face has edges # self.colour = colour # self.points = points # self.hasFaces = has_faces # # def get_points_order(self): # order = [] # for i, _ in enumerate(self.points): # order.append(i) # return order # # points: list[list[float]] = [] # main edges of the face (used for distance and back face culling calculations) # colour: Colour = [] # colour of the face # hasEdges: bool = [] # hasFaces: bool = [] # # # # contains n faces that are all in the same plane # class TexturedFace: # def __init__(self, face: Face): # init from existing face object (probably an easier way of doing this) # self.hasEdges = face.hasEdges # whether the main face has edges # self.colour = face.colour # self.points = face.points # self.hasFaces = face.hasFaces # # def get_points_order(self): # order = [] # for i, _ in enumerate(self.points): # order.append(i) # return order # # def set_sub_faces(self, sub_faces: list[Face]): # self.sub_faces = sub_faces # # def get_sub_faces(self): # return self.sub_faces # # points: list[list[float]] = [] # main edges of the face (used for distance and back face culling calculations) # colour: Colour = [] # colour of the main face (should be unused 99% of the time) # hasEdges: bool = [] # hasFaces: bool = [] # whether the main face renders (should always be false) # sub_faces: list[Face] # # # class Texture: # # Y -> X -> Colour # pixels: list[list[Colour]] = [] # # def __init__(self, path: str): # self.pixels = [] # # image = imageio.imread(path) # for i, image_y in enumerate(image): # self.pixels.append([]) # for image_x in image_y: # self.pixels[i].append(Colour(image_x)) # # def get(self): # return self.pixels # # def get_pixel(self, x, y): # return self.pixels[y][x] # # def get_pixel_alpha(self, x, y): # return self.pixels[y][x].get_alpha() # # # class Cube: # position: list[int] = [] # colours: list[Colour] = [] # list of 6 colours [legacy, will be removed] # textures: list[Texture] = [] # list of 6 textures for each face of the cube # has_edges: bool = [] # has_faces: bool = [] # textured_face_list: list[TexturedFace] = None # # ordered top - north - east - south - west - bottom # cubePoints = [[.5, .5, .5], [.5, .5, -.5], [.5, -.5, .5], [.5, -.5, -.5], # [-.5, .5, .5], [-.5, .5, -.5], [-.5, -.5, .5], [-.5, -.5, -.5]] # cubeSides = [[3, 7, 5, 1], [2, 6, 7, 3], [4, 5, 7, 6], [0, 1, 5, 4], [0, 2, 3, 1], [6, 2, 0, 4]] # # # generate the points for a subdivided 16*16 face # sub_div_points = [] # y = -.5 # for i in range(17): # x = -.5 # sub_div_points.append([]) # for j in range(17): # sub_div_points[i].append([x, y]) # x += 1 / 16 # y += 1 / 16 # # # generate the face_list for a subdivided 16*16 face # sub_div_faces = [] # y = 0 # for i in range(16): # x = 0 # for j in range(16): # sub_div_faces.append([[x, y], [1 + x, y], [1 + x, 1 + y], [x, y + 1]]) # x += 1 # y += 1 # # def __init__(self, position: list[int], colour, has_edges, has_faces): # self.has_edges = has_edges # self.has_faces = has_faces # # self.colours = [[], [], [], [], [], []] # self.textures = [[], [], [], [], [], []] # self.position = position # # if type(colour) is list: # if type(colour[0]) is Texture: # for i in range(6): # self.textures[i] = colour[i] # else: # for i in range(6): # self.colours[i] = colour[i] # else: # for i in range(6): # self.colours[i] = colour # # # legacy function, will be removed # def get_geometry_points(self, player_pos): # points_list = translate(translate(self.cubePoints, player_pos), self.position) # face_list = copy.deepcopy(self.cubeSides) # return [points_list, face_list] # # def has_texture(self): # if not self.textures[0]: # return False # else: # return True # # def get_faces(self, player_pos): # face_list: list[Face] = [] # for x in range(6): # points_list = translate(translate(self.cubePoints, player_pos), self.position) # if not self.colours[0]: # face_list.append(Face([points_list[face] # for face in self.cubeSides[x]], # Colour([0, 255, 0]), self.has_edges, False)) # else: # face_list.append(Face([points_list[face] # for face in self.cubeSides[x]], # self.colours[x], self.has_edges, self.has_faces)) # return face_list # # def get_textured_faces(self, player_pos): # returns a list of 6 textured face objects # # if self.textured_face_list is None: # textured_face_list = [TexturedFace(i) for i in self.get_faces([0, 0, 0])] # # top face (translate up by .5) # new_sub_faces = [] # i = 0 # for j, face in enumerate(self.sub_div_faces): # points = [copy.deepcopy(self.sub_div_points[point[0]][point[1]]) for point in face] # for point in points: # # # set the z coordinate to -.5 for top face positioning # point.insert(2, -.5) # # y = j % 16 # x = j // 16 # if True: # not self.textures[i].get_pixel_alpha(x, y): # new_sub_faces.append(Face(points, self.textures[i].get_pixel(x, y), False, True)) # textured_face_list[i].set_sub_faces(new_sub_faces) # # # bottom face (5) (translate down by .5, mirror in x) # new_sub_faces = [] # i = 5 # for j, face in enumerate(self.sub_div_faces): # points = [copy.deepcopy(self.sub_div_points[point[0]][point[1]]) for point in face] # for point in points: # # # set the z coordinate to .5 for bottom face positioning # point.insert(2, .5) # # mirror the face in x # point[0] = -point[0] # # y = j % 16 # x = j // 16 # if True: # not self.textures[i].get_pixel_alpha(x, y): # new_sub_faces.append(Face(points, self.textures[i].get_pixel(x, y), False, True)) # textured_face_list[i].set_sub_faces(new_sub_faces) # # # north (swap y and z) # new_sub_faces = [] # i = 1 # for j, face in enumerate(self.sub_div_faces): # points = [copy.deepcopy(self.sub_div_points[point[0]][point[1]]) for point in face] # for point in points: # # # set the z coordinate to .5 # point.insert(2, .5) # # swap x and z # temp = point[2] # point[2] = point[1] # point[1] = temp # # mirror the face in x # # point[0] = -point[0] # # y = j % 16 # x = j // 16 # if True: # not self.textures[i].get_pixel_alpha(x, y): # new_sub_faces.append(Face(points, self.textures[i].get_pixel(x, y), False, True)) # textured_face_list[i].set_sub_faces(new_sub_faces) # # # south = swap y and z, mirror in x # new_sub_faces = [] # i = 3 # for j, face in enumerate(self.sub_div_faces): # points = [copy.deepcopy(self.sub_div_points[point[0]][point[1]]) for point in face] # for point in points: # # # set the z coordinate to -.5 # point.insert(2, -.5) # # swap x and z # temp = point[2] # point[2] = point[1] # point[1] = temp # # mirror the face in x # point[0] = -point[0] # # y = j % 16 # x = j // 16 # if True: # not self.textures[i].get_pixel_alpha(x, y): # new_sub_faces.append(Face(points, self.textures[i].get_pixel(x, y), False, True)) # textured_face_list[i].set_sub_faces(new_sub_faces) # # # east face = swap x and z # new_sub_faces = [] # i = 2 # for j, face in enumerate(self.sub_div_faces): # points = [copy.deepcopy(self.sub_div_points[point[0]][point[1]]) for point in face] # for point in points: # # # set the z coordinate to .5 # point.insert(2, .5) # # swap y and z # temp = point[2] # point[2] = point[1] # point[1] = temp # # swap x and y # temp = point[1] # point[1] = point[0] # point[0] = temp # # mirror the face in y # point[1] = -point[1] # y = j % 16 # x = j // 16 # if True: # not self.textures[i].get_pixel_alpha(x, y): # new_sub_faces.append(Face(points, self.textures[i].get_pixel(x, y), False, True)) # textured_face_list[i].set_sub_faces(new_sub_faces) # # west face = swap y and z, and reverse sub lists # new_sub_faces = [] # i = 4 # for j, face in enumerate(self.sub_div_faces): # points = [copy.deepcopy(self.sub_div_points[point[0]][point[1]]) for point in face] # for point in points: # # # set the z coordinate to .5 # point.insert(2, .5) # # swap y and z # temp = point[2] # point[2] = point[1] # point[1] = temp # # swap x and y # temp = point[1] # point[1] = point[0] # point[0] = temp # # mirror the face in x # point[0] = -point[0] # y = j % 16 # x = j // 16 # if True: # not self.textures[i].get_pixel_alpha(x, y): # new_sub_faces.append(Face(points, self.textures[i].get_pixel(x, y), False, True)) # textured_face_list[i].set_sub_faces(new_sub_faces) # # return a face_list with the colours of the texture applied # self.textured_face_list = textured_face_list # textured_face_list = copy.deepcopy(self.textured_face_list) # for textured_face in textured_face_list: # for face in textured_face.sub_faces: # for point in face.points: # for i in range(3): # point[i] += player_pos[i] # return textured_face_list