5
$\begingroup$

I made a Blender addon that thickens surfaces like this: enter image description here

You assign the inner surface to a vertex group named InnerGroupTriangle and the outer surface to OuterGroup. The inner surface must be made of triangles. Then you run the script and the outer surface scales away from the inner surface. The script works but is very slow. I can't figure out why.

Here is the code:

import bpy

def is_edge_in_group(edge,object,group_id):
    for v_index in edge.vertices:
        is_in_group=False
        for g in object.data.vertices[v_index].groups:
            if g.group==group_id:
                is_in_group=True
                break
        if not is_in_group:
            return False
    return True

def check_if_face_in_vertex_group(vertices,group_index):

    for face_vert in vertices:
        face_vert_in_group=False
        for g in face_vert.groups:
            if g.group == group_index:
                face_vert_in_group=True
                break
        if not face_vert_in_group:
            return False
    return True

def is_vertex_in_group(vertex,group_id):
    for g in vertex.groups:
        if g.group == group_id:
            return True
    return False

def change_vector_basis_from_orthogonal(rootv1,rootv2,rootv3,vector_to_transform):
    px=vector_to_transform.x
    py=vector_to_transform.y
    pz=vector_to_transform.z
    x1=rootv1.x
    y1=rootv1.y
    z1=rootv1.z
    x2=rootv2.x
    y2=rootv2.y
    z2=rootv2.z
    x3=rootv3.x
    y3=rootv3.y
    z3=rootv3.z
    #print(rootv1,rootv2,rootv3,vector_to_transform)
    try:
        a=(pz*(-(x3*y2) + x2*y3))/(-(x3*y2*z1) + x2*y3*z1 + x3*y1*z2 - x1*y3*z2 - x2*y1*z3 + x1*y2*z3) + (py*(x3*z2 - x2*z3))/(-(x3*y2*z1) + x2*y3*z1 + x3*y1*z2 - x1*y3*z2 - x2*y1*z3 + x1*y2*z3) + (px*(-(y3*z2) + y2*z3))/(-(x3*y2*z1) + x2*y3*z1 + x3*y1*z2 - x1*y3*z2 - x2*y1*z3 + x1*y2*z3)

        b=(pz*(x3*y1 - x1*y3))/(-(x3*y2*z1) + x2*y3*z1 + x3*y1*z2 - x1*y3*z2 - x2*y1*z3 + x1*y2*z3) + (py*(-(x3*z1) + x1*z3))/(-(x3*y2*z1) + x2*y3*z1 + x3*y1*z2 - x1*y3*z2 - x2*y1*z3 + x1*y2*z3) + (px*(y3*z1 - y1*z3))/(-(x3*y2*z1) + x2*y3*z1 + x3*y1*z2 - x1*y3*z2 - x2*y1*z3 + x1*y2*z3)

        c=  (pz*(-(x2*y1) + x1*y2))/(-(x3*y2*z1) + x2*y3*z1 + x3*y1*z2 - x1*y3*z2 - x2*y1*z3 + x1*y2*z3) + (py*(x2*z1 - x1*z2))/(-(x3*y2*z1) + x2*y3*z1 + x3*y1*z2 - x1*y3*z2 - x2*y1*z3 + x1*y2*z3) + (px*(-(y2*z1) + y1*z2))/(-(x3*y2*z1) + x2*y3*z1 + x3*y1*z2 - x1*y3*z2 - x2*y1*z3 + x1*y2*z3)
    except ZeroDivisionError:
        #print (rootv1,rootv2,rootv3,vector_to_transform)
        return (-1,-1,-1)
    return (a,b,c)



def calculate_root_vectors_and_transform_vertex(point,edge,ob,ig_polygons):

    vertices_in_edge=[ob.data.vertices[x] for x in edge.vertices]
    #print(list(vertices_in_edge))

    polygons=ig_polygons
    polygons_next_to_edge=[]

    for polygon in polygons:#finds the faces next to the edge
        edge_in_polygon=True
        for v_index in edge.vertices:
            if not v_index in polygon.vertices:
                edge_in_polygon=False
                break
        if edge_in_polygon:
            polygons_next_to_edge.append(polygon)
        if len(polygons_next_to_edge)==2:
            break

    edge_vector=(vertices_in_edge[1].co-vertices_in_edge[0].co)
    if len(polygons_next_to_edge)==2:#edge next to two faces
        second_face_normal=polygons_next_to_edge[1].normal
    if len(polygons_next_to_edge)==1:#if edge is next to only one face, condider the normal of the other face to be perpendicular to the edge and the only face's normal, away from the face.

        normal = polygons_next_to_edge[0].normal
        for edge_face_vertex_index in polygons_next_to_edge[0].vertices:#finds the vertex on the face next to the edge that isn't in the edge
            if not edge_face_vertex_index in edge.vertices:
                face_vertex_not_in_edge=ob.data.vertices[edge_face_vertex_index]
                break

        cross=edge_vector.cross(face_vertex_not_in_edge.co-vertices_in_edge[0].co)
        direction=1
        if normal.x !=0:
            if cross.x/normal.x <0:
                direction = -1
        elif normal.y !=0:
            if cross.y/normal.y <0:
                direction = -1
        else:
            if cross.z/normal.z <0:
                direction = -1

        second_face_normal=direction*edge_vector.cross(polygons_next_to_edge[0].normal)


    (a,b,c)=change_vector_basis_from_orthogonal(edge_vector,polygons_next_to_edge[0].normal,second_face_normal,point)
    return (edge_vector,polygons_next_to_edge[0].normal,second_face_normal,a,b,c)

def try_to_move_relative_to_edge(point,ob,ig_edges,multiplier,ig_polygons):
    adequete_edge_and_distance=()
    for edge in ig_edges:#lets go through edges and find the one that is closest to the point 
        (edge_vector,normal1,normal2,a,b,c)=calculate_root_vectors_and_transform_vertex(point.co-ob.data.vertices[edge.vertices[0]].co,edge,ob,ig_polygons)
        #print (edge_vector,normal1,normal2,a,b,c)
        distance=(b*normal1+c*normal2).length

        if (len(adequete_edge_and_distance)==0 or distance<adequete_edge_and_distance[1])and 0<=a<=1 and 0<=b and 0<=c:
            adequete_edge_and_distance=(edge,distance)

    if not len(adequete_edge_and_distance)==0:
        adequete_edge=adequete_edge_and_distance[0]
        (edge_vector,normal1,normal2,a,b,c)=calculate_root_vectors_and_transform_vertex(point.co-ob.data.vertices[adequete_edge_and_distance[0].vertices[0]].co,adequete_edge,ob,ig_polygons)

        point.co=a*edge_vector+multiplier*b*normal1+multiplier*c*normal2+ob.data.vertices[adequete_edge.vertices[0]].co
        return True
    return False




ob = bpy.context.object
multiplier = 2
print ("start")

ogi = ob.vertex_groups["OuterGroup"].index # get group index
igi= ob.vertex_groups["InnerGroupTriangle"].index 
ig_edges=[x for x in ob.data.edges if is_edge_in_group(x,ob,igi)]
ig_polygons=[x for x in ob.data.polygons if check_if_face_in_vertex_group([ob.data.vertices[i] for i in x.vertices],igi)]
ig_vertices=[x for x in ob.data.vertices if is_vertex_in_group(x,igi)]
i=0
to_go_trough=len(ob.data.vertices)
for v in ob.data.vertices:
    i+=1
    print(i,"/",to_go_trough)
    #input("Press Enter to continue...")
    if is_vertex_in_group(v,ogi):#go through only vertices that are in outer group

        projection_was_made=False
        adequate_polygon_and_distance=()

        #print("############# vertex:",v,v.co,"*************")
        for polygon in ig_polygons:
            verts_in_face = polygon.vertices[:]
            #print(verts_in_face," checking")


            #print(verts_in_face,"in inner group")
            (a,b,c)=change_vector_basis_from_orthogonal(ob.data.vertices[verts_in_face[1]].co-ob.data.vertices[verts_in_face[0]].co  ,ob.data.vertices[verts_in_face[2]].co-ob.data.vertices[verts_in_face[0]].co,polygon.normal,    v.co-ob.data.vertices[verts_in_face[0]].co)
            #print("a:",a,"b:",b,"c:",c)

            if 0<=a and 0<=b and a+b<=1 and 0<=c:
                #print("abc in range")
                if len(adequate_polygon_and_distance)==0 or c < adequate_polygon_and_distance[1]:
                    #print("cccccccccccccc",c)
                    adequate_polygon_and_distance=(polygon,c)


        if len(adequate_polygon_and_distance)>0:
            #print(adequate_polygon_and_distance[0].vertices[:],"//////////////////////////////////")

            verts_in_face = adequate_polygon_and_distance[0].vertices[:]
            (a,b,c)=change_vector_basis_from_orthogonal(ob.data.vertices[verts_in_face[1]].co-ob.data.vertices[verts_in_face[0]].co  ,ob.data.vertices[verts_in_face[2]].co-ob.data.vertices[verts_in_face[0]].co,adequate_polygon_and_distance[0].normal,    v.co-ob.data.vertices[verts_in_face[0]].co)
            #print(a,b,c)
            v.co=a*(ob.data.vertices[verts_in_face[1]].co-ob.data.vertices[verts_in_face[0]].co)+b*(ob.data.vertices[verts_in_face[2]].co-ob.data.vertices[verts_in_face[0]].co)+multiplier*c*adequate_polygon_and_distance[0].normal+ob.data.vertices[verts_in_face[0]].co
            print("polygon")
            projection_was_made=True

        if not projection_was_made:#vertex v wasn't above a face in inner group
            #print("edges")
            if try_to_move_relative_to_edge(v,ob,ig_edges,multiplier,ig_polygons):
                print("edge")
                projection_was_made=True


        if  not projection_was_made:#vertex v wasn't above an edge in inner group
            print("vertex")
            point=v
            vertex_and_distance=()
            for inner_vertex in ig_vertices:
                if (len(vertex_and_distance)==0 or vertex_and_distance[1]>(inner_vertex.co-point.co).length):
                    #print("sdfsdf")
                    vertex_and_distance=(inner_vertex,(inner_vertex.co-point.co).length)



            point.co=vertex_and_distance[0].co+multiplier*(point.co-vertex_and_distance[0].co)
    #bpy.context.scene.objects.active = bpy.context.scene.objects.active
$\endgroup$
4
  • 4
    $\begingroup$ Do you run this once, or drag a slider in the redo panel to adjust the thickness? Anyway: there are a lot of loops, and you should optimize them away as much as possible. For group checks, it's wise to build an own lookup dictionary. There are probably more algorithmic improvements like this (pre-compute something that helps to avoid costly operations). RNA access can be slow too. Aliasing may help, or auxiliary data structures stored using primitive Python data types such as dicts, lists... $\endgroup$
    – CodeManX
    Commented Jul 31, 2015 at 18:38
  • 2
    $\begingroup$ Is that printing a lot? Print usually is one of the slowest operations in scripting. Other than that I'd look into custom data arrays. You can try to find the bottlenecks with time.time() checks. $\endgroup$
    – kheetor
    Commented Oct 19, 2016 at 6:11
  • 1
    $\begingroup$ Just to echo kheetor, print slows down everything, even worse when its happening in a loop. $\endgroup$
    – cmomoney
    Commented Jan 21, 2017 at 17:45
  • $\begingroup$ You may want to look up the else clause of a for statement. It won't solve the problem, but will make your code simpler. $\endgroup$
    – dr. Sybren
    Commented Oct 15, 2017 at 19:37

2 Answers 2

1
$\begingroup$

The code has many loops and python calls. In general when optimizing you can look for a better approach in achieving your goal making less clock-cycles and making it therefore faster.

Looking at the script I would first think of a datastructure that will allow you quick lookups and thereby remove the for loops.

This can be done by using python's dict. You can create a dict with keys being faces and their values a list of the vertices. A lookup in a dict is much cheaper than a lookup in a list.

Also the order of execution can help. First find all vertice_indexes and that belong to the InnerGroupTriangle group. It will be easier to use the in statement in stead of the loop you have now.

def is_vertex_in_group(vertex,group):
    """ Use the group object in stead of the group_id """
    return group in vertex.groups

vertex_indexes_in_group = set([i for i, x in ob.data.vertices.items() if is_vertex_in_group(x, ig)])

Finding edges will be much easier as you can just check if both vertex indexes are inside the set

return set(edge.vertices) & vertex_indexes_in_group

As you see with just some simple adjustments I removed a lot of code, complexity and increased the performance. 3D programming is really knowing what datastructure to use for the task.

After making sure your algoritm is more optimized. You can also find area's that need attention by profiling. See https://docs.python.org/3/library/profile.html it will give you a report what part of your code takes most time and most number of calls.

$\endgroup$
0
$\begingroup$

My first script had two nested loops, which resulted in many "saved" steps which can be undone by Ctrl-Z. Using the default value for steps save resulted in the script taking days to complete. Resetting the allowable saved steps to 5 lets the script run in a few hours.
How increase/decrease undo steps?

$\endgroup$
1
  • $\begingroup$ This seems like a "kludge" rather than a solution. There must be some way to avoid this problem, as there are far more complicated addons that don't have this problem. I suspect there's a way to either 1) tell Blender "none of these are 'undoable' steps," 2) prepare all the data ahead of time and execute in one step, or 3) use functions/methods that don't generate 'undo' steps. $\endgroup$
    – Matt
    Commented Aug 8, 2017 at 12:38

You must log in to answer this question.

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