p99will
p99will

Reputation: 250

How can I draw a 3D shape using pygame (no other modules)

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()

advanced-diagram

Could anyone please explain the theory? Could anyone please show me some code to do the math for the points?

Upvotes: 2

Views: 14106

Answers (2)

Nitsan BenHanoch
Nitsan BenHanoch

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

Peter Teoh
Peter Teoh

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

Related Questions