Meshes with Python & Blender: Cubes and Matrices
Welcome to the second part in this series. It’s time to get into some math and learn how to control the position, rotation and scale of the mesh.
This tutorial builds on the lessons of part one. If you find yourself lost too often, try going back. Today we will quickly look into making cubes, and then jump into controlling the mesh and origin point’s positions. Finally we’ll dive into transformation matrices and learn how to perform transformations nice and fast.
Tutorial Series
- Part 1: The 2D Grid
- Part 2: Cubes and Matrices
- Part 3: Icospheres
- Part 4: A Rounded Cube
- Part 5: Circles and Cylinders
The usual setup
Let’s start by importing the packages we need. Besides the usual bpy
, we are also going to use the radians()
function from Python’s math package, as well as the Matrix
class from mathutils (this one is from Blender).
import bpy
import math
from mathutils import Matrix
As before, I’ll have a section to put variables in, then utility functions and finally the main code sections. The vert()
function looks useless now but we’ll need it for the next bit.
# -----------------------------------------------------------------------------
# Settings
name = 'Cubert'
# -----------------------------------------------------------------------------
# Utility Functions
def vert(x,y,z):
""" Make a vertex """
return (x, y, z)
# -----------------------------------------------------------------------------
# Cube Code
verts = []
faces = []
# -----------------------------------------------------------------------------
# Add Object to Scene
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(verts, [], faces)
obj = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.select = True
Making the cube
Making a cube is less glamorous than you would expect since there’s no fancy algorithm to make them. Instead we have to input the vert positions and faces manually. Luckily cubes only have 6 faces and 8 vertices.
verts = [vert(1.0, 1.0, -1.0),
vert(1.0, -1.0, -1.0),
vert(-1.0, -1.0, -1.0),
vert(-1.0, 1.0, -1.0),
vert(1.0, 1.0, 1.0),
vert(1.0, -1.0, 1.0),
vert(-1.0, -1.0, 1.0),
vert(-1.0, 1.0, 1.0)]
faces = [(0, 1, 2, 3),
(4, 7, 6, 5),
(0, 4, 5, 1),
(1, 5, 6, 2),
(2, 6, 7, 3),
(4, 0, 3, 7)]
Run the script now and you will be rewarded with a very default cube. Now that was easy! Let’s see what we can do with it.
Moving the origin point
Every object in Blender has an origin point that determines their position in 3D space. In fact when we talk about an object’s position we actually talk about the position of its origin point. On the other hand when we talk about an origin points’ position we actually talk about the origin points’ position relative to the mesh.
Confused? Look at the following diagram.
As you can see when the orange dot (the origin point) is on the grid, the object’s position is (0, 0, 0) while when it’s up by 1 on the Z axis the position is position is (0, 0, 1). But the interesting bit is the mesh’s position. In both cases the mesh is offset by 1, but in the second case it is ‑1. If the mesh’s position was (0, 0, 0) it would be up in the air, right where the origin is. As you can see, the mesh position is relative to the origin but independent from the origin’s position in the scene.
Knowing this, there are two things we can do:
- Change the mesh in relation to the origin point. This is the same as moving the mesh in edit mode.
- Change the origin point while leaving the mesh in the same place. For this tutorial that would be the center of the scene.
We will begin by changing the position of the mesh relative to the origin point I will be calling this offset to keep things short.
offset = (0, 0, 1)
def vert(x,y,z):
""" Make a vertex """
return (x + offset[0], y + offset[1], z + offset[2])
We can use the vert function to move the mesh with a simple addition.
An offset of 1 on Z will make the cube rest on the grid with the origin point at its bottom.
Now let’s try moving the origin point while keeping the cube at the center of the scene. There’s only one small problem: we can’t move the origin itself. Moving the origin is moving the object, since the origin represents its position in space.
What we have to do is to change the origin is move the mesh as before and move the object by the opposite of that offset.
obj.location = [i * -1 for i in offset]
Remember location is a tuple, so we have to use an expression to set it.
Try running the code now. The cube mesh is back at the center of the scene. But the origin point (and the object) is now at (0, 0, ‑1).
Since we are changing the location using the negative of the offset, the resulting position of the object is negative. Likewise using a negative offset results in a positive position for the object. Give it a shot.
offset = (0, 0, -5)
So what if you want to change the origin and also change the mesh position in any arbitrary way so it’s not always at the center of scene? We can add another offset to represent that. And then we can add this offset in the location expression to move the mesh. Let’s also rename the previous offset to keep things clear.
origin_offset = (0, 0, -5)
mesh_offset = (1, 0, 0)
def vert(x,y,z):
""" Make a vertex """
return (x + origin_offset[0], y + origin_offset[1], z + origin_offset[2])
obj.location = [(i * -1) + mesh_offset[j] for j, i in enumerate(origin_offset)]
Note that we have to use enumerate()
now to get an index for the addition.
There are several other things we could do with expressions and the vert()
function. But there’s another way of transforming meshes and objects. A way that is both cleaner and faster.
Enter the Matrix
Matrices
In math matrices are rectangular arrays of numbers (and sometimes other things). They can be added, subtracted and multiplied between themselves. You will find matrices in almost every field where math is involved.
The only field we care about though, is computer graphics and in this context matrices are used often to represent transformations. This includes things like translation, scaling or rotation. Matrices that represent linear transformations like these are called “Transformation Matrices”.
Objects position, rotation and scale are defined as transformation matrices in relation to a coordinate system. Even when you think there’s no transformation!
For instance, let’s imagine a “default object”. This object sits at coordinates (0, 0, 0) of the scene, has a scale of 1 and a rotation of 0 (on all axis). We can represent the location, scale or rotation of any object as a transformation matrix of this default object. In Blender this is called a World Matrix, and it is a property available in all objects.
By the way, “scene coordinates” are actually called “World Coordinates” in Blender. There are other coordinate spaces, as well as matrices for them but we will look into that in another part of this series (promise!).
Note that you don’t need to be a matrix wizard to use them. The blender devs have blessed us with a Matrix
class that does almost all the work for us, and you might not even have to see a matrix while you’re working with them. Feel free to jump to “Putting it all together” if you’re not interested in the math.
Still here? Alright, let’s play with this matrix concept for a moment. Go ahead and add a new object (using Shift+A
). Select it, then paste this script on a text editor and run it.
import bpy
print('-' * 80)
print('World Matrix \n', bpy.context.object.matrix_world)
You should see this output in the terminal:
--------------------------------------------------------------------------------
World Matrix
<Matrix 4x4 (1.0000, 0.0000, 0.0000, 0.0000)
(0.0000, 1.0000, 0.0000, 0.0000)
(0.0000, 0.0000, 1.0000, 0.0000)
(0.0000, 0.0000, 0.0000, 1.0000)>
Since we haven’t moved, rotated or done anything to the object, its values are the same as our imaginary “default object”. A matrix like this is called an Identity Matrix
in math, and it means there’s no transformation.
Now move the cube somewhere and run the script again.
--------------------------------------------------------------------------------
World Matrix
<Matrix 4x4 (1.0000, 0.0000, 0.0000, -8.8360)
(0.0000, 1.0000, 0.0000, -1.1350)
(0.0000, 0.0000, 1.0000, 8.9390)
(0.0000, 0.0000, 0.0000, 1.0000)>
Aha! Now the matrix includes some change. The values will be different depending on where you move the object. As you can see the last column includes the X, Y, Z coordinates in relation to the center of the scene (world space).
What if we reset the location (Alt-G
) and try scaling instead?
--------------------------------------------------------------------------------
World Matrix
<Matrix 4x4 (0.7280, 0.0000, 0.0000, 0.0000)
(0.0000, -1.4031, 0.0000, 0.0000)
(0.0000, 0.0000, 1.7441, 0.0000)
(0.0000, 0.0000, 0.0000, 1.0000)>
Now the last column has no change since the object is back at (0, 0, 0) However we can see three values have changed to represent scaling on X, Y and Z.
Rotation is more complicated since you can rotate around three axis and each rotation is represented by transformations on the other two axis. This goes a bit out of scope for this tutorial, so I’ll leave several links at the end in case you want to get deeper into the math.
--------------------------------------------------------------------------------
World Matrix
<Matrix 4x4 (-0.9182, 0.3398, -0.2037, 0.0000)
(-0.2168, -0.8612, -0.4597, 0.0000)
(-0.3316, -0.3780, 0.8644, 0.0000)
( 0.0000, 0.0000, 0.0000, 1.0000)>
You might also be wondering about the last row. Transformation matrices for 3D are actually 4D, the last row is an extra dimension. This is a mathematical “trick” to enable the matrix to perform translations. Again, I won’t get too technical about this. Check the links at the end for more info. In any case, it’s not important for our purposes since it will never change.
Here’s a diagram of the different transformations:
Using Matrices
Transformation matrices can be combined to create a single matrix that includes all the result of all the transformations. This is done by multiplying them. That means we can take an object’s world matrix and multiply it by a transformation matrix to get a new matrix that includes the changes in both matrices. We can then assign it as the object’s world matrix and thus transform the object.
Or to put it in code:
obj.matrix_world @= some_transformation_matrix
It’s time to whip out that Matrix
class and see how we can use it to generate matrices.
Translation
Let’s start with the easiest: moving stuff. All we have to do is call the Translation
method of the Matrix
class with a vector (or tuple) of values for each axis.
translation_matrix = Matrix.Translation((0, 0, 2))
obj.matrix_world @= translation_matrix
Scaling
Scaling takes three arguments. The first one is the scale factor. The second one is the size of the matrix, it can be either 2 (2×2) or 4(4x4). But since we are working with 3D objects this should always be 4. The final arguments is a vector to specifies the axis to scale. This can be either zero for no scaling, or 1 to scale.
scale_matrix = Matrix.Scale(2, 4, (0, 0, 1)) # Scale by 2 on Z
obj.matrix_world @= scale_matrix
Rotation
Rotation takes almost the same arguments as scale. The first is the angle of rotation in radians. The second is the size of the matrix (same as before). And the third is the axis of the rotation. You can pass a string like ‘X’, ‘Y’ or ‘Z’, or a vector like the one in scale.
rotation_mat = Matrix.Rotation(math.radians(20), 4, 'X')
obj.matrix_world @= rotation_mat
Putting it all together
We can chain transformations together by multiplying them one after the other. But beware, Matrix multiplication is not commutative. Order does matter. Start by translation, then rotation and scale. If you’re getting strange values that look like rounding errors, look at the order in which you are multiplying.
translation = (0, 0, 2)
scale_factor = 2
scale_axis = (0, 0, 1)
rotation_angle = math.radians(20)
rotation_axis = 'X'
translation_matrix = Matrix.Translation(translation)
scale_matrix = Matrix.Scale(scale_factor, 4, scale_axis)
rotation_mat = Matrix.Rotation(rotation_angle, 4, rotation_axis)
obj.matrix_world @= translation_matrix @ rotation_mat @ scale_matrix
Matrices can also be used to change the mesh instead of the object. To do this we can use the transform()
method of in objects. All it asks for is that you give it a matrix.
obj.data.transform(Matrix.Translation(translation))
Don’t forget we can also combine matrices by multiplication so you can do several transformations in one go.
obj.data.transform(translation_matrix @ scale_matrix)
Not only are matrices faster but this method also goes straight to C, so it performs considerably better than calculating the position of each vertex in Python (which is quite slow and expensive). On top of that, we can do this in a single line.
Awesome.
Final code
import bpy
import math
from mathutils import Matrix
# -----------------------------------------------------------------------------
# Settings
name = 'Cubert'
# Origin point transformation settings
mesh_offset = (0, 0, 0)
origin_offset = (0, 0, 0)
# Matrices settings
translation = (0, 0, 0)
scale_factor = 1
scale_axis = (1, 1, 1)
rotation_angle = math.radians(0)
rotation_axis = 'X'
# -----------------------------------------------------------------------------
# Utility Functions
def vert(x,y,z):
""" Make a vertex """
return (x + origin_offset[0], y + origin_offset[1], z + origin_offset[2])
# -----------------------------------------------------------------------------
# Cube Code
verts = [vert(1.0, 1.0, -1.0),
vert(1.0, -1.0, -1.0),
vert(-1.0, -1.0, -1.0),
vert(-1.0, 1.0, -1.0),
vert(1.0, 1.0, 1.0),
vert(1.0, -1.0, 1.0),
vert(-1.0, -1.0, 1.0),
vert(-1.0, 1.0, 1.0)]
faces = [(0, 1, 2, 3),
(4, 7, 6, 5),
(0, 4, 5, 1),
(1, 5, 6, 2),
(2, 6, 7, 3),
(4, 0, 3, 7)]
# -----------------------------------------------------------------------------
# Add Object to Scene
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(verts, [], faces)
obj = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.select = True
# -----------------------------------------------------------------------------
# Offset mesh to move origin point
obj.location = [(i * -1) + mesh_offset[j] for j, i in enumerate(origin_offset)]
# -----------------------------------------------------------------------------
# Matrix Magic
translation_matrix = Matrix.Translation(translation)
scale_matrix = Matrix.Scale(scale_factor, 4, scale_axis)
rotation_mat = Matrix.Rotation(rotation_angle, 4, rotation_axis)
obj.matrix_world @= translation_matrix @ rotation_mat @ scale_matrix
# -----------------------------------------------------------------------------
# Matrix Magic (in the mesh)
# Uncomment this to change the mesh
# obj.data.transform(translation_matrix @ scale_matrix)
Wrap up
This covers this part in the series. If Matrices have peaked your interest here’s a few links to learn more. The links are in order of difficulty.
- World Matrix Documentation on Blender’s API
- Computerphile’s video on Matrices
- Matrices (Wikipedia)
- Transformation Matrices (Wikipedia)
- Matrix Multiplication (Wikipedia)
- Scratchapixel’s lesson on matrices
- Coding Labs’article on Projection Transformation Matrices
A few things you can try to do yourself:
- Put this all in a loop and make multiple cubes following a sequencial transformation, like making a wave or rotating around an axis.
- Use matrices to move the origin of the cube, using the method in this tutorial
- Try scaling the mesh without matrices or changing the object’s coordinates (hint: don’t forget the vert() function)
- Try applying the world matrix of one object into another
In the next tutorial we’ll be looking at making Icosahedrons and subdividing them to approximate a sphere. Do you have any questions, or suggestions for the next tutorials? Leave a comment below!