3
$\begingroup$

Hi I have been trying to develop code which shows the amount of deformation on a mesh's edges by coloring the vertices a value which depends on the average change in edge length for the connected vertices. The code currently works but is very slow. Does anyone have a good idea on a way to speed it up?

Here is the code I have so far

# Get a BMesh representation
def color_vertex_bm(me): # me is a mesh from me = bpy.context.active_object.data

    bm = bmesh.new()   # create an empty BMesh
    bm.from_mesh(me)   # fill it in from a Mesh
    bm.faces.ensure_lookup_table()

    Ege = get_edges(me)
    sE = (Ege-Ege_0)/Ege_0    # *** Ege_0 is a global variable that stores all initial edge lengths 
    # print(sE)

    # Get first color layer (create if needed)
    if not bm.loops.layers.color:
        bm.loops.layers.color.new('Col')
    layer = bm.loops.layers.color[0]

    for face in bm.faces:

        for loop in face.loops:
            # get vert coord
            current_vert_coord = loop.vert.co
            v = loop.vert
            # get all the edges that belong to this vertex
            Eges = [e.index for e in bm.edges if v in e.verts]
            avg =(np.average(sE[Eges]))
            # custom color map lookup for the average value
            rgba = colormap_jet(avg)
            # assign rgba to the loop
            loop[layer] = rgba
        
    # Finish up, write the bmesh back to the mesh
    bm.to_mesh(me)
    bm.free()  # free and prevent further access

def get_edges(me):

    Ege_local = np.array([])    
    bm = bmesh.new()   # create an empty BMesh
    bm.from_mesh(me)   # fill it in from a Mesh
    bm.faces.ensure_lookup_table()
    
    #get all edge lengths
    for e in bm.edges:
        ege = e.calc_length() 
        Ege_local = np.append(Ege_local, ege)
                
    # Finish up, write the bmesh back to the mesh
    bm.free()  # free and prevent further access

    return Ege_local
$\endgroup$
3
$\begingroup$

Using numpy and foreach_get

In as much as I love using bmesh, for speed it is quicker to use numpy in conjunction with partners in crime foreach_get and foreach_set in object mode from the mesh object.

My numpy knowledge is rudimentary, so there is a chance that this can be optimised further.

enter image description here Simple Test Run, "Copy" on right of cube on left. Black for no link edge change, red else

import bpy
import numpy as np

class BATVertColors:
    def color_by_verts(self, colors):
        cols = colors[self.vert_index_table]
        self.vcolor_layer.foreach_set(
                "color",
                cols.ravel()
                )
    def __init__(self, me, vcollayer):
        
        nloops = len(me.loops)
        vert_index_table = np.zeros(nloops, dtype=np.int32) 
        me.loops.foreach_get(
                "vertex_index",
                vert_index_table
                )
        self.vcolor_layer = vcollayer
        self.vert_index_table = vert_index_table
        

class BATMesh:
    def vert_link_edges(self, index):
        return np.logical_or(*np.isin(self.edges, index).T)    
    def __init__(self, me, vcol="Col"):
        nverts = len(me.vertices)
        nedges = len(me.edges)
        #nloops = len(me.loops)

        vcoords = np.empty(3 * nverts)
        me.vertices.foreach_get("co", vcoords)
        edges = np.zeros(2 * nedges, dtype=np.int32)
        me.edges.foreach_get("vertices", edges)

        vcoords3d = vcoords.reshape((-1, 3))
        a, b = edges.reshape((-1, 2)).T

        edge_lengths = np.linalg.norm(
                vcoords3d[a] - vcoords3d[b],
                axis=1,
                )
        self.edges = edges.reshape((-1, 2))
        self.vcoords3d = vcoords3d
        self.edge_lengths = edge_lengths
        
        # colors
        
        col = me.vertex_colors.get(vcol)
        self.vcol_layer = BATVertColors(me, col.data) if col else None

# give it a test run

context = bpy.context
scene = context.scene
                
ob = bpy.context.object
me = ob.data

mesh0 = BATMesh(me)

# warped object copy
copy = scene.objects.get("Copy")
assert(copy) # <--- 
mesh1 = BATMesh(copy.data)
nverts = len(copy.data.vertices)
delta_edge_length = (mesh1.edge_lengths - mesh0.edge_lengths) / mesh0.edge_lengths
rgbas = np.array([[0, 0, 0, 1]] * nverts)
# could be a more numpy-centric way to do please share
for i in range(nverts):
    link_edges = mesh0.vert_link_edges(i)
    m = np.mean(delta_edge_length[link_edges])
    if m > 0:
        rgbas[i] = (1, 0, 0, 1)

mesh1.vcol_layer.color_by_verts(rgbas)

Some notes.

Have reduced to no bmesh and "for x in y" looping over the vertices once. Looping is generally the rate determining step. Using methods as displayed above is going to be a lot quicker.

Some minor tweaks to speed up question code.

Since it's there, and it works, it's worth making a few comments re possible ways to optimize too

The default cube has 8 verts and 24 loops. In question code each vert is being processed for each connected face (loop), or 3 times. A default dictionary could be used here, or another way to keep tabs not to process a vert more than once. (Example of a vert index to loop index LUT)

>>> from collections import default_dict

>>> lut = defaultdict(list)
>>> for f in bm.faces:
...     for l in f.loops:
...         lut[l.vert.index].append(l.index)
...         
>>> pprint.pprint(lut)
defaultdict(<class 'list'>,
            {0: [0, 15, 19],
             1: [1, 14, 22],
             2: [3, 4, 16],
             3: [2, 5, 21],
             4: [11, 12, 18],
             5: [10, 13, 23],
             6: [7, 8, 17],
             7: [6, 9, 20]})

In this case tho would loop verts, and set color for each then when looping faceloops

loop[layer] = colors[loop.vert.index]

Bmesh has worked out all the links for us

This is going to be very expensive (re above) each time looping over all edges to find a vert's linked edges,

# get all the edges that belong to this vertex
Eges = [e.index for e in bm.edges if v in e.verts]

and can be replaced with v.link_edges eg for cube

>>> for v in bm.verts:
...     [e.index for e in v.link_edges]
...     
[0, 1, 10]
[1, 2, 11]
[0, 3, 4]
[2, 3, 5]
[7, 9, 10]
[8, 9, 11]
[4, 6, 7]
[5, 6, 8]
$\endgroup$
2
  • $\begingroup$ Thank you for your awesome response, it is way more then I expected! * more numpy centric way : for "rgbas = np.array([[0, 0, 0, 1]] * nverts)" * can be changed to rgbas = np.zeros(np.shape(nverts)) $\endgroup$ – Alex Mcghee Mar 8 at 15:51
  • $\begingroup$ Cheers and NP coding up some "BAT" stuff and gave a chance to test it out Intriguing choice to use mean edge lengths over vert co moving. also curious re colormap_jet(avg). Ok re edit yeah that looks better.. (in hindsight np.ones prob matches default better) $\endgroup$ – batFINGER Mar 8 at 16:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.