There are many situations when writing Blender add-ons where you might want to multiply a vector with a matrix. A common example is a coordinate transformation, for example to convert object coordinates to world coordinates.
If you only need to convert one vector, it would be no problem to do this directly in Blender:
(Note that the world matrix is 4x4, so it will not just scale and rotate, but also translate a vector, but then this vector need to be 4d as well! The 4th component, often referred to as 'w', should be 1 if you want to perform that translation, like for positions, and 0 if not, so for normals.)
The challenge
But what if we needed to perform such a transformation on a million vectors? Matrix multiplication itself isn't exactly cheap, requiring 16 multiplications and 12 additions in the 4d case, and loops in Python are exceptionally slow.
Luckily Blender comes bundled with the numpy module and there are efficient ways to get properties, including vertex coordinates into numpy arrays (as illustrated in this article for example), so we only need a way to use numpy functionality to efficiently multiply a list of vectors with a matrix.
Choices
It is always good to have choices and numpy in fact offers several ways to achieve the same result.
The @ operator
The first is matrix multiplication. If vector is a list of vectors with shape = n,4 and matrix is a 4x4 matrix, we can perform the multiplication of each of the vectors in the list with this expression:
(matrix @ vector.T).T
Because numpy's @ operator expects the first dimensions of each of the operands to match, we have to transpose our list of 4d vectors first, and transpose again at the end to get the results in the right shape. This is a bit unintuitive, but at least those transpose operations are very cheap: they just create a view with a different shape without copying anything.
The alternative approach would be to invert the matrix and write vector @ matrix instead, but inverting a 4x4 transformation matrix is not simply np.linalg.inv(matrix) because we would need to extract the translation and scaling parts first, the invert, then recombine. That's too complicated to my taste, so I go with this approach.
The dot() function
The dot function essentially does the exact same thing, and needs the same transpositions:
np.dot(matrix, vector.T).T
This function is supposed to be more general, in the sense it can deal with tensors too, but that might impact performance. We have a look at that in a minute.
The Einstein summation notation
The most versatile tool in numpy's drawer is the einsum() function, which lets us define the order of every implicit loop over the list of vectors and the rows and columns of the matrix:
np.einsum("ni,ji->nj", vector, matrix)
This may look complicated but saves us the double transposition shenanigans, and reads something like:
use index n to index each vector
use index i for each element of a vector
multiply that vector element with an element of the matrix:
use index j for each row of the matrix
use index i for each column of the row
sum the results for the row (because i is used to index the matrix and the vector)
store each of those row sums as the columns of the nth result vector
This may take some time to wrap your head around, but it is very versatile and should be fast too.
Performance
Speaking of fast, lets have a look at some benchmarks:
n
power of two
pure python
dot
matmul
einsum
16384
14
0.038
0.000
0.000
0.000
32768
15
0.073
0.000
0.000
0.001
65536
16
0.143
0.000
0.000
0.001
131072
17
0.268
0.000
0.000
0.002
262144
18
0.543
0.000
0.000
0.005
524288
19
1.103
0.001
0.000
0.010
1048576
20
2.202
0.004
0.006
0.022
2097152
21
0.010
0.016
0.037
4194304
22
0.023
0.021
0.070
8388608
23
0.042
0.034
0.137
16777216
24
0.088
0.067
0.264
33554432
25
0.159
0.131
0.522
67108864
26
0.322
0.262
1.045
134217728
27
0.625
0.511
2.068
268435456
28
1.251
1.027
536870912
29
2.481
2.060
For comparison I have also included a pure Python reference implementation to show how slow that is. It is included in the code on Github.
We measure all the way from 16K 4d vectors to over 500 million of those. Tests that are slow were taken out of the race to save time.
The first thing we notice is that all methods scale with the number of vectors: if we double the number of vectors, the time to multiply them all by the same matrix approximately doubles.
The second thing we notice is that Python is really slow: It already takes more than 2 seconds just to multiply a million vectors. This is what we expected of course, so not really remarkable.
An finally, that the @ operator ('matmul') is fastest of them all. So the Einstein summation might be versatile and the dot() function more general but that does come with some overhead.
We can visualize that in the following graph:
(Note the logarithmic scales, so the y-axis top line is 1 second, next one down 0.1 second, etc)
Apart from some irregularities for shorter lists of vectors, the lines are pretty straight and clearly the @ operator is the winner. It might not look all that much in the graph because of the logarithmic scales, but it consistently is 20% faster than the dot() function, and 20% is nothing to be sniffed at in my book.
Caveats & conclusion
Of course your mileage may vary and the exact numbers aren't all that relevant. These timings were measured on a AMD Ryzen 7 7700X with 64 GB memory, and if you have a different CPU and/or amount of memory these timings will differ, but the trends are expected to be the same.
Also, all code uses 64-bit floating point operations and for Blender vertex coordinates that would be overkill. Using 32-bit floating point might save you quite some time, even on a 64-bit CPU, because the vectors would take only half the space, increasing cache efficiency quite a bit, but I didn't tabulate those timings.
In the end, the fastest and simplest to code is probably using the @ operator.
The summer sale is closing. So this is your last chance at getting a serious discount on road signs in my shop, but there is loads of other good stuff on sale as well.
After publishing the US Road Sign Generator I realized people might want a way to generate those highway shields that show the interstate numbers, so I created just that: All lightweight (both geometry and texture) and of course utilizing the common 'Highway Gothic' typeface.
Yup, the traditional summer sale is there again. So if you are in the market for some road signs you may want to check my shop, but there is loads of other good stuff on sale as well.
In Blender 5.2, theImBufobject now features a method that can be used as a context manager, yielding a Pythonmemoryviewobject. This opens up a fast and convenient way to use anImBufto draw things using theblfmodule, and then copy the result to a BlenderImageobject'spixelsattribute.
However, we cannot simply write:
with myimbuf.with_buffer() as mv:
myimage.pixels[:] = mv[:]
This fails because Python's memoryview slice assignments have limitations when working with non-flat array configurations, which is what an Imbuf always is (width x height x color channels). Instead, we have to cast the memoryview to a flat array.
Furthermore, because either the source or the destination needs to be in a compatible byte format, we cannot cast a multidimensional float view directly to a flat float view in one go. We must resort to a rather inelegant double cast coupled with the foreach_set method:
This first casts everything to a flat array of bytes, then to a flat array of floats, what is what foreach_set expects.
It works, and both cast() and foreach_set() are highly performant (cast() does not even touch the data, just the metadata), but it does feel a bit clunky. This isn't Blender's fault, though—it has been a long-standing behavior regarding multidimensional array manipulation discussed on the Python Core Development Issue Tracker.
Note that we now have a fast way to move data from an ImBuf to an Image, but ImBuf objects aren't all that common: we can create one and write text into it with the functions in the blf module, but as far as I can tell the is currently no way to copy a framebuffer to an Image or an ImBuf, nor is there a way to use the gpu module directly to draw into an ImBuf, so quickly creating an Image still requires a laborious route that involves copying the data from the framebuffer pixel-by-pixel to the Image.
Hopefully framebuffers will get buffer protocol support in the future, but that's probably not an easy task as there the actual data lives on the GPU and needs to be transferred. So let's keep an eye on the dev logs. 😀
After publishing a few sets of US road signs, I went back a created a geometry nodes based road sign generator that can be used to generate the iconic green signs you see along highways in the US.
Configurable to up to three lines, will all sort of options and three different materials. All lightweight (both geometry and texture) and of course utilizing the common 'Highway Gothic' typeface.
After publishing a set of general US road signs, I thought why not publish one with signs commonly seen around road works?
The set is conveniently organized as an asset library and available on Superhive (formerly BlenderMarket). Check it out if you want to keep your rendered road crews and passing driver safe!
I thought I'd quit, but I guess working with Blender is too much fun 😀.
In the past I created some collections with Dutch and German road signs but I thought some US road signs would be a nice addition. There seems to be a bewildering variety of those, much more than their European counterparts, so I limited myself to some of the more common ones seen in (sub)urban setting. If there is some demand for it I might add some other sets.
The set is conveniently organized as an asset library and available on Superhive (formerly BlenderMarket). Check it out if you want to add some details to your rendered neighborhood!
To get a feeling on how spur gear teeth get their particular shape, I created a short visualizer:
The whole process of emulating the action of the cutter on the rotating blank was done in Blender, this time not using a add-on for once, but a simple script that runs from the text editor.
The script renders a frame, then applies the boolean difference modifier that the spur gear object (the 'blank') has, moves both the blank and the cutter and adds again a boolean modifier and repeats this as many times as desired.
I am not yet sure if I will write a small article explaining the code, it isn´t all that complicated, but in the meanwhile you can download the .blend file from my GitHub repository (click on the 'download raw file' icon in the upper right corner to download).
I have been experimenting a bit with geometry nodes lately and I thought I´d share this one
The .blend file is available from my GitHub repository. (Click the 'download raw file' button in the upper right corner to download it; Inside is a sample scene, and the geometry node itself is called 'Hexagon pattern')
Tips
The input mesh is simply repeated across the pattern and nothing fancy is done to it. If the repeated meshes are butted up to each other you may want to remove any coinciding vertices, and I could have done that in the geometry nodes itself but that is unnecessary as you can easily apply a weld modifier to achieve the exact same effect. And other modifier too of course, like a solidify modifier perhaps, to give the grid some thickness.
If you just want a quick hexagonal pattern in a shader, you may want to have a look at this post.
Some details
The node setup is pretty straight forward:
We get the bounding box of the object we want to repeat, scale it a bit so we can add a gap if we like, and the repeat the mesh for a set number of iterations along the x-axis. Then we replicate the result along the y-direction and end with applying a material index.
If we look at the repeat x section, we see that there isn´t much to it:
We simply join a shifted version of the input for a set number of times. The offset in the x-direction is twice the maximum of x dimension of the bounding box, i.e. we assume that the input mesh is symmetrical around the origin.
The repeat y section isn´t all that much different, except for a little detail:
That detail is that we move the new mesh up in the y-direction by a configurable scaling factor, where the default is fit for a six sided cylinder, a.k.a. a hexagon, but you can change that to something else if needed.
We also move the row either to the right or the left, depending on whether we are in an odd or even row. We determine this odd/evenness by taking the iteration number module 2 and using a switch node to provide a multiplication factor of -1 or 1 respectively that we apply to our x offset. If we wouldn´t alternate this move in the x-direction we would get a skewed grid, which might be find, but I prefer to work with a square grid.
I'm interested in more than just Blender 😀 so I started a new blog. It might interest some readers of this blog as well as it has a bit of a mathematical focus, just like quite a few articles here.
It is all about assumptions and questions that may pop up in everyday life and that may be solved with a bit of thinking and pen and paper, hence the title "On the back of an envelope".
Focus is on doing our own research up, backed up by proper references to articles by real people, and no easy AI slop.
In this module we will refactor the render_done add-on encountered in the previous module
into a multi-file add-on.
The first video will take a look at why splitting up an add-on might be beneficial for
maintenance, reuse and the options to include non-Python files, something we will make use of
in the second video where we will add a custom icon to one of the operators.
In this module we will build an add-on that installs application handlers that will send an email once a tender job has finished.
The first video will focus on application handlers, the second one the layout of user preferences to configure things like recipient and email server while the third is about the actual mail code. The fourth video will then tie this all together in a functional add-on. We end with a video that is decidedly not strictly beginner level where we cover creating presets in the user preferences, something that is a bit more involved than adding presets to operators.
To illustrate how to create a modal operator I created a small add-on, plumb_line, to illustrates a few key principles.
The add-on itself is a bit of a toy, allowing the user to move the 3d cursor around while showing the distance to an intersection point directly below it.
This is not very useful in itself, but it does
implement a modal operator
show how to create overlays in the 3d view, and
how to use Object.ray_cast() and Scene.ray_cast() to find intersections
And because the code covers all kinds of functionality, we structured the code into separate modules,
so this add-on also implements some tricks to force module reloading on reinstalling the add-on to prevent having to restart Blender every time we change something.
It also touches briefly on using numpy, and offers a lot of configuration options in the user preferences.
Quite a lot for a demo add-on, but in this article we focus on the modal operator.
Anatomy of a modal operator
Not all modal operators look the same, but a common pattern is shown below:
The poll() method has the same function as in a non-modal operator, but typically there is no execute() method.
If an operator is invoked, for example from a menu entry, its default invoke() method will typically call its execute() method.
In a modal operator we override the invoke() method, and instead of calling execute() we add a modal handler to the window manager and return "RUNNING_MODAL".
This will cause the window manager to call the modal() method on the handler, typically the operator itself, and keep on doing so as long as that method returns "RUNNING_MODAL".
The window manager will call the the modal() method each time an event occurs, and passes this event as an argument to the call.
This event can be anything from a key press to a mouse move and we can even cause timer events to be passed if we enable a timer in the window manager.
This arrangement makes it possible to create interactive add-ons where the user uses keyboard or mouse actions to work with objects in a scene, until they end the interaction, typically by pressing escape or with a right mouse click.
The plumb_line invoke() method
Our operator will be invoked from the Object menu and it needs to do a few things:
position the the 3d cursor somewhere above the active object,
add a modal handler, and
add a pair of draw handlers that show the line and intersection point as well as some text with the distance.
It does a few other things as well, but the relevant lines of code look like this:
It uses a helper function defined elsewhere to calculate the highest z-coordinate of the active object's bounding box
and then copies the location to keep the cursor centered above the object in the x and y directions, but sets its z-coordinate
to whatever we calculated plus an arbitrary offset, 3 in this case. Note that we needed to copy the object location
because assigning it to the cursor location would also cause the object location to change every time the cursor location is updated
as they would refer to the same Vector object.
Next, we add a modal handler, in this case the operator itself. This will cause the window manager to calle the modal() method and we will look at that in the next section.
Finally, we install two draw handlers, also defined in a separate module. The post view draw handler works in 3d space, and will show the actual 'plumb line' going from the 3d cursor to the intersection with the active object.
The post pixel draw handler works in 2d on top of everything, and is used to show the text with the measured distance and the intersection highlight. We might cover these handlers in a future blog post.
The return value indicates that we are not done yet, and want to keep on running.
The plumb_line modal() method
defmodal(self, context, event):
context.area.tag_redraw()
if event.type in {"RIGHTMOUSE", "ESC"}:
self.cancel(context)
return {"CANCELLED"}
...
if event.type in {"UP_ARROW", "DOWN_ARROW", ...}:
if (
event.value == "PRESS"
):
...
context.scene.cursor.location.y += increment
... # calculate the intersection with the object below the cursor
context.window_manager.target = worldspace_location
context.window_manager.distance_label = ...
if (
event.type.find("MOUSE") >= 0or event.type.find("NUMPAD") >= 0
):
return {"PASS_THROUGH"}
return {"RUNNING_MODAL"}
The first thing we do is to make sure that we mark the area for redraw, so that no matter what we do, our draw handlers will be executed.
Next we check if the event was a press of the escape key or a right mouse click, as these are the common way in Blender to interrupt an operation.
If it was, we call the cancel() method to remove our draw handlers and return "CANCELLED" to signal that we are done.
We look for some other key presses as well, but we skip that here, but we also look for arrow keys. If such a key was pressed, so not released because we do not want to process a key click twice,
we calculate how the position of the 3d cursor should change and the update the cursor location. By looking for just key presses, as opposed to key releases, the user can also keep the key pressed and cause rapid repetition.
With the new cursor location we then calculate the point of intersection with object straight below the cursor. We don´t show that code here, we might cover that in a future blog article, but the result ends up in the worldspace_location variable.
We store that in the target property of the window_manager where it will be picked up by our draw handlers to draw a line from the 3d cursor to that point. We also calculate the distance and store that in a window manager property too, to be picked up by our other draw handler that will display this in a text label next to the line.
Because we also want to give the user the option to navigate the 3d view to get a different perspective, so we also check if the event was a mouse event or numpad key, in which case we return "PASS_THROUGH". This will not end our operator but cause the event to pass to Blender's regular event processing, which will move the view around accordingly.
Any other events are simply ignored and we return "RUNNING_MODAL" to keep the operator running.
You may have noticed that none of the paths through the code ever return "FINISHED" and that is intentional: This add-on doesn´t change anything in the scene, just displays information in an overlay, so there is no distinction between canceling an operation or finishing it. That is also why we didn't add "UNDO" to the bl_options variable, as there will never anything to be undone. (In fact, we didn´t define bl_options at all, but its default is just {"REGISTER"})
Code availability
The full code, with full type annotation and lots of additional information in the comments and a readme is available on GitHub.