Newer
Older
3d_renderer / model.py
@cory cory on 20 Mar 2022 6 KB factor out classes
import colorsys
import copy
from typing import NamedTuple

import imageio
import numpy


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 WindowSize(NamedTuple):
    height: int
    width: int


class Point2D(NamedTuple):
    x: float
    y: float

    def get(self):
        return [self.x, self.y]

    def conv_coord(self, size, scale):
        return Point2D((self.x * scale) + size[0]/2, (self.y * scale) + size[1]/2)


class Point3D(NamedTuple):
    # todo:
    # check if i have missed anything obvious
    x: float
    y: float
    z: float

    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

        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 (replacement for the projection function)
        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:
    # this class *should* be finished
    # 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
    # todo:
    # make a cleaner way of extracting values

    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
        if type(colour) is list:
            raise ZeroDivisionError
        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

    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, 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):
        self.faces = []
        self.position = Point3D([0, 0, 0])
        # 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):
            new_points = 0
            self.faces.append(Face([point.translate(self.position) for point in copy.deepcopy(self.cubeFaces[i])],
                                   self.colours[i], self.has_edges, self.has_faces))
        print(self.position.xyz())

    def get_faces(self):
        return copy.deepcopy(self.faces)