3D graphics with Tkinter

Tkinter's Canvas widget provides for drawing with exact coordinate specifications. Therefore, it can be used to create all sorts of 3D graphics. Furthermore, we have already seen the animation abilities of Tkinter. We can apply these abilities to also animate in 3D.

Let's create a simple application where we create a cube in the center. We add event listeners to rotate the cube on mouse events. We also make a small animation in which the cube keeps rotating by itself when no mouse intervention occurs.
In its final form, the application would look as follows (8.13_3D_graphics.py):

Transposing or unzipping can be done in Python by using the special * operator, any point in a 3D space can be represented by x, y, and z coordinates. This is usually represented by a vector of the form:

This is an example of a row vector as all three points are written in a single row.

This is convenient for humans to read. However, as per convention and for some mathematical advantage that we will see later, positions are taken as a column vector. So it is written in a column as follows:

Since a shape is a collection of points, it is, therefore, a collection of column vectors. A collection of column vectors is a matrix, where each individual column of the matrix represents a single point in 3D space:

Let's take the example of a cube. A cube has eight defining vertices. A representative cube could have the following eight points with its center located at [0,0,0]:

Vertex 1 : [-100,-100,-100],
Vertex 2 : [-100, 100,-100],
Vertex 3: [-100,-100,100],
Vertex 4: [-100,100,100],
Vertex 5: [100,-100,-100],
Vertex 6: [100,100,-100],
Vertex 7: [100,-100,100],
Vertex 8: [100,100,100]

However, here the vertices are represented as row vectors. To represent the vectors as column vectors, we need to transpose the preceding matrix. Since transposition will be a common operation, let's start by building a class called MatrixHelpers and defining a method named transpose_matrix8.13_3D_graphics.py):

class MatrixHelpers():

def transpose_matrix(self,matrix):
return list(zip(*matrix))

Transposing or unzipping can be done in Python by using the special * operator, which makes zip its own inverse.

Another issue with the preceding coordinates is that it centers at (0,0,0). This means that if we try to plot the preceding points on a canvas, it will show up only partly, centered at the top-left corner of the canvas, something like this:

We need to move all the points to the center of the screen. We can achieve this by adding x and y offset values to the original matrix.


We accordingly define a new method named translate_matrix as follows:

def translate_vector(self, x,y,dx,dy):
return x+dx, y+dy

Now let's draw the actual cube. We define a new class named Cube that inherits from the MatrixHelper class because we want to use the transpose_matrix and translate_vector methods defined in the MatrixHelper class (see code 8.13_3D_graphics.py):

class Cube(MatrixHelpers):
def __init__(self, root):
self.root = root
self.init_data()
self.create_canvas()
self.draw_cube()

The __init__ method simply calls four new methods. The init_data method sets the coordinate values for all the eight vertices of the cube (8.13_3D_graphics.py):

def init_data(self):
self.cube = self.transpose_matrix([
[-100,-100,-100],
[-100, 100,-100],
[-100,-100,100],
[-100,100,100],
[100,-100,-100],
[100,100,-100],
[100,-100,100],
[100,100,100]
])

The create_canvas method creates a 400 x 400 sized canvas on top of the root window and assigns a background and fill color to the canvas:

 def create_canvas(self):
self.canvas = Canvas(self.root, width=400, height=400, background=self.bg_color)
self.canvas.pack(fill=BOTH,expand=YES)

Lastly, we define the draw_cube method, which uses canvas.create_line to draw lines between selected points. We do not want lines between all the points, but rather lines between some selected vertices to create a cube. We accordingly define the method as follows (8.13_3D_graphics.py):

def draw_cube(self):
cube_points_to_draw_line = [[0, 1, 2, 4],
[3, 1, 2, 7],
[5, 1, 4, 7],
[6, 2, 4, 7]]
w = self.canvas.winfo_width()/2
h = self.canvas.winfo_height()/2
self.canvas.delete(ALL)
for i in cube_points_to_draw_line:
for j in i:
self.canvas.create_line(self.translate_vector(self.cube[0][i[0]],
self.cube[1][i[0]], w, h),
self.translate_vector(self.cube[0][j], self.cube[1][j], w, h), fill
= self.fg_color)

This code draws a cube on the canvas. However, since the cube draws upfront, all we see is a square from the front. In order to see the cube, we need to rotate the cube to a different angle. That brings us to the topic of 3D transformations.

A wide variety of 3D transformations, such as scaling, rotation, shearing, reflection, and orthogonal projections, can be accomplished by multiplying the shape matrix with another matrix known as a transformation matrix.

For example, the transformation matrix for scaling a shape is:

Where Sx, Sy, and Sz are scaling factors in x, y, and z directions. Multiply any shape matrix with this matrix and you get the matrices for the scaled shape.


Let's, therefore, add a new method named matrix_multiply to our MatrixHelper class (8.13_3D_graphics.py):

def matrix_multiply(self, matrix_a, matrix_b):
zip_b = list(zip(*matrix_b))
return [[sum(ele_a*ele_b for ele_a, ele_b in zip(row_a, col_b))
for col_b in zip_b] for row_a in matrix_a]

Next, let's add the ability to rotate the cube. We will be using the rotation transformation matrix. Furthermore, since rotation can happen along any of the x, y, or z axes, there are actually three different transformation matrices. The three rotation matrices are as follows:

Multiply the shape coordinates by the first matrix for a given value of a and you get the shape rotated by an angle a about the axis in a counterclockwise direction. Similarly, the other two matrices rotate along the axis and axis respectively. 

To rotate in a clockwise direction, we simply need to flip the sign of all sin values in the preceding matrix.
Note, however, that the order of rotation matters. So if you first rotate along the axis and then rotate along the axis, it is not the same as first rotating along y and then along the axis.

More details on rotation matrices can be found at https://en.wikipedia.org/wiki/Rotation_matrix.

So now that we know the three rotation matrices, let's define the following three methods in our MatrixHelper class (8.13_3D_graphics.py):

def rotate_along_x(self, x, shape):
return self.matrix_multiply([[1, 0, 0],
[0, cos(x), -sin(x)],
[0, sin(x), cos(x)]], shape)

def rotate_along_y(self, y, shape):
return self.matrix_multiply([[cos(y), 0, sin(y)],
[0, 1, 0],
[-sin(y), 0, cos(y)]], shape)

def rotate_along_z(self, z, shape):
return self.matrix_multiply([[cos(z), sin(z), 0],
[-sin(z), cos(z), 0],
[0, 0, 1]], shape)

Next, we define a method named continually_rotate and call this method from the __init__ method of our Cube class:

def continually_rotate(self):
self.cube = self.rotate_along_x(0.01, self.cube)
self.cube = self.rotate_along_y(0.01, self.cube)
self.cube = self.rotate_along_z(0.01, self.cube)
self.draw_cube()
self.root.after(15, self.continually_rotate)

The method uses root.after to call itself back every 15 milliseconds. At each loop, the coordinates of the cube are rotated by 0.01 degrees along all three axes. This is followed by a call to draw the cube with a fresh set of coordinates. Now, if you run this code, the cube rotates continuously.

Next, let's bind the rotation of the cube to a mouse button click and mouse motion. This will let the user rotate the cube by clicking and dragging the mouse over the cube.

Accordingly, we define the following method and call it from the __init__ method of the Cube class:

def bind_mouse_buttons(self):
self.canvas.bind("<Button-1>", self.on_mouse_clicked)
self.canvas.bind("<B1-Motion>", self.on_mouse_motion)

The methods linked from the preceding event binding are defined as follows:

def on_mouse_clicked(self, event):
self.last_x = event.x
self.last_y = event.y

def on_mouse_motion(self, event):
dx = self.last_y - event.y
self.cube = self.rotate_along_x(self.epsilon(-dx), self.cube)
dy = self.last_x - event.x
self.cube = self.rotate_along_y(self.epsilon(dy), self.cube)
self.draw_cube()
self.on_mouse_clicked(event)

Note that the preceding method maps mouse displacements along the axis to rotations along the axis and vice versa.
Also, note that the last line of the code calls on_mouse_clicked() to update the value of last_x and last_y. If you skip that line, the rotation becomes exceedingly fast as you increase the displacement from the last clicked position.

The method also refers to another method, named epsilon, which translates the distance into an equivalent angle for rotation. The epsilon method is defined as follows:

self.epsilon = lambda d: d * 0.01

The epsilon here is obtained by multiplying the displacement, d, with an arbitrary value of 0.01. You can increase or decrease the sensitivity of rotation to mouse displacement by changing this value.

Now the cube becomes responsive to mouse click and drag over the canvas. This concludes the last project of this chapter.

Here, we have just scratched the surface of 3D graphics. A much more detailed discussion on 3D programming with Tkinter can be found at https://sites.google.com/site/3dprogramminginpython/.

There have also been attempts to further abstract and build 3D programming frameworks for Tkinter. You can find an example of a 3D framework for Tkinter at https://github.com/calroc/Tkinter3D.

That concludes the chapter, and also our experiments with the Canvas widget. In the next chapter, we will look at some of the most commonly recurring themes of writing GUI applications, such as using a queue data structure, database programming, network programming, interprocess communication, use of the asyncio module, and a few other important concepts in programming.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.136.142