3
$\begingroup$

I am doing a visualization where I need to transform a sphere with a matrix. Easiest solution would be to just set the matrix as local transform but this gives me unexpected results.

I was trying around a little and found out, that setting matrix_local, delta_... and operating on vertex positions directly yields very different results. As an example, I tried it with a simple shear matrix. Here are the results:

Using matrix on vertex positions Using matrix on vertex positions

Using local transformation matrix Using local matrix

Using delta transform enter image description here This is the code:

import bpy
from mathutils import *

def transform_verts(context):
    obj = context.active_object
    mat = Matrix.Shear('XY', 4, (3,3))

    #this does what it is supposed to do
    for j in range(0, len(obj.data.vertices)):
        obj.data.vertices[j].co = mat @ obj.data.vertices[j].co

def transform_delta(context):
    obj = context.active_object

    # This does NOT work as intended :(
    # this deforms the sphere into an elongated Ellipsoide
    d_loc, d_rot, d_sc = Matrix.Shear('XY', 4, (3,3)).decompose()
    obj.delta_location = d_loc
    obj.delta_rotation_quaternion = d_rot
    obj.delta_scale = d_sc

def transform_matrix_local(context):
    obj = context.active_object

    # for unknown reasons it is not possible to do this with an object transformation matrix
    # setting matrix_local yields similar weird results as setting delta transform
    # I suppose it may have to do with Blender internally decomposing it
    obj.matrix_local = Matrix.Shear('XY', 4, (3,3))

class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):
        #transform_verts(context)
        #transform_delta(context)
        transform_matrix_local(context)
        return {'FINISHED'}


def register():
    bpy.utils.register_class(SimpleOperator)


def unregister():
    bpy.utils.unregister_class(SimpleOperator)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.object.simple_operator()

Why does setting the matrix_local also does something very different from what is intended? In my understanding, setting the matrix_local should do exactly the same as applying the matrix to vertex positions.

Is there a workaround for this issue that does not involve looping through vertices and setting the position manually? The above is just an example. My actual use case is a modal operator and right now I need to store the original vertex positions and loop through them each time modal is called. That is really slow compared to just setting the local transformation matrix. If there isn't, any idea how to speed it up?

Edit: obj.data.transform() is not possible because I need to apply the matrix to the sphere each time. This matrix is not necessarily invertable, so I can't just do the inverse transformation from the previous call and then do the current transformation. I would only be able to use it if I can somehow restore the positions the vertices had before the transformation.

$\endgroup$
0

2 Answers 2

4
$\begingroup$

Mesh.transform

Can transform mesh directly by a matrix with Mesh.transform(matrix) The matrix must be a 4x4. Use in object mode. Update the mesh to see result.

Python console code, C = bpy.context

>>> mat = Matrix.Shear('XY', 4, (3,3))
>>> mat
Matrix(((1.0, 0.0, 3.0, 0.0),
        (0.0, 1.0, 3.0, 0.0),
        (0.0, 0.0, 1.0, 0.0),
        (0.0, 0.0, 0.0, 1.0)))

>>> C.object.data.transform(mat)
>>> C.object.data.update()

But this is transforms the vertices for good. In the modal operator, I need to apply it to the original positions of the sphere on each update. And the matrix is not necessarily invertable, so I can't just do the inverse beforehand, which makes it tricky

Shear matrix is not orthogonal, this is why it is not seen as an object matrix, and only in edit mode.

>>> mat.is_orthogonal
False

AFAIK will need to apply to verts to shear.

Some ways to copy mesh coords, swap out a mesh copy

>>> me = C.object.data
>>> me2 = C.object.data.copy()
>>> me2.transform(mat)
>>> C.object.data = me2 # see transform
>>> C.object.data = me # back to original
>>> bpy.data.meshes.remove(me2) # remove it when done

Foreach set and get

load the 3d vert coordinates into a ravelled array. Have used numpy as the matrix algebra could be performed on it reshaped, although will find Mesh.transform is quick too.

>>> import numpy as np
>>> data = np.zeros(len(me.vertices) * 3)
>>> me.vertices.foreach_get("co", data)
>>> data
array([ 0.00000000e+00,  1.95090324e-01,  9.80785251e-01, ...,
        1.51397259e-08,  1.95089921e-01, -9.80785310e-01])

The data is ravelled, here it is reshaped to display the coordinates

>>> np.reshape(data, (-1, 3))
array([[ 0.00000000e+00,  1.95090324e-01,  9.80785251e-01],
       [ 0.00000000e+00,  3.82683456e-01,  9.23879504e-01],
       [ 0.00000000e+00,  5.55570245e-01,  8.31469595e-01],
       ...,
       [ 1.52975474e-07,  7.07106352e-01, -7.07106769e-01],
       [ 7.84696610e-08,  3.82682920e-01, -9.23879623e-01],
       [ 1.51397259e-08,  1.95089921e-01, -9.80785310e-01]])

Transform the data, update to see result

>>> me.transform(mat)
>>> me.update()

write back, update to see result.

>>> me.vertices.foreach_set("co", data)
>>> me.update()

Another OTT method would be to make a shape key and transform that.

$\endgroup$
5
  • $\begingroup$ But this is transforms the vertices for good. In the modal operator, I need to apply it to the original positions of the sphere on each update. And the matrix is not necessarily invertable, so I can't just do the inverse beforehand, which makes it tricky. $\endgroup$
    – LivelyLiz
    Mar 29, 2020 at 13:46
  • 1
    $\begingroup$ Copy the mesh on invoke? Or copy vert coords with foreach_get and write with foreach_set or similarly with a bmesh AFAIK shear is not applicable as an object matrix since the axes need to be orthogonal, hence no shear available outside of edit mode. $\endgroup$
    – batFINGER
    Mar 29, 2020 at 13:59
  • $\begingroup$ I didn't know about foreach_get and foreach_set! Thanks for the hint. Do you how to use it with vectors? The documentation states "Only works for ‘basic type’ properties (bool, int and float)!" $\endgroup$
    – LivelyLiz
    Mar 29, 2020 at 14:12
  • 1
    $\begingroup$ Thanks a bunch. I already spend hours trying to wrap my head around this problem with the vertex transformation $\endgroup$
    – LivelyLiz
    Mar 29, 2020 at 14:49
  • $\begingroup$ Pleasure. Thanks for updating Q on request. Good question. $\endgroup$
    – batFINGER
    Mar 29, 2020 at 15:01
1
$\begingroup$

I guess I'm too late to the party but there is still a bit hacky way to do it - won't affect the actual geometry and will allow you to add ANY possible transform to your project, even a shear transform.

In Blender there is a weird thing called Parent Inverse Matrix that's getting applied when you parent one object to another and don't use Without Inverse option. The way it's calculated it's basically just object.parent.matrix_world.inverted() at the moment when you added a parent - so it's kind of compensating the initial parent transform so child transform stays intact when you just created a relationship between objects. What is important is that how it is applied and stored. Taking those two parent matrices into account matrix_world for object is calculated like obj.matrix_world = obj.parent.matrix_world @ (obj.matrix_parent_inverse @ obj.matrix_local).

There is annoyoing thing about matrix_parent_inverse that you cannot see it from the UI, you can access it only from python. Reason for that is that it can contain non-uniform transformations like a shear trasform that cannot be decomposed to location+rotation+scale. But this is the advantage we're going to use.

Basic object transform (and kind of delta transform too) is what I call decomposion safe matrix - meaning Blender keep it that way so that it can be safely decomposed and assembled back to the same matrix. Basically, Matrix.LocRotScale(*matrix.decompose()) == matrix. This is why it didn't worked for the author with matrix_local and delta_location/delta_rotation/delta_scale.

So, the solution I suggest is to store a shear in matrix_parent_inverse:

  1. Create an empty object B that will be used only to make matrix_parent_inverse available.
  2. Make B parent of A.
  3. Set A.matrix_parent_inverse to shear transformation.
  4. Voila.

Easy way to reproduce it is to run the code below in any Blender scene.

import bpy
from mathutils import Matrix

for obj in bpy.data.objects:
    bpy.data.objects.remove(obj)

weird_transform = bpy.data.objects.new("Weird Transform", None)
bpy.context.scene.collection.objects.link(weird_transform)

bpy.ops.mesh.primitive_cube_add()
shear_cube = bpy.data.objects['Cube']
shear_cube.name = "Sheared Cube"

shear_cube.parent = weird_transform
shear_cube.matrix_parent_inverse = Matrix.Shear('XY', 4, (3,3))
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .