1
$\begingroup$

The fastest way I know to get the vertices coordinates from the mesh is to use .foreach_get() method:

import bpy
import numpy as np


def vertices_co_to_numpy():
    ob = bpy.context.object
    vertices = ob.data.vertices
    target = np.zeros([len(vertices)*3], dtype="f")

    vertices.foreach_get("co", target)
    print(target)


vertices_co_to_numpy()

It works extremely fast, but returns vertices in the local coordinates. The way to get vertex global coordinates is to multiply object matrix world by these coordinates:

import bpy
from bpy.types import Object, MeshVertex


def vertex_global_co(obj: Object, vertex: MeshVertex):
    print(obj.matrix_world @ vertex.co)


vertex_global_co(bpy.context.object, bpy.context.object.data.vertices[0])

So the most straightforward way to get global coordinates is to multiply object matrix world by each vertex coordinates and assign them to numpy array in a for loop, but it is obviously too slow. Before I proceed I have to warn you that I am really not good in linear algebra, so I have only a very rough idea of what I am trying to perform below. I've tried to make numpy.matrix from the object matrix world, but it could not be multiplied by numpy array made of vertices coordinates because object matrix world is 4D and the vertices coordinates are 3D. So I've tried to convert object matrix world to 3x3 matrix first and it even works, but returns a matrix instead of converted array and the values are different:

import bpy
import numpy as np


def vertices_global_co():
    obj = bpy.context.object
    verts = obj.data.vertices
    mw = obj.matrix_world.to_3x3()
    obj_matrix = np.matrix(mw)

    # Store vertices to 1D numpy array
    verts_co_1D = np.zeros([len(verts)*3], dtype='f')
    verts.foreach_get("co", verts_co_1D)
    
    # Convert 1D array to array of 3D arrays
    verts_co_3D = np.zeros([len(verts), 3], dtype='f')
    verts_co_3D[:, 0] = verts_co_1D[0::3]
    verts_co_3D[:, 1] = verts_co_1D[1::3]
    verts_co_3D[:, 2] = verts_co_1D[2::3]
    
    reference_matrix = np.array([obj.matrix_world @ v.co for v in verts])
    result_matrix = verts_co_3D[:] @ obj_matrix

    print(f"Reference: {reference_matrix}\n")
    print(f"Result: {result_matrix}\n")

    
vertices_global_co()

Then I've found out that object matrix world starts to ignore object location after converting it to 3x3 matrix.

import bpy


def matrix_check():
    obj = bpy.context.object
    print(obj.matrix_world.decompose())
    print(obj.matrix_world.to_3x3().to_4x4().decompose())

    
matrix_check()

So I have tried to sum the result with array filled with object location coordinates. I've tried to do it before and after multiplying it by the world matrix, but it doesn't give a correct result in either ways:

import bpy
import numpy as np
    
    
def vertices_global_co():
    obj = bpy.context.object
    verts = obj.data.vertices
    mw = obj.matrix_world.to_3x3()
    obj_matrix = np.matrix(mw)

    # Store vertices to 1D numpy array
    verts_co_1D = np.zeros([len(verts)*3], dtype='f')
    verts.foreach_get("co", verts_co_1D)
    
    # Convert 1D array to array of 3D arrays
    verts_co_3D = np.zeros([len(verts), 3], dtype='f')
    verts_co_3D[:, 0] = verts_co_1D[0::3]
    verts_co_3D[:, 1] = verts_co_1D[1::3]
    verts_co_3D[:, 2] = verts_co_1D[2::3]

    # Get numpy array with object location coordinates
    loc_array = np.zeros([len(verts), 3], dtype='f')
    loc_array[:, 0].fill(obj.location[0])
    loc_array[:, 1].fill(obj.location[1])
    loc_array[:, 2].fill(obj.location[2])
    
    reference_matrix = np.array([obj.matrix_world @ v.co for v in verts])
    
#    verts_co_3D += loc_array    
    verts_co_3D[:] = verts_co_3D @ obj_matrix
    verts_co_3D += loc_array # you may comment this line and uncomment the same line above

    print(f"Reference: {reference_matrix}\n")
    print(f"Result: {verts_co_3D}\n")

    
vertices_global_co()

I even tried to make a copy of the object and its mesh, apply transforms and get vertices from the resulting mesh, but it is even slower than making it directly in a for loop. To set vertex coordinates to numpy array for 1 200 000 polygons object it takes about:

  • .foreach_get() method: 0.007-0.008 seconds,
  • for loop: 1,3-1,5 seconds
  • get from temporal object and mesh copy: 3,5+ seconds I believe there should be a way to get the result with numpy and some linear algebra, but I've got not enough skills and knowledge for this. Can anyone help, please?
$\endgroup$
1

1 Answer 1

1
$\begingroup$

The mistake in the last example was in attempt to multiply coordinates by matrix while the proper way should be to multiply matrix by coordinates. I haven't figured out how to make it via Numpy, but the opposite way turned out to be to invert matrix first and then to use it for vertex coordinates multiplication. Also from here I've figured out that top left 3x3 values in the 4x4 matrix determine rotation of the vector, and the top 3 values in the right column determine location offset of the vector. So the proper way was to:

  • Invert object matrix world
  • Convert it to 3x3 matrix
  • Multiply (get dot product of) vertices coordinates by the resulting matrix
  • Add object location (first 3 values in the 4x4 matrix right column) to all vertices coordinates
import bpy
import numpy as np
import time
    
def vertices_global_co():
    t1 = time.perf_counter()
    obj = bpy.context.object
    verts = obj.data.vertices
    vlen = len(verts)
    obj_matrix = np.matrix(obj.matrix_world.inverted().to_3x3())

    
    verts_co_1D = np.zeros([vlen*3], dtype='f')
    verts.foreach_get("co", verts_co_1D)
    verts_co_3D = verts_co_1D.reshape(vlen, 3)
    
#    reference = np.array([obj.matrix_world @ v.co for v in verts])
      
    verts_co_3D = verts_co_3D @ obj_matrix
    verts_co_3D += np.array(obj.location)

#    print(f"Reference: {reference}\n")
    print(f"Result: {verts_co_3D}\n")
    print(f"Time: {time.perf_counter()-t1}")
    
vertices_global_co()

I've tried it only with rotation and location changes. The scale adjustment probably needs more operations.

UPD. Thanks to @scurest for the explanation of the difference between inverted and transposed matrices, for pointing at difference between np.zeros vs np.empty and for the given example in the comment. Here is the updated code which takes all objects transformations into account:

import bpy
from bpy.types import Object
import numpy as np
import time
    
    
def vertices_global_co(obj: Object) -> np.ndarray:
    """Return numpy array with object vertices global coordinates"""
    
    rotation_and_scale = obj.matrix_world.to_3x3().transposed()  # rotation and scale matrix (mathutils.Matrix)
    offset = obj.matrix_world.translation  # location offset vector (mathutils.Vector)
    vertices_blender = obj.data.vertices  # Blender mesh vertices (bpy.types.MeshVertices)
    vlen = len(vertices_blender)  # Number of vertices (int)
    
    vertices_local = np.empty([vlen*3], dtype='f')  # Empty numpy 1D array, each value needs to be initialized
    vertices_blender.foreach_get("co", vertices_local)  # Store each local coordinate of each vertex to empty 1D array
    vertices_local = vertices_local.reshape(vlen, 3)  # Make 3D array with vertices local coordinates from 1D array
    vertices_global = np.matmul(vertices_local, rotation_and_scale)  # multiply each vertex coordinates by given matrix
    vertices_global += offset  # Apply location offset
    
#    # uncomment the following to compare with Blender matrix multiplication reference:
#    reference_matrix = np.array([obj.matrix_world @ v.co for v in verts])
#    print(f"Reference: {reference_matrix}\n")
    
    return vertices_global
    

if __name__ == "__main__":
    t1 = time.perf_counter()
    result = vertices_global_co(bpy.context.object)
    print(f"\nResult: {result}")
    print(f"Time: {time.perf_counter()-t1} sec")
$\endgroup$
2
  • 2
    $\begingroup$ See this. It's not the inverse but the transpose (it's a row-major/column-major conversion). The inverse only works for pure rotations (since for rotations the inverse is the transpose). Also for the translation you need the translation part of the world matrix, not ob.location. Btw, it may be slightly faster to use np.empty over np.zeros. $\endgroup$
    – scurest
    Commented May 25, 2022 at 0:11
  • $\begingroup$ Thank you! This is really useful, I've updated the answer. $\endgroup$ Commented May 25, 2022 at 9:07

You must log in to answer this question.

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