Reputation: 250
How can I create and render a 3D shape using pygame and without using any other modules. I want to create my own simple 3D engine. I can draw a 3D box just don't know how to adjust the lengths and position of the lines to give the 3D effect when rotating the box.
I struggle to understand the physics
in shadowing, depth-perception and lighting when rotating an object
Say I have a box:
class box1():
x=100
y=100
z=100
size = 150 #length for distance between each point
point1 = 0,0,0 # top left front
point2 = 0,0,0 # top right front
point3 = 0,0,0 # bottom left front
point4 = 0,0,0 # bottom right front
point5 = 0,0,0 # top left back
point6 = 0,0,0 # top right back
point7 = 0,0,0 # bottom left back
point8 = 0,0,0 # bottom right back
def set_points():
x=box1.x
y=box1.y
z=box1.z
size = box1.size
#this part sets all the points x,y,x co-cords at the correct locations
# _____ 4____6
# |\____\ 1____2
# | | | Middle [x,y,z]
# |_| ` | 7____8
# \|____| 3____4
#
# the +50 is just a test to show the 'offset' of the behind points
box1.point1 = [x-(size/2),y-(size/2),z-(size/2)] # top left front
box1.point2 = [x+(size/2),y-(size/2),z-(size/2)] # top right front
box1.point3 = [x-(size/2),y+(size/2),z-(size/2)] # bottom left front
box1.point4 = [x+(size/2),y+(size/2),z-(size/2)] # bottom right front
box1.point5 = [x-(size/2)+50,y-(size/2)+50,z+(size/2)] # top left back
box1.point6 = [x+(size/2)+50,y-(size/2)+50,z+(size/2)] # top right back
box1.point7 = [x-(size/2)+50,y+(size/2)+50,z+(size/2)] # bottom left back
box1.point8 = [x+(size/2)+50,y+(size/2)+50,z+(size/2)] # bottom right back
camara_pos = [20,20,20] # I don't know how to make the points based off this
camara_angle = [45,0,0] # or this
while True:
set_points()
g.DISPLAYSURF.fill((0,0,0))
for event in pygame.event.get():
if event.type == QUIT:
exit()
#draws all the lines connecting all the points .
pygame.draw.line(g.DISPLAYSURF, (128,128,128), (box1.point1[0],box1.point1[1]),(box1.point2[0],box1.point2[1]))
pygame.draw.line(g.DISPLAYSURF, (128,128,128), (box1.point3[0],box1.point3[1]),(box1.point4[0],box1.point4[1]))
pygame.draw.line(g.DISPLAYSURF, (128,128,128), (box1.point2[0],box1.point2[1]),(box1.point4[0],box1.point4[1]))
pygame.draw.line(g.DISPLAYSURF, (128,128,128), (box1.point1[0],box1.point1[1]),(box1.point3[0],box1.point3[1]))
pygame.draw.line(g.DISPLAYSURF, (128,128,128), (box1.point5[0],box1.point5[1]),(box1.point6[0],box1.point6[1]))
pygame.draw.line(g.DISPLAYSURF, (128,128,128), (box1.point7[0],box1.point7[1]),(box1.point8[0],box1.point8[1]))
pygame.draw.line(g.DISPLAYSURF, (128,128,128), (box1.point6[0],box1.point6[1]),(box1.point8[0],box1.point8[1]))
pygame.draw.line(g.DISPLAYSURF, (128,128,128), (box1.point5[0],box1.point5[1]),(box1.point7[0],box1.point7[1]))
pygame.draw.line(g.DISPLAYSURF, (128,128,128), (box1.point1[0],box1.point1[1]),(box1.point5[0],box1.point5[1]))
pygame.draw.line(g.DISPLAYSURF, (128,128,128), (box1.point2[0],box1.point2[1]),(box1.point6[0],box1.point6[1]))
pygame.draw.line(g.DISPLAYSURF, (128,128,128), (box1.point3[0],box1.point3[1]),(box1.point7[0],box1.point7[1]))
pygame.draw.line(g.DISPLAYSURF, (128,128,128), (box1.point4[0],box1.point4[1]),(box1.point8[0],box1.point8[1]))
pygame.display.update()
Could anyone please explain the theory? Could anyone please show me some code to do the math for the points?
Upvotes: 2
Views: 14106
Reputation: 679
The only magic you need to know is called Rotation Matrices.
If you perform multiplication between such a matrix, and a vector, you get that vector rotated.
Armed with this information (i.e. after copying wikipedia's 3D Rotation Matrices), I ended up with this nice thing:
import pygame
from numpy import array
from math import cos, sin
######################
# #
# math section #
# #
######################
X, Y, Z = 0, 1, 2
def rotation_matrix(α, β, γ):
"""
rotation matrix of α, β, γ radians around x, y, z axes (respectively)
"""
sα, cα = sin(α), cos(α)
sβ, cβ = sin(β), cos(β)
sγ, cγ = sin(γ), cos(γ)
return (
(cβ*cγ, -cβ*sγ, sβ),
(cα*sγ + sα*sβ*cγ, cα*cγ - sγ*sα*sβ, -cβ*sα),
(sγ*sα - cα*sβ*cγ, cα*sγ*sβ + sα*cγ, cα*cβ)
)
class Physical:
def __init__(self, vertices, edges):
"""
a 3D object that can rotate around the three axes
:param vertices: a tuple of points (each has 3 coordinates)
:param edges: a tuple of pairs (each pair is a set containing 2 vertices' indexes)
"""
self.__vertices = array(vertices)
self.__edges = tuple(edges)
self.__rotation = [0, 0, 0] # radians around each axis
def rotate(self, axis, θ):
self.__rotation[axis] += θ
@property
def lines(self):
location = self.__vertices.dot(rotation_matrix(*self.__rotation)) # an index->location mapping
return ((location[v1], location[v2]) for v1, v2 in self.__edges)
######################
# #
# gui section #
# #
######################
BLACK, RED = (0, 0, 0), (255, 128, 128)
class Paint:
def __init__(self, shape, keys_handler):
self.__shape = shape
self.__keys_handler = keys_handler
self.__size = 450, 450
self.__clock = pygame.time.Clock()
self.__screen = pygame.display.set_mode(self.__size)
self.__mainloop()
def __fit(self, vec):
"""
ignore the z-element (creating a very cheap projection), and scale x, y to the coordinates of the screen
"""
# notice that len(self.__size) is 2, hence zip(vec, self.__size) ignores the vector's last coordinate
return [round(70 * coordinate + frame / 2) for coordinate, frame in zip(vec, self.__size)]
def __handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
exit()
self.__keys_handler(pygame.key.get_pressed())
def __draw_shape(self, thickness=4):
for start, end in self.__shape.lines:
pygame.draw.line(self.__screen, RED, self.__fit(start), self.__fit(end), thickness)
def __mainloop(self):
while True:
self.__handle_events()
self.__screen.fill(BLACK)
self.__draw_shape()
pygame.display.flip()
self.__clock.tick(40)
######################
# #
# main start #
# #
######################
def main():
from pygame import K_q, K_w, K_a, K_s, K_z, K_x
cube = Physical( # 0 1 2 3 4 5 6 7
vertices=((1, 1, 1), (1, 1, -1), (1, -1, 1), (1, -1, -1), (-1, 1, 1), (-1, 1, -1), (-1, -1, 1), (-1, -1, -1)),
edges=({0, 1}, {0, 2}, {2, 3}, {1, 3},
{4, 5}, {4, 6}, {6, 7}, {5, 7},
{0, 4}, {1, 5}, {2, 6}, {3, 7})
)
counter_clockwise = 0.05 # radians
clockwise = -counter_clockwise
params = {
K_q: (X, clockwise),
K_w: (X, counter_clockwise),
K_a: (Y, clockwise),
K_s: (Y, counter_clockwise),
K_z: (Z, clockwise),
K_x: (Z, counter_clockwise),
}
def keys_handler(keys):
for key in params:
if keys[key]:
cube.rotate(*params[key])
pygame.init()
pygame.display.set_caption('Control - q,w : X a,s : Y z,x : Z')
Paint(cube, keys_handler)
if __name__ == '__main__':
main()
Note that I did use the module NumPy for matrix multiplication (and used math for trig); I assumed that by "no other modules" you meant "without any 3D libraries". Anyway, you can implement your own matrix multiplication function and calculate sin\cos using Taylor series, but that's quite unnecessary.
Upvotes: 7
Reputation: 6713
There is lots of examples out there:
http://codentronix.com/2011/04/21/rotating-3d-wireframe-cube-with-python/
http://www.pygame.org/pcr/3d_wireframe/index.php
http://www.petercollingridge.co.uk/book/export/html/460
First you have to know the OpenGL is based on RHS (right hand rule):
Is the OpenGL Coordinate System right-handed or left-handed?
http://www.ntu.edu.sg/home/ehchua/programming/opengl/CG_BasicsTheory.html
And so the Z axis should be pointing towards you (this contrast with some of the links and formula among the URL links above).
So assuming the Z axis is pointing to the lower left hand side, the projection of the Z axis onto the XY plane will mean the following (original 3D coord is orig_X, orig_Y, orig_Z, and theta is the angle of Z axis, point to the left, with respect to X axis):
X = orig_X - orig_Z * cos(theta)
Y = orig_Y - orig_Z * sin(theta)
Hopefully you can understand why the negative sign in front of orig_Z comes about.
Upvotes: 0