import math
import copy
import colorsys
import random
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):
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
# 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]
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
def __init__(self, xyz : list[float]):
self.x = xyz[0]
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
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)
# new functions
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 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
# this function returns the face no matter what (face angle checking must be done elsewhere
_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:
# 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