7

Having recently answered a question re the Z surface math function, from the add mesh extras addon, made me get back to coding same via numpy for instance with a numpy meshgrid.

For example, to create a 4 x 5 vertices grid in blender, the grid's vertex indices would, from top view, look like

import numpy as np
rows = 5
cols = 4

A = np.flip(np.arange(rows * cols).reshape((-1, cols)), 0)

[[16 17 18 19] [12 13 14 15] [ 8 9 10 11] [ 4 5 6 7] [ 0 1 2 3]]

and then to skin the thing would make each face by winding CCW over each 2 x 2 quad

eg indices of some of the 12 faces

(0, 1, 5, 4)
(1, 2, 6, 5)
(2, 3, 7, 6)
...
(9, 10, 14, 13)
(10, 11, 15, 14)

and pass this to Mesh.from_pydata(verts, edges, faces)

All good, very quick, however, always feel there is a faster more numpy-centric way to generate the quad face indices.

Extract blocks or patches from NumPy Array

Reverse even rows in a numpy array

The question: given rows x cols vertices, what is the quickest way to create the faces?

batFINGER
  • 84,216
  • 10
  • 108
  • 233

2 Answers2

6

Using numpy 'only', in order to avoid Python iterations, that can be:

  • Generate all faces with an extra face for each row, using np.linspace.
  • Reshape to place the last face of each row as last element (in second dimension)
  • Delete the last column
  • Reshape again to obtain the result

Sample code (I've done no performance tests):

import numpy as np

Example

rows = 3 cols = 5

10, 11, 12, 13, 14,

5, 6, 7, 8, 9,

0, 1, 2, 3, 4,

first_face = [0, 1, 6, 5]

first_face = [0, 1, cols + 1, cols]

Last face index is on the top right

last_face_index = cols * (rows - 1)

last_face = [9, 10, 15, 14]

last_face = [i + last_face_index - 1 for i in first_face]

Generates all faces with an extra face per row (at the end of each row)

by linear interpolation

faces = np.linspace(first_face, last_face, last_face_index)

Deletes extra faces that are on the right

Result is rows - 1, all cols and 4 vertices

faces = np.reshape(faces, (rows - 1, cols, 4))

Remove the last col considering second axis

faces = np.delete(faces, cols - 1, 1)

Final result is arrays of 4 indices

faces = np.reshape(faces, (-1, 4))

print(faces) print(len(faces))

Alternatively, you can also use several np.linspace to define:

  • The first row (or column)
  • The last row (or column)
  • Interpolate between them
lemon
  • 60,295
  • 3
  • 66
  • 136
6

Test Results.

Big thanks to @lemon. The original thought behind this question was toward making a grid via numpy as fast as possible.

Test Script to add a grid using numpy, bmesh operator and operator.

For trivial sizes 10 x 10 numpy is well ahead, by 100 x 100 it is well, last. Cursory investigation shows mesh.from_pydata in script below, is the rate determining step.

Points toward using a hybrid method to create the grid with bmesh then manipulate later with numpy using foreach_get and foreach_set

import bpy
import numpy as np
import bmesh
from time import time
from bpy import context

def to_object(name, me): ob = bpy.data.objects.new(name, me) context.collection.objects.link(ob) return ob

def bmesh_grid(rows, cols): me = bpy.data.meshes.new("BMGrid") bm = bmesh.new() bmesh.ops.create_grid( bm, x_segments=cols, y_segments=rows, ) bm.to_mesh(me)

def np_grid(rows, cols):

m = np.dstack(
    np.meshgrid(
        np.linspace(-1, 1, cols),
        np.linspace(-1, 1, rows),
        )
    ).reshape(-1, 2)
verts = np.dstack(
    [m[:,0], 
    m[:, 1], 
    np.zeros(rows * cols)]
    ).reshape(-1, 3)

# skinning code courtesy of @lemon
first_face = [0, 1, cols + 1, cols]
last_face_index = cols  * (rows - 1)
last_face = [i + last_face_index - 1 for i in first_face]
faces = np.linspace(first_face, last_face, last_face_index)

#faces = faces[np.mod(np.arange(last_face_index), cols) > 0]
faces = np.reshape(faces, (rows - 1, cols, 4))
faces = np.delete(faces, cols - 1, 1)
faces = np.reshape(faces, (-1, 4))
me = bpy.data.meshes.new("NPGrid")
#https://developer.blender.org/T90268    
me.from_pydata(verts, [], faces)
# pre fix
#me.from_pydata(verts, [], list(faces))
to_object("NPGrid", me)  


test call

rows, cols = 500, 100

t = time() bmesh_grid(rows, cols) bm_time = time() - t print("BM", bm_time)

t = time()

np_grid(rows, cols) np_time = time() - t print("NP", np_time)

t = time() bpy.ops.mesh.primitive_grid_add( x_subdivisions=cols, y_subdivisions=rows, ) op_time = time() - t print("OP", op_time)

ugly one liner to remove all add grids

#bpy.data.batch_remove([o for o in bpy.data.objects if any(o.name.startswith(k) for k in ("Grid", "NPGrid", "BMGrid"))])

print(f"{np_time / op_time :.3f}")

batFINGER
  • 84,216
  • 10
  • 108
  • 233
  • In Blender 3.2 had to switch '#me.from_pydata(verts, [], list(faces))' for 'me.from_pydata(verts.tolist(), [], faces.astype(int))' – Tortenrandband Jun 23 '22 at 09:24