How I Built A Simple (Horrible) 3D Graphics Engine in python matplotlib - Part 3: Square but 3D

published-date: 20 Jan 2023 18:02 +0700
categories: dumber-day-by-day python-hijinks
tags: python matplotlib

Posts in This Series


Now that we have the basic on plotting a line in plt, in this post I want to expand more on creating cube primitive.

How to Vertex

But first how would you store vertex coordinate in python? The easiest method is to store everything as 2D list buffer (or array if you prefer).

# vertices = [
#   [x1, y1, z1, w1], Vertex, or point coordinate 1
#   [x2, y2, z2, w2], Vertex 2
#   [x3, y3, z3, w3], Vertex 3
#   ...
# ]

But wait, what’s up with w? The most obvious answer lays on the matrix calculation I used in the entire project, which uses 4x4 matrix templates because 3x3 matrix transformation does not support perspective projection. Honestly I don’t bother with projection matrixes and can’t really explain for sure. But to quote a more advanced users:

“by dividing x, y and z by w you actually project this 4d point into 3d space.”

For now ignore its existence, and we assign w1 = w2 = w3 = wn = 1; n ∈ real positive integers

Now say, you want to make cube with width, length, and height as parameters. Center of mass lies in the center of the cube (duh). To add these into our vertices list, you can do one of the following.

# cube dimensions
width = 10
length = 10
height = 10

# empty vertex list as buffer
vertices = []

for i in (width, -width):
  for j in (length, -length):
    for k in (height, -height):
      vertices.append((i/2, j/2, k/2, 1))

# or

vertices.append(
  ( width/2,  length/2,  height/2, 1),
  ( width/2,  length/2, -height/2, 1),
  ( width/2, -length/2,  height/2, 1),
  ( width/2, -length/2, -height/2, 1),
  (-width/2,  length/2,  height/2, 1),
  (-width/2,  length/2, -height/2, 1),
  (-width/2, -length/2,  height/2, 1),
  (-width/2, -length/2, -height/2, 1)
)

# either one is fine. Also notice the vertex order by the index in the list/array. 
#  6 - - - - 4
#  | \       | \
#  |   \     |   \
#  |     2 - - - - 0        \ Width
#  |     | c |     |        - Length
#  7 - - | - 5     |        | Height
#    \   |     \   |        c center(origin)
#      \ |       \ |
#        3 - - - - 1

Cool!

Plotting the Cube with ✨Magic Index✨

To plot the cube in plt, we’d have to cast this into np.array for splicing each of the axis coordinate.

# assuming you haven't already
import numpy as np

# casts the entire list buffer as np.array
vertices = np.array(vertices)

x_values = vertices[:,0]
y_values = vertices[:,1]
z_values = vertices[:,2] 

fig, ax = ConstructSubplot3D(limit=((min(x_values), max(x_values)), (min(y_values), max(y_values)), (min(z_values), max(z_values)))) 
ax.plot3D(x_values, y_values, z_values)
fig.show()

Figure 1

Whoa, shits fucked yo.

Looking back at the previous cube vertices generation code, the ordering of the vertex coordinate does not represent on how the edges should be rendered. In order to render edges or the wireframe of the cube, the order of the points should be reconstructed in a way that every line goes only at the edges. That’s where magic index comes to help reordering the vertices list buffer.

def verts_to_wireframe(vertices):
  # this, does in fact, very inefficient memory-wise. Since we're basically cloning the list.

  n_b = len(vertices) // 8 # if there are more than one cube in the vertex buffer, floor-division with the amount of vertex in cube (8)

  magic_index = [
    (0, 1, 3, 2, 0),
    (0, 1, 5, 4, 0),
    (0, 2, 6, 4, 0),
    (0, 2, 3, 7),
    (7, 3, 2, 6, 7),
    (7, 5, 4, 6, 7),
    (7, 3, 1, 5, 7),
  ]

  temp = []
  for n in range(n_b):
    for r in magic_index:
      prev = None
      for i in r:
        if i == prev:
          continue
        else:
          temp.append(vertices[(8*n) + i]) 
        prev = i  

  return np.array(temp)

This magic index reordering follows the rule at previous cube generation. Different pattern may differ at how you’d reorder the vertices. Basically the order draws one faces at each pass. Here’s the sequence of the passes:

Also unhelpful flow diagram showing all of index jumps.

flowchart LR 0 -- front --> 1 1 -- front --> 3 3 -- front --> 2 2 -- front --> 0 0 -- right --> 1 1 -- right --> 5 5 -- right --> 4 4 -- right --> 0 0 -- top --> 2 2 -- top --> 6 6 -- top --> 4 4 -- top --> 0 0 -. trans .-> 2 2 -. trans .-> 3 3 -. trans .-> 7 7 -- left --> 3 3 -- left --> 2 2 -- left --> 6 6 -- left --> 7 7 -- back --> 5 5 -- back --> 4 4 -- back --> 6 6 -- back --> 7 7 -- bottom --> 3 3 -- bottom --> 1 1 -- bottom --> 5 5 -- bottom --> 7

Now Everything All At Once

 1import numpy as np
 2import matplotlib.pyplot as plt
 3
 4def ConstructSubplot3D(x=1, y=1, figsize=(5,5), limit=((-2,2),(-2,2),(-2,2)), elev=60, azim=30, dpi=150):
 5  return plt.subplots(
 6    x, y,
 7    figsize = figsize,
 8    dpi = dpi,
 9    subplot_kw = {
10    "projection": "3d", 
11    "proj_type":'ortho', 
12    "elev": elev, 
13    "azim": azim,
14    "xlim": limit[0],
15    "ylim": limit[1],
16    "zlim": limit[2],
17    }
18  )
19
20def verts_to_wireframe(vertices):
21
22  n_b = len(vertices) // 8 
23
24  magic_index = [
25    (0, 1, 3, 2, 0),
26    (0, 1, 5, 4, 0),
27    (0, 2, 6, 4, 0),
28    (0, 2, 3, 7),
29    (7, 3, 2, 6, 7),
30    (7, 5, 4, 6, 7),
31    (7, 3, 1, 5, 7),
32  ]
33
34  temp = []
35  for n in range(n_b):
36    for r in magic_index:
37      prev = None
38      for i in r:
39        if i == prev:
40          continue
41        else:
42          temp.append(vertices[(8*n) + i]) 
43        prev = i  
44
45  return np.array(temp)
46
47if __name__ == "__main__":
48  width = 10
49  length = 10
50  height = 10
51
52  vertices = []
53
54  for i in (width, -width):
55    for j in (length, -length):
56      for k in (height, -height):
57        vertices.append((i/2, j/2, k/2, 1))
58
59  vertices = np.array(vertices)
60  vertices = verts_to_wireframe(vertices)
61
62  x_values = vertices[:,0]
63  y_values = vertices[:,1]
64  z_values = vertices[:,2] 
65
66  fig, ax = ConstructSubplot3D(limit=((min(x_values), max(x_values)), (min(y_values), max(y_values)), (min(z_values), max(z_values)))) 
67  ax.plot3D(x_values, y_values, z_values)
68  fig.show() # if the image does not show up from your interpreter, use colab instead

Figure 2

Golly! Now in the next post I’ll cover linear transformation to the vertices we have made.