Newer
Older
3d_renderer / functions.py
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