I am currently designing my own 3D engine from scratch in Python using Pygame and Numpy (I know Python is not good for this task however good for learning).
I have implemented the various transformation matrices and the 3d objects move as expected. I am now trying to create a bounding box so that only the points in front of the camera are rendered. This means that I will need to know where the camera is in 3d space. I have set up an axis that is at the point (0,0,0) so that I can define my camera position relative to this.
I have been playing around moving the camera etc, but I don't understand why when moving the camera, the x coordinate for example does not move depending on where the camera is pointing. So say the camera is pointing forwards, the x coordinate should change if you move left and right. When you rotate the camera to the left 90 degrees, and then move left and right, the z coordinate of the camera should change and not the x coordinates. However it seems as though no matter what orientation you turn the camera, if you move it left or right, this always changes the x coordinate. I think this is due to the fact that the points are always defined relative to the camera frame. So when you turn, the positions of the points change, even though they don't move in 3d space. How would I use this to render only the points in front of the camera?
Main Class:
from projectionViewer import ProjectionViewer
import wireframe
import numpy as np
from obj_loader import OBJ_loader
import random
from mesh_floor import *
# cube = wireframe.Wireframe()
# cube2 = wireframe.Wireframe()
# cube3 = wireframe.Wireframe()
# cube_nodes = [(x, y, z) for x in (50, 250) for y in (50, 500) for z in (50, 250)]
# cube_nodes2 = [(x, y, z) for x in (250, 400) for y in (250, 500) for z in (250, 5000)]
# #cube_nodes3 = [(x, y, z) for x in (1, 800) for y in (400, 401) for z in (1, 800)]
# print(cube_nodes)
# cube.addNodes(np.array(cube_nodes))
# cube.addEdges([(n, n + 4) for n in range(0, 4)])
# cube.addEdges([(n, n + 1) for n in range(0, 8, 2)])
# cube.addEdges([(n, n + 2) for n in (0, 1, 4, 5)])
# cube2.addNodes(np.array(cube_nodes2))
# cube2.addEdges([(n, n + 4) for n in range(0, 4)])
# cube2.addEdges([(n, n + 1) for n in range(0, 8, 2)])
# cube2.addEdges([(n, n + 2) for n in (0, 1, 4, 5)])
# cube3.addNodes(np.array(cube_nodes3))
# cube3.addEdges([(n, n + 4) for n in range(0, 4)])
# cube3.addEdges([(n, n + 1) for n in range(0, 8, 2)])
# cube3.addEdges([(n, n + 2) for n in (0, 1, 4, 5)])
# loader = OBJ_loader("./teapot.obj")
# Object = loader.create_wireframe()
cube = wireframe.Wireframe()
axes = wireframe.Wireframe()
a = np.array([[0,0,0],[1000,0,0], [0,1000,0], [0,0,1000]])
axes.addNodes(a)
axes.addEdges([(0,1), (0,2), (0,3)])
cube_nodes = grid_generation(10000,10000,False)
print(len(cube_nodes))
a = np.array(cube_nodes)
for i in a:
i[1] = random.randint(1,1000)
cube.addNodes(a)
cube.addEdges([(n, n + 1) for n in range(0, 9, 1)])
cube.addEdges([(n, n + 1) for n in range(10, 19, 1)])
cube.addEdges([(n, n + 1) for n in range(20, 29, 1)])
cube.addEdges([(n, n + 1) for n in range(30, 39, 1)])
cube.addEdges([(n, n + 1) for n in range(40, 49, 1)])
cube.addEdges([(n, n + 1) for n in range(50, 59, 1)])
cube.addEdges([(n, n + 1) for n in range(60, 69, 1)])
cube.addEdges([(n, n + 1) for n in range(70, 79, 1)])
cube.addEdges([(n, n + 1) for n in range(80, 89, 1)])
cube.addEdges([(n, n + 1) for n in range(90, 99, 1)])
for i in range(0,10):
cube.addEdges([(0+i,10+i),(10+i,20+i),(20+i,30+i),(30+i,40+i),(40+i,50+i),(50+i,60+i),(60+i,70+i),(70+i,80+i),(80+i,90+i)])
pv = ProjectionViewer(1200, 1000, axes)
# pv.addWireframe('object', Object)
pv.addWireframe('floormesh', cube)
pv.addWireframe('axes', axes)
# pv.addWireframe('cube', cube)
# pv.addWireframe('cube2', cube2)
# pv.addWireframe('cube3', cube3)
pv.run()
Projection Viewer Class:
from wireframe import *
import pygame
import numpy as np
from camera import *
class ProjectionViewer:
''' Displays 3D Objects on a Pygame Screen '''
def __init__(self, width, height, center_point):
self.width = width
self.height = height
self.screen = pygame.display.set_mode((width, height))
pygame.display.set_caption('Wireframe Display')
self.background = (10,10,50)
#Setup camera
self.camera = Camera([0,0,0],0,0)
self.center_point = center_point
self.wireframes = {}
self.displayNodes = True
self.displayEdges = True
self.nodeColour = (255,255,255)
self.edgeColour = (200,200,200)
self.nodeRadius = 4
def run(self):
key_to_function = {
pygame.K_LEFT: (lambda x: x.rotate_about_camera('Y', 0.05)),
pygame.K_RIGHT:(lambda x: x.rotate_about_camera('Y', -0.05)),
pygame.K_DOWN: (lambda x: x.translateAll([0, 10, 0])),
pygame.K_UP: (lambda x: x.translateAll([0, -10, 0])),
pygame.K_w: (lambda x: x.move_cam_forward(20)),
pygame.K_s: (lambda x: x.move_cam_backward(20)),
pygame.K_a: (lambda x: x.move_cam_left(20)),
pygame.K_d: (lambda x: x.move_cam_right(20)),
pygame.K_EQUALS: (lambda x: x.scale_centre([1.25,1.25,1.25])),
pygame.K_MINUS: (lambda x: x.scale_centre([0.8,0.8,0.8])),
pygame.K_q: (lambda x: x.rotateAll('X', 0.1)),
pygame.K_z: (lambda x: x.rotateAll('Z', 0.1)),
pygame.K_x: (lambda x: x.rotateAll('Z', -0.1)),
}
running = True
flag = False
while running:
keys = pygame.key.get_pressed()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if keys[pygame.K_LEFT]:
key_to_function[pygame.K_LEFT](self)
if keys[pygame.K_RIGHT]:
key_to_function[pygame.K_RIGHT](self)
if keys[pygame.K_DOWN]:
key_to_function[pygame.K_DOWN](self)
if keys[pygame.K_UP]:
key_to_function[pygame.K_UP](self)
if keys[pygame.K_EQUALS]:
key_to_function[pygame.K_EQUALS](self)
if keys[pygame.K_MINUS]:
key_to_function[pygame.K_MINUS](self)
if keys[pygame.K_LEFT]:
key_to_function[pygame.K_LEFT](self)
if keys[pygame.K_q]:
key_to_function[pygame.K_q](self)
if keys[pygame.K_w]:
key_to_function[pygame.K_w](self)
if keys[pygame.K_a]:
key_to_function[pygame.K_a](self)
if keys[pygame.K_s]:
key_to_function[pygame.K_s](self)
if keys[pygame.K_z]:
key_to_function[pygame.K_z](self)
if keys[pygame.K_x]:
key_to_function[pygame.K_x](self)
if keys[pygame.K_p]:
key_to_function[pygame.K_p](self)
if keys[pygame.K_t]:
key_to_function[pygame.K_t](self)
if keys[pygame.K_d]:
key_to_function[pygame.K_d](self)
self.display()
pygame.display.flip()
def addWireframe(self, name, wireframe):
self.wireframes[name] = wireframe
#translate to center
wf = Wireframe()
matrix = wf.translationMatrix(-self.width/2,-self.height/2,0)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
wf = Wireframe()
matrix = wf.translationMatrix(self.width,self.height,0)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
def display(self):
self.screen.fill(self.background)
for wireframe in self.wireframes.values():
wireframe.transform_for_perspective((self.width/2, self.height/2), self.camera.fov, self.camera.zoom)
if self.displayNodes:
for node in wireframe.perspective_nodes:
pygame.draw.circle(self.screen, self.nodeColour, (int(node[0]), int(node[1])), self.nodeRadius, 0)
if self.displayEdges:
for n1, n2 in wireframe.edges:
pygame.draw.aaline(self.screen, self.edgeColour, wireframe.perspective_nodes[n1][:2], wireframe.perspective_nodes[n2][:2], 1)
def translateAll(self, vector):
''' Translate all wireframes along a given axis by d units '''
wf = Wireframe()
matrix = wf.translationMatrix(*vector)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
def scaleAll(self, vector):
wf = Wireframe()
matrix = wf.scaleMatrix(*vector)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
def rotateAll(self, axis, theta):
wf = Wireframe()
if axis == 'X':
matrix = wf.rotateXMatrix(theta)
elif axis == 'Y':
matrix = wf.rotateYMatrix(theta)
elif axis == 'Z':
matrix = wf.rotateZMatrix(theta)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
#wireframe.transform_for_perspective()
def rotate_about_Center(self, Axis, theta):
#First translate Centre of screen to 0,0
wf = Wireframe()
matrix = wf.translationMatrix(-self.width/2,-self.height/2,0)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
#Do Rotation
wf = Wireframe()
if Axis == 'X':
matrix = wf.rotateXMatrix(theta)
elif Axis == 'Y':
matrix = wf.rotateYMatrix(theta)
elif Axis == 'Z':
matrix = wf.rotateZMatrix(theta)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
#Translate back to centre of screen
wf = Wireframe()
matrix = wf.translationMatrix(self.width/2,self.height/2,0)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
def rotate_about_camera(self, Axis, theta):
wf = Wireframe()
matrix = wf.translationMatrix(-self.width/2, -self.height/2,0)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
#Do Rotation
wf = Wireframe()
if Axis == 'X':
matrix = wf.rotateXMatrix(theta)
elif Axis == 'Y':
matrix = wf.rotateYMatrix(theta)
elif Axis == 'Z':
matrix = wf.rotateZMatrix(theta)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
#Translate back to original position
wf = Wireframe()
matrix = wf.translationMatrix(self.width/2,self.height/2,0)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
self.camera.hor_angle += theta
if self.camera.hor_angle >= 2*math.pi:
self.camera.hor_angle -= 2*math.pi
elif self.camera.hor_angle < -2*math.pi:
self.camera.hor_angle += 2*math.pi
self.camera.define_render_space()
print((self.camera.hor_angle/(2*math.pi))*360)
def scale_centre(self, vector):
#Transform center of screen to origin
wf = Wireframe()
matrix = wf.translationMatrix(-self.width/2,-self.height/2,0)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
#Scale the origin by vector
wf = Wireframe()
matrix = wf.scaleMatrix(*vector)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
wf = Wireframe()
matrix = wf.translationMatrix(self.width/2,self.height/2,0)
for wireframe in self.wireframes.values():
wireframe.transform(matrix)
def move_cam_forward(self, amount):
#Moving the camera forward will be a positive translation in the z axis for every other object.
self.camera.set_position(self.center_point)
self.translateAll([0,0,-amount])
print("Camera position: ")
print(self.camera.pos)
def move_cam_backward(self, amount):
self.camera.set_position(self.center_point)
self.translateAll([0,0,amount])
print("Camera position: ")
print(self.camera.pos)
def move_cam_left(self, amount):
self.camera.set_position(self.center_point)
self.translateAll([-amount,0,0])
print("Camera position: ")
print(self.camera.pos)
def move_cam_right(self, amount):
self.camera.set_position(self.center_point)
self.translateAll([amount,0,0])
print("Camera position: ")
print(self.camera.pos)
Wireframe Class:
import math
import numpy as np
# class Node:
# def __init__(self, coordinates):
# self.x = coordinates[0]
# self.y = coordinates[1]
# self.z = coordinates[2]
# class Edge:
# def __init__(self, start, stop):
# self.start = start
# self.stop = stop
class Wireframe:
def __init__(self):
self.nodes = np.zeros((0,4))
self.perspective_nodes = None
self.edges = []
def addNodes(self, node_array):
ones_column = np.ones((len(node_array), 1))
ones_added = np.hstack((node_array, ones_column))
self.nodes = np.vstack((self.nodes, ones_added))
def addEdges(self, edgeList):
self.edges += edgeList
def outputNodes(self):
print("\n --- Nodes ---")
for i, (x, y, z, _) in enumerate(self.nodes):
print(" %d: (%.2f, %.2f, %.2f)" % (i, node.x, node.y, node.z))
def outputEdges(self):
print("\n --- Edges ---")
for i, (node1, node2) in enumerate(self.edges):
print(" %d: %d -> %d" % (i, node1, node2))
def translate(self, axis, d):
if axis in ['x', 'y', 'z']:
for node in self.nodes:
setattr(node, axis, getattr(node, axis) + d)
def scale(self, centre_x, centre_y, scale):
for node in self.nodes:
node.x = centre_x + scale * (node.x - centre_x)
node.y = centre_y + scale * (node.y - centre_y)
node.z *= scale
def findCentre(self):
num_nodes = len(self.nodes)
meanX = sum([node.x for node in self.nodes]) / num_nodes
meanY = sum([node.y for node in self.nodes]) / num_nodes
meanZ = sum([node.z for node in self.nodes]) / num_nodes
return (meanX, meanY, meanZ)
def rotateZ(self, centre, radians):
cx, cy, cz = centre
for node in self.nodes:
x = node.x - cx
y = node.y - cy
d = math.hypot(y,x)
theta = math.atan2(y,x) + radians
node.x = cx + d * math.cos(theta)
node.y = cy + d * math.sin(theta)
def rotateX(self, centre, radians):
cx, cy, cz = centre
for node in self.nodes:
y = node.y - cy
z = node.z - cz
d = math.hypot(y,z)
theta = math.atan2(y, z) + radians
node.z = cz + d * math.cos(theta)
node.y = cy + d * math.sin(theta)
def rotateY(self, centre, radians):
cx, cy, cz = centre
for node in self.nodes:
x = node.x - cx
z = node.z - cz
d = math.hypot(x, z)
theta = math.atan2(x, z) + radians
node.z = cz + d * math.cos(theta)
node.x = cx + d * math.sin(theta)
def transform(self, matrix):
self.nodes = np.dot(self.nodes, matrix)
def transform_for_perspective(self, center, fov, zoom):
self.perspective_nodes = self.nodes.copy()
for i in range(len(self.nodes)):
node = self.nodes[i]
p_node = self.perspective_nodes[i]
# print(node[0], node[1], node[2])
if node[2] != 0:
p_node[0] = center[0] + (node[0]-center[0])*fov/(zoom-(node[2]))
p_node[1] = center[1] + (node[1]-center[1])*fov/(zoom-(node[2]))
p_node[2] = node[2] * 1
def translationMatrix(self, dx=0, dy=0, dz=0):
return np.array([[1,0,0,0],
[0,1,0,0],
[0,0,1,0],
[dx,dy,dz,1]])
def scaleMatrix(self, sx=0, sy=0, sz=0):
return np.array([[sx, 0, 0, 0],
[0, sy, 0, 0],
[0, 0, sz, 0],
[0, 0, 0, 1]])
def rotateXMatrix(self, radians):
c = np.cos(radians)
s = np.sin(radians)
return np.array([[1,0,0,0],
[0,c,-s,0],
[0,s,c,0],
[0,0,0,1]])
def rotateYMatrix(self, radians):
c = np.cos(radians)
s = np.sin(radians)
return np.array([[c,0,s,0],
[0,1,0,0],
[-s,0,c,0],
[0,0,0,1]])
def rotateZMatrix(self, radians):
c = np.cos(radians)
s = np.sin(radians)
return np.array([[c,-s, 0, 0],
[s,c,0,0],
[0,0,1,0],
[0,0,0,1]])
Camera Class:
import math
'''
Camera class
position
FOV
zoom
'''
class Camera:
def __init__(self, pos, hor_angle, ver_angle, fov=250, zoom=500):
self.pos = pos
self.fov = fov
self.hor_angle = hor_angle
self.ver_angle = ver_angle
self.zoom = self.pos[2]
def set_position(self, center_point):
print(center_point.nodes[0])
self.pos[0] = -center_point.nodes[0][0]
self.pos[1] = -center_point.nodes[0][1]
self.pos[2] = -center_point.nodes[0][2]
def define_render_space(self):
#So we want to only render objects that are within a bounding box infront of the camera
back_limit = 100 #The distance to the back limit from the camera
back_limit_length = 100
front_limit = 20 #The distance to the front limit from the camera
adding_x_val = back_limit*math.sin(self.hor_angle)
adding_z_val = back_limit*math.cos(self.hor_angle)
back_straight_x = self.pos[0] + adding_x_val
back_straight_z = self.pos[2] + adding_z_val
print("back_straight_x")
print(back_straight_x)
print("back_straight_z")
print(back_straight_z)