diff --git a/functions.py b/functions.py index 5dc562d..710188e 100644 --- a/functions.py +++ b/functions.py @@ -5,6 +5,9 @@ import imageio import numpy +# todo (once everything is mildly sorted) - find out which of these are unused + + # 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): @@ -151,14 +154,6 @@ 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: @@ -178,6 +173,7 @@ else: return[p1, p2] + # new classes class Colour: r = 0 @@ -220,6 +216,7 @@ else: return False + class Point2D: x: float y: float @@ -231,7 +228,14 @@ def get(self): return [self.x, self.y] + def conv_coord(self, size, scale): + self.x = (self.x * scale) + size[0]/2 + self.y = (self.y * scale) + size[0]/2 + + class Point3D: + # todo: + # check if i have missed anything obvious x: float y: float z: float @@ -241,17 +245,35 @@ self.y = xyz[1] self.z = xyz[2] + def xyz(self): + xyz: list[float] = [self.x, self.y, self.z] + return xyz + 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): + return self + + def inv_translate(self, t_vec): # but whyy + self.x -= t_vec.x + self.y -= t_vec.y + self.z -= t_vec.z + + return self + + def snap(self): # sets x y z to the nearest integer + self.x = round(self.x) + self.y = round(self.y) + self.z = round(self.z) + + def apply_matrix(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 + # converts a 3d point into a 2d point (replacement for the projection function) distance = 0 # calculate x pos try: @@ -266,7 +288,9 @@ def to_list(self): return [self.x, self.y, self.z] + class Texture: + # this class *should* be finished # Y -> X -> Colour pixels: list[list[Colour]] = [] @@ -288,8 +312,12 @@ 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 + # todo: + # make a cleaner way of extracting values + points: list[Point3D] colour: Colour has_faces: bool @@ -297,6 +325,8 @@ def __init__(self, points: list[Point3D], colour: Colour, has_edges: bool, has_faces: bool): self.hasEdges = has_edges # whether the main face has edges + if type(colour) is list: + raise ZeroDivisionError self.colour = colour self.points = points self.hasFaces = has_faces @@ -305,30 +335,113 @@ # returns the positions of points (not rotated, not relative to the player) return self.points + def fix_points_behind_camera(self): + for point in self.points: + if point.y <= 0.5: + self.points.remove(point) + + class TexturedFace(Face): + # todo: + # write the ability to get sub faces + # figure out a way of storing the 'index' of the face (probably in the cube class) + 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 + self.texture = texture - def get_sub_faces(self): + def get_sub_faces(self, face_point_dict): + # idk + self.texture = 0 + + +class Cube: + position: Point3D + colour: list[Colour] + texture: list[Texture] + + faces: list[Face] = [] + # ordered top - north - east - south - west - bottom + cubePoints = [Point3D([.5, .5, .5]), Point3D([.5, .5, -.5]), Point3D([.5, -.5, .5]), Point3D([.5, -.5, -.5]), + Point3D([-.5, .5, .5]), Point3D([-.5, .5, -.5]), Point3D([-.5, -.5, .5]), Point3D([-.5, -.5, -.5])] + + # same as the dictionary, but no longer used. not sure why it still exists. + # 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]] + + # hardcoded dict for the relative positions of cube faces + cubeFaces: list[list[Point3D]] = [ + [cubePoints[3], cubePoints[5], cubePoints[7], cubePoints[1]], # top + [cubePoints[2], cubePoints[6], cubePoints[7], cubePoints[3]], # north + [cubePoints[4], cubePoints[5], cubePoints[7], cubePoints[6]], # east + [cubePoints[0], cubePoints[1], cubePoints[5], cubePoints[4]], # south + [cubePoints[0], cubePoints[2], cubePoints[3], cubePoints[1]], # west + [cubePoints[6], cubePoints[2], cubePoints[0], cubePoints[4]]] # bottom] + + def __init__(self, position: Point3D, texture : list[Colour], has_faces, has_edges): + # todo + # implement texture + + self.position = position + self.colours = texture + + self.has_faces = has_faces + self.has_edges = has_edges + + # initialises faces when the cube is placed. Faces are in global coordinates + for i in range(6): + self.faces.append(Face([point.translate(self.position) for point in self.cubeFaces[i]], + self.colours[i], self.has_edges, self.has_faces)) + + def get_faces(self): + return copy.deepcopy(self.faces) # new functions -def generateRotMatrix(Rx: int, Ry: int, Rz: int): - # generates a rotation matrix for the current camera rotations +def generate_cam_matrix(rx: int, ry: int, rz: int, player_pos): + rotation_matrix = numpy.array( + [[[math.cos(ry)*math.cos(rz)], [math.sin(rx)*math.sin(ry)*math.cos(rz) - math.cos(rx)*math.sin(rz)], [math.cos(rx)*math.sin(ry)*math.cos(rz) + math.sin(rx)*math.sin(rz)], [0]], + [[math.cos(ry)*math.sin(rz)], [math.sin(rx)*math.sin(ry)*math.sin(rz) + math.cos(rx)*math.cos(rz)], [math.cos(rx)*math.sin(ry)*math.sin(rz) - math.sin(rx)*math.sin(rz)], [0]], + [[-math.sin(ry)], [math.sin(rx)*math.cos(ry)], [math.cos(rx)*math.cos(ry)], [0]], + [[0], [0], [0], [1]]] + ) + + translation_matrix = numpy.array( + [[[1], [0], [0], [player_pos.x]], + [[0], [1], [0], [player_pos.y]], + [[0], [0], [1], [player_pos.z]], + [[0], [0], [0], [1]]] + ) + # todo + # make this thing + # generates a transformation matrix for the current camera # should only be run once per frame (hopefully a performance improvement?) + return numpy.multiply(translation_matrix, rotation_matrix) -def drawFace(face: Face, rot_matrix: numpy.array, playerPos: Point3D): + +def draw_face(face: Face, cam_matrix: numpy.array, fp_dis): + if type(face) is TexturedFace: + raise TypeError # 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) + # this function returns the face no matter what (face angle checking must be done elsewhere -def drawTexturedFace(face: Face, transform_matrix: numpy.array): + _2dPoints: list[Point2D] = [] + # apply camera position and rotation + for point in face.points: + point.apply_matrix(cam_matrix) + _2dPoints.append(point.get_2d_point(fp_dis)) + return _2dPoints + + +def draw_textured_face(face: Face, transform_matrix: numpy.array): # has face and a camera rotation + position matrix as input, draws the textured face onto the screen + return None + # old classes # class Face: diff --git a/main.py b/main.py index 33f1524..eaf7955 100644 --- a/main.py +++ b/main.py @@ -12,13 +12,17 @@ size = (1000, 1000) # screen with in units scrWidth = 3.2 -playerPos = [0, 10, 0] -playerSpeed = [0, 0.01, 0] +# player information +playerPos: Point3D = Point3D([0, 10, 0]) +playerSpeed: Point3D = Point3D([0, 0, 0]) + +# todo implement these variables into the code acc = 20 # units per second per second maxSpeed = 4 # units per second sensitivity = 0.05 + frameRate = 60 # measures the frame-rate (if this decreases then other things are sped up) # program variables @@ -32,30 +36,26 @@ allFaces: list[Face] = [] # colours -bgColour = (0, 0, 0) -fgColour = (255, 255, 255) -polyColour = [(255, 120, 120), (255, 120, 120), (255, 120, 120), (255, 120, 120), (255, 120, 120), (255, 120, 120)] -hue = 0 +bgColour: Colour = Colour([0, 0, 0]) +fgColour: Colour = Colour([255, 255, 255]) # placing colours is to be replaced with placingTextures placingColours: list[list[Colour]] = [[Colour([100, 100, 100]), Colour([100, 100, 100]), Colour([100, 100, 100]), Colour([100, 100, 100]), Colour([100, 100, 100]), Colour([100, 100, 100])], [Colour([162, 84, 57]), Colour([162, 84, 57]), Colour([162, 84, 57]), Colour([162, 84, 57]), Colour([162, 84, 57]), Colour([162, 84, 57])], [Colour([50, 255, 50]), Colour([100, 255, 100]), Colour([100, 255, 100]), Colour([100, 255, 100]), Colour([100, 255, 100]), Colour([162, 84, 57])]] -print(placingColours) currentTexture: int = 0 # create cubes cubes: list[Cube] = [] for x in [[-12, 3, 4], [2, 2, 2], [2, 0, 0], [0, 2, 0], [0, 0, 0], [6, -8, 4]]: - cubes.append(Cube(x, Colour([255, 0, 0]), True, True)) + cubes.append(Cube(Point3D(x), [Colour([255, 0, 0]) for x in range(6)], True, True)) +# todo update or remove # axes points axesPoints = [[1000, 0, 0], [-1000, 0, 0], [0, 1000, 0], [0, -1000, 0], [0, 0, 1000], [0, 0, -1000]] axesEdges = [[0, 1], [2, 3], [4, 5]] axesColours = [(0, .8, .8), (.2, .8, 1), (.8, .8, .8)] - - i = 0 for c in axesColours: axesColours[i] = colorsys.hsv_to_rgb(c[0], c[1], c[2]) @@ -73,6 +73,9 @@ # fov camFOV = 120 +# matrices +camMatrix: numpy.array = generate_cam_matrix(rotX, rotY, rotZ, playerPos) + FPDis = fov_calc(camFOV, scrWidth) # start @@ -81,11 +84,11 @@ lastMillis[2] = pygame.time.get_ticks() + 1000 timerOffset = 0 screen = pygame.display.set_mode(size) -screen.fill(bgColour) +screen.fill(bgColour.get_rgb()) -# initial values for variables so pycharm doesn't get annoyed +# initial values for variables so pycharm doesn't get mad keys = pygame.key.get_pressed() -newCubePos = [0, 0, 0] +newCubePos: Point3D = Point3D([0, 0, 0]) # settings pygame.mouse.set_visible(True) @@ -95,13 +98,6 @@ # create surfaces mainSurface = pygame.Surface(size) -# grass = [Texture("grass.png") for x in range(6)] -# cubes.append(Cube([0, 2, 3], grass, True, True)) - -for i in range(len(polyColour)): - polyColour[i] = tuple(255 * x for x in colorsys.hsv_to_rgb(hue, .5, 1)) - hue += 1 / 6 - while running: # every tick: if lastMillis[1] + 16.6 <= pygame.time.get_ticks() or triggerDraw: @@ -124,117 +120,106 @@ if not triggerDraw: lastMillis[1] = pygame.time.get_ticks() triggerDraw = False + # reset surface area - mainSurface.fill(fgColour) + mainSurface.fill(fgColour.get_rgb()) # save currently pressed keys keys = pygame.key.get_pressed() + # generate matrices + camMatrix = generate_cam_matrix(rotX, rotY, rotZ, playerPos) + # __draw cubes__ # calculate 3d and project 3d point locations col = 0 for cube in cubes: - if cube.has_texture(): - texturedFaces = cube.get_textured_faces(playerPos) - for texturedFace in texturedFaces: - newSubFaces = [] - try: - faces = texturedFace.get_sub_faces() - for face in faces: - newPoints = fix_points_behind_camera(rotate(face.points, rotX, rotY, rotZ), camDistance) - if len(newPoints) > 2: - face.points = newPoints - newSubFaces.append(face) - except AttributeError: - pass - texturedFace.set_sub_faces(newSubFaces) - allFaces.append(texturedFace) - faces = cube.get_faces(playerPos) - # fix points behind camera and remove faces with two or fewer remaining points - for face in faces: - newPoints = fix_points_behind_camera(rotate(face.points, rotX, rotY, rotZ), camDistance) - if len(newPoints) > 2: - face.points = newPoints - allFaces.append(face) + for face in cube.get_faces(): + allFaces.append(face) + + # if cube.has_texture(): + # texturedFaces = cube.get_textured_faces(playerPos) + # for texturedFace in texturedFaces: + # newSubFaces = [] + # try: + # faces = texturedFace.get_sub_faces() + # for face in faces: + # newPoints = fix_points_behind_camera(rotate(face.points, rotX, rotY, rotZ), camDistance) + # if len(newPoints) > 2: + # face.points = newPoints + # newSubFaces.append(face) + # except AttributeError: + # pass + # texturedFace.set_sub_faces(newSubFaces) + # allFaces.append(texturedFace) + # faces = cube.get_faces(playerPos) + # # fix points behind camera and remove faces with two or fewer remaining points + # for face in faces: + # newPoints = fix_points_behind_camera(rotate(face.points, rotX, rotY, rotZ), camDistance) + # if len(newPoints) > 2: + # face.points = newPoints + # allFaces.append(face) # add faces for placeable cube if placing: # generate the position for the new cube ('ray cast' out of camera and do weird things) - newCubePos = reverse_rotate([[blockDistance, 0, 0]], rotX, rotY, -rotZ + 90) - newCubePos = translate(newCubePos, [-x for x in playerPos]) - newCubePos = [[int(round(x)) for x in newCubePos[0]]] + newCubePos = Point3D(reverse_rotate([[blockDistance, 0, 0]], rotX, rotY, -rotZ + 90)[0]) # todo change this to a .applymatrix + newCubePos.inv_translate(playerPos) + newCubePos.snap() + # generate cube object with position of newCubePos - newCube = Cube(newCubePos[0], placingColours[currentTexture], True, keys[pygame.K_LCTRL]) + newCube = Cube(newCubePos, placingColours[currentTexture], True, keys[pygame.K_LCTRL]) # take faces out of cube object and never speak of it again - faces = newCube.get_faces(playerPos) - # fix points behind camera and remove faces with two or fewer remaining points - for face in faces: - newPoints = fix_points_behind_camera(rotate(face.points, rotX, rotY, rotZ), camDistance) - if len(newPoints) > 2: - face.points = newPoints - allFaces.append(face) - # ______ TESTING ____ - # generate the points for a subdivided 16*16 face - grass = Texture("grass.png") + for face in newCube.get_faces(): + print(face.points[0].xyz) + allFaces.append(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 - - for i, face in enumerate(sub_div_faces): - points = [copy.deepcopy(sub_div_points[point[0]][point[1]]) for point in sub_div_faces[i]] - for point in points: - point.insert(2, 1) - y = i % 16 - x = i // 16 - newPoints = fix_points_behind_camera(rotate(translate(points, playerPos), rotX, rotY, rotZ), camDistance) - if len(newPoints) > 2: - allFaces.append(Face(newPoints, grass.get_pixel(x, y), False, True)) + # # fix points behind camera and remove faces with two or fewer remaining points + # for face in faces: + # newPoints = fix_points_behind_camera(rotate(face.points, rotX, rotY, rotZ), camDistance) + # if len(newPoints) > 2: + # face.points = newPoints + # allFaces.append(face) # ______draw all faces______ + # apply camMatrix to allFaces + for face in allFaces: + for point in face.points: + point.apply_matrix(camMatrix) + + # fix points behind the camera + face.fix_points_behind_camera() + if len(face.points) < 3: + allFaces.remove(face) # sort the list of allFaces # iterates through all faces - iterates through each point axis - iterates through points within faces # (strange order) allFaces_sort = [] for face in allFaces: - average = [0, 0, 0] + average = Point3D([0, 0, 0]) for index in range(3): for point in face.points: - average[index] += point[index] - average[index] = average[index] / 3 + average.xyz()[index] += point.xyz()[index] + average.xyz()[index] = average.xyz()[index] / 3 # 3d pythagoras to find the distance to the camera - allFaces_sort.append(-(average[0] ** 2 + average[1] ** 2 + average[2] ** 2)) + allFaces_sort.append(-(average.x ** 2 + average.y ** 2 + average.z ** 2)) allFaces = [x for _, x in sorted(zip(allFaces_sort, allFaces), key=lambda item: item[0])] # draw allFaces i = 0 for i, face in enumerate(allFaces): - _2dPoints = conv_coord(project(face.points, FPDis, camDistance), size, scale) + _2dPoints = [point.get_2d_point(FPDis) for point in face.points] + [point.conv_coord(size, scale) for point in _2dPoints] # this seems really illegal + try: - if True: # face_angle([face.points[0], face.points[1], face.points[2]], FPDis, camDistance) > 90: + if True: # face_angle([face.points[0], face.points[1], face.points[2]], FPDis, camDistance) > 90: if face.hasFaces: - pygame.draw.polygon(mainSurface, tuple(face.colour.get_rgb()), _2dPoints, 0) + pygame.draw.polygon(mainSurface, tuple(face.colour.get_rgb()), [point.get() for point in _2dPoints], 0) if face.hasEdges: for i2 in range(len(_2dPoints)): - pygame.draw.aaline(mainSurface, (0, 0, 0), _2dPoints[i2], - _2dPoints[(i2 + 1) % len(_2dPoints)]) + pygame.draw.aaline(mainSurface, (0, 0, 0), _2dPoints[i2].get(), + _2dPoints[(i2 + 1) % len(_2dPoints)].get()) except IndexError: print("found face with only 2 points (This error occurring here indicates a bug in the code somewhere)") allFaces.clear() @@ -268,13 +253,12 @@ vectoredDirection = [x * magnitude for x in pressedKeys] rotatedVector = rotate([vectoredDirection], 0, 0, -rotZ)[0] - playerSpeed = [(speed + ((acc / frameRate) * kPress)) * (1 - (((acc-1) / maxSpeed)/frameRate)) - for speed, kPress in zip(playerSpeed, rotatedVector)] + playerSpeed = Point3D([(speed + ((acc / frameRate) * kPress)) * (1 - (((acc-1) / maxSpeed)/frameRate)) + for speed, kPress in zip(playerSpeed.xyz(), rotatedVector)]) # handle speed and movement in that direction - move = [x / frameRate for x in playerSpeed] - for j in range(3): - playerPos[j] += move[j] + move = Point3D([x / frameRate for x in playerSpeed.xyz()]) + playerPos.translate(move) screen.blit(mainSurface, (0, 0)) pygame.display.update() @@ -306,16 +290,17 @@ grabbed = True elif event.button == pygame.BUTTON_RIGHT: if placing: - cubes.append(Cube(newCubePos[0], placingColours[currentTexture], True, keys[pygame.K_LCTRL])) + cubes.append(Cube(newCubePos, placingColours[currentTexture], True, keys[pygame.K_LCTRL])) elif event.button == pygame.BUTTON_LEFT: if placing: try: for x, cube in enumerate(cubes): - if cube.position == newCubePos[0]: + if cube.position == newCubePos: cubes.pop(x) except ValueError: continue + elif event.type == pygame.MOUSEWHEEL: if keys[pygame.K_LCTRL]: currentTexture = (currentTexture + 1) % len(placingColours)