Geometry Nodes: Use Mesh instead of BMesh in split edges node

Rewrite the edge split code to operate directly on Mesh instead
of BMesh. This allows for the use of multi-threading and makes
the node around 2 times faster. Around 15% of the time is spent
just on the creation of the topology maps, so these being cached
on the mesh could cause an even greater speedup. The new node
gave identical results compared to the BMesh version on all the
meshes I tested it on (up to permutation of the indices).

Here are some of the results on a few simple test cases:
(Intel i7-7700HQ (8 cores) @ 2.800GHz , with 50% of edges selected)
|       | 370x370 UV Sphere | 400x400 Grid | Suzanne 4 subdiv levels |
| ----- | ----------------- | -------------- | --------------------- |
| Mesh  | 89ms              | 111ms          | 76ms                  |
| BMesh | 200ms             | 276ms          | 208ms                 |

Differential Revision: https://developer.blender.org/D16399
This commit is contained in:
Wannes Malfait 2022-11-20 15:42:10 -06:00 committed by Hans Goudey
parent 97e0cc41ca
commit e83f46ea76
4 changed files with 437 additions and 23 deletions

View File

@ -352,11 +352,21 @@ Array<int> build_loop_to_poly_map(Span<MPoly> polys, int loops_num);
Array<Vector<int>> build_vert_to_edge_map(Span<MEdge> edges, int verts_num);
Array<Vector<int>> build_vert_to_loop_map(Span<MLoop> loops, int verts_num);
Array<Vector<int>> build_edge_to_loop_map(Span<MLoop> loops, int edges_num);
Vector<Vector<int>> build_edge_to_loop_map_resizable(Span<MLoop> loops, int edges_num);
inline int previous_poly_loop(const MPoly &poly, int loop_i)
{
return loop_i - 1 + (loop_i == poly.loopstart) * poly.totloop;
}
inline int next_poly_loop(const MPoly &poly, int loop_i)
{
if (loop_i == poly.loopstart + poly.totloop - 1) {
return poly.loopstart;
}
return loop_i + 1;
}
} // namespace blender::bke::mesh_topology
#endif

View File

@ -586,6 +586,24 @@ Array<Vector<int>> build_vert_to_loop_map(const Span<MLoop> loops, const int ver
return map;
}
Array<Vector<int>> build_edge_to_loop_map(const Span<MLoop> loops, const int edges_num)
{
Array<Vector<int>> map(edges_num);
for (const int64_t i : loops.index_range()) {
map[loops[i].e].append(int(i));
}
return map;
}
Vector<Vector<int>> build_edge_to_loop_map_resizable(const Span<MLoop> loops, const int edges_num)
{
Vector<Vector<int>> map(edges_num);
for (const int64_t i : loops.index_range()) {
map[loops[i].e].append(int(i));
}
return map;
}
} // namespace blender::bke::mesh_topology
/** \} */

View File

@ -1,13 +1,15 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_array_utils.hh"
#include "BLI_task.hh"
#include "DNA_mesh_types.h"
#include "BKE_attribute_math.hh"
#include "BKE_mesh.h"
#include "BKE_mesh_mapping.h"
#include "BKE_mesh_runtime.h"
#include "bmesh.h"
#include "bmesh_tools.h"
#include "node_geometry_util.hh"
namespace blender::nodes::node_geo_edge_split_cc {
@ -19,30 +21,410 @@ static void node_declare(NodeDeclarationBuilder &b)
b.add_output<decl::Geometry>(N_("Mesh"));
}
static Mesh *mesh_edge_split(const Mesh &mesh, const IndexMask selection)
/* Naively checks if the first vertices and the second vertices are the same. */
static inline bool naive_edges_equal(const MEdge &edge1, const MEdge &edge2)
{
BMeshCreateParams bmesh_create_params{};
bmesh_create_params.use_toolflags = true;
const BMAllocTemplate allocsize = {0, 0, 0, 0};
BMesh *bm = BM_mesh_create(&allocsize, &bmesh_create_params);
return edge1.v1 == edge2.v1 && edge1.v2 == edge2.v2;
}
BMeshFromMeshParams bmesh_from_mesh_params{};
bmesh_from_mesh_params.cd_mask_extra.vmask = CD_MASK_ORIGINDEX;
bmesh_from_mesh_params.cd_mask_extra.emask = CD_MASK_ORIGINDEX;
bmesh_from_mesh_params.cd_mask_extra.pmask = CD_MASK_ORIGINDEX;
BM_mesh_bm_from_me(bm, &mesh, &bmesh_from_mesh_params);
static void transfer_attributes(const Map<AttributeIDRef, AttributeKind> &attributes,
const Span<int> new_to_old_verts_map,
const Span<int> new_to_old_edges_map,
const AttributeAccessor src_attributes,
MutableAttributeAccessor dst_attributes)
{
for (Map<AttributeIDRef, AttributeKind>::Item entry : attributes.items()) {
const AttributeIDRef attribute_id = entry.key;
GAttributeReader src_attribute = src_attributes.lookup(attribute_id);
if (!src_attribute) {
continue;
}
const eCustomDataType data_type = bke::cpp_type_to_custom_data_type(
src_attribute.varray.type());
GSpanAttributeWriter dst_attribute = dst_attributes.lookup_or_add_for_write_only_span(
attribute_id, src_attribute.domain, data_type);
if (!dst_attribute) {
continue;
}
BM_mesh_elem_table_ensure(bm, BM_EDGE);
for (const int i : selection) {
BMEdge *edge = BM_edge_at_index(bm, i);
BM_elem_flag_enable(edge, BM_ELEM_TAG);
attribute_math::convert_to_static_type(data_type, [&](auto dummy) {
using T = decltype(dummy);
VArraySpan<T> span{src_attribute.varray.typed<T>()};
MutableSpan<T> dst_span = dst_attribute.span.typed<T>();
switch (src_attribute.domain) {
case ATTR_DOMAIN_POINT:
array_utils::gather(span, new_to_old_verts_map, dst_span);
break;
case ATTR_DOMAIN_EDGE:
array_utils::gather(span, new_to_old_edges_map, dst_span);
break;
case ATTR_DOMAIN_FACE:
dst_span.copy_from(span);
break;
case ATTR_DOMAIN_CORNER:
dst_span.copy_from(span);
break;
default:
BLI_assert_unreachable();
}
});
dst_attribute.finish();
}
}
/**
* Merge the new_edge into the original edge.
*
* NOTE: This function is very specific to the situation and makes a lot of assumptions.
*/
static void merge_edges(const int orig_edge_i,
const int new_edge_i,
MutableSpan<MLoop> new_loops,
Vector<Vector<int>> &edge_to_loop_map,
Vector<MEdge> &new_edges,
Vector<int> &new_to_old_edges_map)
{
/* Merge back into the original edge by undoing the topology changes. */
BLI_assert(edge_to_loop_map[new_edge_i].size() == 1);
const int loop_i = edge_to_loop_map[new_edge_i][0];
new_loops[loop_i].e = orig_edge_i;
/* We are putting the last edge in the location of new_edge in all the maps, to remove
* new_edge efficiently. We have to update the topology information for this last edge
* though. Essentially we are replacing every instance of last_edge_i with new_edge_i. */
const int last_edge_i = new_edges.size() - 1;
if (last_edge_i != new_edge_i) {
BLI_assert(edge_to_loop_map[last_edge_i].size() == 1);
const int last_edge_loop_i = edge_to_loop_map[last_edge_i][0];
new_loops[last_edge_loop_i].e = new_edge_i;
}
BM_mesh_edgesplit(bm, false, true, false);
/* We can now safely swap-remove. */
new_edges.remove_and_reorder(new_edge_i);
edge_to_loop_map.remove_and_reorder(new_edge_i);
new_to_old_edges_map.remove_and_reorder(new_edge_i);
}
Mesh *result = BKE_mesh_from_bmesh_for_eval_nomain(bm, nullptr, &mesh);
BM_mesh_free(bm);
/**
* Replace the vertex of an edge with a new one, and update the connected loops.
*
* NOTE: This only updates the loops containing the edge and the old vertex. It should therefore
* also be called on the adjacent edge.
*/
static void swap_vertex_of_edge(MEdge &edge,
const int old_vert,
const int new_vert,
MutableSpan<MLoop> loops,
const Span<int> connected_loops)
{
if (edge.v1 == old_vert) {
edge.v1 = new_vert;
}
else if (edge.v2 == old_vert) {
edge.v2 = new_vert;
}
else {
BLI_assert_unreachable();
}
for (const int loop_i : connected_loops) {
if (loops[loop_i].v == old_vert) {
loops[loop_i].v = new_vert;
}
/* The old vertex is on the loop containing the adjacent edge. Since this function is also
* called on the adjacent edge, we don't replace it here. */
}
}
/** Split the vertex into duplicates so that each fan has a different vertex. */
static void split_vertex_per_fan(const int vertex,
const int start_offset,
const Span<int> fans,
const Span<int> fan_sizes,
const Span<Vector<int>> edge_to_loop_map,
MutableSpan<MEdge> new_edges,
MutableSpan<MLoop> new_loops,
MutableSpan<int> new_to_old_verts_map)
{
int fan_start = 0;
/* We don't need to create a new vertex for the last fan. That fan can just be connected to the
* original vertex. */
for (const int i : fan_sizes.index_range().drop_back(1)) {
const int new_vert_i = start_offset + i;
new_to_old_verts_map[new_vert_i] = vertex;
for (const int edge_i : fans.slice(fan_start, fan_sizes[i])) {
swap_vertex_of_edge(
new_edges[edge_i], vertex, new_vert_i, new_loops, edge_to_loop_map[edge_i]);
}
fan_start += fan_sizes[i];
}
}
/**
* Get the index of the adjacent edge to a loop connected to a vertex. In other words, for the
* given polygon return the unique edge connected to the given vertex and not on the given loop.
*/
static int adjacent_edge(Span<MLoop> loops, const int loop_i, const MPoly &poly, const int vertex)
{
const int adjacent_loop_i = (loops[loop_i].v == vertex) ?
bke::mesh_topology::previous_poly_loop(poly, loop_i) :
bke::mesh_topology::next_poly_loop(poly, loop_i);
return loops[adjacent_loop_i].e;
}
/**
* Calculate the disjoint fans connected to the vertex, where a fan is a group of edges connected
* through polygons. The connected_edges vector is rearranged in such a way that edges in the same
* fan are grouped together. The r_fans_sizes Vector gives the sizes of the different fans, and can
* be used to retreive the fans from connected_edges.
*/
static void calc_vertex_fans(const int vertex,
const Span<MLoop> new_loops,
const Span<MPoly> polys,
const Span<Vector<int>> edge_to_loop_map,
const Span<int> loop_to_poly_map,
MutableSpan<int> connected_edges,
Vector<int> &r_fan_sizes)
{
if (connected_edges.size() <= 1) {
r_fan_sizes.append(connected_edges.size());
return;
}
Vector<int> search_edges;
int total_found_edges_num = 0;
int fan_size = 0;
const int total_edge_num = connected_edges.size();
/* Iteratively go through the connected edges. The front contains already handled edges, while
* the back contains unhandled edges. */
while (true) {
/* This edge has not been visited yet. */
int curr_i = total_found_edges_num;
int curr_edge_i = connected_edges[curr_i];
/* Gather all the edges in this fan. */
while (true) {
fan_size++;
/* Add adjacent edges to search stack. */
for (const int loop_i : edge_to_loop_map[curr_edge_i]) {
const int adjacent_edge_i = adjacent_edge(
new_loops, loop_i, polys[loop_to_poly_map[loop_i]], vertex);
/* Find out if this edge was visited already. */
int i = curr_i + 1;
for (; i < total_edge_num; i++) {
if (connected_edges[i] == adjacent_edge_i) {
break;
}
}
if (i == total_edge_num) {
/* Already visited this edge. */
continue;
}
search_edges.append(adjacent_edge_i);
curr_i++;
std::swap(connected_edges[curr_i], connected_edges[i]);
}
if (search_edges.is_empty()) {
break;
}
curr_edge_i = search_edges.pop_last();
}
/* We have now collected all the edges in this fan. */
total_found_edges_num += fan_size;
BLI_assert(total_found_edges_num <= total_edge_num);
r_fan_sizes.append(fan_size);
if (total_found_edges_num == total_edge_num) {
/* We have found all the edges, so this final batch must be the last connected fan. */
break;
}
fan_size = 0;
}
}
/**
* Splits the edge into duplicates, so that each edge is connected to one poly.
*/
static void split_edge_per_poly(const int edge_i,
const int new_edge_start,
MutableSpan<Vector<int>> edge_to_loop_map,
MutableSpan<MLoop> new_loops,
MutableSpan<MEdge> new_edges,
MutableSpan<int> new_to_old_edges_map)
{
if (edge_to_loop_map[edge_i].size() <= 1) {
return;
}
int new_edge_index = new_edge_start;
for (const int loop_i : edge_to_loop_map[edge_i].as_span().drop_front(1)) {
const MEdge new_edge(new_edges[edge_i]);
new_edges[new_edge_index] = new_edge;
new_to_old_edges_map[new_edge_index] = edge_i;
edge_to_loop_map[new_edge_index].append({loop_i});
new_loops[loop_i].e = new_edge_index;
new_edge_index++;
}
/* Only the first loop is now connected to this edge. */
edge_to_loop_map[edge_i].resize(1);
}
static Mesh *mesh_edge_split(const Mesh &mesh,
const IndexMask mask,
const Map<AttributeIDRef, AttributeKind> &attributes)
{
/* Flag vertices that need to be split. */
Array<bool> should_split_vert(mesh.totvert, false);
const Span<MEdge> edges = mesh.edges();
for (const int edge_i : mask) {
const MEdge edge = edges[edge_i];
should_split_vert[edge.v1] = true;
should_split_vert[edge.v2] = true;
}
/* Precalculate topology info. */
Array<Vector<int>> vert_to_edge_map = bke::mesh_topology::build_vert_to_edge_map(edges,
mesh.totvert);
Vector<Vector<int>> edge_to_loop_map = bke::mesh_topology::build_edge_to_loop_map_resizable(
mesh.loops(), mesh.totedge);
Array<int> loop_to_poly_map = bke::mesh_topology::build_loop_to_poly_map(mesh.polys(),
mesh.totloop);
/* Store offsets, so we can split edges in parallel. */
Array<int> edge_offsets(edges.size());
Array<int> num_edge_duplicates(edges.size());
int new_edges_size = edges.size();
for (const int edge : mask) {
edge_offsets[edge] = new_edges_size;
/* We add duplicates of the edge for each poly (except the first). */
const int num_connected_loops = edge_to_loop_map[edge].size();
const int num_duplicates = std::max(0, num_connected_loops - 1);
new_edges_size += num_duplicates;
num_edge_duplicates[edge] = num_duplicates;
}
const Span<MPoly> polys = mesh.polys();
Array<MLoop> new_loops(mesh.loops());
Vector<MEdge> new_edges(new_edges_size);
new_edges.as_mutable_span().take_front(edges.size()).copy_from(edges);
edge_to_loop_map.resize(new_edges_size);
/* Used for transferring attributes. */
Vector<int> new_to_old_verts_map(IndexRange(mesh.totvert).as_span());
Vector<int> new_to_old_edges_map(IndexRange(new_edges.size()).as_span());
/* Step 1: Split the edges. */
threading::parallel_for(mask.index_range(), 512, [&](IndexRange range) {
for (const int mask_i : range) {
const int edge_i = mask[mask_i];
split_edge_per_poly(edge_i,
edge_offsets[edge_i],
edge_to_loop_map,
new_loops,
new_edges,
new_to_old_edges_map);
}
});
/* Step 1.5: Update topology information (can't parallelize). */
for (const int edge_i : mask) {
const MEdge &edge = edges[edge_i];
for (const int duplicate_i : IndexRange(edge_offsets[edge_i], num_edge_duplicates[edge_i])) {
vert_to_edge_map[edge.v1].append(duplicate_i);
vert_to_edge_map[edge.v2].append(duplicate_i);
}
}
/* Step 2: Calculate vertex fans. */
Array<Vector<int>> vertex_fan_sizes(mesh.totvert);
threading::parallel_for(IndexRange(mesh.totvert), 512, [&](IndexRange range) {
for (const int vert : range) {
if (!should_split_vert[vert]) {
continue;
}
calc_vertex_fans(vert,
new_loops,
polys,
edge_to_loop_map,
loop_to_poly_map,
vert_to_edge_map[vert],
vertex_fan_sizes[vert]);
}
});
/* Step 2.5: Calculate offsets for next step. */
Array<int> vert_offsets(mesh.totvert);
int new_verts_size = mesh.totvert;
for (const int vert : IndexRange(mesh.totvert)) {
if (!should_split_vert[vert]) {
continue;
}
vert_offsets[vert] = new_verts_size;
/* We only create a new vertex for each fan different from the first. */
new_verts_size += vertex_fan_sizes[vert].size() - 1;
}
new_to_old_verts_map.resize(new_verts_size);
/* Step 3: Split the vertices. */
threading::parallel_for(IndexRange(mesh.totvert), 512, [&](IndexRange range) {
for (const int vert : range) {
if (!should_split_vert[vert]) {
continue;
}
split_vertex_per_fan(vert,
vert_offsets[vert],
vert_to_edge_map[vert],
vertex_fan_sizes[vert],
edge_to_loop_map,
new_edges,
new_loops,
new_to_old_verts_map);
}
});
/* Step 4: Deduplicate edges. We loop backwards so we can use remove_and_reorder. Although this
* does look bad (3 nested loops), in practice the inner loops are very small. For most meshes,
* there are at most 2 polygons connected to each edge, and hence you'll only get at most 1
* duplicate per edge. */
for (int mask_i = mask.size() - 1; mask_i >= 0; mask_i--) {
const int edge = mask[mask_i];
int start_of_duplicates = edge_offsets[edge];
int end_of_duplicates = start_of_duplicates + num_edge_duplicates[edge] - 1;
for (int duplicate = end_of_duplicates; duplicate >= start_of_duplicates; duplicate--) {
if (naive_edges_equal(new_edges[edge], new_edges[duplicate])) {
merge_edges(edge, duplicate, new_loops, edge_to_loop_map, new_edges, new_to_old_edges_map);
break;
}
for (int other = start_of_duplicates; other < duplicate; other++) {
if (naive_edges_equal(new_edges[other], new_edges[duplicate])) {
merge_edges(
other, duplicate, new_loops, edge_to_loop_map, new_edges, new_to_old_edges_map);
break;
}
}
}
}
/* Copy to the new mesh. */
Mesh *result = BKE_mesh_new_nomain(
new_verts_size, new_edges.size(), 0, mesh.totloop, mesh.totpoly);
transfer_attributes(attributes,
new_to_old_verts_map,
new_to_old_edges_map,
mesh.attributes(),
result->attributes_for_write());
MutableSpan<MEdge> dst_edges = result->edges_for_write();
dst_edges.copy_from(new_edges);
MutableSpan<MLoop> dst_loops = result->loops_for_write();
dst_loops.copy_from(new_loops);
MutableSpan<MPoly> dst_polys = result->polys_for_write();
dst_polys.copy_from(polys);
return result;
}
@ -53,16 +435,19 @@ static void node_geo_exec(GeoNodeExecParams params)
const Field<bool> selection_field = params.extract_input<Field<bool>>("Selection");
geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) {
if (const Mesh *mesh = geometry_set.get_mesh_for_write()) {
if (const Mesh *mesh = geometry_set.get_mesh_for_read()) {
bke::MeshFieldContext field_context{*mesh, ATTR_DOMAIN_EDGE};
fn::FieldEvaluator selection_evaluator{field_context, mesh->totedge};
selection_evaluator.add(selection_field);
selection_evaluator.evaluate();
const IndexMask selection = selection_evaluator.get_evaluated_as_mask(0);
const IndexMask mask = selection_evaluator.get_evaluated_as_mask(0);
Mesh *result = mesh_edge_split(*mesh, selection);
Map<AttributeIDRef, AttributeKind> attributes;
geometry_set.gather_attributes_for_propagation(
{GEO_COMPONENT_TYPE_MESH}, GEO_COMPONENT_TYPE_MESH, false, attributes);
Mesh *result = mesh_edge_split(*mesh, mask, attributes);
geometry_set.replace_mesh(result);
}
});

View File

@ -795,6 +795,7 @@ set(geo_node_tests
mesh_primitives
mesh
mesh/extrude
mesh/split_edges
points
utilities
vector