# Why is this Blender matrix multiplication not distributive?

I was surprised in working on a Blender problem recently to find that matrix multiplication doesn't seem to be distributive:

>>> bpy.data.objects["Camera"].matrix_world
Matrix(((0.7071065306663513, -0.5, 0.5000001788139343, 11.922225952148438),
(0.7071070075035095, 0.49999985098838806, -0.4999997019767761, -16.922224044799805),
(-1.6974146888060204e-07, 0.7071067094802856, 0.7071067094802856, 8.609989166259766),
(0.0, 0.0, 0.0, 1.0)))

>>> top_verts
[Vector((-0.7777432203292847, 0.7777432203292847, -1.0)), Vector((0.7777432203292847, 0.7777432203292847, -1.0))]

>>> bpy.data.objects["Camera"].matrix_world @ (top_verts[1] - top_verts[0])
Vector((13.022120475769043, -15.822328567504883, 8.609989166259766))

>>> bpy.data.objects["Camera"].matrix_world @ top_verts[1] - bpy.data.objects["Camera"].matrix_world @ top_verts[0]
Vector((1.0998945236206055, 1.0998945236206055, -9.5367431640625e-07))


Why is this the case?

It is distributive if you convert the world matrix to a $$3 \times 3$$ matrix first or extend the vector by one dimension. The vector needs to have as many elements as the matrix has columns.

>>> x = bpy.data.objects["Camera"].matrix_world.to_3x3() @ (top_verts[1] - top_verts[0])
>>> x
Vector((1.0669403076171875, 1.1318906545639038, 0.0))

>>> y = bpy.data.objects["Camera"].matrix_world.to_3x3() @ top_verts[1] - bpy.data.objects["Camera"].matrix_world.to_3x3() @ top_verts[0]
>>> y
Vector((1.0669403076171875, 1.1318907737731934, 0.0))


Generally it is not possible to multiply a $$4 \times 4$$ matrix with a $$3 \times 1$$ vector, because the dimensions don't match. Blender tries to be smart and extends the dimension of the vector by adding elements with value $$1.0$$ when necessary. However, since this is only done when the multiplication is performed, it is no longer distributive:

$$M \times \left(\overrightarrow{v} + \overrightarrow{v}\right)_{extended} \neq M \times \overrightarrow{v_{extended}} + M \times \overrightarrow{v_{extended}}$$

The reason for the difference is that Blender can perform the addition $$\overrightarrow{v} + \overrightarrow{v}$$ without extending the individual vectors. The element is only added afterwards when the multiplications with $$M$$ is performed. In comparison, $$M \times \overrightarrow{v_{extended}} + M \times \overrightarrow{v_{extended}}$$ requires to extend both vectors right away. The results are not equal since in the former case the vector should have been extended prior to the addition, resulting in a fourth element of value $$2.0$$. However, Blender performs the extension afterwards with an element of value $$1.0$$.

Blender tries to help the developer, perhaps a bit too much in this case, thus masking the error of wrong dimensions. This is especially hard to see since the output is truncated to the size of the original vector.

• I see; that is interesting. Do you know how Blender(/python) interprets the 4x4 @ 3x1 matrix multiplication depicted in the question? I had assumed that might be the cause; it is something of a mystery to me what Blender does with the extra column in the first matrix. – NeverConvex Aug 28 '20 at 21:45
• it seems to set the missing elements to 1. >>> matrix@ Vector(( 1, 0, 0, 1)) Vector((12.629332542419434, -16.215116500854492, 8.609989166259766, 1.0)) >>> matrix@ Vector(( 1, 0, 0)) Vector((12.629332542419434, -16.215116500854492, 8.609989166259766)) >>> matrix@ Vector(( 1, 0, 0, 0)) Vector((0.7071065306663513, 0.7071070075035095, -1.6974146888060204e-07, 0.0)) – Ron Jensen Aug 28 '20 at 21:49
• Ah, that makes sense. And, together with Robert's observation, I think that explains the failure to distribute, since explicitly adding the 0s "fixes" it:  >>> v1 = Vector( (top_verts[1].x, top_verts[1].y, top_verts[1].z, 0.) ) >>> v0 = Vector( (top_verts[0].x, top_verts[0].y, top_verts[0].z, 0.) ) >>> bpy.data.objects["Camera"].matrix_world @ (v1 - v0) Vector((1.099894642829895, 1.0998953580856323, -2.640305467593862e-07, 0.0))  – NeverConvex Aug 28 '20 at 22:44