1

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)
Matthew Haywood
  • 80
  • 3
  • 13
  • Rotating the camera around... what axis? It can be a "world" axis or a "local" one. I know it's tricky to get used to it. I recomend a good book/tutorial about Computers Graphics or Opengl (a multiplatform graphics API). – Ripi2 Jun 22 '21 at 19:37
  • The global axis, I am also trying to rotate singular objects around a global axis however I am running into the same problem, as I move the camera, the axes which objects are rotated around also changes with the camera axis. How do I project from the camera axis to a global axis? – Matthew Haywood Jun 23 '21 at 13:40
  • I think I understand it now, in order to project to a local objects axes, do i perform translation from camera axes to the objects local axes, then a rotation to the correct orientation of the local axes, perform transformation and then reverse the rotation and the translation? does it matter what order they are in? – Matthew Haywood Jun 23 '21 at 13:51
  • 1
    To rotate an object around its local axis do IN THIS ORDER: 1.Translate object to (0,0,0) 2. Rotate around local 3. Translate back to its point. – Ripi2 Jun 23 '21 at 17:42
  • How do I remove the camera rotation from an object? when I would like to rotate an object locally and the camera is rotated, the rotation plane becomes rotated at the same angle as the camera. – Matthew Haywood Jun 28 '21 at 11:29
  • I have tried yawing the object into the same plane as the rotation plane and then yawing it back however this does not seem to work. – Matthew Haywood Jun 28 '21 at 11:31
  • Do you undersntand concepts like "World-space" or "Camera-space"? Do you understand the matrix operations needed to transform from one space to other? Do you have a graphics book at hand? – Ripi2 Jun 28 '21 at 17:49
  • Yes I sort of understand it now. World space is everything in the real coordinate system. Camera space is everything in relation to the camera coordinates and then local axes is axes in relation to an object in the world space. I have been doing a bit of robotics transformation matrices and I’m finding it’s similar to that. No I don’t have a graphics book at hand could you tell me a good book to look at please? – Matthew Haywood Jun 28 '21 at 18:59
  • I learnt by learning OpenGL. For example [this](https://learnopengl.com/Getting-started/Coordinate-Systems) and [this](https://paroj.github.io/gltut/Positioning/Tut08%20Camera%20Relative%20Orientation.html). The thing is that when you move/rotate the camera coordinates relative to it don't change, so the whole world coordiantes must change (i.e. use some matrix transformation). For example you rotate the camera around the Earth; this is the same as some translation and some rotation done in camera-space. – Ripi2 Jun 28 '21 at 19:15

0 Answers0