13
$\begingroup$

Currently I only know of one way to join meshes - using the join operator:

bpy.ops.object.join()

This has an annoying overhead of additional operations required to select the objects you want to join, etc.

But what if I only want to join elements from two different meshes? This will be even more cumbersome with traditional methods: select the object, go to edit mode, select the relevant elements, duplicate them, separate them to a new mesh, join the new mesh with the other mesh...

It would be much nicer if I could just create two bmesh objects, then directly copy elements from one to the other. Something like this currently illegal code:

for v in bm1.verts:
    bm2.verts.add( v )

# or:
bm2.edges.add( bm1.edges[5] )

At the moment this requires adding the vertices first via their coordinates, then adding faces or edges by referencing the relevant new verts.

# Example for adding all the edges from another bmesh object
current_last_vert_index = len( bm2.verts ) - 1
for v in bm1.verts:
    bm2.verts.new( v.co )

for e in bm1.edges:
    verts = [ bm2.verts[ current_last_vert_index + v.index ] for v in e.verts ]
    bm2.edges.new( verts )

So, any better way to do this?

$\endgroup$
5
  • $\begingroup$ It sounds like you could copy the whole source mesh object in question 1st, then isolate the mesh verts/edges/faces you wish to keep, then inverse that selection to delete those unwanted items, then join that mesh to your intended destination mesh. It should at least keep you from having to do the rebuilding of the edges and faces. $\endgroup$
    – Rick Riggs
    Commented Apr 5, 2016 at 16:42
  • $\begingroup$ Yeah that might save a few steps, though hopefully there's an elegant and more direct way to do this $\endgroup$
    – TLousky
    Commented Apr 5, 2016 at 18:33
  • 2
    $\begingroup$ There is bmesh.ops.split but unfortunately you get "NotImplementedError: split: keyword "dest" type 4 not working yet!" when making another bmesh the destination. My suggestion would be use from_pydata and even that has a complication of moving the origin of the second objects vert coords to first object's local space first (which you need to do in code above), as well as materials UVs etc etc. Perhaps the best option is overriding context (eg 'override_context["selected_objects"] = [ob1, ob2]' and passing to the join operator. $\endgroup$
    – batFINGER
    Commented Apr 6, 2016 at 4:39
  • 4
    $\begingroup$ @batFINGER there is also bmesh.ops.duplicate that unfortunately is also unimplemented when using dest. Maybe a friendly word to some devs can get this working in the next release. I think I'll go add it to rightclickselect.com $\endgroup$
    – sambler
    Commented Apr 6, 2016 at 17:42
  • 2
    $\begingroup$ I struggle with this as well. I wrote a util to do exactly as you describe, transform coordinates and create new verts in the target mesh. Sometimes though, the indexing freaks out and any new faces are all screwed up, even when I am keeping track if the indexing in my utility. I'm really ready for bmesh.ops.duplicate to work! $\endgroup$
    – patmo141
    Commented Jun 17, 2016 at 16:19

4 Answers 4

10
$\begingroup$

The best solution I found so far is using the bmesh.from_mesh( mesh ) method. Apparently, if you call this method more than once, it will add the 2nd mesh to the first, thus effectively joining them:

import bpy, bmesh

bm = bmesh.new()
bm.from_mesh( mesh1 ) # Add first mesh
bm.from_mesh( mesh2 ) # Add 2nd mesh

However, this does not really help with adding individual components from another mesh or bmesh in a direct manner.

$\endgroup$
2
  • 1
    $\begingroup$ Would need to translate the vert coords of second mesh, to local space of first, before loading mesh2 to emulate bpy.ops.object.join. Eg def cube, duplicate, translate, rotate and scale the dupe, then join. Just loading the two in bmesh gives 24 duplicate verts, whereas join op will give a mesh with two distinct cubes. $\endgroup$
    – batFINGER
    Commented Apr 6, 2016 at 18:18
  • $\begingroup$ That makes sense, @batFINGER. I only tested it with two different meshes that both had their origins at the world origin, so it didn't make a difference. $\endgroup$
    – TLousky
    Commented Apr 6, 2016 at 19:07
7
$\begingroup$

Here's what i'm using in Sverchok at the moment, it's not heavily tested yet. It is derived from Sverchok's bmesh_from_pydata function (which is extensively tested)

import bmesh

def bmesh_join(list_of_bmeshes, normal_update=False):
    """ takes as input a list of bm references and outputs a single merged bmesh 
    allows an additional 'normal_update=True' to force _normal_ calculations.
    """

    bm = bmesh.new()
    add_vert = bm.verts.new
    add_face = bm.faces.new
    add_edge = bm.edges.new

    for bm_to_add in list_of_bmeshes:
        offset = len(bm.verts)

        for v in bm_to_add.verts:
            add_vert(v.co)

        bm.verts.index_update()
        bm.verts.ensure_lookup_table()

        if bm_to_add.faces:
            for face in bm_to_add.faces:
                add_face(tuple(bm.verts[i.index+offset] for i in face.verts))
            bm.faces.index_update()

        if bm_to_add.edges:
            for edge in bm_to_add.edges:
                edge_seq = tuple(bm.verts[i.index+offset] for i in edge.verts)
                try:
                    add_edge(edge_seq)
                except ValueError:
                    # edge exists!
                    pass
            bm.edges.index_update()

    if normal_update:
        bm.normal_update()

    return bm

you may have to clear or free the content of the list_of_bmeshes variable after it completes, but i suspect the memory is freed after the scope ends.


In the scenario where you are operating on bmeshes derived from objects with unapplied Matrices, one can do

def bmesh_join(list_of_bmeshes, list_of_matrices, normal_update=False):

    ...

    for bm_to_add, matrix in zip(list_of_bmeshes, list_of_matrices):
        bm_to_add.transform(matrix)

        ...

or transform them before passing them to the function at the top of this answer.

$\endgroup$
3
$\begingroup$

I need to go through and optimize this, but it's working for me

def join_bmesh_map(source, target, src_trg_map = None, src_mx = None, trg_mx = None):
    '''
    source - bmesh to bring data from
    target - bmesh to place data into
    scr_trg_map - if overlapping parts of source and target, this will substitute new and old geometry.  Good for filling holes and sewing an inner patch to the perimeter of the hole

    '''
    L = len(target.verts)

    if not src_trg_map:
        src_trg_map = {-1:-1}  #dummy dictionary
    if not src_mx:
        src_mx = Matrix.Identity(4)
    if not trg_mx:
        trg_mx = Matrix.Identity(4)
        i_trg_mx = Matrix.Identity(4)
    else:
        i_trg_mx = trg_mx.inverted()


    old_bmverts = [v for v in target.verts]  #this will store them in order
    new_bmverts = [] #these will be created in order

    source.verts.ensure_lookup_table()

    for v in source.verts:
        if v.index not in src_trg_map:
            new_ind = len(target.verts)
            new_bv = target.verts.new(i_trg_mx * src_mx * v.co)
            new_bmverts.append(new_bv)  #gross...append
            src_trg_map[v.index] = new_ind

    lverts = old_bmverts + new_bmverts

    new_bmfaces = []
    for f in source.faces:
        v_inds = []
        for v in f.verts:
            new_ind = src_trg_map[v.index]
            v_inds.append(new_ind)

        new_bmfaces += [target.faces.new(tuple(lverts[i] for i in v_inds))]

    target.faces.ensure_lookup_table()
    target.verts.ensure_lookup_table()

    new_L = len(target.verts)

    if src_trg_map:
        if new_L != L + len(source.verts) -l:
            print('seems some verts were left in that should not have been')
$\endgroup$
1
  • 1
    $\begingroup$ This has worked so far, even if the BMeshe's involved have been modified which can cause vertex indices to go NUTS and I mean NUTS every time a new vertex is added. So if you are wondering why I keep a list of all BMVerts, please read here blender.stackexchange.com/questions/14636/… $\endgroup$
    – patmo141
    Commented Jun 17, 2016 at 20:01
2
$\begingroup$

Below is another way to copy bmesh data from one into another. By using tags and layers (thanks to this discussion: Python: assign custom tag to vertices?), more information is held in the vertices of the source-bmesh.

def join_bmesh(target_bm, source_bm):
    '''
    source_bm into target_bm
    returns target_bm with added geometry, if source_bm is not empty.
    '''

    source_bm.verts.layers.int.new('index')
    idx_layer = source_bm.verts.layers.int['index']

    for face in source_bm.faces:
        new_verts = []
        for old_vert in face.verts:
            #tag is False by defualt, Im using it to mean its been added
            if not old_vert.tag:
                new_vert = target_bm.verts.new(old_vert.co)
                target_bm.verts.index_update()
                old_vert[idx_layer] = new_vert.index
                old_vert.tag = True

            target_bm.verts.ensure_lookup_table()
            idx = old_vert[idx_layer]
            new_verts.append(target_bm.verts[idx])

        target_bm.faces.new(new_verts)
    return target_bm
$\endgroup$

You must log in to answer this question.

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