7
$\begingroup$

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?

$\endgroup$
6
  • $\begingroup$ geometry nodes! :D $\endgroup$
    – Chris
    Jul 14, 2021 at 12:02
  • 3
    $\begingroup$ I can already feel a great self answer coming $\endgroup$ Jul 14, 2021 at 12:21
  • $\begingroup$ i can smell that too....! i just wonder why it takes so long.... $\endgroup$
    – Chris
    Jul 14, 2021 at 12:40
  • 2
    $\begingroup$ Interesting problem. I'm not a numpy guy, no idea if my observations are useful at all, but vertex indices repeat (each appears up to 4 times), therefore simply reshaping the array is not going to work - but duplicating it 4 times and then reshaping and then intertwining (zipping) could work... And getting the CCW would be about reversing odd columns with even columns on odd rows... $\endgroup$ Jul 14, 2021 at 12:46
  • 1
    $\begingroup$ Maybe using a 1d array instead of 2d array ? Btw, look like rows and cols are inverted ? $\endgroup$
    – Gorgious
    Jul 14, 2021 at 13:12

2 Answers 2

6
$\begingroup$

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
$\endgroup$
0
6
$\begingroup$

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}")
$\endgroup$
1
  • $\begingroup$ In Blender 3.2 had to switch '#me.from_pydata(verts, [], list(faces))' for 'me.from_pydata(verts.tolist(), [], faces.astype(int))' $\endgroup$ Jun 23, 2022 at 9:24

You must log in to answer this question.

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