From 3a82ac0a33e9003e088fe2f0ea239cdd8b8b5e5c Mon Sep 17 00:00:00 2001 From: Hans Goudey Date: Wed, 8 Oct 2025 16:37:07 +0200 Subject: [PATCH] Mesh: Use attribute API in merge by distance code Split the result mesh generation to work into two loops. The first loop builds topology maps used for propagating attribute values, and the next loop mixes attribute values with those maps. This has a few benefits: The propagation can be done in parallel, and the topology mapping code can get simpler. Also, we reduce the overhead that comes with iterating over all attributes for every single element. Overall I didn't measure a large performance difference though, because the new code has the downside of needing to allocate two more arrays. There is a small difference of a propagated byte color (250 vs. 251) in the geometry nodes test due to a subtle difference in the mixing implementation. Part of #122398. Pull Request: https://projects.blender.org/blender/blender/pulls/147367 --- .../geometry/intern/mesh_merge_by_distance.cc | 433 ++++++++++++------ .../geometry/merge_by_distance.blend | 4 +- 2 files changed, 284 insertions(+), 153 deletions(-) diff --git a/source/blender/geometry/intern/mesh_merge_by_distance.cc b/source/blender/geometry/intern/mesh_merge_by_distance.cc index f562075c46e..f90c931d129 100644 --- a/source/blender/geometry/intern/mesh_merge_by_distance.cc +++ b/source/blender/geometry/intern/mesh_merge_by_distance.cc @@ -5,18 +5,22 @@ // #define USE_WELD_DEBUG // #define USE_WELD_DEBUG_TIME +#include "BKE_attribute_math.hh" #include "BLI_array.hh" #include "BLI_bit_vector.hh" #include "BLI_index_mask.hh" #include "BLI_kdtree.h" +#include "BLI_listbase.h" #include "BLI_math_vector.h" #include "BLI_offset_indices.hh" #include "BLI_vector.hh" +#include "BKE_attribute.hh" #include "BKE_customdata.hh" #include "BKE_mesh.hh" #include "DNA_meshdata_types.h" +#include "DNA_object_types.h" #include "GEO_mesh_merge_by_distance.hh" #include "GEO_randomize.hh" @@ -1279,7 +1283,7 @@ static void weld_mesh_context_create(const Mesh &mesh, /** \} */ /* -------------------------------------------------------------------- */ -/** \name CustomData +/** \name Merging * \{ */ /** @@ -1329,107 +1333,8 @@ static void merge_groups_create(Span dest_map, } } -static void customdata_weld( - const CustomData *source, CustomData *dest, const int *src_indices, int count, int dest_index) -{ - if (count == 1) { - CustomData_copy_data(source, dest, src_indices[0], dest_index, 1); - return; - } - - CustomData_interp(source, dest, src_indices, nullptr, count, dest_index); - - int src_i, dest_i; - int j; - - int vs_flag = 0; - - /* interpolates a layer at a time */ - dest_i = 0; - for (src_i = 0; src_i < source->totlayer; src_i++) { - const eCustomDataType type = eCustomDataType(source->layers[src_i].type); - - /* find the first dest layer with type >= the source type - * (this should work because layers are ordered by type) - */ - while (dest_i < dest->totlayer && dest->layers[dest_i].type < type) { - dest_i++; - } - - /* if there are no more dest layers, we're done */ - if (dest_i == dest->totlayer) { - break; - } - - /* if we found a matching layer, add the data */ - if (dest->layers[dest_i].type == type) { - void *src_data = source->layers[src_i].data; - if (type == CD_MVERT_SKIN) { - /* The `typeInfo->interp` of #CD_MVERT_SKIN does not include the flags, so #MVERT_SKIN_ROOT - * and #MVERT_SKIN_LOOSE are lost after the interpolation. - * - * This behavior is not incorrect. Ideally, islands should be checked to avoid repeated - * roots. - * - * However, for now, to prevent the loss of flags, they are simply re-added if any of the - * merged vertices have them. */ - for (j = 0; j < count; j++) { - MVertSkin *vs = &((MVertSkin *)src_data)[src_indices[j]]; - vs_flag |= vs->flag; - } - } - else if (CustomData_layer_has_interp(dest, dest_i)) { - /* Already calculated. - * TODO: Optimize by exposing `typeInfo->interp`. */ - } - else if (CustomData_layer_has_math(dest, dest_i)) { - const int size = CustomData_sizeof(type); - void *dst_data = dest->layers[dest_i].data; - void *v_dst = POINTER_OFFSET(dst_data, size_t(dest_index) * size); - for (j = 0; j < count; j++) { - CustomData_data_add( - type, v_dst, POINTER_OFFSET(src_data, size_t(src_indices[j]) * size)); - } - } - else { - CustomData_copy_layer_type_data(source, dest, type, src_indices[0], dest_index, 1); - } - - /* if there are multiple source & dest layers of the same type, - * we don't want to copy all source layers to the same dest, so - * increment dest_i - */ - dest_i++; - } - } - - float fac = 1.0f / count; - - for (dest_i = 0; dest_i < dest->totlayer; dest_i++) { - CustomDataLayer *layer_dst = &dest->layers[dest_i]; - const eCustomDataType type = eCustomDataType(layer_dst->type); - if (type == CD_MVERT_SKIN) { - MVertSkin *vs = &((MVertSkin *)layer_dst->data)[dest_index]; - vs->flag = vs_flag; - } - else if (CustomData_layer_has_interp(dest, dest_i)) { - /* Already calculated. */ - } - else if (CustomData_layer_has_math(dest, dest_i)) { - const int size = CustomData_sizeof(type); - void *dst_data = layer_dst->data; - void *v_dst = POINTER_OFFSET(dst_data, size_t(dest_index) * size); - CustomData_data_multiply(type, v_dst, fac); - } - } -} - /** - * \brief Applies to `CustomData *dest` the values in `CustomData *source`. - * - * This function creates the CustomData of the resulting mesh according to the merge map in - * `dest_map`. The resulting customdata will not have the source elements, so the indexes will be - * modified. To indicate the new indices `r_final_map` is also created. + * To indicate the new indices `r_final_map` is created. * * \param dest_map: Map that defines the source and target elements. The source elements will be * merged into the target. Each target corresponds to a group. @@ -1439,17 +1344,17 @@ static void customdata_weld( * * \return r_final_map: Array indicating the new indices of the elements. */ -static void merge_customdata_all(const CustomData *source, - CustomData *dest, - Span dest_map, +static void merge_customdata_all(Span dest_map, Span double_elems, const int dest_size, const bool do_mix_data, + Vector &r_src_index_offsets, + Vector &r_src_index_data, Array &r_final_map) { - UNUSED_VARS_NDEBUG(dest_size); - const int source_size = dest_map.size(); + r_src_index_offsets.reserve(dest_size + 1); + r_src_index_data.reserve(source_size); MutableSpan groups_offs_; Array groups_buffer; @@ -1468,31 +1373,25 @@ static void merge_customdata_all(const CustomData *source, bool finalize_map = false; int dest_index = 0; for (int i = 0; i < source_size; i++) { - const int source_index = i; - int count = 0; while (i < source_size && dest_map[i] == OUT_OF_CONTEXT) { - r_final_map[i] = dest_index + count; - count++; + r_final_map[i] = dest_index; + r_src_index_offsets.append_unchecked(r_src_index_data.size()); + r_src_index_data.append(i); + dest_index++; i++; } - if (count) { - CustomData_copy_data(source, dest, source_index, dest_index, count); - dest_index += count; - } + if (i == source_size) { break; } if (dest_map[i] == i) { if (do_mix_data) { - const IndexRange grp_buffer_range = groups_offs[i]; - customdata_weld(source, - dest, - &groups_buffer[grp_buffer_range.start()], - grp_buffer_range.size(), - dest_index); + r_src_index_offsets.append_unchecked(r_src_index_data.size()); + r_src_index_data.extend(groups_buffer.as_span().slice(groups_offs[i])); } else { - CustomData_copy_data(source, dest, i, dest_index, 1); + r_src_index_offsets.append_unchecked(r_src_index_data.size()); + r_src_index_data.append(i); } r_final_map[i] = dest_index; dest_index++; @@ -1527,6 +1426,8 @@ static void merge_customdata_all(const CustomData *source, } } + r_src_index_offsets.append_unchecked(r_src_index_data.size()); + BLI_assert(dest_index == dest_size); } @@ -1536,6 +1437,124 @@ static void merge_customdata_all(const CustomData *source, /** \name Mesh Vertex Merging * \{ */ +template +static void copy_first_from_src(const Span src, + const GroupedSpan dst_to_src, + MutableSpan dst) +{ + for (const int dst_index : dst.index_range()) { + const int src_index = dst_to_src[dst_index].first(); + dst[dst_index] = src[src_index]; + } +} + +static void mix_src_indices(const GSpan src_attr, + const GroupedSpan dst_to_src, + GMutableSpan dst_attr) +{ + bke::attribute_math::convert_to_static_type(src_attr.type(), [&](auto dummy) { + using T = decltype(dummy); + const Span src = src_attr.typed(); + MutableSpan dst = dst_attr.typed(); + threading::parallel_for(dst.index_range(), 2048, [&](const IndexRange range) { + for (const int dst_index : range) { + const Span src_indices = dst_to_src[dst_index]; + if (src_indices.size() == 1) { + dst[dst_index] = src[src_indices.first()]; + continue; + } + bke::attribute_math::DefaultMixer mixer({&dst[dst_index], 1}); + for (const int src_index : src_indices) { + mixer.mix_in(0, src[src_index]); + } + mixer.finalize(); + } + }); + }); +} + +static void mix_attributes(const bke::AttributeAccessor src_attributes, + const GroupedSpan dst_to_src, + const bke::AttrDomain domain, + const Set &skip_names, + bke::MutableAttributeAccessor dst_attributes) +{ + src_attributes.foreach_attribute([&](const bke::AttributeIter &iter) { + if (iter.domain != domain) { + return; + } + if (skip_names.contains(iter.name)) { + return; + } + const GVArraySpan src_attr = *iter.get(); + bke::GSpanAttributeWriter dst_attr = dst_attributes.lookup_or_add_for_write_only_span( + iter.name, iter.domain, iter.data_type); + mix_src_indices(src_attr, dst_to_src, dst_attr.span); + dst_attr.finish(); + }); +} + +static void mix_vertex_groups(const Mesh &mesh_src, + const GroupedSpan dst_to_src, + Mesh &mesh_dst) +{ + const char *func = __func__; + const Span src_dverts = mesh_src.deform_verts(); + if (src_dverts.is_empty()) { + return; + } + MutableSpan dst_dverts = mesh_dst.deform_verts_for_write(); + threading::parallel_for(dst_to_src.index_range(), 256, [&](const IndexRange range) { + struct WeightIndexGetter { + int operator()(const MDeformWeight &value) const + { + return value.def_nr; + } + }; + CustomIDVectorSet weights; + + for (const int dst_vert : range) { + MDeformVert &dst_dvert = dst_dverts[dst_vert]; + + const Span src_verts = dst_to_src[dst_vert]; + if (src_verts.size() == 1) { + const MDeformVert &src_dvert = src_dverts[src_verts.first()]; + dst_dvert.dw = MEM_malloc_arrayN(src_dvert.totweight, func); + std::copy_n(src_dvert.dw, src_dvert.totweight, dst_dvert.dw); + dst_dvert.totweight = src_dvert.totweight; + continue; + } + + const float src_num_inv = math::rcp(float(src_verts.size())); + for (const int src_vert : src_verts) { + const MDeformVert &src_dvert = src_dverts[src_vert]; + for (const MDeformWeight &src_weight : Span(src_dvert.dw, src_dvert.totweight)) { + const int i = weights.index_of_or_add(MDeformWeight{src_weight.def_nr, 0.0f}); + const_cast(weights[i]).weight += src_weight.weight * src_num_inv; + } + } + + std::sort(const_cast(weights.begin()), + const_cast(weights.end()), + [](const auto &a, const auto &b) { return a.def_nr < b.def_nr; }); + + dst_dvert.dw = MEM_malloc_arrayN(weights.size(), func); + dst_dvert.totweight = weights.size(); + std::copy(weights.begin(), weights.end(), dst_dvert.dw); + weights.clear_and_keep_capacity(); + } + }); +} + +static Set get_vertex_group_names(const Mesh &mesh) +{ + Set names; + LISTBASE_FOREACH (bDeformGroup *, group, &mesh.vertex_group_names) { + names.add(group->name); + } + return names; +} + static Mesh *create_merged_mesh(const Mesh &mesh, MutableSpan vert_dest_map, const int removed_vertex_count, @@ -1545,9 +1564,11 @@ static Mesh *create_merged_mesh(const Mesh &mesh, SCOPED_TIMER(__func__); #endif + const Span src_edges = mesh.edges(); const OffsetIndices src_faces = mesh.faces(); const Span src_corner_verts = mesh.corner_verts(); const Span src_corner_edges = mesh.corner_edges(); + const bke::AttributeAccessor src_attributes = mesh.attributes(); const int totvert = mesh.verts_num; const int totedge = mesh.edges_num; @@ -1559,61 +1580,122 @@ static Mesh *create_merged_mesh(const Mesh &mesh, const int result_nloops = src_corner_verts.size() - weld_mesh.loop_kill_len; const int result_nfaces = src_faces.size() - weld_mesh.face_kill_len + weld_mesh.wpoly_new_len; - Mesh *result = BKE_mesh_new_nomain_from_template( - &mesh, result_nverts, result_nedges, result_nfaces, result_nloops); + Mesh *result = BKE_mesh_new_nomain(result_nverts, result_nedges, result_nfaces, result_nloops); + BKE_mesh_copy_parameters_for_eval(result, &mesh); MutableSpan dst_edges = result->edges_for_write(); MutableSpan dst_face_offsets = result->face_offsets_for_write(); MutableSpan dst_corner_verts = result->corner_verts_for_write(); MutableSpan dst_corner_edges = result->corner_edges_for_write(); + bke::MutableAttributeAccessor dst_attributes = result->attributes_for_write(); /* Vertices. */ Array vert_final_map; - - merge_customdata_all(&mesh.vert_data, - &result->vert_data, - vert_dest_map, + Vector vert_src_index_offset_data; + Vector vert_src_index_data; + merge_customdata_all(vert_dest_map, weld_mesh.double_verts, result_nverts, do_mix_data, + vert_src_index_offset_data, + vert_src_index_data, vert_final_map); + const GroupedSpan dst_to_src_verts(OffsetIndices(vert_src_index_offset_data), + vert_src_index_data); + + mix_attributes(src_attributes, + dst_to_src_verts, + bke::AttrDomain::Point, + get_vertex_group_names(mesh), + dst_attributes); + mix_vertex_groups(mesh, dst_to_src_verts, *result); + if (CustomData_has_layer(&mesh.vert_data, CD_ORIGINDEX)) { + const Span src(static_cast(CustomData_get_layer(&mesh.vert_data, CD_ORIGINDEX)), + mesh.verts_num); + MutableSpan dst(static_cast(CustomData_add_layer( + &result->vert_data, CD_ORIGINDEX, CD_CONSTRUCT, result->verts_num)), + result->verts_num); + copy_first_from_src(src, dst_to_src_verts, dst); + } + if (CustomData_has_layer(&mesh.vert_data, CD_MVERT_SKIN)) { + const Span src( + static_cast(CustomData_get_layer(&mesh.vert_data, CD_MVERT_SKIN)), + mesh.verts_num); + MutableSpan dst(static_cast(CustomData_add_layer( + &result->vert_data, CD_MVERT_SKIN, CD_CONSTRUCT, result->verts_num)), + result->verts_num); + threading::parallel_for(dst.index_range(), 2048, [&](const IndexRange range) { + for (const int dst_vert : range) { + const Span src_verts = dst_to_src_verts[dst_vert]; + if (src_verts.size() == 1) { + dst[dst_vert] = src[src_verts.first()]; + continue; + } + const float src_num_inv = math::rcp(float(src_verts.size())); + for (const int src_vert : src_verts) { + madd_v3_v3fl(dst[dst_vert].radius, src[src_vert].radius, src_num_inv); + dst[dst_vert].flag |= src[src_vert].flag; + } + } + }); + } /* Edges. */ Array edge_final_map; - - merge_customdata_all(&mesh.edge_data, - &result->edge_data, - weld_mesh.edge_dest_map, + Vector edge_src_index_offset_data; + Vector edge_src_index_data; + merge_customdata_all(weld_mesh.edge_dest_map, weld_mesh.double_edges, result_nedges, do_mix_data, + edge_src_index_offset_data, + edge_src_index_data, edge_final_map); + const GroupedSpan dst_to_src_edges(OffsetIndices(edge_src_index_offset_data), + edge_src_index_data); - for (int2 &edge : dst_edges) { - edge[0] = vert_final_map[edge[0]]; - edge[1] = vert_final_map[edge[1]]; - BLI_assert(edge[0] != edge[1]); - BLI_assert(IN_RANGE_INCL(edge[0], 0, result_nverts - 1)); - BLI_assert(IN_RANGE_INCL(edge[1], 0, result_nverts - 1)); + mix_attributes( + src_attributes, dst_to_src_edges, bke::AttrDomain::Edge, {".edge_verts"}, dst_attributes); + if (CustomData_has_layer(&mesh.edge_data, CD_ORIGINDEX)) { + const Span src(static_cast(CustomData_get_layer(&mesh.edge_data, CD_ORIGINDEX)), + mesh.edges_num); + MutableSpan dst(static_cast(CustomData_add_layer( + &result->edge_data, CD_ORIGINDEX, CD_CONSTRUCT, result->edges_num)), + result->edges_num); + copy_first_from_src(src, dst_to_src_edges, dst); } + threading::parallel_for(dst_edges.index_range(), 2048, [&](const IndexRange range) { + for (const int dst_edge_index : range) { + const int src_edge_index = dst_to_src_edges[dst_edge_index].first(); + const int2 src_edge = src_edges[src_edge_index]; + dst_edges[dst_edge_index] = int2(vert_final_map[src_edge[0]], vert_final_map[src_edge[1]]); + } + }); + /* Faces/Loops. */ + Vector corner_src_index_offset_data; + Vector corner_src_index_data; + + corner_src_index_offset_data.reserve(result->corners_num + 1); + corner_src_index_data.reserve(mesh.corners_num); int r_i = 0; int loop_cur = 0; + Array dst_face_unaffected(result_nfaces - weld_mesh.wpoly_new_len); + Array dst_to_src_faces(result_nfaces - weld_mesh.wpoly_new_len); Array group_buffer(weld_mesh.max_face_len); for (const int i : src_faces.index_range()) { const int loop_start = loop_cur; const int poly_ctx = weld_mesh.face_map[i]; if (poly_ctx == OUT_OF_CONTEXT) { - int mp_loop_len = src_faces[i].size(); - CustomData_copy_data( - &mesh.corner_data, &result->corner_data, src_faces[i].start(), loop_cur, mp_loop_len); - for (; mp_loop_len--; loop_cur++) { - dst_corner_verts[loop_cur] = vert_final_map[dst_corner_verts[loop_cur]]; - dst_corner_edges[loop_cur] = edge_final_map[dst_corner_edges[loop_cur]]; + for (const int loop_orig : src_faces[i]) { + corner_src_index_offset_data.append_unchecked(corner_src_index_data.size()); + corner_src_index_data.append(loop_orig); + loop_cur++; } + dst_face_unaffected[r_i] = true; } else { const WeldPoly &wp = weld_mesh.wpoly[poly_ctx]; @@ -1632,19 +1714,17 @@ static Mesh *create_merged_mesh(const Mesh &mesh, if (wp.poly_dst != OUT_OF_CONTEXT) { continue; } + dst_face_unaffected[r_i] = false; do { - customdata_weld(&mesh.corner_data, - &result->corner_data, - group_buffer.data(), - iter.group_len, - loop_cur); + corner_src_index_offset_data.append_unchecked(corner_src_index_data.size()); + corner_src_index_data.extend(Span(group_buffer.data(), iter.group_len)); dst_corner_verts[loop_cur] = vert_final_map[iter.v]; dst_corner_edges[loop_cur] = edge_final_map[iter.e]; loop_cur++; } while (weld_iter_loop_of_poly_next(iter)); } - CustomData_copy_data(&mesh.face_data, &result->face_data, i, r_i, 1); + dst_to_src_faces[r_i] = i; dst_face_offsets[r_i] = loop_start; r_i++; } @@ -1669,8 +1749,8 @@ static Mesh *create_merged_mesh(const Mesh &mesh, continue; } do { - customdata_weld( - &mesh.corner_data, &result->corner_data, group_buffer.data(), iter.group_len, loop_cur); + corner_src_index_offset_data.append_unchecked(corner_src_index_data.size()); + corner_src_index_data.extend(Span(group_buffer.data(), iter.group_len)); dst_corner_verts[loop_cur] = vert_final_map[iter.v]; dst_corner_edges[loop_cur] = edge_final_map[iter.e]; loop_cur++; @@ -1680,6 +1760,57 @@ static Mesh *create_merged_mesh(const Mesh &mesh, r_i++; } + corner_src_index_offset_data.append_unchecked(corner_src_index_data.size()); + + const GroupedSpan dst_to_src_corners(OffsetIndices(corner_src_index_offset_data), + corner_src_index_data); + + const OffsetIndices dst_faces = result->faces(); + + src_attributes.foreach_attribute([&](const bke::AttributeIter &iter) { + if (iter.domain != bke::AttrDomain::Face) { + return; + } + const GVArray src_attr = *iter.get(); + const CPPType &type = src_attr.type(); + bke::GSpanAttributeWriter dst_attr = dst_attributes.lookup_or_add_for_write_only_span( + iter.name, iter.domain, iter.data_type); + bke::attribute_math::gather( + src_attr, dst_to_src_faces, dst_attr.span.drop_back(weld_mesh.wpoly_new_len)); + type.fill_assign_n(type.default_value(), + dst_attr.span.take_back(weld_mesh.wpoly_new_len).data(), + weld_mesh.wpoly_new_len); + dst_attr.finish(); + }); + + if (CustomData_has_layer(&mesh.face_data, CD_ORIGINDEX)) { + const Span src(static_cast(CustomData_get_layer(&mesh.face_data, CD_ORIGINDEX)), + mesh.faces_num); + MutableSpan dst(static_cast(CustomData_add_layer( + &result->face_data, CD_ORIGINDEX, CD_CONSTRUCT, result->faces_num)), + result->faces_num); + bke::attribute_math::gather(src, dst_to_src_faces, dst.drop_back(weld_mesh.wpoly_new_len)); + dst.take_back(weld_mesh.wpoly_new_len).fill(ORIGINDEX_NONE); + } + + IndexMaskMemory memory; + const IndexMask out_of_context_faces = IndexMask::from_bools(dst_face_unaffected, memory); + + out_of_context_faces.foreach_index(GrainSize(1024), [&](const int dst_face_index) { + const IndexRange src_face = src_faces[dst_to_src_faces[dst_face_index]]; + const IndexRange dst_face = dst_faces[dst_face_index]; + for (const int i : src_face.index_range()) { + dst_corner_verts[dst_face[i]] = vert_final_map[src_corner_verts[src_face[i]]]; + dst_corner_edges[dst_face[i]] = edge_final_map[src_corner_edges[src_face[i]]]; + } + }); + + mix_attributes(src_attributes, + dst_to_src_corners, + bke::AttrDomain::Corner, + {".corner_vert", ".corner_edge"}, + dst_attributes); + BLI_assert(int(r_i) == result_nfaces); BLI_assert(loop_cur == result_nloops); diff --git a/tests/files/modeling/geometry_nodes/geometry/merge_by_distance.blend b/tests/files/modeling/geometry_nodes/geometry/merge_by_distance.blend index bf42caff12c..7eb5d35f5e9 100644 --- a/tests/files/modeling/geometry_nodes/geometry/merge_by_distance.blend +++ b/tests/files/modeling/geometry_nodes/geometry/merge_by_distance.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0575be951d78276af99a335d2e98fd1e15b2bc33e65d49b7c85dad5fb4edfccb -size 121120 +oid sha256:58d1a9ef4925f8b846e1d1ffea3584f2d566eaff46f4e9be9b90764a8dc30298 +size 1308685