# Where to improve performance of my Skript?

Hello everyone,

I currently work on a python script, which involves a kind of self-made array modifier. It creates a mesh along a path, whose coordinates are stored in an array named “Coordinates”. The coordinates of the object, which is supposed to be repeated along this path, are stored in an array named “CrossSection”.

My script is working absolutely fine for me in terms of actual results. But it takes my computer about 20 minutes to create the mesh of a path with 2500 points and a cross-section with 16 points. Additionally to that, the calculation time does not increase linear at all. 1000 points of the same cross-section would only take two minutes. If you run my script, you’ll see the non-linear progress in the system console.

Since I am not a professional programmer at all, this is the first time I ever had to wonder about the performance of my script. I already did everything I could, but my knowledge is quite restricted, so I hoped such a high-skill-level-community could maybe give me a hint or two of how to improve my efficiency here.

Thanks to everyone in advance, who even reads my script

``````#The following script is not performance critical.
#But the ciritical part needs it to work in Blender.
#So skip reading this part for now.
#You will only need it, if you want to test the script in Blender.

import bpy
import os.path
import bmesh
import math
import numpy as np

Coordinates = {}
CrossSection = {}
TotalRows = 1000
TotalPoints = 10
CoordRows = np.zeros((3,1))
CoordPoints = np.zeros((3,1))
CoordRes = np.zeros((3,1))
RotX = np.zeros((3,3))
RotY = np.zeros((3,3))
RotZ = np.zeros((3,3))
RotZX = np.zeros((3,3))
RotRes = np.zeros((3,3))
RotX[0,0] = 1
RotY[1,1] = 1
RotZ[2,2] = 1

for RowNo in range(0,TotalRows):
Coordinates[RowNo,0] = RowNo
Coordinates[RowNo,1] = 0
Coordinates[RowNo,2] = 0
Coordinates[RowNo,3] = 0
Coordinates[RowNo,4] = 0
Coordinates[RowNo,5] = 0

for PointNo in range(0,TotalPoints):
CrossSection[PointNo,0] = 0
CrossSection[PointNo,1] = PointNo-round(TotalPoints/2)
CrossSection[PointNo,2] = 0

ObjMesh = bpy.data.meshes.new("Mesh")
Obj = bpy.data.objects.new("Object", ObjMesh)
bpy.context.scene.objects.link(Obj)
bpy.context.scene.objects.active = Obj

#So now the interesting stuff...
#This is the performance-critical part:

for RowNo in range(0,TotalRows):    #loop through every row in the coordinates array
AngleX = Coordinates[RowNo,3]*math.pi/180   #some variable readouts from the coordinates array
AngleY = Coordinates[RowNo,4]*math.pi/180
AngleZ = Coordinates[RowNo,5]*math.pi/180
RotX[1,1] = math.cos(AngleX)    #definition of rotation matrices
RotX[1,2] = (-1)*math.sin(AngleX)
RotX[2,2] = math.cos(AngleX)
RotX[2,1] = math.sin(AngleX)
RotY[0,0] = math.cos(AngleY)
RotY[0,2] = math.sin(AngleY)
RotY[2,0] = (-1)*math.sin(AngleY)
RotY[2,2] = math.cos(AngleY)
RotZ[0,0] = math.cos(AngleZ)
RotZ[0,1] = (-1)*math.sin(AngleZ)
RotZ[1,0] = math.sin(AngleZ)
RotZ[1,1] = math.cos(AngleZ)
RotZX = np.dot(RotZ,RotX)   #generating resulting rotation matrix
RotRes = np.dot(RotZX,RotY)
for PointNo in range(0,TotalPoints):    #loop through every point of the cross-section
PreviousVertex = (RowNo-1)*TotalPoints+PointNo  #defining vertices, that will be used for new faces
CurrentVertex = RowNo*TotalPoints+PointNo
bm = bmesh.new()    #prepares blender for new mesh coming up
bm.from_mesh(Obj.data)
for Dimension in range(0,3):    #read out the coordinates of current row
CoordRows[Dimension,0] = Coordinates[RowNo,Dimension]
CoordPoints[Dimension,0] = CrossSection[PointNo,Dimension]
CoordPoints[0] = 0  #otherwise this slot would content absolute nonsense. A cross sections does not content x-coordinates
CoordPoints[1,0] = CoordPoints[1,0]
CoordRes = CoordRows+np.dot(RotRes,CoordPoints) #calculates coordinates in context with the rotation matrix
bm.verts.new((CoordRes[0,0], CoordRes[1,0], CoordRes[2,0])) #creates new vertex
bm.to_mesh(Obj.data)    #assings new vertex to object
bm.free()   #dunno myself why I have to put this here...
if (PointNo > 0) and (RowNo > 0):   #to create new faces you need an existing row already
bpy.context.tool_settings.mesh_select_mode=[True,False,False]   #I want to select vertices next
if (((RowNo/2) - int(RowNo/2)) == ((PointNo/2) - int(PointNo/2))):  #I want neighboring triangle pairs to have different orientation. Dont't ask...
bm = bmesh.new()    #again new mesh (this time for the faces instead of the vertices)
bm.from_mesh(Obj.data)
bm.verts.ensure_lookup_table() #I received an error message telling me to put this line here
bm.faces.new([bm.verts[PreviousVertex-1], bm.verts[PreviousVertex],bm.verts[CurrentVertex-1]])  #Creates my two triangles connected to the new vertex
bm.faces.new([bm.verts[CurrentVertex-1], bm.verts[PreviousVertex],bm.verts[CurrentVertex]])
bm.to_mesh(Obj.data)    #assings new faces to object
bm.free()
else:   #simply the same as above, but the different orientation of the faces
bm = bmesh.new()
bm.from_mesh(Obj.data)
bm.verts.ensure_lookup_table()
bm.faces.new([bm.verts[CurrentVertex-1], bm.verts[PreviousVertex-1],bm.verts[CurrentVertex]])
bm.faces.new([bm.verts[PreviousVertex-1], bm.verts[PreviousVertex],bm.verts[CurrentVertex]])
bm.to_mesh(Obj.data)
bm.free()
print(str(RowNo) + ' of ' + str(TotalRows)) # your progress output in the system console, Sir!
``````

Variables should be named with lowercase letters, it will make your script easier to read for us. What programming language do you come from?

The main problem is probably in calling bmesh.from_object() so many times. I’m quite sure it’s enough to call this function once in your script, if you do it right. Get the call out of the `for` loop.

Instead of using dictionaries with 2d-indexing like `CrossSection[PointNo,Dimension]`, use a list of mathutils.Vector.

Regarding RotX, RotY and RotZ, it would save you a lot of thinking if you use mathutils.Matrix.Rotation.

1 Like

First of all: I always thought my way to name variables was great cause you could easily identify whats variable and whats command. Sorry for that. The only language I previously worked with was VBA (and probably matlab if it counts).

Your idea to call bmesh.from_object() outside the for loop was brilliant. I took me some time to figure it out, but finally I managed to use the command just twice. Now I get the same result within couple of seconds instead of some 20 minutes. Great! I’m really thankful for this advice. It has completely solved the entire issue. Yey!

Sadly I didn’t figure out how to use the rotation matrix, though. I guess it’s quite simple but it didn’t work for me yet. Using a mathutils Vector might be a great idea for the coordinates, but in my - let’s say more complex - real script it doesn’t make a lot of sense, because the dictionarys also content some strings.

I basically just split the troubleshooting part in two. In the first part I create all the verts and in the second part I use them to create the faces. First I thought this had to be much less time efficient, since I run through the same loop twice now, but it turned out to be about 1000x quicker. Here is the improved version of my script (this time in lowercase letters):

``````bm = bmesh.new()
bm.from_mesh(obj.data)
for rowno in range(0,totalrows):
anglex = coordinates[rowno ,3]*math.pi/180
angley = coordinates[rowno ,4]*math.pi/180
anglez = coordinates[rowno ,5]*math.pi/180
rotx[1,1] = math.cos(anglex)
rotx[1,2] = (-1)*math.sin(anglex)
rotx[2,2] = math.cos(anglex)
rotx[2,1] = math.sin(anglex)
roty[0,0] = math.cos(angley)
roty[0,2] = math.sin(angley)
roty[2,0] = (-1)*math.sin(angley)
roty[2,2] = math.cos(angley)
rotz[0,0] = math.cos(anglez)
rotz[0,1] = (-1)*math.sin(anglez)
rotz[1,0] = math.sin(anglez)
rotz[1,1] = math.cos(anglez)
rotzx = np.dot(rotz,rotx)
rotres = np.dot(rotzx,roty)
for pointno in range(0,totalpoints):
for dimension in range(0,3):
coordrows[dimension,0] = coordinates[rowno ,dimension]
coordpoints[dimension,0] = crosssection[pointno ,dimension]
coordpoints[0] = 0
coordpoints[1,0] = coordpoints[1,0]
coordres = coordrows+np.dot(rotres,coordpoints)
bm.verts.new((coordres[0,0], coordres[1,0], coordres[2,0]))
print(str(rowno) + ' of ' + str(totalrows))
bm.to_mesh(obj.data)
bm.free()

bpy.context.tool_settings.mesh_select_mode=[True,False,False]
bm = bmesh.new()
bm.from_mesh(obj.data)
for rowno in range(1,totalrows):
for pointno in range(1,totalpoints):
previousvertex = (rowno-1)*totalpoints+pointno
currentvertex = rowno*totalpoints+pointno
bm.verts.ensure_lookup_table()
if ((( rowno/2) - int( rowno/2)) == ((pointno/2) - int(pointno/2))):
bm.faces.new([bm.verts[previousvertex-1], bm.verts[previousvertex],bm.verts[currentvertex-1]])
bm.faces.new([bm.verts[currentvertex-1], bm.verts[previousvertex],bm.verts[currentvertex]])
else:
bm.faces.new([bm.verts[currentvertex-1], bm.verts[previousvertex-1],bm.verts[currentvertex]])
bm.faces.new([bm.verts[previousvertex-1], bm.verts[previousvertex],bm.verts[currentvertex]])
print(str(rowno) + ' of ' + str(totalrows))
bm.to_mesh(obj.data)
bm.free()
``````

I took the liberty to rewrite the relevant parts using `mathutils.Vector` and `mathutils.Matrix`. You may use it as reference, or copy the code as you wish:

``````#The following script is not performance critical.
#But the ciritical part needs it to work in Blender.
#So skip reading this part for now.
#You will only need it, if you want to test the script in Blender.

import bpy
import os.path
import bmesh
import mathutils
import math
import numpy as np

coordinates = []
crossection = []
angles = []
totalrows = 100
totalpoints = 10

for rowno in range(totalrows):
co = mathutils.Vector((rowno, 0, 0))
angle = [0, 0, 0]
coordinates.append((co, angle))

for pointno in range(totalpoints):
co = mathutils.Vector((0, pointno - round(totalpoints / 2), 0))
crossection.append(co)

objmesh = bpy.data.meshes.new("Mesh")
obj = bpy.data.objects.new("Object", objmesh)
bpy.context.scene.objects.link(obj)
bpy.context.scene.objects.active = obj

#So now the interesting stuff...
#This is the performance-critical part:

bm = bmesh.new()
bm.from_mesh(obj.data)
for coordinate, angle in coordinates:
anglex, angley, anglez = (math.radians(value) for value in angle)
rotx = mathutils.Matrix.Rotation(anglex, 3, 'X')
roty = mathutils.Matrix.Rotation(angley, 3, 'Y')
rotz = mathutils.Matrix.Rotation(anglez, 3, 'Z')
rotres = rotz * rotx * roty
for coordpoints in crossection:
coordrows = coordinate
coordpoints.x = 0
coordres = coordrows + rotres * coordpoints
bm.verts.new(coordres)
print(str(rowno) + ' of ' + str(totalrows))

bm.verts.ensure_lookup_table()
for rowno in range(1, totalrows):
for pointno in range(1, totalpoints):
previousvertex = (rowno-1)*totalpoints+pointno
currentvertex = rowno*totalpoints+pointno
if ((( rowno/2) - int( rowno/2)) == ((pointno/2) - int(pointno/2))):
bm.faces.new([bm.verts[previousvertex-1], bm.verts[previousvertex],bm.verts[currentvertex-1]])
bm.faces.new([bm.verts[currentvertex-1], bm.verts[previousvertex],bm.verts[currentvertex]])
else:
bm.faces.new([bm.verts[currentvertex-1], bm.verts[previousvertex-1],bm.verts[currentvertex]])
bm.faces.new([bm.verts[previousvertex-1], bm.verts[previousvertex],bm.verts[currentvertex]])
print(str(rowno) + ' of ' + str(totalrows))
bm.to_mesh(obj.data)
bm.free()
``````

The `*` sign represents matrix multiplication, quite the same as `np.dot`. In blender 2.80, it will change to the `@` sign.

Instead of using `range`, it is often easier to loop through a list directly. Instead of “`for pointno in range(0,totalpoints)`”, I wrote “`for coordpoints in crossection`”.

If you’d like perfection in the variable names, uppercase letters etc., you can look into PEP 8.