Mesh: Restructure join operator for simplicity, speed, consistency

Previously the joining code put the final data into temporary mesh
data, which meant more complexity because it couldn't use the slightly
higher level APIs more commonly used for transferring data.

The process can be simplified by just merging into the active mesh
directly (though some care must be taken when that is used by
multiple selected objects).

This process avoids iterating over attribute data twice, by processing
values at the same time as copying them. Also some of the inner hot
loops are parallelized. There is more opportunity for trivial multi-
threading in the future.

Mismatched attribute types and domains will now choose the higher
complexity type and domain, which should help avoid information loss.

There was a bug with the existing face set processing which would
modify the source meshes. That is now fixed.
This commit is contained in:
Hans Goudey
2025-08-29 16:38:51 -04:00
committed by Hans Goudey
parent 7409dc5171
commit 113d91aba8
3 changed files with 391 additions and 412 deletions

View File

@@ -342,8 +342,6 @@ void CustomData_copy_data_layer(const CustomData *source,
int src_index,
int dst_index,
int count);
void CustomData_copy_data_named(
const CustomData *source, CustomData *dest, int source_index, int dest_index, int count);
void CustomData_copy_elements(eCustomDataType type,
void *src_data_ofs,
void *dst_data_ofs,

View File

@@ -3313,25 +3313,6 @@ void CustomData_copy_data_layer(const CustomData *source,
}
}
void CustomData_copy_data_named(const CustomData *source,
CustomData *dest,
const int source_index,
const int dest_index,
const int count)
{
/* copies a layer at a time */
for (int src_i = 0; src_i < source->totlayer; src_i++) {
int dest_i = CustomData_get_named_layer_index(
dest, eCustomDataType(source->layers[src_i].type), source->layers[src_i].name);
/* if we found a matching layer, copy the data */
if (dest_i != -1) {
CustomData_copy_data_layer(source, dest, src_i, dest_i, source_index, dest_index, count);
}
}
}
void CustomData_copy_data(const CustomData *source,
CustomData *dest,
const int source_index,

View File

@@ -13,7 +13,6 @@
#include "BLI_listbase.h"
#include "BLI_math_matrix.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector.h"
#include "BLI_vector.hh"
#include "BLI_virtual_array.hh"
@@ -43,6 +42,7 @@
#include "DEG_depsgraph_build.hh"
#include "DEG_depsgraph_query.hh"
#include "ED_geometry.hh"
#include "ED_mesh.hh"
#include "ED_object.hh"
#include "ED_view3d.hh"
@@ -54,240 +54,330 @@
namespace blender::ed::mesh {
/* join selected meshes into the active mesh, context sensitive
* return 0 if no join is made (error) and 1 if the join is done */
static void join_mesh_single(Depsgraph *depsgraph,
Main *bmain,
Scene *scene,
Object *ob_dst,
Object *ob_src,
const float4x4 &world_to_active_object,
MutableSpan<float3> dst_positions,
MutableSpan<int2> dst_edges,
MutableSpan<int> dst_corner_verts,
MutableSpan<int> dst_corner_edges,
MutableSpan<int> dst_face_offsets,
CustomData *vert_data,
CustomData *edge_data,
CustomData *face_data,
CustomData *corner_data,
int verts_num,
int edges_num,
int faces_num,
int corners_num,
Key *key,
Key *nkey,
Vector<Material *> &materials,
const IndexRange vert_range,
const IndexRange edge_range,
const IndexRange face_range,
const IndexRange corner_range)
static VectorSet<std::string> join_vertex_groups(const Span<const Object *> objects_to_join,
const OffsetIndices<int> vert_ranges,
Mesh &dst_mesh)
{
int a;
VectorSet<std::string> vertex_group_names;
LISTBASE_FOREACH (const bDeformGroup *, dg, &dst_mesh.vertex_group_names) {
vertex_group_names.add_new(dg->name);
}
Mesh *mesh_src = static_cast<Mesh *>(ob_src->data);
if (mesh_src->verts_num) {
/* standard data */
CustomData_merge_layout(
&mesh_src->vert_data, vert_data, CD_MASK_MESH.vmask, CD_SET_DEFAULT, verts_num);
CustomData_copy_data_named(
&mesh_src->vert_data, vert_data, 0, vert_range.start(), mesh_src->verts_num);
/* vertex groups */
MDeformVert *dvert = (MDeformVert *)CustomData_get_layer_for_write(
vert_data, CD_MDEFORMVERT, verts_num);
const MDeformVert *dvert_src = (const MDeformVert *)CustomData_get_layer(&mesh_src->vert_data,
CD_MDEFORMVERT);
/* Remap to correct new vgroup indices, if needed. */
if (dvert_src) {
BLI_assert(dvert != nullptr);
/* Build src to merged mapping of vgroup indices. */
int *vgroup_index_map;
int vgroup_index_map_len;
vgroup_index_map = BKE_object_defgroup_index_map_create(
ob_src, ob_dst, &vgroup_index_map_len);
BKE_object_defgroup_index_map_apply(
&dvert[vert_range.start()], mesh_src->verts_num, vgroup_index_map, vgroup_index_map_len);
if (vgroup_index_map != nullptr) {
MEM_freeN(vgroup_index_map);
bool any_vertex_group_data = false;
for (const int i : objects_to_join.index_range().drop_front(1)) {
const Mesh &mesh = *static_cast<const Mesh *>(objects_to_join[i]->data);
any_vertex_group_data |= CustomData_has_layer(&mesh.vert_data, CD_MDEFORMVERT);
LISTBASE_FOREACH (const bDeformGroup *, dg, &mesh.vertex_group_names) {
if (vertex_group_names.add_as(dg->name)) {
BLI_addtail(&dst_mesh.vertex_group_names, BKE_defgroup_duplicate(dg));
}
}
}
/* if this is the object we're merging into, no need to do anything */
if (ob_src != ob_dst) {
float cmat[4][4];
if (!any_vertex_group_data) {
return vertex_group_names;
}
/* Watch this: switch matrix multiplication order really goes wrong. */
mul_m4_m4m4(cmat, world_to_active_object.ptr(), ob_src->object_to_world().ptr());
MDeformVert *dvert = (MDeformVert *)CustomData_add_layer(
&dst_mesh.vert_data, CD_MDEFORMVERT, CD_CONSTRUCT, dst_mesh.verts_num);
math::transform_points(float4x4(cmat), dst_positions.slice(vert_range));
for (const int i : objects_to_join.index_range().drop_front(1)) {
const Mesh &src_mesh = *static_cast<const Mesh *>(objects_to_join[i]->data);
const Span<MDeformVert> src_dverts = src_mesh.deform_verts().take_front(vert_ranges[i].size());
if (src_dverts.is_empty()) {
continue;
}
Vector<int, 32> index_map;
LISTBASE_FOREACH (const bDeformGroup *, dg, &dst_mesh.vertex_group_names) {
index_map.append(vertex_group_names.index_of_as(dg->name));
}
for (const int vert : src_dverts.index_range()) {
const MDeformVert &src = src_dverts[vert];
MDeformVert &dst = dvert[vert_ranges[i][vert]];
dst = src;
dst.dw = MEM_malloc_arrayN<MDeformWeight>(src.totweight, __func__);
for (const int weight : IndexRange(src.totweight)) {
dst.dw[weight].def_nr = index_map[src.dw[weight].def_nr];
dst.dw[weight].weight = src.dw[weight].weight;
}
}
}
/* For each shape-key in destination mesh:
* - if there's a matching one, copy it across
* (will need to transform vertices into new space...).
* - otherwise, just copy its own coordinates of mesh
* (no need to transform vertex coordinates into new space).
*/
if (key) {
/* if this mesh has any shape-keys, check first, otherwise just copy coordinates */
LISTBASE_FOREACH (KeyBlock *, kb, &key->block) {
MutableSpan<float3> key_data(static_cast<float3 *>(kb->data), kb->totelem);
if (const KeyBlock *src_kb = mesh_src->key ?
BKE_keyblock_find_name(mesh_src->key, kb->name) :
nullptr)
{
const Span<float3> src_kb_data(static_cast<float3 *>(src_kb->data), src_kb->totelem);
math::transform_points(src_kb_data, float4x4(cmat), key_data);
}
else {
key_data.slice(vert_range).copy_from(dst_positions.slice(vert_range));
}
return vertex_group_names;
}
static void join_positions_and_shape_keys(const Span<const Object *> objects_to_join,
const OffsetIndices<int> vert_ranges,
const float4x4 &world_to_dst_mesh,
Mesh &dst_mesh)
{
Vector<KeyBlock *> key_blocks;
VectorSet<std::string> key_names;
if (Key *key = dst_mesh.key) {
LISTBASE_FOREACH (KeyBlock *, kb, &key->block) {
key_names.add_new(kb->name);
key_blocks.append(kb);
}
}
const auto ensure_dst_key = [&]() {
if (!dst_mesh.key) {
dst_mesh.key = BKE_key_add(nullptr, nullptr);
dst_mesh.key->type = KEY_RELATIVE;
}
};
MutableSpan<float3> dst_positions = dst_mesh.vert_positions_for_write();
for (const int i : objects_to_join.index_range().drop_front(1)) {
const Key *src_key = static_cast<const Mesh *>(objects_to_join[i]->data)->key;
if (!src_key) {
continue;
}
ensure_dst_key();
LISTBASE_FOREACH (const KeyBlock *, src_kb, &src_key->block) {
if (key_names.add_as(src_kb->name)) {
KeyBlock *dst_kb = BKE_keyblock_add(dst_mesh.key, src_kb->name);
BKE_keyblock_copy_settings(dst_kb, src_kb);
dst_kb->data = MEM_malloc_arrayN<float3>(dst_mesh.verts_num, __func__);
dst_kb->totelem = dst_mesh.verts_num;
/* Initialize the new shape key data with the base positions for the active object. */
MutableSpan<float3> key_data(static_cast<float3 *>(dst_kb->data), dst_kb->totelem);
key_data.take_front(vert_ranges[0].size())
.copy_from(dst_positions.take_front(vert_ranges[0].size()));
/* Remap `KeyBlock::relative`. */
if (const KeyBlock *src_kb_relative = static_cast<KeyBlock *>(
BLI_findlink(&src_key->block, src_kb->relative)))
{
dst_kb->relative = key_names.index_of_as(src_kb_relative->name);
}
}
}
}
for (const int i : objects_to_join.index_range().drop_front(1)) {
const Object &src_object = *objects_to_join[i];
const IndexRange dst_range = vert_ranges[i];
const Mesh &src_mesh = *static_cast<const Mesh *>(src_object.data);
const Span<float3> src_positions = src_mesh.vert_positions().take_front(dst_range.size());
const float4x4 transform = world_to_dst_mesh * src_object.object_to_world();
math::transform_points(src_positions, transform, dst_positions.slice(dst_range));
if (Key *dst_key = dst_mesh.key) {
LISTBASE_FOREACH (KeyBlock *, kb, &dst_key->block) {
MutableSpan<float3> key_data(static_cast<float3 *>(kb->data), kb->totelem);
if (const KeyBlock *src_kb = src_mesh.key ?
BKE_keyblock_find_name(src_mesh.key, kb->name) :
nullptr)
{
const Span<float3> src_kb_data(static_cast<float3 *>(src_kb->data), dst_range.size());
math::transform_points(src_kb_data, transform, key_data.slice(dst_range));
}
else {
key_data.slice(dst_range).copy_from(dst_positions.slice(dst_range));
}
}
}
}
}
static void join_generic_attributes(const Span<const Object *> objects_to_join,
const VectorSet<std::string> &all_vertex_group_names,
const OffsetIndices<int> vert_ranges,
const OffsetIndices<int> edge_ranges,
const OffsetIndices<int> face_ranges,
const OffsetIndices<int> corner_ranges,
Mesh &dst_mesh)
{
Set<StringRef> skip_names{"position",
".edge_verts",
".corner_vert",
".corner_edge",
"material_index",
".sculpt_face_set"};
bke::GeometrySet::GatheredAttributes attr_info;
for (const int i : objects_to_join.index_range()) {
const Mesh &mesh = *static_cast<const Mesh *>(objects_to_join[i]->data);
mesh.attributes().foreach_attribute([&](const bke::AttributeIter &attr) {
if (skip_names.contains(attr.name) || all_vertex_group_names.contains(attr.name)) {
return;
}
attr_info.add(attr.name, {attr.domain, attr.data_type});
});
}
bke::MutableAttributeAccessor dst_attributes = dst_mesh.attributes_for_write();
const Set<StringRefNull> attribute_names = dst_attributes.all_ids();
for (const int attr_i : attr_info.names.index_range()) {
const StringRef name = attr_info.names[attr_i];
const bke::AttrDomain domain = attr_info.kinds[attr_i].domain;
const bke::AttrType data_type = attr_info.kinds[attr_i].data_type;
if (const std::optional<bke::AttributeMetaData> meta_data = dst_attributes.lookup_meta_data(
name))
{
if (meta_data->domain != domain || meta_data->data_type != data_type) {
AttributeOwner owner = AttributeOwner::from_id(&dst_mesh.id);
geometry::convert_attribute(
owner, dst_attributes, name, meta_data->domain, meta_data->data_type, nullptr);
}
}
else {
/* for each shape-key in destination mesh:
* - if it was an 'original', copy the appropriate data from nkey
* - otherwise, copy across plain coordinates (no need to transform coordinates)
*/
if (key) {
LISTBASE_FOREACH (KeyBlock *, kb, &key->block) {
MutableSpan<float3> key_data(static_cast<float3 *>(kb->data), kb->totelem);
if (const KeyBlock *src_kb = nkey ? BKE_keyblock_find_name(nkey, kb->name) : nullptr) {
const Span<float3> src_kb_data(static_cast<float3 *>(src_kb->data), src_kb->totelem);
key_data.slice(vert_range).copy_from(src_kb_data);
}
else {
key_data.slice(vert_range).copy_from(dst_positions.slice(vert_range));
}
dst_attributes.add(name, domain, data_type, bke::AttributeInitConstruct());
}
}
for (const int attr_i : attr_info.names.index_range()) {
const StringRef name = attr_info.names[attr_i];
const bke::AttrDomain domain = attr_info.kinds[attr_i].domain;
const bke::AttrType data_type = attr_info.kinds[attr_i].data_type;
bke::GSpanAttributeWriter dst = dst_attributes.lookup_for_write_span(name);
for (const int i : objects_to_join.index_range().drop_front(1)) {
const Mesh &src_mesh = *static_cast<const Mesh *>(objects_to_join[i]->data);
const bke::AttributeAccessor src_attributes = src_mesh.attributes();
const GVArray src = *src_attributes.lookup_or_default(name, domain, data_type);
const IndexRange dst_range = [&]() {
switch (domain) {
case bke::AttrDomain::Point:
return vert_ranges[i];
case bke::AttrDomain::Edge:
return edge_ranges[i];
case bke::AttrDomain::Face:
return face_ranges[i];
case bke::AttrDomain::Corner:
return corner_ranges[i];
default:
BLI_assert_unreachable();
return IndexRange();
}
}();
src.materialize(IndexRange(dst_range.size()), dst.span.slice(dst_range).data());
}
dst.finish();
}
}
static VectorSet<Material *> join_materials(const Span<const Object *> objects_to_join,
const OffsetIndices<int> face_ranges,
Mesh &dst_mesh)
{
VectorSet<Material *> materials;
for (const int i : objects_to_join.index_range()) {
const Object &src_object = *objects_to_join[i];
const Mesh &src_mesh = *static_cast<const Mesh *>(src_object.data);
if (src_mesh.totcol == 0) {
materials.add(nullptr);
continue;
}
for (const int material_index : IndexRange(src_mesh.totcol)) {
Material *material = BKE_object_material_get(&const_cast<Object &>(src_object),
material_index + 1);
if (materials.size() < MAXMAT) {
materials.add(material);
}
}
}
if (mesh_src->edges_num) {
CustomData_merge_layout(
&mesh_src->edge_data, edge_data, CD_MASK_MESH.emask, CD_SET_DEFAULT, edges_num);
CustomData_copy_data_named(
&mesh_src->edge_data, edge_data, 0, edge_range.start(), mesh_src->edges_num);
bke::MutableAttributeAccessor dst_attributes = dst_mesh.attributes_for_write();
if (materials.size() <= 1) {
dst_attributes.remove("material_index");
return materials;
}
for (int2 &edge : dst_edges.slice(edge_range)) {
edge += vert_range.start();
bke::SpanAttributeWriter dst_material_indices = dst_attributes.lookup_or_add_for_write_span<int>(
"material_index", bke::AttrDomain::Face);
if (!dst_material_indices) {
return {};
}
for (const int i : objects_to_join.index_range().drop_front(1)) {
const Object &src_object = *objects_to_join[i];
const IndexRange dst_range = face_ranges[i];
const Mesh &src_mesh = *static_cast<const Mesh *>(src_object.data);
const bke::AttributeAccessor src_attributes = src_mesh.attributes();
const VArray<int> material_indices = *src_attributes.lookup<int>("material_index",
bke::AttrDomain::Face);
if (material_indices.is_empty()) {
Material *first_material = src_mesh.totcol == 0 ?
nullptr :
BKE_object_material_get(&const_cast<Object &>(src_object), 1);
dst_material_indices.span.slice(dst_range).fill(materials.index_of(first_material));
continue;
}
if (src_mesh.totcol == 0) {
/* These material indices are invalid, but copy them anyway to avoid destroying user data. */
material_indices.materialize(dst_range.index_range(),
dst_material_indices.span.slice(dst_range));
continue;
}
Array<int, 32> index_map(src_mesh.totcol);
for (const int material_index : IndexRange(src_mesh.totcol)) {
Material *material = BKE_object_material_get(&const_cast<Object &>(src_object),
material_index + 1);
const int dst_index = materials.index_of_try(material);
index_map[material_index] = dst_index == -1 ? 0 : dst_index;
}
const int max = src_mesh.totcol - 1;
for (const int face : dst_range.index_range()) {
const int src = std::clamp(material_indices[face], 0, max);
dst_material_indices.span[dst_range[face]] = index_map[src];
}
}
if (mesh_src->corners_num) {
if (ob_src != ob_dst) {
MultiresModifierData *mmd;
dst_material_indices.finish();
multiresModifier_prepare_join(depsgraph, scene, ob_src, ob_dst);
if ((mmd = get_multires_modifier(scene, ob_src, true))) {
object::iter_other(bmain, ob_src, true, object::multires_update_totlevels, &mmd->totlvl);
}
}
CustomData_merge_layout(
&mesh_src->corner_data, corner_data, CD_MASK_MESH.lmask, CD_SET_DEFAULT, corners_num);
CustomData_copy_data_named(
&mesh_src->corner_data, corner_data, 0, corner_range.start(), mesh_src->corners_num);
for (int &vert : dst_corner_verts.slice(corner_range)) {
vert += vert_range.start();
}
for (int &edge : dst_corner_edges.slice(corner_range)) {
edge += edge_range.start();
}
}
/* Make remapping for material indices. Assume at least one slot,
* that will be null if there are no actual slots. */
const int totcol = std::max(ob_src->totcol, 1);
Vector<int> matmap(totcol);
if (mesh_src->faces_num) {
for (a = 1; a <= totcol; a++) {
Material *ma = (a <= ob_src->totcol) ? BKE_object_material_get(ob_src, a) : nullptr;
/* Try to reuse existing slot. */
int b = 0;
for (; b < materials.size(); b++) {
if (ma == materials[b]) {
matmap[a - 1] = b;
break;
}
}
if (b == materials.size()) {
if (materials.size() == MAXMAT) {
/* Reached max limit of materials, use first slot. */
matmap[a - 1] = 0;
}
else {
/* Add new slot. */
matmap[a - 1] = materials.size();
materials.append(ma);
if (ma) {
id_us_plus(&ma->id);
}
}
}
}
CustomData_merge_layout(
&mesh_src->face_data, face_data, CD_MASK_MESH.pmask, CD_SET_DEFAULT, faces_num);
CustomData_copy_data_named(
&mesh_src->face_data, face_data, 0, face_range.start(), mesh_src->faces_num);
/* Apply matmap. In case we don't have material indices yet, create them if more than one
* material is the result of joining. */
int *material_indices = static_cast<int *>(CustomData_get_layer_named_for_write(
face_data, CD_PROP_INT32, "material_index", faces_num));
if (!material_indices && materials.size() > 1) {
material_indices = (int *)CustomData_add_layer_named(
face_data, CD_PROP_INT32, CD_SET_DEFAULT, faces_num, "material_index");
}
if (material_indices) {
for (a = 0; a < mesh_src->faces_num; a++) {
/* Clamp invalid slots, matching #BKE_object_material_get_p. */
const int mat_index = std::clamp(material_indices[a + face_range.start()], 0, totcol - 1);
material_indices[a + face_range.start()] = matmap[mat_index];
}
}
const Span<int> src_face_offsets = mesh_src->face_offsets();
for (const int i : face_range.index_range()) {
dst_face_offsets[face_range[i]] = src_face_offsets[i] + corner_range.start();
}
}
return materials;
}
/* Face Sets IDs are a sparse sequence, so this function offsets all the IDs by face_set_offset and
* updates face_set_offset with the maximum ID value. This way, when used in multiple meshes, all
* of them will have different IDs for their Face Sets. */
static void mesh_join_offset_face_sets_ID(Mesh *mesh, int *face_set_offset)
static void join_face_sets(const Span<const Object *> objects_to_join,
const OffsetIndices<int> face_ranges,
Mesh &dst_mesh)
{
bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
bke::SpanAttributeWriter<int> face_sets = attributes.lookup_for_write_span<int>(
bke::MutableAttributeAccessor dst_attributes = dst_mesh.attributes_for_write();
bke::SpanAttributeWriter dst_face_sets = dst_attributes.lookup_for_write_span<int>(
".sculpt_face_set");
if (!face_sets) {
if (!dst_face_sets) {
return;
}
if (dst_face_sets.domain != bke::AttrDomain::Face) {
return;
}
int max_face_set = 0;
for (const int i : face_sets.span.index_range()) {
/* As face sets encode the visibility in the integer sign, the offset needs to be added or
* subtracted depending on the initial sign of the integer to get the new ID. */
if (face_sets.span[i] <= *face_set_offset) {
face_sets.span[i] += *face_set_offset;
int max_face_set = 1;
for (const int i : objects_to_join.index_range().drop_front(1)) {
const Object &src_object = *objects_to_join[i];
const IndexRange dst_range = face_ranges[i];
const Mesh &src_mesh = *static_cast<const Mesh *>(src_object.data);
const bke::AttributeAccessor src_attributes = src_mesh.attributes();
const VArraySpan src_face_sets = *src_attributes.lookup<int>(".sculpt_face_set",
bke::AttrDomain::Face);
if (src_face_sets.is_empty()) {
dst_face_sets.span.slice(dst_range).fill(max_face_set);
}
max_face_set = max_ii(max_face_set, face_sets.span[i]);
else {
for (const int face : dst_range.index_range()) {
dst_face_sets.span[dst_range[face]] = src_face_sets[face] + max_face_set;
}
max_face_set = std::max(
max_face_set,
*std::max_element(src_face_sets.begin(), src_face_sets.begin() + dst_range.size()));
}
max_face_set++;
}
*face_set_offset = max_face_set;
face_sets.finish();
dst_face_sets.finish();
}
wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op)
@@ -328,8 +418,6 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op)
objects_to_join.prepend(active_object);
}
int haskey = 0;
Array<int> vert_offset_data(objects_to_join.size() + 1);
Array<int> edge_offset_data(objects_to_join.size() + 1);
Array<int> face_offset_data(objects_to_join.size() + 1);
@@ -340,9 +428,6 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op)
edge_offset_data[i] = mesh.edges_num;
face_offset_data[i] = mesh.faces_num;
corner_offset_data[i] = mesh.corners_num;
if (mesh.key) {
haskey++;
}
}
const OffsetIndices<int> vert_ranges = offset_indices::accumulate_counts_to_offsets(
@@ -384,201 +469,117 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op)
return OPERATOR_CANCELLED;
}
/* Active object materials in new main array, is nicer start! */
Vector<Material *> materials;
for (const int a : IndexRange(active_object->totcol)) {
materials.append(BKE_object_material_get(active_object, a + 1));
id_us_plus((ID *)materials[a]);
/* increase id->us : will be lowered later */
CustomData_realloc(&dst_mesh->vert_data, dst_mesh->verts_num, vert_ranges.total_size());
CustomData_realloc(&dst_mesh->edge_data, dst_mesh->edges_num, edge_ranges.total_size());
CustomData_realloc(&dst_mesh->face_data, dst_mesh->faces_num, face_ranges.total_size());
CustomData_realloc(&dst_mesh->corner_data, dst_mesh->corners_num, corner_ranges.total_size());
if (face_ranges.total_size() != dst_mesh->faces_num) {
implicit_sharing::resize_trivial_array(&dst_mesh->face_offset_indices,
&dst_mesh->runtime->face_offsets_sharing_info,
dst_mesh->faces_num,
face_ranges.total_size() + 1);
}
/* - If destination mesh had shape-keys, move them somewhere safe, and set up placeholders
* with arrays that are large enough to hold shape-key data for all meshes.
* - If destination mesh didn't have shape-keys, but we encountered some in the meshes we're
* joining, set up a new key-block and assign to the mesh.
*/
Key *nkey = nullptr;
if (key) {
/* make a duplicate copy that will only be used here... (must remember to free it!) */
nkey = (Key *)BKE_id_copy(bmain, &key->id);
/* for all keys in old block, clear data-arrays */
dst_mesh->verts_num = vert_ranges.total_size();
dst_mesh->edges_num = edge_ranges.total_size();
dst_mesh->faces_num = face_ranges.total_size();
dst_mesh->corners_num = corner_ranges.total_size();
if (Key *key = dst_mesh->key) {
LISTBASE_FOREACH (KeyBlock *, kb, &key->block) {
if (kb->data) {
MEM_freeN(kb->data);
}
kb->data = MEM_callocN(sizeof(float[3]) * vert_ranges.total_size(), "join_shapekey");
kb->totelem = vert_ranges.total_size();
}
}
else if (haskey) {
/* add a new key-block and add to the mesh */
key = dst_mesh->key = BKE_key_add(bmain, (ID *)dst_mesh);
key->type = KEY_RELATIVE;
}
/* Update face_set_id_offset with the face set data in the active object first. This way the Face
* Sets IDs in the active object are not the ones that are modified. */
int face_set_id_offset = 0;
mesh_join_offset_face_sets_ID(dst_mesh, &face_set_id_offset);
/* Copy materials, vertex-groups, face sets & face-maps across objects. */
for (const Object *ob_iter : objects_to_join) {
if (ob_iter == active_object) {
continue;
}
Mesh *mesh = static_cast<Mesh *>(ob_iter->data);
/* Join this object's vertex groups to the base one's */
LISTBASE_FOREACH (bDeformGroup *, dg, &mesh->vertex_group_names) {
/* See if this group exists in the object (if it doesn't, add it to the end) */
if (!BKE_object_defgroup_find_name(active_object, dg->name)) {
bDeformGroup *odg = MEM_mallocN<bDeformGroup>(__func__);
memcpy(odg, dg, sizeof(bDeformGroup));
BLI_addtail(&dst_mesh->vertex_group_names, odg);
}
}
if (!BLI_listbase_is_empty(&dst_mesh->vertex_group_names) &&
mesh->vertex_group_active_index == 0)
{
mesh->vertex_group_active_index = 1;
}
mesh_join_offset_face_sets_ID(mesh, &face_set_id_offset);
if (mesh->verts_num) {
/* If this mesh has shape-keys,
* check if destination mesh already has matching entries too. */
if (mesh->key && key) {
/* for remapping KeyBlock.relative */
int *index_map = MEM_malloc_arrayN<int>(mesh->key->totkey, __func__);
KeyBlock **kb_map = MEM_malloc_arrayN<KeyBlock *>(mesh->key->totkey, __func__);
int i;
LISTBASE_FOREACH_INDEX (KeyBlock *, kb, &mesh->key->block, i) {
BLI_assert(i < mesh->key->totkey);
KeyBlock *kbn = BKE_keyblock_find_name(key, kb->name);
/* if key doesn't exist in destination mesh, add it */
if (kbn) {
index_map[i] = BLI_findindex(&key->block, kbn);
}
else {
index_map[i] = key->totkey;
kbn = BKE_keyblock_add(key, kb->name);
BKE_keyblock_copy_settings(kbn, kb);
/* adjust settings to fit (allocate a new data-array) */
kbn->data = MEM_callocN(sizeof(float[3]) * vert_ranges.total_size(),
"joined_shapekey");
kbn->totelem = vert_ranges.total_size();
}
kb_map[i] = kbn;
}
/* remap relative index values */
LISTBASE_FOREACH_INDEX (KeyBlock *, kb, &mesh->key->block, i) {
/* sanity check, should always be true */
if (LIKELY(kb->relative < mesh->key->totkey)) {
kb_map[i]->relative = index_map[kb->relative];
}
}
MEM_freeN(index_map);
MEM_freeN(kb_map);
}
kb->data = MEM_reallocN(kb->data, sizeof(float3) * dst_mesh->verts_num);
kb->totelem = dst_mesh->verts_num;
}
}
/* setup new data for destination mesh */
CustomData vert_data;
CustomData edge_data;
CustomData face_data;
CustomData corner_data;
CustomData_reset(&vert_data);
CustomData_reset(&edge_data);
CustomData_reset(&corner_data);
CustomData_reset(&face_data);
MutableSpan<float3> vert_positions(
(float3 *)CustomData_add_layer_named(
&vert_data, CD_PROP_FLOAT3, CD_SET_DEFAULT, vert_ranges.total_size(), "position"),
vert_ranges.total_size());
MutableSpan<int2> edge(
(int2 *)CustomData_add_layer_named(
&edge_data, CD_PROP_INT32_2D, CD_CONSTRUCT, edge_ranges.total_size(), ".edge_verts"),
edge_ranges.total_size());
MutableSpan<int> corner_verts(
(int *)CustomData_add_layer_named(
&corner_data, CD_PROP_INT32, CD_CONSTRUCT, corner_ranges.total_size(), ".corner_vert"),
corner_ranges.total_size());
MutableSpan<int> corner_edges(
(int *)CustomData_add_layer_named(
&corner_data, CD_PROP_INT32, CD_CONSTRUCT, corner_ranges.total_size(), ".corner_edge"),
corner_ranges.total_size());
int *face_offsets = MEM_malloc_arrayN<int>(face_ranges.total_size() + 1, __func__);
face_offsets[face_ranges.total_size()] = corner_ranges.total_size();
BKE_mesh_runtime_clear_geometry(dst_mesh);
/* Inverse transform for all selected meshes in this object,
* See #object_join_exec for detailed comment on why the safe version is used. */
float4x4 world_to_active_object;
invert_m4_m4_safe_ortho(world_to_active_object.ptr(), active_object->object_to_world().ptr());
for (const int i : objects_to_join.index_range()) {
Object *ob_iter = objects_to_join[i];
join_mesh_single(depsgraph,
bmain,
scene,
active_object,
ob_iter,
world_to_active_object,
vert_positions,
edge,
corner_verts,
corner_edges,
{face_offsets, face_ranges.total_size()},
&vert_data,
&edge_data,
&face_data,
&corner_data,
vert_ranges.total_size(),
edge_ranges.total_size(),
face_ranges.total_size(),
corner_ranges.total_size(),
key,
nkey,
materials,
vert_ranges[i],
edge_ranges[i],
face_ranges[i],
corner_ranges[i]);
join_positions_and_shape_keys(objects_to_join, vert_ranges, world_to_active_object, *dst_mesh);
/* free base, now that data is merged */
if (ob_iter != active_object) {
object::base_free_and_unlink(bmain, scene, ob_iter);
MutableSpan<int2> dst_edges = dst_mesh->edges_for_write();
for (const int i : objects_to_join.index_range().drop_front(1)) {
const Object &src_object = *objects_to_join[i];
const IndexRange dst_range = edge_ranges[i];
const Mesh &src_mesh = *static_cast<const Mesh *>(src_object.data);
const Span<int2> src_edges = src_mesh.edges();
for (const int edge : dst_range.index_range()) {
dst_edges[dst_range[edge]] = src_edges[edge] + int(vert_ranges[i].start());
}
}
BKE_mesh_clear_geometry(dst_mesh);
if (face_offsets) {
dst_mesh->face_offset_indices = face_offsets;
dst_mesh->runtime->face_offsets_sharing_info = implicit_sharing::info_for_mem_free(
face_offsets);
MutableSpan<int> dst_corner_verts = dst_mesh->corner_verts_for_write();
for (const int i : objects_to_join.index_range().drop_front(1)) {
const Object &src_object = *objects_to_join[i];
const IndexRange dst_range = corner_ranges[i];
const Mesh &src_mesh = *static_cast<const Mesh *>(src_object.data);
const Span<int> src_corner_verts = src_mesh.corner_verts();
for (const int corner : dst_range.index_range()) {
dst_corner_verts[dst_range[corner]] = src_corner_verts[corner] + int(vert_ranges[i].start());
}
}
dst_mesh->verts_num = vert_ranges.total_size();
dst_mesh->edges_num = edge_ranges.total_size();
dst_mesh->faces_num = face_ranges.total_size();
dst_mesh->corners_num = corner_ranges.total_size();
MutableSpan<int> dst_corner_edges = dst_mesh->corner_edges_for_write();
for (const int i : objects_to_join.index_range().drop_front(1)) {
const Object &src_object = *objects_to_join[i];
const IndexRange dst_range = corner_ranges[i];
const Mesh &src_mesh = *static_cast<const Mesh *>(src_object.data);
const Span<int> src_corner_edges = src_mesh.corner_edges();
for (const int corner : dst_range.index_range()) {
dst_corner_edges[dst_range[corner]] = src_corner_edges[corner] + int(edge_ranges[i].start());
}
}
dst_mesh->vert_data = vert_data;
dst_mesh->edge_data = edge_data;
dst_mesh->corner_data = corner_data;
dst_mesh->face_data = face_data;
MutableSpan<int> dst_face_offsets = dst_mesh->face_offsets_for_write();
for (const int i : objects_to_join.index_range().drop_front(1)) {
const Object &src_object = *objects_to_join[i];
const IndexRange dst_range = face_ranges[i];
const Mesh &src_mesh = *static_cast<const Mesh *>(src_object.data);
const Span<int> src_face_offsets = src_mesh.face_offsets();
for (const int face : dst_range.index_range()) {
dst_face_offsets[dst_range[face]] = src_face_offsets[face] + corner_ranges[i].start();
}
}
dst_face_offsets.last() = dst_mesh->corners_num;
for (const int i : objects_to_join.index_range().drop_front(1)) {
const Object &src_object = *objects_to_join[i];
const Mesh &src_mesh = *static_cast<const Mesh *>(src_object.data);
const Key *src_key = src_mesh.key;
if (!src_key) {
continue;
}
}
for (const int i : objects_to_join.index_range().drop_front(1)) {
Object &src_object = *objects_to_join[i];
multiresModifier_prepare_join(depsgraph, scene, &src_object, active_object);
if (MultiresModifierData *mmd = get_multires_modifier(scene, &src_object, true)) {
object::iter_other(
bmain, &src_object, true, object::multires_update_totlevels, &mmd->totlvl);
}
}
join_face_sets(objects_to_join, face_ranges, *dst_mesh);
VectorSet<Material *> materials = join_materials(objects_to_join, face_ranges, *dst_mesh);
VectorSet<std::string> vertex_group_names = join_vertex_groups(
objects_to_join, vert_ranges, *dst_mesh);
join_generic_attributes(objects_to_join,
vertex_group_names,
vert_ranges,
edge_ranges,
face_ranges,
corner_ranges,
*dst_mesh);
for (Object *object : objects_to_join.as_span().drop_front(1)) {
object::base_free_and_unlink(bmain, scene, object);
}
/* old material array */
for (const int a : IndexRange(active_object->totcol)) {
@@ -602,8 +603,13 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op)
const int totcol = materials.size();
if (totcol) {
dst_mesh->mat = MEM_calloc_arrayN<Material *>(totcol, __func__);
std::copy_n(materials.data(), totcol, dst_mesh->mat);
VectorData data = materials.extract_vector().release();
dst_mesh->mat = data.data;
for (const int i : IndexRange(totcol)) {
if (Material *ma = dst_mesh->mat[i]) {
id_us_plus((ID *)ma);
}
}
active_object->mat = MEM_calloc_arrayN<Material *>(totcol, __func__);
active_object->matbits = MEM_calloc_arrayN<char>(totcol, __func__);
}
@@ -613,12 +619,6 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op)
/* other mesh users */
BKE_objects_materials_sync_length_all(bmain, (ID *)dst_mesh);
/* Free temporary copy of destination shape-keys (if applicable). */
if (nkey) {
/* We can assume nobody is using that ID currently. */
BKE_id_free_ex(bmain, nkey, LIB_ID_FREE_NO_UI_USER, false);
}
/* ensure newly inserted keys are time sorted */
if (key && (key->type != KEY_RELATIVE)) {
BKE_key_sort(key);