Search code examples
python3dpygameprojectionperspective

How to improve my simulation of 3d space in pygame?


As for my previous question, I'm trying to simulate a 3d space in pygame. So far, I came up with a very simple idea that uses the third coordenate as a denominator to 'compress' (pretty sure there's some terminology here I'm not aware of) the furthest points around the center of the screen and reduce their sizes.

Could anyone suggest a simple improvement to this idea? I feel like I can just tune that denominator used for the projection (see the code) to create a way more accurate simulation.

If you run the code bellow you'll have a not-that-bad simulation of (let's say) a spaceship passing by some stars (pressing w or s). They dissapear if they get to far and a new one is created after that. But, if I apply a rotation (a or d), it becomes obvious that the simulation is not doing well, as I'm not really projecting the 3d points onto the 2d screen.

import pygame
import random
import numpy as np

pygame.init()
run=True

#screensize
screensize = (width,height)=(600,600)
center=(int(width/2),int(height/2))
screen = pygame.display.set_mode(screensize)

#delta mov
ds=0.1
do=0.0001

#Stars
points=[]
for i in range(1000):
    n1 = random.randrange(-5000,5000)
    n2 = random.randrange(-5000,5000)
    n3 = random.randrange(-30,30)
    points.append([n1,n2,n3])

while run:
    pygame.time.delay(20)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run=False

    ################## keys
    keys=pygame.key.get_pressed()

    if keys[pygame.K_w]:
        for p in points:
            p[2]-=ds
    if keys[pygame.K_s]:
        for p in points:
            p[2]+=ds

    if keys[pygame.K_a] or keys[pygame.K_d]:
        if keys[pygame.K_a]:
            for p in points:
                p[0]=np.cos(-do)*p[0]-np.sin(-do)*p[2]
                p[2]=np.sin(-do)*p[0]+np.cos(-do)*p[2]
        else:
            for p in points:
                p[0]=np.cos(do)*p[0]-np.sin(do)*p[2]
                p[2]=np.sin(do)*p[0]+np.cos(do)*p[2]


    ###############################projection###################

    for p in points:
        #this is to create new stars
        if p[2]<=-30 or p[2]>=30:
            p[0] = random.randrange(-5000,5000)
            p[1] = random.randrange(-5000,5000)
            p[2] =30
        else:
            #this is to ignore stars which are behind the ship
            if p[2]<=0:
                pass
            else:
                try:
                    #THIS IS THE PROJECTION I USE, I TAKE THE RADIUS BECAUSE I GUESS I'LL NEED IT... BUT I DON'T USE IT XD
                    r = ((p[0]**2+p[1]**2+p[2]**2)**(1/2))
                    pygame.draw.circle(screen,(255,255,0),(int(p[0]/p[2]+center[0]),int(p[1]/p[2]+center[1])),int(10/p[2]))
                #this is to prevent division by cero and alike
                except Exception as e:
                    pass

    pygame.display.update()
    screen.fill((0,0,0))


pygame.quit()

Solution

  • In general perspective is achieved by Homogeneous coordinates. Your approach is close to that.

    I recommend to operate Cartesian coordinates, where the 3 dimensions have the same scale.
    Emulate a Perspective projection when you draw the points.
    This means you've to calculate the w component of the Homogeneous coordinates dependent on the depth (z coordiante) of the point (e.g. w = p[2] * 30 / 5000) and to perform a "perspective divide" of the x, y and z components by the w component, before you draw the points. e.g:

    #delta mov
    ds=10
    do=0.01
    
    #Stars
    points=[]
    for i in range(1000):
        n1 = random.randrange(-5000,5000)
        n2 = random.randrange(-5000,5000)
        n3 = random.randrange(-5000,5000)
        points.append([n1,n2,n3])
    
    while run:
        pygame.time.delay(20)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run=False
    
        ################## keys
        keys=pygame.key.get_pressed()
    
        if keys[pygame.K_w]:
            for p in points:
                p[2]-=ds
        if keys[pygame.K_s]:
            for p in points:
                p[2]+=ds
    
        if keys[pygame.K_a] or keys[pygame.K_d]:
            if keys[pygame.K_a]:
                for p in points:
                    p[0], p[2] = np.cos(-do)*p[0]-np.sin(-do)*p[2], np.sin(-do)*p[0]+np.cos(-do)*p[2]
            else:
                for p in points:
                    p[0], p[2] = np.cos(do)*p[0]-np.sin(do)*p[2], np.sin(do)*p[0]+np.cos(do)*p[2]
    
        ###############################projection###################
    
        screen.fill((0,0,0))
        for p in points:
            #this is to create new stars
            if p[2]<=-5000 or p[2]>=5000:
                p[0], p[1], p[2] = random.randrange(-5000,5000), random.randrange(-5000,5000), 5000
            else:
                #this is to ignore stars which are behind the ship
                if p[2]<=0:
                    pass
                else:
                    w = p[2] * 30 / 5000
                    pygame.draw.circle(screen,(255,255,0),(int(p[0]/w+center[0]),int(p[1]/w+center[1])),int(10/w))
    
        pygame.display.update()
    

    Furthermore, the rotation is not correct. When you do

    p[0]=np.cos(-do)*p[0]-np.sin(-do)*p[2]
    p[2]=np.sin(-do)*p[0]+np.cos(-do)*p[2]
    

    the p[0] is changed in the first line, but the original value should be used in the 2nd line.
    Do "tuple" assignment to solve the issue:

    p[0], p[2] = np.cos(-do)*p[0]-np.sin(-do)*p[2], np.sin(-do)*p[0]+np.cos(-do)*p[2]