NEVER access mesh.vertices, mesh.edges, or mesh.polygons in Edit Mode — data is not synchronized. Use bmesh.from_edit_mesh() instead.
NEVER skip bm.free() after bmesh.new() or bm.from_mesh() — causes memory leaks.
NEVER skip mesh.update() after mesh.from_pydata() — normals and internal state remain invalid.
NEVER skip bm.verts.ensure_lookup_table() before index access — raises IndexError.
NEVER use mesh.calc_normals() in Blender 4.0+ — removed, normals are auto-calculated.
ALWAYS call obj_eval.to_mesh_clear() after reading evaluated mesh data — prevents memory leaks.
ALWAYS call mesh.validate() after building mesh from raw data — catches degenerate geometry silently.
Essential Patterns
Pattern 1: Create Mesh from Vertices (from_pydata)
Use for simple meshes under ~1000 vertices. Suitable for building floor plans, room outlines, simple architectural elements.
# Blender 3.x/4.x/5.ximport bpy
defcreate_mesh_from_pydata(name, verts, edges, faces):
"""Create a mesh object from vertex/edge/face data.
Args:
name: Object and mesh name
verts: List of (x, y, z) tuples
edges: List of (v1, v2) tuples. Pass [] if faces are provided.
faces: List of vertex index tuples, e.g. [(0,1,2,3)]
Returns:
The created bpy.types.Object
"""
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(verts, edges, faces)
mesh.update() # REQUIRED: recalculates normals and internal state
mesh.validate() # Catches degenerate geometry
obj = bpy.data.objects.new(name, mesh)
bpy.context.collection.objects.link(obj) # REQUIRED: makes object visiblereturn obj
from_pydata rules:
Pass edges=[] when providing faces — edges are auto-generated from face definitions.
Vertex indices in faces MUST be valid indices into the verts list.
Face winding order determines normal direction (counter-clockwise = outward by Blender convention).
Use for meshes with > 1000 vertices. Required for IFC geometry visualization and large AEC models. 10-100x faster than per-element access.
# Blender 3.x/4.x/5.ximport bpy
import numpy as np
defcreate_mesh_fast(name, coords, face_indices, face_sizes=None):
"""Create mesh using pre-allocation and foreach_set.
Args:
name: Mesh and object name
coords: numpy array of shape (N, 3), dtype float32 — vertex positions
face_indices: flat numpy array of vertex indices for all faces, dtype int32
face_sizes: numpy array of verts-per-face, dtype int32. Default: all triangles.
Returns:
The created bpy.types.Object
"""
vert_count = len(coords)
loop_count = len(face_indices)
if face_sizes isNone:
face_count = loop_count // 3
face_sizes = np.full(face_count, 3, dtype=np.int32)
else:
face_count = len(face_sizes)
mesh = bpy.data.meshes.new(name)
mesh.vertices.add(vert_count)
mesh.loops.add(loop_count)
mesh.polygons.add(face_count)
# Set vertex coordinates — flatten to (N*3,) for foreach_set
mesh.vertices.foreach_set("co", coords.reshape(-1).astype(np.float32))
# Set loop vertex indices
mesh.loops.foreach_set("vertex_index", face_indices.astype(np.int32))
# Set polygon loop_start and loop_total
loop_starts = np.zeros(face_count, dtype=np.int32)
loop_starts[1:] = np.cumsum(face_sizes[:-1])
mesh.polygons.foreach_set("loop_start", loop_starts)
mesh.polygons.foreach_set("loop_total", face_sizes)
mesh.update()
mesh.validate()
obj = bpy.data.objects.new(name, mesh)
bpy.context.collection.objects.link(obj)
return obj
foreach_set rules:
Input array MUST be flat (1D). For coordinates: shape (N*3,), not (N, 3).
dtype MUST match: float32 for coordinates/normals, int32 for indices.
Array length MUST exactly match count * attribute_dimension. Mismatches cause crashes.
loop_start values MUST be cumulative sums of loop_total values.
Call mesh.update() after all foreach_set calls.
Pattern 3: BMesh for Topology Operations
Use BMesh when you need extrude, inset, dissolve, merge, or other topology-modifying operations. Required for parametric building element generation.
# Blender 3.x/4.x/5.ximport bpy
import bmesh
defbmesh_create_and_extrude(name, base_verts, base_face, extrude_height):
"""Create a mesh from a base polygon and extrude upward.
Args:
name: Object name
base_verts: List of (x, y, z) for the base polygon
base_face: Tuple of vertex indices for the base face
extrude_height: Height to extrude along Z
Returns:
The created bpy.types.Object
"""
bm = bmesh.new()
# Create base vertices
bm_verts = [bm.verts.new(co) for co in base_verts]
bm.verts.ensure_lookup_table()
# Create base face
base = bm.faces.new([bm_verts[i] for i in base_face])
# Extrude face upward
result = bmesh.ops.extrude_face_region(bm, geom=[base])
extruded_verts = [e for e in result['geom'] ifisinstance(e, bmesh.types.BMVert)]
bmesh.ops.translate(bm, vec=(0, 0, extrude_height), verts=extruded_verts)
# Write to mesh
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
bm.free() # ALWAYS free
mesh.update()
obj = bpy.data.objects.new(name, mesh)
bpy.context.collection.objects.link(obj)
return obj
BMesh rules:
ALWAYS call bm.verts.ensure_lookup_table() (and edges/faces) before index access.
ALWAYS call bm.free() when using bmesh.new() or bm.from_mesh().
NEVER call bm.free() when using bmesh.from_edit_mesh() — use bmesh.update_edit_mesh() instead.
After bmesh.ops.* calls, element references may be invalidated. Re-query from results.
BMesh element .index values are NOT stable across topology operations. Use ensure_lookup_table() after modifications.
Pattern 4: Read Evaluated (Post-Modifier) Mesh
Use to read mesh data after all modifiers are applied. Required for mesh analysis and export workflows.
# Blender 3.x/4.x/5.ximport bpy
defget_evaluated_mesh_data(obj):
"""Get vertex and face data from evaluated (post-modifier) mesh.
Args:
obj: bpy.types.Object with mesh data
Returns:
Tuple of (vertices_list, faces_list)
"""
depsgraph = bpy.context.evaluated_depsgraph_get()
obj_eval = obj.evaluated_get(depsgraph)
mesh_eval = obj_eval.to_mesh()
verts = [v.co.copy() for v in mesh_eval.vertices]
faces = [list(p.vertices) for p in mesh_eval.polygons]
obj_eval.to_mesh_clear() # ALWAYS clean upreturn verts, faces
Pattern 5: IFC Geometry Visualization in Blender
Use to display IFC model geometry in Blender viewport. Combines IfcOpenShell geometry processing with foreach_set for performance.
# Blender 3.x/4.x/5.x: requires ifcopenshellimport bpy
import numpy as np
import ifcopenshell
import ifcopenshell.geom
defvisualize_ifc_element(ifc_file, element, collection=None):
"""Create Blender mesh from IFC element geometry.
Args:
ifc_file: ifcopenshell.file object
element: IFC element with geometry (e.g., IfcWall, IfcSlab)
collection: Target collection. Default: active collection.
Returns:
The created bpy.types.Object, or None if no geometry
"""
settings = ifcopenshell.geom.settings()
settings.set("use-world-coords", True)
try:
shape = ifcopenshell.geom.create_shape(settings, element)
except RuntimeError:
returnNone# Element has no geometry representation# Extract geometry data
verts_flat = shape.geometry.verts # Flat list: [x0,y0,z0, x1,y1,z1, ...]
faces_flat = shape.geometry.faces # Flat list: [i0,i1,i2, i3,i4,i5, ...]
coords = np.array(verts_flat, dtype=np.float32).reshape(-1, 3)
indices = np.array(faces_flat, dtype=np.int32)
vert_count = len(coords)
tri_count = len(indices) // 3
mesh = bpy.data.meshes.new(element.Name orf"IFC_{element.id()}")
mesh.vertices.add(vert_count)
mesh.loops.add(tri_count * 3)
mesh.polygons.add(tri_count)
mesh.vertices.foreach_set("co", coords.flatten())
mesh.loops.foreach_set("vertex_index", indices)
loop_starts = np.arange(0, tri_count * 3, 3, dtype=np.int32)
loop_totals = np.full(tri_count, 3, dtype=np.int32)
mesh.polygons.foreach_set("loop_start", loop_starts)
mesh.polygons.foreach_set("loop_total", loop_totals)
mesh.update()
mesh.validate()
obj = bpy.data.objects.new(element.Name orf"IFC_{element.id()}", mesh)
target = collection or bpy.context.collection
target.objects.link(obj)
return obj
For AEC models exceeding 10,000 vertices, ALWAYS use foreach_get/foreach_set with numpy arrays. Per-element Python loops on large meshes cause unacceptable latency in Blender's main thread.
Reference Links
references/methods.md — Complete API signatures for Mesh, BMesh, and foreach operations