Rewritten Array Modifier D443

Patch by PatB with own edits

- replace BMesh with CDDM functions.
- faster remove-vertex merging.
- extend CDDM_merge_verts to be more flexible.
This commit is contained in:
2014-08-12 13:52:17 +10:00
parent 06020eb02e
commit ab06ec7a24
4 changed files with 764 additions and 339 deletions

View File

@@ -58,7 +58,13 @@ struct DerivedMesh *CDDM_from_bmesh(struct BMesh *bm, const bool use_mdisps);
DerivedMesh *CDDM_from_editbmesh(struct BMEditMesh *em, const bool use_mdisps, const bool use_tessface);
/* merge verts */
DerivedMesh *CDDM_merge_verts(DerivedMesh *dm, const int *vtargetmap, const int tot_vtargetmap);
/* Enum for merge_mode of CDDM_merge_verts.
* Refer to cdderivedmesh.c for details. */
enum {
CDDM_MERGE_VERTS_DUMP_IF_MAPPED,
CDDM_MERGE_VERTS_DUMP_IF_EQUAL,
};
DerivedMesh *CDDM_merge_verts(DerivedMesh *dm, const int *vtargetmap, const int tot_vtargetmap, const int merge_mode);
/* creates a CDDerivedMesh from the given curve object */
struct DerivedMesh *CDDM_from_curve(struct Object *ob);

View File

@@ -2571,26 +2571,201 @@ void CDDM_calc_normals_tessface(DerivedMesh *dm)
#if 1
/**
* Poly compare with vtargetmap
* Function used by #CDDM_merge_verts.
* The function compares poly_source after applying vtargetmap, with poly_target.
* The two polys are identical if they share the same vertices in the same order, or in reverse order,
* but starting position loopstart may be different.
* The function is called with direct_reverse=1 for same order (i.e. same normal),
* and may be called again with direct_reverse=-1 for reverse order.
* \return 1 if polys are identical, 0 if polys are different.
*/
static int cddm_poly_compare(MLoop *mloop_array, MPoly *mpoly_source, MPoly *mpoly_target, const int *vtargetmap, const int direct_reverse)
{
int vert_source, first_vert_source, vert_target;
int i_loop_source;
int i_loop_target, i_loop_target_start, i_loop_target_offset, i_loop_target_adjusted;
bool compare_completed = false;
bool same_loops = false;
MLoop *mloop_source, *mloop_target;
BLI_assert(direct_reverse == 1 || direct_reverse == -1);
i_loop_source = 0;
mloop_source = mloop_array + mpoly_source->loopstart;
vert_source = mloop_source->v;
if (vtargetmap[vert_source] != -1) {
vert_source = vtargetmap[vert_source];
}
else {
/* All source loop vertices should be mapped */
BLI_assert(false);
}
/* Find same vertex within mpoly_target's loops */
mloop_target = mloop_array + mpoly_target->loopstart;
for (i_loop_target = 0; i_loop_target < mpoly_target->totloop; i_loop_target++, mloop_target++) {
if (mloop_target->v == vert_source) {
break;
}
}
/* If same vertex not found, then polys cannot be equal */
if (i_loop_target >= mpoly_target->totloop) {
return false;
}
/* Now mloop_source and m_loop_target have one identical vertex */
/* mloop_source is at position 0, while m_loop_target has advanced to find identical vertex */
/* Go around the loop and check that all vertices match in same order */
/* Skipping source loops when consecutive source vertices are mapped to same target vertex */
i_loop_target_start = i_loop_target;
i_loop_target_offset = 0;
first_vert_source = vert_source;
compare_completed = false;
same_loops = false;
while (!compare_completed) {
vert_target = mloop_target->v;
/* First advance i_loop_source, until it points to different vertex, after mapping applied */
do {
i_loop_source++;
if (i_loop_source == mpoly_source->totloop) {
/* End of loops for source, must match end of loop for target. */
if (i_loop_target_offset == mpoly_target->totloop - 1) {
compare_completed = true;
same_loops = true;
break; /* Polys are identical */
}
else {
compare_completed = true;
same_loops = false;
break; /* Polys are different */
}
}
mloop_source++;
vert_source = mloop_source->v;
if (vtargetmap[vert_source] != -1) {
vert_source = vtargetmap[vert_source];
}
else {
/* All source loop vertices should be mapped */
BLI_assert(false);
}
} while (vert_source == vert_target);
if (compare_completed) {
break;
}
/* Now advance i_loop_target as well */
i_loop_target_offset++;
if (i_loop_target_offset == mpoly_target->totloop) {
/* End of loops for target only, that means no match */
/* except if all remaining source vertices are mapped to first target */
for (; i_loop_source < mpoly_source->totloop; i_loop_source++, mloop_source++) {
vert_source = vtargetmap[mloop_source->v];
if (vert_source != first_vert_source) {
compare_completed = true;
same_loops = false;
break;
}
}
if (!compare_completed) {
same_loops = true;
}
break;
}
/* Adjust i_loop_target for cycling around and for direct/reverse order defined by delta = +1 or -1 */
i_loop_target_adjusted = (i_loop_target_start + direct_reverse * i_loop_target_offset) % mpoly_target->totloop;
if (i_loop_target_adjusted < 0) {
i_loop_target_adjusted += mpoly_target->totloop;
}
mloop_target = mloop_array + mpoly_target->loopstart + i_loop_target_adjusted;
vert_target = mloop_target->v;
if (vert_target != vert_source) {
same_loops = false; /* Polys are different */
break;
}
}
return same_loops;
}
/* Utility stuff for using GHash with polys */
typedef struct PolyKey {
int poly_index; /* index of the MPoly within the derived mesh */
int totloops; /* number of loops in the poly */
unsigned int hash_sum; /* Sum of all vertices indices */
unsigned int hash_xor; /* Xor of all vertices indices */
} PolyKey;
static unsigned int poly_gset_hash_fn(const void *key)
{
const PolyKey *pk = key;
return pk->hash_sum;
}
static int poly_gset_compare_fn(const void *k1, const void *k2)
{
const PolyKey *pk1 = k1;
const PolyKey *pk2 = k2;
if ((pk1->hash_sum == pk2->hash_sum) &&
(pk1->hash_xor == pk2->hash_xor) &&
(pk1->totloops == pk2->totloops))
{
/* Equality - note that this does not mean equality of polys */
return 0;
}
else {
return 1;
}
}
/**
* Merge Verts
*
* This frees dm, and returns a new one.
*
* \param vtargetmap The table that maps vertices to target vertices. a value of -1
* indicates a vertex is a target, and is to be kept.
* This array is aligned with 'dm->numVertData'
*
* \param tot_vtargetmap The number of non '-1' values in vtargetmap.
* (not the size )
* \param tot_vtargetmap The number of non '-1' values in vtargetmap. (not the size)
*
* this frees dm, and returns a new one.
* \param merge_mode enum with two modes.
* - #CDDM_MERGE_VERTS_DUMP_IF_MAPPED
* When called by the Mirror Modifier,
* In this mode it skips any faces that have all vertices merged (to avoid creating pairs
* of faces sharing the same set of vertices)
* - #CDDM_MERGE_VERTS_DUMP_IF_EQUAL
* When called by the Array Modifier,
* In this mode, faces where all vertices are merged are double-checked,
* to see whether all target vertices actually make up a poly already.
* Indeed it could be that all of a poly's vertices are merged,
* but merged to vertices that do not make up a single poly,
* in which case the original poly should not be dumped.
* Actually this later behavior could apply to the Mirror Modifier as well, but the additional checks are
* costly and not necessary in the case of mirror, because each vertex is only merged to its own mirror.
*
* note, CDDM_recalc_tessellation has to run on the returned DM if you want to access tessfaces.
*
* Note: This function is currently only used by the Mirror modifier, so it
* skips any faces that have all vertices merged (to avoid creating pairs
* of faces sharing the same set of vertices). If used elsewhere, it may
* be necessary to make this functionality optional.
* \note #CDDM_recalc_tessellation has to run on the returned DM if you want to access tessfaces.
*/
DerivedMesh *CDDM_merge_verts(DerivedMesh *dm, const int *vtargetmap, const int tot_vtargetmap)
DerivedMesh *CDDM_merge_verts(DerivedMesh *dm, const int *vtargetmap, const int tot_vtargetmap, const int merge_mode)
{
// #define USE_LOOPS
CDDerivedMesh *cddm = (CDDerivedMesh *)dm;
@@ -2631,7 +2806,10 @@ DerivedMesh *CDDM_merge_verts(DerivedMesh *dm, const int *vtargetmap, const int
EdgeHash *ehash = BLI_edgehash_new_ex(__func__, totedge);
int i, j, c;
PolyKey *poly_keys;
GSet *poly_gset = NULL;
STACK_INIT(oldv, totvert_final);
STACK_INIT(olde, totedge);
STACK_INIT(oldl, totloop);
@@ -2673,10 +2851,9 @@ DerivedMesh *CDDM_merge_verts(DerivedMesh *dm, const int *vtargetmap, const int
med = cddm->medge;
c = 0;
for (i = 0; i < totedge; i++, med++) {
if (LIKELY(med->v1 != med->v2)) {
const unsigned int v1 = (vtargetmap[med->v1] != -1) ? vtargetmap[med->v1] : med->v1;
const unsigned int v2 = (vtargetmap[med->v2] != -1) ? vtargetmap[med->v2] : med->v2;
const unsigned int v1 = (vtargetmap[med->v1] != -1) ? vtargetmap[med->v1] : med->v1;
const unsigned int v2 = (vtargetmap[med->v2] != -1) ? vtargetmap[med->v2] : med->v2;
if (LIKELY(v1 != v2)) {
void **eh_p = BLI_edgehash_lookup_p(ehash, v1, v2);
if (eh_p) {
@@ -2695,13 +2872,49 @@ DerivedMesh *CDDM_merge_verts(DerivedMesh *dm, const int *vtargetmap, const int
}
}
if (merge_mode == CDDM_MERGE_VERTS_DUMP_IF_EQUAL) {
/* In this mode, we need to determine, whenever a poly' vertices are all mapped */
/* if the targets already make up a poly, in which case the new poly is dropped */
/* This poly equality check is rather complex. We use a BLI_ghash to speed it up with a first level check */
PolyKey *mpgh;
poly_keys = MEM_mallocN(sizeof(PolyKey) * totpoly, __func__);
poly_gset = BLI_gset_new_ex(poly_gset_hash_fn, poly_gset_compare_fn, __func__, totpoly);
/* Duplicates allowed because our compare function is not pure equality */
BLI_gset_flag_set(poly_gset, GHASH_FLAG_ALLOW_DUPES);
mp = cddm->mpoly;
mpgh = poly_keys;
for (i = 0; i < totpoly; i++, mp++, mpgh++) {
mpgh->poly_index = i;
mpgh->totloops = mp->totloop;
ml = cddm->mloop + mp->loopstart;
mpgh->hash_sum = mpgh->hash_xor = 0;
for (j = 0; j < mp->totloop; j++, ml++) {
mpgh->hash_sum += ml->v;
mpgh->hash_xor ^= ml->v;
}
BLI_gset_insert(poly_gset, mpgh);
}
if (cddm->pmap) {
MEM_freeN(cddm->pmap);
MEM_freeN(cddm->pmap_mem);
}
/* Can we optimise by reusing an old pmap ? How do we know an old pmap is stale ? */
/* When called by MOD_array.c, the cddm has just been created, so it has no valid pmap. */
BKE_mesh_vert_poly_map_create(&cddm->pmap, &cddm->pmap_mem,
cddm->mpoly, cddm->mloop,
totvert, totpoly, totloop);
} /* done preparing for fast poly compare */
mp = cddm->mpoly;
for (i = 0; i < totpoly; i++, mp++) {
MPoly *mp_new;
ml = cddm->mloop + mp->loopstart;
/* skip faces with all vertices merged */
/* check faces with all vertices merged */
{
bool all_vertices_merged = true;
@@ -2713,16 +2926,86 @@ DerivedMesh *CDDM_merge_verts(DerivedMesh *dm, const int *vtargetmap, const int
}
if (UNLIKELY(all_vertices_merged)) {
continue;
if (merge_mode == CDDM_MERGE_VERTS_DUMP_IF_MAPPED) {
/* In this mode, all vertices merged is enough to dump face */
continue;
}
else if (merge_mode == CDDM_MERGE_VERTS_DUMP_IF_EQUAL) {
/* Additional condition for face dump: target vertices must make up an identical face */
/* The test has 2 steps: (1) first step is fast ghash lookup, but not failproof */
/* (2) second step is thorough but more costly poly compare */
int i_poly, v_target, v_prev;
bool found = false;
PolyKey pkey;
/* Use poly_gset for fast (although not 100% certain) identification of same poly */
/* First, make up a poly_summary structure */
ml = cddm->mloop + mp->loopstart;
pkey.hash_sum = pkey.hash_xor = 0;
pkey.totloops = 0;
v_prev = vtargetmap[(ml + mp->totloop -1)->v]; /* since it loops around, the prev of first is the last */
for (j = 0; j < mp->totloop; j++, ml++) {
v_target = vtargetmap[ml->v]; /* Cannot be -1, they are all mapped */
if (v_target == v_prev) {
/* consecutive vertices in loop map to the same target: discard */
/* but what about last to first ? */
continue;
}
pkey.hash_sum += v_target;
pkey.hash_xor ^= v_target;
pkey.totloops++;
v_prev = v_target;
}
if (BLI_gset_haskey(poly_gset, &pkey)) {
/* There might be a poly that matches this one.
* We could just leave it there and say there is, and do a "continue".
* ... but we are checking whether there is an exact poly match.
* It's not so costly in terms of CPU since it's very rare, just a lot of complex code.
*/
/* Consider current loop again */
ml = cddm->mloop + mp->loopstart;
/* Consider the target of the loop's first vert */
v_target = vtargetmap[ml->v];
/* Now see if v_target belongs to a poly that shares all vertices with source poly,
* in same order, or reverse order */
for (i_poly = 0; i_poly < cddm->pmap[v_target].count; i_poly++) {
MPoly *target_poly = cddm->mpoly + *(cddm->pmap[v_target].indices + i_poly);
if (cddm_poly_compare(cddm->mloop, mp, target_poly, vtargetmap, +1) ||
cddm_poly_compare(cddm->mloop, mp, target_poly, vtargetmap, -1))
{
found = true;
break;
}
}
if (found) {
/* Current poly's vertices are mapped to a poly that is strictly identical */
/* Current poly is dumped */
continue;
}
}
}
}
}
/* Here either the poly's vertices were not all merged
* or they were all merged, but targets do not make up an identical poly,
* the poly is retained.
*/
ml = cddm->mloop + mp->loopstart;
c = 0;
for (j = 0; j < mp->totloop; j++, ml++) {
unsigned int v1, v2;
med = cddm->medge + ml->e;
if (LIKELY(med->v1 != med->v2)) {
v1 = (vtargetmap[med->v1] != -1) ? vtargetmap[med->v1] : med->v1;
v2 = (vtargetmap[med->v2] != -1) ? vtargetmap[med->v2] : med->v2;
if (LIKELY(v1 != v2)) {
#ifdef USE_LOOPS
newl[j + mp->loopstart] = STACK_SIZE(mloop);
#endif
@@ -2742,6 +3025,14 @@ DerivedMesh *CDDM_merge_verts(DerivedMesh *dm, const int *vtargetmap, const int
mp_new->loopstart = STACK_SIZE(mloop) - c;
STACK_PUSH(oldp, i);
} /* end of the loop that tests polys */
if (poly_gset) {
// printf("hash quality %.6f\n", BLI_gset_calc_quality(poly_gset));
BLI_gset_free(poly_gset, NULL);
MEM_freeN(poly_keys);
}
/*create new cddm*/

View File

@@ -22,7 +22,8 @@
* Ton Roosendaal,
* Ben Batt,
* Brecht Van Lommel,
* Campbell Barton
* Campbell Barton,
* Patrice Bertrand
*
* ***** END GPL LICENSE BLOCK *****
*
@@ -30,16 +31,14 @@
/** \file blender/modifiers/intern/MOD_array.c
* \ingroup modifiers
*
* Array modifier: duplicates the object multiple times along an axis.
*/
/* Array modifier: duplicates the object multiple times along an axis */
#include "MEM_guardedalloc.h"
#include "BLI_math.h"
#include "BLI_utildefines.h"
#include "BLI_ghash.h"
#include "DNA_curve_types.h"
#include "DNA_meshdata_types.h"
@@ -53,14 +52,8 @@
#include "MOD_util.h"
#include "bmesh.h"
#include "depsgraph_private.h"
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
/* Due to cyclic dependencies it's possible that curve used for
* deformation here is not evaluated at the time of evaluating
* this modifier.
@@ -140,7 +133,7 @@ static void updateDepgraph(ModifierData *md, DagForest *forest,
}
}
static float vertarray_size(MVert *mvert, int numVerts, int axis)
static float vertarray_size(const MVert *mvert, int numVerts, int axis)
{
int i;
float min_co, max_co;
@@ -159,206 +152,303 @@ static float vertarray_size(MVert *mvert, int numVerts, int axis)
return max_co - min_co;
}
static int *find_doubles_index_map(BMesh *bm, BMOperator *dupe_op,
const ArrayModifierData *amd,
int *index_map_length)
BLI_INLINE float sum_v3(const float v[3])
{
BMOperator find_op;
BMOIter oiter;
BMVert *v, *v2;
BMElem *ele;
int *index_map, i;
BMO_op_initf(bm, &find_op, (BMO_FLAG_DEFAULTS & ~BMO_FLAG_RESPECT_HIDE),
"find_doubles verts=%av dist=%f keep_verts=%s",
amd->merge_dist, dupe_op, "geom");
BMO_op_exec(bm, &find_op);
i = 0;
BMO_ITER (ele, &oiter, dupe_op->slots_in, "geom", BM_ALL) {
BM_elem_index_set(ele, i); /* set_dirty */
i++;
}
BMO_ITER (ele, &oiter, dupe_op->slots_out, "geom.out", BM_ALL) {
BM_elem_index_set(ele, i); /* set_dirty */
i++;
}
/* above loops over all, so set all to dirty, if this is somehow
* setting valid values, this line can be removed - campbell */
bm->elem_index_dirty |= BM_ALL;
(*index_map_length) = i;
index_map = MEM_callocN(sizeof(int) * (*index_map_length), "index_map");
/*element type argument doesn't do anything here*/
BMO_ITER (v, &oiter, find_op.slots_out, "targetmap.out", 0) {
v2 = BMO_iter_map_value_ptr(&oiter);
index_map[BM_elem_index_get(v)] = BM_elem_index_get(v2) + 1;
}
BMO_op_finish(bm, &find_op);
return index_map;
return v[0] + v[1] + v[2];
}
/* Used for start/end cap.
*
* this function expects all existing vertices to be tagged,
* so we can know new verts are not tagged.
*
* All verts will be tagged on exit.
/* Structure used for sorting vertices, when processing doubles */
typedef struct SortVertsElem {
int vertex_num; /* The original index of the vertex, prior to sorting */
float co[3]; /* Its coordinates */
float sum_co; /* sum_v3(co), just so we don't do the sum many times. */
} SortVertsElem;
static int svert_sum_cmp(const void *e1, const void *e2)
{
const SortVertsElem *sv1 = (SortVertsElem *)e1;
const SortVertsElem *sv2 = (SortVertsElem *)e2;
if (sv1->sum_co > sv2->sum_co) return 1;
else if (sv1->sum_co < sv2->sum_co) return -1;
else return 0;
}
static void svert_from_mvert(SortVertsElem *sv, const MVert *mv, const int i_begin, const int i_end)
{
int i;
for (i = i_begin; i < i_end; i++, sv++, mv++) {
sv->vertex_num = i;
copy_v3_v3(sv->co, mv->co);
sv->sum_co = sum_v3(mv->co);
}
}
/**
* Take as inputs two sets of verts, to be processed for detection of doubles and mapping.
* Each set of verts is defined by its start within mverts array and its num_verts;
* It builds a mapping for all vertices within source, to vertices within target, or -1 if no double found
* The int doubles_map[num_verts_source] array must have been allocated by caller.
*/
static void bm_merge_dm_transform(BMesh *bm, DerivedMesh *dm, float mat[4][4],
const ArrayModifierData *amd,
BMOperator *dupe_op,
BMOpSlot dupe_op_slot_args[BMO_OP_MAX_SLOTS], const char *dupe_slot_name,
BMOperator *weld_op)
static void dm_mvert_map_doubles(
int *doubles_map,
const MVert *mverts,
const int target_start,
const int target_num_verts,
const int source_start,
const int source_num_verts,
const float dist,
const bool with_follow)
{
const bool is_input = (dupe_op->slots_in == dupe_op_slot_args);
BMVert *v, *v2, *v3;
BMIter iter;
const float dist3 = (M_SQRT3 + 0.00005f) * dist; /* Just above sqrt(3) */
int i_source, i_target, i_target_low_bound, target_end, source_end;
SortVertsElem *sorted_verts_target, *sorted_verts_source;
SortVertsElem *sve_source, *sve_target, *sve_target_low_bound;
bool target_scan_completed;
/* Add the DerivedMesh's elements to the BMesh. The pre-existing
* elements were already tagged, so the new elements can be
* identified by not having the BM_ELEM_TAG flag set. */
DM_to_bmesh_ex(dm, bm, false);
target_end = target_start + target_num_verts;
source_end = source_start + source_num_verts;
if (amd->flags & MOD_ARR_MERGE) {
/* if merging is enabled, find doubles */
BMOIter oiter;
BMOperator find_op;
BMOpSlot *slot_targetmap;
/* build array of MVerts to be tested for merging */
sorted_verts_target = MEM_mallocN(sizeof(SortVertsElem) * target_num_verts, __func__);
sorted_verts_source = MEM_mallocN(sizeof(SortVertsElem) * source_num_verts, __func__);
BMO_op_initf(bm, &find_op, (BMO_FLAG_DEFAULTS & ~BMO_FLAG_RESPECT_HIDE),
is_input ? /* ugh */
"find_doubles verts=%Hv dist=%f keep_verts=%s" :
"find_doubles verts=%Hv dist=%f keep_verts=%S",
BM_ELEM_TAG, amd->merge_dist,
dupe_op, dupe_slot_name);
/* Copy target vertices index and cos into SortVertsElem array */
svert_from_mvert(sorted_verts_target, mverts + target_start, target_start, target_end);
/* append the dupe's geom to the findop input verts */
if (is_input) {
BMO_slot_buffer_append(&find_op, slots_in, "verts",
dupe_op, slots_in, dupe_slot_name);
}
else if (dupe_op->slots_out == dupe_op_slot_args) {
BMO_slot_buffer_append(&find_op, slots_in, "verts",
dupe_op, slots_out, dupe_slot_name);
}
else {
BLI_assert(0);
/* Copy source vertices index and cos into SortVertsElem array */
svert_from_mvert(sorted_verts_source, mverts + source_start, source_start, source_end);
/* sort arrays according to sum of vertex coordinates (sumco) */
qsort(sorted_verts_target, target_num_verts, sizeof(SortVertsElem), svert_sum_cmp);
qsort(sorted_verts_source, source_num_verts, sizeof(SortVertsElem), svert_sum_cmp);
sve_target_low_bound = sorted_verts_target;
i_target_low_bound = 0;
target_scan_completed = false;
/* Scan source vertices, in SortVertsElem sorted array, */
/* all the while maintaining the lower bound of possible doubles in target vertices */
for (i_source = 0, sve_source = sorted_verts_source;
i_source < source_num_verts;
i_source++, sve_source++)
{
bool double_found;
float sve_source_sumco;
/* If source has already been assigned to a target (in an earlier call, with other chunks) */
if (doubles_map[sve_source->vertex_num] != -1) {
continue;
}
/* transform and tag verts */
BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) {
if (!BM_elem_flag_test(v, BM_ELEM_TAG)) {
mul_m4_v3(mat, v->co);
BM_elem_flag_enable(v, BM_ELEM_TAG);
/* If target fully scanned already, then all remaining source vertices cannot have a double */
if (target_scan_completed) {
doubles_map[sve_source->vertex_num] = -1;
continue;
}
sve_source_sumco = sum_v3(sve_source->co);
/* Skip all target vertices that are more than dist3 lower in terms of sumco */
/* and advance the overall lower bound, applicable to all remaining vertices as well. */
while ((i_target_low_bound < target_num_verts) &&
(sve_target_low_bound->sum_co < sve_source_sumco - dist3))
{
i_target_low_bound++;
sve_target_low_bound++;
}
/* If end of target list reached, then no more possible doubles */
if (i_target_low_bound >= target_num_verts) {
doubles_map[sve_source->vertex_num] = -1;
target_scan_completed = true;
continue;
}
/* Test target candidates starting at the low bound of possible doubles, ordered in terms of sumco */
i_target = i_target_low_bound;
sve_target = sve_target_low_bound;
/* i_target will scan vertices in the [v_source_sumco - dist3; v_source_sumco + dist3] range */
double_found = false;
while ((i_target < target_num_verts) &&
(sve_target->sum_co <= sve_source_sumco + dist3))
{
/* Testing distance for candidate double in target */
/* v_target is within dist3 of v_source in terms of sumco; check real distance */
if (compare_len_v3v3(sve_source->co, sve_target->co, dist)) {
/* Double found */
/* If double target is itself already mapped to other vertex,
* behavior depends on with_follow option */
int target_vertex = sve_target->vertex_num;
if (doubles_map[target_vertex] != -1) {
if (with_follow) { /* with_follow option: map to initial target */
target_vertex = doubles_map[target_vertex];
}
else {
/* not with_follow: if target is mapped, then we do not map source, and stop searching */
break;
}
}
doubles_map[sve_source->vertex_num] = target_vertex;
double_found = true;
break;
}
i_target++;
sve_target++;
}
BMO_op_exec(bm, &find_op);
slot_targetmap = BMO_slot_get(weld_op->slots_in, "targetmap");
/* add new merge targets to weld operator */
BMO_ITER (v, &oiter, find_op.slots_out, "targetmap.out", 0) {
v2 = BMO_iter_map_value_ptr(&oiter);
/* check in case the target vertex (v2) is already marked
* for merging */
while ((v3 = BMO_slot_map_elem_get(slot_targetmap, v2))) {
v2 = v3;
}
BMO_slot_map_elem_insert(weld_op, slot_targetmap, v, v2);
/* End of candidate scan: if none found then no doubles */
if (!double_found) {
doubles_map[sve_source->vertex_num] = -1;
}
BMO_op_finish(bm, &find_op);
}
else {
/* transform and tag verts */
BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) {
if (!BM_elem_flag_test(v, BM_ELEM_TAG)) {
mul_m4_v3(mat, v->co);
BM_elem_flag_enable(v, BM_ELEM_TAG);
}
}
MEM_freeN(sorted_verts_source);
MEM_freeN(sorted_verts_target);
}
static void dm_merge_transform(
DerivedMesh *result, DerivedMesh *cap_dm, float cap_offset[4][4],
unsigned int cap_verts_index, unsigned int cap_edges_index, int cap_loops_index, int cap_polys_index,
int cap_nverts, int cap_nedges, int cap_nloops, int cap_npolys)
{
int *index_orig;
int i;
MVert *mv;
MEdge *me;
MLoop *ml;
MPoly *mp;
/* needed for subsurf so arrays are allocated */
cap_dm->getVertArray(cap_dm);
cap_dm->getEdgeArray(cap_dm);
cap_dm->getNumLoops(cap_dm);
cap_dm->getNumPolys(cap_dm);
DM_copy_vert_data(cap_dm, result, 0, cap_verts_index, cap_nverts);
DM_copy_edge_data(cap_dm, result, 0, cap_edges_index, cap_nedges);
DM_copy_loop_data(cap_dm, result, 0, cap_loops_index, cap_nloops);
DM_copy_poly_data(cap_dm, result, 0, cap_polys_index, cap_npolys);
mv = CDDM_get_verts(result) + cap_verts_index;
for (i = 0; i < cap_nverts; i++, mv++) {
mul_m4_v3(cap_offset, mv->co);
/* Reset MVert flags for caps */
mv->flag = mv->bweight = 0;
}
/* adjust cap edge vertex indices */
me = CDDM_get_edges(result) + cap_edges_index;
for (i = 0; i < cap_nedges; i++, me++) {
me->v1 += cap_verts_index;
me->v2 += cap_verts_index;
}
/* adjust cap poly loopstart indices */
mp = CDDM_get_polys(result) + cap_polys_index;
for (i = 0; i < cap_npolys; i++, mp++) {
mp->loopstart += cap_loops_index;
}
/* adjust cap loop vertex and edge indices */
ml = CDDM_get_loops(result) + cap_loops_index;
for (i = 0; i < cap_nloops; i++, ml++) {
ml->v += cap_verts_index;
ml->e += cap_edges_index;
}
/* set origindex */
index_orig = result->getVertDataArray(result, CD_ORIGINDEX);
if (index_orig) {
fill_vn_i(index_orig + cap_verts_index, cap_nverts, ORIGINDEX_NONE);
}
index_orig = result->getEdgeDataArray(result, CD_ORIGINDEX);
if (index_orig) {
fill_vn_i(index_orig + cap_edges_index, cap_nedges, ORIGINDEX_NONE);
}
index_orig = result->getPolyDataArray(result, CD_ORIGINDEX);
if (index_orig) {
fill_vn_i(index_orig + cap_polys_index, cap_npolys, ORIGINDEX_NONE);
}
index_orig = result->getLoopDataArray(result, CD_ORIGINDEX);
if (index_orig) {
fill_vn_i(index_orig + cap_loops_index, cap_nloops, ORIGINDEX_NONE);
}
}
static void merge_first_last(BMesh *bm,
const ArrayModifierData *amd,
BMOperator *dupe_first,
BMOperator *dupe_last,
BMOperator *weld_op)
static DerivedMesh *arrayModifier_doArray(
ArrayModifierData *amd,
Scene *scene, Object *ob, DerivedMesh *dm,
ModifierApplyFlag flag)
{
BMOperator find_op;
BMOIter oiter;
BMVert *v, *v2;
BMOpSlot *slot_targetmap;
const float eps = 1e-6f;
const MVert *src_mvert;
MVert *mv, *mv_prev, *result_dm_verts;
BMO_op_initf(bm, &find_op, (BMO_FLAG_DEFAULTS & ~BMO_FLAG_RESPECT_HIDE),
"find_doubles verts=%s dist=%f keep_verts=%s",
dupe_first, "geom", amd->merge_dist,
dupe_first, "geom");
/* append the last dupe's geom to the findop input verts */
BMO_slot_buffer_append(&find_op, slots_in, "verts",
dupe_last, slots_out, "geom.out");
BMO_op_exec(bm, &find_op);
/* add new merge targets to weld operator */
slot_targetmap = BMO_slot_get(weld_op->slots_in, "targetmap");
BMO_ITER (v, &oiter, find_op.slots_out, "targetmap.out", 0) {
if (!BMO_slot_map_contains(slot_targetmap, v)) {
v2 = BMO_iter_map_value_ptr(&oiter);
BMO_slot_map_elem_insert(weld_op, slot_targetmap, v, v2);
}
}
BMO_op_finish(bm, &find_op);
}
static DerivedMesh *arrayModifier_doArray(ArrayModifierData *amd,
Scene *scene, Object *ob, DerivedMesh *dm,
ModifierApplyFlag flag)
{
DerivedMesh *result;
BMesh *bm = DM_to_bmesh(dm, false);
BMOperator first_dupe_op, dupe_op, old_dupe_op, weld_op;
BMVert **first_geom = NULL;
int i, j;
int index_len = -1; /* initialize to an invalid value */
MEdge *me;
MLoop *ml;
MPoly *mp;
int i, j, c, count;
float length = amd->length;
/* offset matrix */
float offset[4][4];
float scale[3];
bool offset_has_scale;
float current_offset[4][4];
float final_offset[4][4];
float length = amd->length;
int count = amd->count, maxVerts;
int *indexMap = NULL;
DerivedMesh *start_cap = NULL, *end_cap = NULL;
MVert *src_mvert;
BMOpSlot *slot_targetmap = NULL; /* for weld_op */
int *full_doubles_map = NULL;
int tot_doubles;
/* need to avoid infinite recursion here */
if (amd->start_cap && amd->start_cap != ob && amd->start_cap->type == OB_MESH)
start_cap = get_dm_for_modifier(amd->start_cap, flag);
if (amd->end_cap && amd->end_cap != ob && amd->end_cap->type == OB_MESH)
end_cap = get_dm_for_modifier(amd->end_cap, flag);
int start_cap_nverts = 0, start_cap_nedges = 0, start_cap_npolys = 0, start_cap_nloops = 0;
int end_cap_nverts = 0, end_cap_nedges = 0, end_cap_npolys = 0, end_cap_nloops = 0;
int result_nverts = 0, result_nedges = 0, result_npolys = 0, result_nloops = 0;
int chunk_nverts, chunk_nedges, chunk_nloops, chunk_npolys;
int first_chunk_start, first_chunk_nverts, last_chunk_start, last_chunk_nverts;
DerivedMesh *result, *start_cap_dm = NULL, *end_cap_dm = NULL;
chunk_nverts = dm->getNumVerts(dm);
chunk_nedges = dm->getNumEdges(dm);
chunk_nloops = dm->getNumLoops(dm);
chunk_npolys = dm->getNumPolys(dm);
count = amd->count;
if (amd->start_cap && amd->start_cap != ob && amd->start_cap->type == OB_MESH) {
start_cap_dm = get_dm_for_modifier(amd->start_cap, flag);
if (start_cap_dm) {
start_cap_nverts = start_cap_dm->getNumVerts(start_cap_dm);
start_cap_nedges = start_cap_dm->getNumEdges(start_cap_dm);
start_cap_nloops = start_cap_dm->getNumLoops(start_cap_dm);
start_cap_npolys = start_cap_dm->getNumPolys(start_cap_dm);
}
}
if (amd->end_cap && amd->end_cap != ob && amd->end_cap->type == OB_MESH) {
end_cap_dm = get_dm_for_modifier(amd->end_cap, flag);
if (end_cap_dm) {
end_cap_nverts = end_cap_dm->getNumVerts(end_cap_dm);
end_cap_nedges = end_cap_dm->getNumEdges(end_cap_dm);
end_cap_nloops = end_cap_dm->getNumLoops(end_cap_dm);
end_cap_npolys = end_cap_dm->getNumPolys(end_cap_dm);
}
}
/* Build up offset array, cumulating all settings options */
unit_m4(offset);
src_mvert = dm->getVertArray(dm);
maxVerts = dm->getNumVerts(dm);
if (amd->offset_type & MOD_ARR_OFF_CONST)
add_v3_v3v3(offset[3], offset[3], amd->offset);
if (amd->offset_type & MOD_ARR_OFF_RELATIVE) {
for (j = 0; j < 3; j++)
offset[3][j] += amd->scale[j] * vertarray_size(src_mvert, maxVerts, j);
offset[3][j] += amd->scale[j] * vertarray_size(src_mvert, chunk_nverts, j);
}
if ((amd->offset_type & MOD_ARR_OFF_OBJ) && (amd->offset_ob)) {
@@ -371,10 +461,14 @@ static DerivedMesh *arrayModifier_doArray(ArrayModifierData *amd,
unit_m4(obinv);
mul_m4_series(result_mat, offset,
obinv, amd->offset_ob->obmat);
obinv, amd->offset_ob->obmat);
copy_m4_m4(offset, result_mat);
}
/* Check if there is some scaling. If scaling, then we will not translate mapping */
mat4_to_size(scale, offset);
offset_has_scale = !is_one_v3(scale);
if (amd->fit_type == MOD_ARR_FITCURVE && amd->curve_ob) {
Curve *cu = amd->curve_ob->data;
if (cu) {
@@ -396,195 +490,229 @@ static DerivedMesh *arrayModifier_doArray(ArrayModifierData *amd,
if (amd->fit_type == MOD_ARR_FITLENGTH || amd->fit_type == MOD_ARR_FITCURVE) {
float dist = len_v3(offset[3]);
if (dist > 1e-6f)
if (dist > eps) {
/* this gives length = first copy start to last copy end
* add a tiny offset for floating point rounding errors */
count = (length + 1e-6f) / dist;
else
count = (length + eps) / dist;
}
else {
/* if the offset has no translation, just make one copy */
count = 1;
}
}
if (count < 1)
count = 1;
/* calculate the offset matrix of the final copy (for merging) */
unit_m4(final_offset);
/* The number of verts, edges, loops, polys, before eventually merging doubles */
result_nverts = chunk_nverts * count + start_cap_nverts + end_cap_nverts;
result_nedges = chunk_nedges * count + start_cap_nedges + end_cap_nedges;
result_nloops = chunk_nloops * count + start_cap_nloops + end_cap_nloops;
result_npolys = chunk_npolys * count + start_cap_npolys + end_cap_npolys;
for (j = 0; j < count - 1; j++) {
float tmp_mat[4][4];
mul_m4_m4m4(tmp_mat, offset, final_offset);
copy_m4_m4(final_offset, tmp_mat);
}
/* BMESH_TODO: bumping up the stack level avoids computing the normals
* after every top-level operator execution (and this modifier has the
* potential to execute a *lot* of top-level BMOps. There should be a
* cleaner way to do this. One possibility: a "mirror" BMOp would
* certainly help by compressing it all into one top-level BMOp that
* executes a lot of second-level BMOps. */
BM_mesh_elem_toolflags_ensure(bm);
BMO_push(bm, NULL);
bmesh_edit_begin(bm, 0);
/* Initialize a result dm */
result = CDDM_from_template(dm, result_nverts, result_nedges, 0, result_nloops, result_npolys);
result_dm_verts = CDDM_get_verts(result);
if (amd->flags & MOD_ARR_MERGE) {
BMO_op_init(bm, &weld_op, (BMO_FLAG_DEFAULTS & ~BMO_FLAG_RESPECT_HIDE),
"weld_verts");
slot_targetmap = BMO_slot_get(weld_op.slots_in, "targetmap");
/* Will need full_doubles_map for handling merge */
full_doubles_map = MEM_mallocN(sizeof(int) * result_nverts, "mod array doubles map");
fill_vn_i(full_doubles_map, result_nverts, -1);
}
BMO_op_initf(bm, &dupe_op, (BMO_FLAG_DEFAULTS & ~BMO_FLAG_RESPECT_HIDE),
"duplicate geom=%avef");
first_dupe_op = dupe_op;
/* copy customdata to original geometry */
DM_copy_vert_data(dm, result, 0, 0, chunk_nverts);
DM_copy_edge_data(dm, result, 0, 0, chunk_nedges);
DM_copy_loop_data(dm, result, 0, 0, chunk_nloops);
DM_copy_poly_data(dm, result, 0, 0, chunk_npolys);
for (j = 0; j < count - 1; j++) {
BMVert *v, *v2, *v3;
BMOpSlot *geom_slot;
BMOpSlot *geom_out_slot;
BMOIter oiter;
/* subsurf for eg wont have mesh data in the
* now add mvert/medge/mface layers */
if (j != 0) {
BMO_op_initf(bm, &dupe_op,
(BMO_FLAG_DEFAULTS & ~BMO_FLAG_RESPECT_HIDE),
"duplicate geom=%S", &old_dupe_op, "geom.out");
}
BMO_op_exec(bm, &dupe_op);
if (!CustomData_has_layer(&dm->vertData, CD_MVERT)) {
dm->copyVertArray(dm, result_dm_verts);
}
if (!CustomData_has_layer(&dm->edgeData, CD_MEDGE)) {
dm->copyEdgeArray(dm, CDDM_get_edges(result));
}
if (!CustomData_has_layer(&dm->polyData, CD_MPOLY)) {
dm->copyLoopArray(dm, CDDM_get_loops(result));
dm->copyPolyArray(dm, CDDM_get_polys(result));
}
geom_slot = BMO_slot_get(dupe_op.slots_in, "geom");
geom_out_slot = BMO_slot_get(dupe_op.slots_out, "geom.out");
/* Remember first chunk, in case of cap merge */
first_chunk_start = 0;
first_chunk_nverts = chunk_nverts;
if ((amd->flags & MOD_ARR_MERGEFINAL) && j == 0) {
int first_geom_bytes = sizeof(BMVert *) * geom_slot->len;
/* make a copy of the initial geometry ordering so the
* last duplicate can be merged into it */
first_geom = MEM_mallocN(first_geom_bytes, "first_geom");
memcpy(first_geom, geom_slot->data.buf, first_geom_bytes);
unit_m4(current_offset);
for (c = 1; c < count; c++) {
/* copy customdata to new geometry */
DM_copy_vert_data(result, result, 0, c * chunk_nverts, chunk_nverts);
DM_copy_edge_data(result, result, 0, c * chunk_nedges, chunk_nedges);
DM_copy_loop_data(result, result, 0, c * chunk_nloops, chunk_nloops);
DM_copy_poly_data(result, result, 0, c * chunk_npolys, chunk_npolys);
mv_prev = result_dm_verts;
mv = mv_prev + c * chunk_nverts;
/* recalculate cumulative offset here */
mul_m4_m4m4(current_offset, current_offset, offset);
/* apply offset to all new verts */
for (i = 0; i < chunk_nverts; i++, mv++, mv_prev++) {
mul_m4_v3(current_offset, mv->co);
}
/* apply transformation matrix */
BMO_ITER (v, &oiter, dupe_op.slots_out, "geom.out", BM_VERT) {
mul_m4_v3(offset, v->co);
/* adjust edge vertex indices */
me = CDDM_get_edges(result) + c * chunk_nedges;
for (i = 0; i < chunk_nedges; i++, me++) {
me->v1 += c * chunk_nverts;
me->v2 += c * chunk_nverts;
}
if (amd->flags & MOD_ARR_MERGE) {
/*calculate merge mapping*/
if (j == 0) {
indexMap = find_doubles_index_map(bm, &dupe_op,
amd, &index_len);
}
mp = CDDM_get_polys(result) + c * chunk_npolys;
for (i = 0; i < chunk_npolys; i++, mp++) {
mp->loopstart += c * chunk_nloops;
}
#define _E(s, i) ((BMVert **)(s)->data.buf)[i]
/* adjust loop vertex and edge indices */
ml = CDDM_get_loops(result) + c * chunk_nloops;
for (i = 0; i < chunk_nloops; i++, ml++) {
ml->v += c * chunk_nverts;
ml->e += c * chunk_nedges;
}
/* ensure this is set */
BLI_assert(index_len != -1);
for (i = 0; i < index_len; i++) {
if (!indexMap[i]) continue;
/* merge v (from 'geom.out') into v2 (from old 'geom') */
v = _E(geom_out_slot, i - geom_slot->len);
v2 = _E(geom_slot, indexMap[i] - 1);
/* check in case the target vertex (v2) is already marked
* for merging */
while ((v3 = BMO_slot_map_elem_get(slot_targetmap, v2))) {
v2 = v3;
/* Handle merge between chunk n and n-1 */
if ((amd->flags & MOD_ARR_MERGE) && (c >= 1)) {
if (!offset_has_scale && (c >= 2)) {
/* Mapping chunk 3 to chunk 2 is a translation of mapping 2 to 1
* ... that is except if scaling makes the distance grow */
int k;
int this_chunk_index = c * chunk_nverts;
int prev_chunk_index = (c - 1) * chunk_nverts;
for (k = 0; k < chunk_nverts; k++, this_chunk_index++, prev_chunk_index++) {
int target = full_doubles_map[prev_chunk_index];
if (target != -1) {
target += chunk_nverts; /* translate mapping */
/* The rule here is to not follow mapping to chunk N-2, which could be too far
* so if target vertex was itself mapped, then this vertex is not mapped */
if (full_doubles_map[target] != -1) {
target = -1;
}
}
full_doubles_map[this_chunk_index] = target;
}
BMO_slot_map_elem_insert(&weld_op, slot_targetmap, v, v2);
}
#undef _E
else {
dm_mvert_map_doubles(
full_doubles_map,
result_dm_verts,
(c - 1) * chunk_nverts,
chunk_nverts,
c * chunk_nverts,
chunk_nverts,
amd->merge_dist,
false);
}
}
/* already copied earlier, but after executation more slot
* memory may be allocated */
if (j == 0)
first_dupe_op = dupe_op;
if (j >= 2)
BMO_op_finish(bm, &old_dupe_op);
old_dupe_op = dupe_op;
}
last_chunk_start = (count - 1) * chunk_nverts;
last_chunk_nverts = chunk_nverts;
copy_m4_m4(final_offset, current_offset);
if ((amd->flags & MOD_ARR_MERGE) &&
(amd->flags & MOD_ARR_MERGEFINAL) &&
(count > 1))
{
/* Merge first and last copies. Note that we can't use the
* indexMap for this because (unless the array is forming a
* loop) the offset between first and last is different from
* dupe X to dupe X+1. */
merge_first_last(bm, amd, &first_dupe_op, &dupe_op, &weld_op);
/* Merge first and last copies */
dm_mvert_map_doubles(
full_doubles_map,
result_dm_verts,
last_chunk_start,
last_chunk_nverts,
first_chunk_start,
first_chunk_nverts,
amd->merge_dist,
false);
}
/* start capping */
if (start_cap || end_cap) {
BM_mesh_elem_hflag_enable_all(bm, BM_VERT, BM_ELEM_TAG, false);
if (start_cap) {
float startoffset[4][4];
invert_m4_m4(startoffset, offset);
bm_merge_dm_transform(bm, start_cap, startoffset, amd,
&first_dupe_op, first_dupe_op.slots_in, "geom", &weld_op);
if (start_cap_dm) {
float start_offset[4][4];
int start_cap_start = result_nverts - start_cap_nverts - end_cap_nverts;
invert_m4_m4(start_offset, offset);
dm_merge_transform(
result, start_cap_dm, start_offset,
result_nverts - start_cap_nverts - end_cap_nverts,
result_nedges - start_cap_nedges - end_cap_nedges,
result_nloops - start_cap_nloops - end_cap_nloops,
result_npolys - start_cap_npolys - end_cap_npolys,
start_cap_nverts, start_cap_nedges, start_cap_nloops, start_cap_npolys);
/* Identify doubles with first chunk */
if (amd->flags & MOD_ARR_MERGE) {
dm_mvert_map_doubles(
full_doubles_map,
result_dm_verts,
first_chunk_start,
first_chunk_nverts,
start_cap_start,
start_cap_nverts,
amd->merge_dist,
false);
}
}
if (end_cap) {
float endoffset[4][4];
mul_m4_m4m4(endoffset, offset, final_offset);
bm_merge_dm_transform(bm, end_cap, endoffset, amd,
&dupe_op, (count == 1) ? dupe_op.slots_in : dupe_op.slots_out,
(count == 1) ? "geom" : "geom.out", &weld_op);
if (end_cap_dm) {
float end_offset[4][4];
int end_cap_start = result_nverts - end_cap_nverts;
mul_m4_m4m4(end_offset, current_offset, offset);
dm_merge_transform(
result, end_cap_dm, end_offset,
result_nverts - end_cap_nverts,
result_nedges - end_cap_nedges,
result_nloops - end_cap_nloops,
result_npolys - end_cap_npolys,
end_cap_nverts, end_cap_nedges, end_cap_nloops, end_cap_npolys);
/* Identify doubles with last chunk */
if (amd->flags & MOD_ARR_MERGE) {
dm_mvert_map_doubles(
full_doubles_map,
result_dm_verts,
last_chunk_start,
last_chunk_nverts,
end_cap_start,
end_cap_nverts,
amd->merge_dist,
false);
}
}
/* done capping */
/* free remaining dupe operators */
BMO_op_finish(bm, &first_dupe_op);
if (count > 2)
BMO_op_finish(bm, &dupe_op);
/* run merge operator */
if (amd->flags & MOD_ARR_MERGE) {
BMO_op_exec(bm, &weld_op);
BMO_op_finish(bm, &weld_op);
/* Handle merging */
tot_doubles = 0;
if (full_doubles_map) {
for (i = 0; i < result_nverts; i++) {
if (full_doubles_map[i] != -1) {
tot_doubles++;
}
}
if (tot_doubles > 0) {
result = CDDM_merge_verts(result, full_doubles_map, tot_doubles, CDDM_MERGE_VERTS_DUMP_IF_EQUAL);
}
MEM_freeN(full_doubles_map);
}
/* Bump the stack level back down to match the adjustment up above */
BMO_pop(bm);
result = CDDM_from_bmesh(bm, false);
if ((dm->dirty & DM_DIRTY_NORMALS) ||
((amd->offset_type & MOD_ARR_OFF_OBJ) && (amd->offset_ob)))
{
/* Update normals in case offset object has rotation. */
result->dirty |= DM_DIRTY_NORMALS;
}
BM_mesh_free(bm);
if (indexMap)
MEM_freeN(indexMap);
if (first_geom)
MEM_freeN(first_geom);
return result;
}
static DerivedMesh *applyModifier(ModifierData *md, Object *ob,
DerivedMesh *dm,
ModifierApplyFlag flag)
{
DerivedMesh *result;
ArrayModifierData *amd = (ArrayModifierData *) md;
result = arrayModifier_doArray(amd, md->scene, ob, dm, flag);
return result;
return arrayModifier_doArray(amd, md->scene, ob, dm, flag);
}

View File

@@ -290,7 +290,7 @@ static DerivedMesh *doMirrorOnAxis(MirrorModifierData *mmd,
/* slow - so only call if one or more merge verts are found,
* users may leave this on and not realize there is nothing to merge - campbell */
if (tot_vtargetmap) {
result = CDDM_merge_verts(result, vtargetmap, tot_vtargetmap);
result = CDDM_merge_verts(result, vtargetmap, tot_vtargetmap, CDDM_MERGE_VERTS_DUMP_IF_MAPPED);
}
MEM_freeN(vtargetmap);
}