1

I was recently inspired to make a simple 3D engine in python, but upon doing a little research I concluded it was too advanced for my skill level (I'm self taught and pretty new to python), but I decided to try it anyway. After a couple of days I was able to make what looks to me like an accurate 3D engine using only trig, no matrices or vectors.

My method is to extend a plane from the camera down the y-axis, and the plane will grow the further away it is from the origin. If the plane intersects a point, the coordinates of said point will be rescaled to the size of the screen I'm projecting the point onto. Rather than changing the direction that the plane extends I rotate the points around the camera to make it seem as if the camera is looking in a different direction. I do a similar thing for moving the points, rather than moving the camera I move the points around the camera.

Is this an accurate representation of 3D space, or is do you need vectors and matrices to make that?

Here's my code:

import pygame
import math

pygame.init()
screen = pygame.display.set_mode((2000,1300))
pygame.display.set_caption("3D Engine")

cube = [[0, 200, 0], [0, 250, 0], [50, 250, 0], [50, 200, 0], [50, 250, 50], [50, 200, 50], [0, 250, 50], [0, 200, 50]]
camera_pos = [0, 0, 0]
camera_angle = [00, 0]
clock = pygame.time.Clock()
esc = False
render_list = []

def get_angle_flat(p):
    a = 69
    if p[0] > 0:
        if p[1] > 0:
            quad = 1
        elif p[1] < 0:
            quad = 4
        else:
            a = 0
            
    elif p[0] < 0:
        if p[1] > 0:
            quad = 2
        elif p[1] < 0:
            quad = 3
        else:
            a = math.pi
    else:
        if p[1] > 0:
            a = math.pi/2
        elif p[1] < 0:
            a = -math.pi/2
        else:
            a = "null"
    
    if a != 0 and a != math.pi and a != math.pi/2 and a != -math.pi/2:
        if quad == 1:
            a = math.atan(p[1]/p[0])
        elif quad == 2:
            a = math.pi+math.atan(p[1]/p[0])
        elif quad == 3:
            a = -(math.pi-math.atan(p[1]/p[0]))
        elif quad == 4:
            a = math.atan(p[1]/p[0])
    return a

def get_angle_side(p):
    a = 2
    if p[1] > 0:
        if p[2] > 0:
            quad = 1
        elif p[2] < 0:
            quad = 4
        else:
            a = 0
            
    elif p[1] < 0:
        if p[2] > 0:
            quad = 2
        elif p[2] < 0:
            quad = 3
        else:
            a = math.pi
    else:
        if p[2] > 0:
            a = math.pi/2
        elif p[2] < 0:
            a = -math.pi/2
        else:
            a = "null"
    
    if a != 0 and a != math.pi and a != math.pi/2 and a != -math.pi/2:
        if quad == 1:
            a = math.atan(p[2]/p[1])
        elif quad == 2:
            a = math.pi+math.atan(p[2]/p[1])
        elif quad == 3:
            a = -(math.pi-math.atan(p[2]/p[1]))
        elif quad == 4:
            a = math.atan(p[2]/p[1])
    return a


def to_degrees(a):
    return a*180/math.pi

def distance(p):
    dis = math.sqrt(pow(p[0], 2) + pow(p[1], 2))
    return dis

def distance_2(p):
    dis = math.sqrt(pow(p[1], 2) + pow(p[2], 2))
    return dis

def to_radians(a):
    return a/180*math.pi

while True:
    render_list = []
    if not esc:
        pygame.mouse.set_pos((1000, 650))
        pygame.mouse.set_visible(False)
    else:
        pygame.mouse.set_visible(True)
    screen.fill((0, 0, 0))
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            break
    pressed = pygame.key.get_pressed()

    if pressed[pygame.K_w]:
        for item in cube:
            item[1] -= 1
    if pressed[pygame.K_a]:
        for item in cube:
            item[0] += 1
    if pressed[pygame.K_s]:
        for item in cube:
            item[1] += 1
    if pressed[pygame.K_d]:
        for item in cube:
            item[0] -= 1
    if pressed[pygame.K_SPACE]:
        for item in cube:
            item[2] += 1
    if pressed[pygame.K_LSHIFT]:
        for item in cube:
            item[2] -= 1

    if pressed[pygame.K_ESCAPE]:
        esc = not esc
    mouse_pos = pygame.mouse.get_pos()
    mouse_pos = list(mouse_pos)
    mouse_pos[0] = 1000 - mouse_pos[0]
    mouse_pos[1] = 650 - mouse_pos[1]

    print(math.cos(to_radians(camera_angle[0])))

    past_c_a = camera_angle.copy()
    if not esc:
        camera_angle[0] += mouse_pos[0]/3
        camera_angle[1] -= mouse_pos[1]/3

    if camera_angle[0] > 360:
        camera_angle[0] = 0
    elif camera_angle[0] < 0:
        camera_angle[0] = 360
    if camera_angle[1] > 360:
        camera_angle[1] = 0
    elif camera_angle[1] < 0:
        camera_angle[1] = 360
    if camera_angle != past_c_a:
        for i in range(len(cube)):
            dist = distance(cube[i])
            cube[i] = [math.cos(to_radians(to_degrees(get_angle_flat(cube[i])) + (past_c_a[0]-camera_angle[0])))*dist, math.sin(to_radians(to_degrees(get_angle_flat(cube[i])) + (past_c_a[0]-camera_angle[0])))*dist, cube[i][-1]]
            dist2 = distance_2(cube[i])
            cube[i] = [cube[i][0], math.cos(to_radians(to_degrees(get_angle_side(cube[i])) + (past_c_a[1]-camera_angle[1])))*dist2, math.sin(to_radians(to_degrees(get_angle_side(cube[i])) + (past_c_a[1]-camera_angle[1])))*dist2]

    for i in range(len(cube)):
        if cube[i][1] > 0:
            size = [cube[i][1]*5/2, cube[i][1]*3.25/2]
            render_list.append([cube[i][0]*(2000/size[0])+1000, cube[i][2]*(1300/size[1])+650])
        else:
            render_list.append("null")

    if "null" not in render_list:
        pygame.draw.line(screen, (255, 255, 255), render_list[0], render_list[1], 2)
        pygame.draw.line(screen, (255, 255, 255), render_list[0], render_list[3], 2)
        pygame.draw.line(screen, (255, 255, 255), render_list[0], render_list[-1], 2)

        pygame.draw.line(screen, (255, 255, 255), render_list[1], render_list[-2], 2)
        pygame.draw.line(screen, (255, 255, 255), render_list[1], render_list[2], 2)

        pygame.draw.line(screen, (255, 255, 255), render_list[2], render_list[4], 2)

        pygame.draw.line(screen, (255, 255, 255), render_list[3], render_list[-3], 2)

        pygame.draw.line(screen, (255, 255, 255), render_list[4], render_list[-2], 2)
        pygame.draw.line(screen, (255, 255, 255), render_list[4], render_list[-3], 2)

        pygame.draw.line(screen, (255, 255, 255), render_list[7], render_list[-2], 2)
        pygame.draw.line(screen, (255, 255, 255), render_list[7], render_list[-3], 2)

        pygame.draw.line(screen, (255, 255, 255), render_list[3], render_list[2], 2)

    pygame.display.update()
    clock.tick(120)

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • 4
    The advantage of vector and matrix arithmetic is that one does not have to calculate trigonometric functions (`atan`) for every point. These functions take a lot of time and make your engine incredibly slow. This effect is not noticeable for a few points , but will increase greatly when you have many points. Maybe your calculations are "accurate" but they are completely impractical. Do not try to reinvent the wheel. – Rabbid76 Aug 08 '22 at 14:27
  • I would use the `math.radians`, `math.degrees` and `math.hypoth` functions for distance calculation and angle converting. – The_spider Aug 08 '22 at 19:07
  • @The_spider No. In pygame prefer to use [`pygame.math`](https://www.pygame.org/docs/ref/math.html). For the vector and matrix arithmetic you can also use [PyGLM](https://pypi.org/project/PyGLM/), [Pyrr](https://pyrr.readthedocs.io/en/latest/) or [NumPy](https://numpy.org/). – Rabbid76 Aug 08 '22 at 19:13
  • And to add to @Rabbid76's answer: Using matrices, it's also possible to combine multiple operations into a single matrix. So applying the matrix *once* to a point might project, translate & rotate it. Despite the number of operations in say a matrix multiplication, overall it's much more efficient. But kudos for what you have achieved. – Kingsley Aug 08 '22 at 23:48
  • @Rabbid76 I just wanted to say that it is inefficient and unnecessary to make your own functions `to_degrees` and `to_radians` when they are already implemented in the math library. – The_spider Aug 09 '22 at 18:00

0 Answers0