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 :wink:

#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.