Newer
Older
3d_renderer / main.py
# TODO
# make todo list

# import
import colorsys
import copy
import pygame
from functions import *
import multiprocessing as mp

# variables
# screen size in pixels
from model import Colour, Point3D, Face, Cube

size = (1000, 1000)
# screen with in units
scrWidth = 1

# player information
playerPos: Point3D = Point3D(0, 0, 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
running = True
triggerDraw = True
placing = True
blockDistance = 3

lastMillis = [0, 0, 0]
scale = size[1] / scrWidth
allFaces: list[Face] = []

# colours
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])]]

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(Point3D(x[0], x[1], x[2]), [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])
    axesColours[i] = tuple(255 * x for x in axesColours[i])
    i += 1

# rendering variables
# rotation
rotX = 90
rotY = 0
rotZ = 0

# distance
camDistance = 0
# fov
camFOV = 120
FPDis = fov_calc(camFOV, scrWidth)
print(FPDis)
# matrices
camMatrix: numpy.array = generate_cam_matrix(rotX, rotY, rotZ, playerPos)

map_matrix = numpy.array([[1, 0, 0 / FPDis],
                          [0, 1, 0 / FPDis],
                          [0, 0, 1 / FPDis]])
# start
pygame.init()
lastMillis[1] = pygame.time.get_ticks() + 1000
lastMillis[2] = pygame.time.get_ticks() + 1000
timerOffset = 0
screen = pygame.display.set_mode(size)
screen.fill(bgColour.get_rgb())

# initial values for variables so pycharm doesn't get mad
keys = pygame.key.get_pressed()
newCube = Cube(Point3D(0, 0, 0), placingColours[0], False, False)

# settings
pygame.mouse.set_visible(True)
grabbed = False
pygame.event.set_grab(False)

# create surfaces
mainSurface = pygame.Surface(size)

while running:
    # every tick:
    if lastMillis[1] + (16.6 * 2) <= pygame.time.get_ticks() or triggerDraw:
        frameRate = 1 / ((pygame.time.get_ticks() - lastMillis[1]) / 1000)
        # change colours and apply rotation from mouse
        if grabbed:
            # rotate camera if mouse moves
            dZ, dX = pygame.mouse.get_rel()
            rotX -= dX * sensitivity
            rotZ += dZ * sensitivity

            # fix camera within movement bounds
            rotZ = rotZ % 360
            if rotX > 180:
                rotX = 180
            elif rotX < 0:
                rotX = 0

        if not triggerDraw:
            lastMillis[1] = pygame.time.get_ticks()
        triggerDraw = False

        # reset surface area
        mainSurface.fill(fgColour.get_rgb())

        # save currently pressed keys
        keys = pygame.key.get_pressed()

        # generate matrices
        camMatrix = generate_cam_matrix(rotX, rotY, rotZ, playerPos)
        invRotMatrix = gen_inv_rot_matrix(rotX, rotY, rotZ)

        # __draw cubes__
        # calculate 3d and project 3d point locations
        for cube in cubes:
            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:
            # todo fix invRotMatrix and add translation
            # todo figure out why the snapping seems to apply to individual points instead of the entire cube
            # generate the position for the new cube ('ray cast' out of the camera, rotate the vector and then snap
            # that position to the nearest integer.
            newCubePos = Point3D(blockDistance, 0, 0).apply_matrix(invRotMatrix)
            newCubePos = newCubePos.inv_translate(playerPos)
            newCubePos = newCubePos.snap()

            # generate new cube object with position of newCubePos
            newCube = Cube(newCubePos, placingColours[currentTexture], keys[pygame.K_LCTRL], True)
            # take faces out of cube object and never speak of it again (woo efficiency!!)
            for face in newCube.get_faces():
                allFaces.append(face)

        # attempt at multiprocessing (failed)
        # def apply_cam_matrix(values): #allFaces, start, step):
        #     new_faces = []
        #     all_faces = values[0]
        #     start = values[1]
        #     step = values[2]
        #     # todo fix this
        #     for face_i in range(start, len(all_faces), step):
        #         new_face = all_faces[face_i]
        #         new_points = []
        #         for point_i in range(len(all_faces[face_i].points)):
        #             # apply the camera matrix to the points
        #             new_points.append(all_faces[face_i].points[point_i].apply_matrix(camMatrix))
        #         # fix points behind the camera
        #         new_face.points = new_points
        #         new_face.fix_points_behind_camera()
        #         if len(new_face.points) > 2:
        #             new_faces.append(new_face)
        #     return new_faces
        #
        # # ______draw all faces______
        # # apply camMatrix to allFaces
        # with mp.Pool(8) as p:
        #     newFacesList = p.map(apply_cam_matrix, [(allFaces, x, 8) for x in range(8)])
        # allFaces = []
        # for faces in newFacesList:
        #     allFaces = allFaces + faces

        for face in allFaces:
            for i, point in enumerate(face.points):
                # apply the camera matrix to the points
                face.points[i] = face.points[i].apply_matrix(camMatrix)
            # fix points behind the camera
            face.fix_points_behind_camera(FPDis)

        # 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]
            for index in range(3):
                for point in face.points:
                    average[index] += point.xyz()[index]
                average[index] = average[index] / 3

            # 3d pythagoras to find the distance to the camera
            allFaces_sort.append(-(average[0] ** 2 + average[1] ** 2 + average[2] ** 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 = [point.get_2d_point(map_matrix).conv_coord(size, scale) for point in face.points]

            try:
                if True:  # face_angle([face.points[0], face.points[1], face.points[2]], FPDis, camDistance) > 90:
                    if face.hasFaces and len(face.points) > 2:
                        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].get(),
                                               _2dPoints[(i2 + 1) % len(_2dPoints)].get())
            except IndexError:
                pass
        allFaces.clear()
        # _____end face drawing_____
        # player movement
        # speed increase with key press
        if keys[pygame.K_LCTRL] == 1:
            sprint = 5
        else:
            sprint = 1

        # calculate the key presses, figure out a force direction, then rotate it, then apply a magnitude,
        # then apply that to the player speed, subtract the drag (value proportional to the speed) from the vector.
        pressedKeys = [(keys[pygame.K_a] - keys[pygame.K_d]),
                       (keys[pygame.K_w] - keys[pygame.K_s]),
                       (keys[pygame.K_SPACE] - keys[pygame.K_LSHIFT])]
        try:
            magnitude = 1 / math.sqrt(pressedKeys[0] ** 2 + pressedKeys[1] ** 2 + pressedKeys[2] ** 2)
        except ZeroDivisionError:
            magnitude = 0

        vectoredDirection = [x * magnitude for x in pressedKeys]
        rotatedVector = rotate(vectoredDirection, 0, 0, rotZ)

        playerSpeed = Point3D._make(
            [(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 = Point3D._make([x / frameRate for x in playerSpeed.xyz()])
        playerPos = playerPos.translate(move)

        # handle Q and E rotation
        #rotY += keys[pygame.K_q] - keys[pygame.K_e]

        screen.blit(mainSurface, (0, 0))
        pygame.display.update()
    # END EVERY FRAME

    # handle events
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
            pygame.display.quit()
        # keyboard
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_BACKSPACE:
                running = False
            elif event.key == pygame.K_ESCAPE:
                grabbed = False
                pygame.event.set_grab(False)
                pygame.mouse.set_visible(True)
            elif event.key == pygame.K_0:
                placing = False
            elif event.key == pygame.K_1:
                placing = True

        # mouse
        elif event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == pygame.BUTTON_MIDDLE:
                pygame.mouse.get_rel()
                pygame.event.set_grab(True)
                pygame.mouse.set_visible(False)
                grabbed = True
            elif event.button == pygame.BUTTON_RIGHT:
                if placing:
                    # check if there is an existing cube in the location. If not,
                    # append the new cube and set faces and edges to visible.
                    occupied = False
                    for x, cube in enumerate(cubes):
                        if cube.position == newCube.position:
                            occupied = True
                    if not occupied:
                        cubes.append(copy.deepcopy(newCube).set_faces(True))
                        cubes.append(Cube(newCube.position, newCube.colours, True, True))

            elif event.button == pygame.BUTTON_LEFT:
                if placing:
                    try:
                        for x, cube in enumerate(cubes):
                            if cube.position == newCube.position:
                                cubes.pop(x)
                    except ValueError:
                        continue

        elif event.type == pygame.MOUSEWHEEL:
            if keys[pygame.K_LCTRL]:
                currentTexture = (currentTexture + 1) % len(placingColours)
            else:
                blockDistance += event.y