Search code examples
pythonctypesblenderbpy

Accessing C pointers to vertices in Blender's Python API


I'm currently making a render engine in C and C++ for Blender. I want to access the vertices of a mesh from C via a pointer, to reduce the time spent in Python and avoid unneeded data duplication.

I am aware that objects derived from the ID class, there is an as_pointer() method available. However, when I tried to use as_pointer() on the vertices collection of a mesh like so:

mesh = bpy.context.object.data
pointer = mesh.vertices.as_pointer()

I received an error stating that bpy_prop_collection object has no attribute as_pointer, which makes sense as bpy_prop_collection isn't derived from ID.

The documentation mentions states that the type of verticesis "MeshVertices bpy_prop_collection of MeshVertex, (readonly)" (doc), and MeshVertices should be abe to return a pointer, but this isn't the type of neither vertices nor it elements.

As a workaround, I've been retrieving the vertices data into a numpy array which is then passed onto my C library, as shown in the following example code:

import bpy
import numpy as np
import ctypes

obj = bpy.context.object  # Suppose 'obj' is the mesh object
mesh = obj.data

# Allocate an array and copy the data, then set the pointer
# of the struct to the array
vert_array = np.zeros((len(mesh.vertices) * 3), dtype=np.float32)
mesh.vertices.foreach_get("co", vert_array)
vert_ptr = vert_array.ctypes.data_as(ctypes.POINTER(ctypes.c_float))

# Pass the pointer
lib = ctypes.CDLL("bin/libengine.so")
lib.load_vert.argtypes = [ctypes.POINTER(ctypes.float)]
lib.load_vert(vert_ptr)

However, this approach duplicates the vertex data in memory (once in Blender's internal data structures, and once in the numpy array) and require processing which could be avoided.

I've looked into Blender's source code and noticed that the underlying C/C++ API does allow direct memory access. By looking at the BKE_mesh_vert_positions and CustomData_get_layer_named functions, we see that the vertices are stored in a contiguous data block:

BLI_INLINE const float (*BKE_mesh_vert_positions(const Mesh *mesh))[3]
{
  return (const float(*)[3])CustomData_get_layer_named(&mesh->vdata, CD_PROP_FLOAT3, "position");
}
const void *CustomData_get_layer_named(const CustomData *data,
                                       const eCustomDataType type,
                                       const char *name)
{
  int layer_index = CustomData_get_named_layer_index(data, type, name);
  if (layer_index == -1) {
    return nullptr;
  }
  return data->layers[layer_index].data;
}

This means that we could have, at least in theory, a pointer to the data.

Is there a method in the Python API to expose these pointers or a way to work with the C/C++ API from Python to get this memory directly, without having to compile a custom version of Blender?

Any guidance on how to directly access these pointers or alternative solutions that avoid memory duplication would be highly appreciated.


Solution

  • The ctypes module can create a C-compatible array from a Python list of Vertex objects. But you can also just take the pointer of the first vertex mesh.vertices[0].as_pointer() because the pointer to the first element is also the pointer to the entire array. I tested this on Windows 11 but it should also work on Linux. I compiled the following C code print_pointer.c using the Ubuntu app with cross-compiler command x86_64-w64-mingw32-gcc -shared -o print_pointer.dll print_pointer.c. I assume you are on Linux and compiled it with gcc -shared -fPIC -o print_pointer.so print_pointer.c

    #include <stdio.h>
    
    typedef struct {
        float* data;
        int num_vertices;
    } VertexData;
    
    void print_pointer(VertexData* vertex_data) {
        printf("Vertices:\n");
        for (int i = 0; i < vertex_data->num_vertices; i += 3) {
            float* vertex = vertex_data->data + i;
            printf("Vertex %d: (%f, %f, %f)\n", i / 3, vertex[0], vertex[1], vertex[2]);
        }
    }
    

    Here's the Python script:

    import bpy
    import ctypes
    
    lib = ctypes.CDLL('/path/to/dll_or_so/print_pointer.dll') # in your case your .so 
    
    class VertexData(ctypes.Structure):
        _fields_ = [("data", ctypes.POINTER(ctypes.c_float)), ("num_vertices", ctypes.c_int)]
    
    obj = bpy.context.object
    mesh = obj.data
    
    num_vertices = len(mesh.vertices) * 3
    ptr = mesh.vertices[0].as_pointer()
    ptr_as_float = ctypes.cast(ptr, ctypes.POINTER(ctypes.c_float))
    vertex_data = VertexData(ptr_as_float, num_vertices)
    
    lib.print_pointer(ctypes.byref(vertex_data))
    

    Result for the Default Cube: enter image description here