Mesh: Handle free custom normals properly for object joining
Use the same rules as the realize instances node to make sure that free and tangent-space custom normals aren't joined together with implicit conversions and that there is as little data loss as possible when joining combinations of no custom normals, free normals, etc. Pull Request: https://projects.blender.org/blender/blender/pulls/147241
This commit is contained in:
@@ -20,6 +20,7 @@ namespace blender::bke {
|
||||
|
||||
enum class AttrDomain : int8_t;
|
||||
enum class AttrType : int16_t;
|
||||
struct AttributeMetaData;
|
||||
struct AttributeAccessorFunctions;
|
||||
|
||||
namespace mesh {
|
||||
@@ -249,6 +250,23 @@ void edges_sharp_from_angle_set(OffsetIndices<int> faces,
|
||||
const float split_angle,
|
||||
MutableSpan<bool> sharp_edges);
|
||||
|
||||
/** Return true if the type and domain represent the tangent-space custom normals storage. */
|
||||
bool is_corner_fan_normals(const AttributeMetaData &meta_data);
|
||||
|
||||
/** Tracks the storage format for a resulting mesh based on a combination of input meshes. */
|
||||
class NormalJoinInfo {
|
||||
public:
|
||||
enum class Output : int8_t { None, CornerFan, Free };
|
||||
Output result_type = Output::None;
|
||||
std::optional<bke::AttrDomain> result_domain;
|
||||
|
||||
void add_no_custom_normals(bke::MeshNormalDomain domain);
|
||||
void add_corner_fan_normals();
|
||||
void add_domain(bke::AttrDomain domain);
|
||||
void add_free_normals(bke::AttrDomain domain);
|
||||
void add_mesh(const Mesh &mesh);
|
||||
};
|
||||
|
||||
} // namespace mesh
|
||||
|
||||
/**
|
||||
|
||||
@@ -1710,6 +1710,85 @@ void mesh_set_custom_normals_from_verts_normalized(Mesh &mesh, MutableSpan<float
|
||||
mesh::mesh_set_custom_normals(mesh, vert_normals, true);
|
||||
}
|
||||
|
||||
namespace mesh {
|
||||
|
||||
constexpr AttributeMetaData CORNER_FAN_META_DATA{AttrDomain::Corner, AttrType::Int16_2D};
|
||||
|
||||
bool is_corner_fan_normals(const AttributeMetaData &meta_data)
|
||||
{
|
||||
return meta_data == CORNER_FAN_META_DATA;
|
||||
}
|
||||
|
||||
static bke::AttrDomain normal_domain_to_domain(bke::MeshNormalDomain domain)
|
||||
{
|
||||
switch (domain) {
|
||||
case bke::MeshNormalDomain::Point:
|
||||
return bke::AttrDomain::Point;
|
||||
case bke::MeshNormalDomain::Face:
|
||||
return bke::AttrDomain::Face;
|
||||
case bke::MeshNormalDomain::Corner:
|
||||
return bke::AttrDomain::Corner;
|
||||
}
|
||||
BLI_assert_unreachable();
|
||||
return bke::AttrDomain::Point;
|
||||
}
|
||||
|
||||
void NormalJoinInfo::add_no_custom_normals(const bke::MeshNormalDomain domain)
|
||||
{
|
||||
this->add_domain(normal_domain_to_domain(domain));
|
||||
}
|
||||
|
||||
void NormalJoinInfo::add_corner_fan_normals()
|
||||
{
|
||||
this->add_domain(bke::AttrDomain::Corner);
|
||||
if (this->result_type == Output::None) {
|
||||
this->result_type = Output::CornerFan;
|
||||
}
|
||||
}
|
||||
|
||||
void NormalJoinInfo::add_domain(const bke::AttrDomain domain)
|
||||
{
|
||||
if (this->result_domain) {
|
||||
/* Any combination of point/face domains puts the result normals on the corner domain. */
|
||||
if (this->result_domain != domain) {
|
||||
this->result_domain = bke::AttrDomain::Corner;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this->result_domain = domain;
|
||||
}
|
||||
}
|
||||
|
||||
void NormalJoinInfo::add_free_normals(const bke::AttrDomain domain)
|
||||
{
|
||||
this->add_domain(domain);
|
||||
this->result_type = Output::Free;
|
||||
}
|
||||
|
||||
void NormalJoinInfo::add_mesh(const Mesh &mesh)
|
||||
{
|
||||
const bke::AttributeAccessor attributes = mesh.attributes();
|
||||
const std::optional<bke::AttributeMetaData> custom_normal = attributes.lookup_meta_data(
|
||||
"custom_normal");
|
||||
if (!custom_normal) {
|
||||
this->add_no_custom_normals(mesh.normals_domain());
|
||||
return;
|
||||
}
|
||||
if (custom_normal->data_type == bke::AttrType::Float3) {
|
||||
if (custom_normal->domain == bke::AttrDomain::Edge) {
|
||||
/* Skip invalid storage on the edge domain. */
|
||||
this->add_no_custom_normals(mesh.normals_domain());
|
||||
return;
|
||||
}
|
||||
this->add_free_normals(custom_normal->domain);
|
||||
}
|
||||
else if (*custom_normal == CORNER_FAN_META_DATA) {
|
||||
this->add_corner_fan_normals();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mesh
|
||||
|
||||
} // namespace blender::bke
|
||||
|
||||
#undef LNOR_SPACE_TRIGO_THRESHOLD
|
||||
|
||||
@@ -118,6 +118,78 @@ static void join_positions(const Span<const Object *> objects_to_join,
|
||||
}
|
||||
}
|
||||
|
||||
static void join_normals(const Span<const Object *> objects_to_join,
|
||||
const OffsetIndices<int> vert_ranges,
|
||||
const OffsetIndices<int> face_ranges,
|
||||
const OffsetIndices<int> corner_ranges,
|
||||
const float4x4 &world_to_dst_mesh,
|
||||
Mesh &dst_mesh)
|
||||
{
|
||||
bke::mesh::NormalJoinInfo normal_info;
|
||||
for (const Object *object : objects_to_join) {
|
||||
const Mesh &mesh = *static_cast<const Mesh *>(object->data);
|
||||
normal_info.add_mesh(mesh);
|
||||
}
|
||||
|
||||
bke::MutableAttributeAccessor dst_attributes = dst_mesh.attributes_for_write();
|
||||
switch (normal_info.result_type) {
|
||||
case bke::mesh::NormalJoinInfo::Output::None: {
|
||||
break;
|
||||
}
|
||||
case bke::mesh::NormalJoinInfo::Output::CornerFan: {
|
||||
bke::SpanAttributeWriter dst_attr = dst_attributes.lookup_or_add_for_write_only_span<short2>(
|
||||
"custom_normal", bke::AttrDomain::Corner);
|
||||
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);
|
||||
const bke::AttributeAccessor attributes = src_mesh.attributes();
|
||||
const bke::GAttributeReader src = attributes.lookup("custom_normal");
|
||||
if (!src) {
|
||||
dst_attr.span.slice(corner_ranges[i]).fill(short2(0));
|
||||
continue;
|
||||
}
|
||||
const bke::AttrType data_type = bke::cpp_type_to_attribute_type(src.varray.type());
|
||||
if (!bke::mesh::is_corner_fan_normals({src.domain, data_type})) {
|
||||
dst_attr.span.slice(corner_ranges[i]).fill(short2(0));
|
||||
continue;
|
||||
}
|
||||
src.typed<short2>().varray.materialize(dst_attr.span.slice(corner_ranges[i]));
|
||||
}
|
||||
dst_attr.finish();
|
||||
break;
|
||||
}
|
||||
case bke::mesh::NormalJoinInfo::Output::Free: {
|
||||
bke::SpanAttributeWriter dst_attr = dst_attributes.lookup_or_add_for_write_only_span<float3>(
|
||||
"custom_normal", *normal_info.result_domain);
|
||||
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);
|
||||
switch (*normal_info.result_domain) {
|
||||
case bke::AttrDomain::Point:
|
||||
math::transform_normals(src_mesh.vert_normals(),
|
||||
float3x3(world_to_dst_mesh),
|
||||
dst_attr.span.slice(vert_ranges[i]));
|
||||
break;
|
||||
case bke::AttrDomain::Face:
|
||||
math::transform_normals(src_mesh.face_normals(),
|
||||
float3x3(world_to_dst_mesh),
|
||||
dst_attr.span.slice(face_ranges[i]));
|
||||
break;
|
||||
case bke::AttrDomain::Corner:
|
||||
math::transform_normals(src_mesh.corner_normals(),
|
||||
float3x3(world_to_dst_mesh),
|
||||
dst_attr.span.slice(corner_ranges[i]));
|
||||
break;
|
||||
default:
|
||||
BLI_assert_unreachable();
|
||||
}
|
||||
}
|
||||
dst_attr.finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void join_shape_keys(Main *bmain,
|
||||
const Span<const Object *> objects_to_join,
|
||||
const OffsetIndices<int> vert_ranges,
|
||||
@@ -212,6 +284,7 @@ static void join_generic_attributes(const Span<const Object *> objects_to_join,
|
||||
".corner_vert",
|
||||
".corner_edge",
|
||||
"material_index",
|
||||
"custom_normal",
|
||||
".sculpt_face_set"};
|
||||
|
||||
Array<std::string> names;
|
||||
@@ -510,6 +583,8 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op)
|
||||
|
||||
join_shape_keys(bmain, objects_to_join, vert_ranges, world_to_active_object, *active_mesh);
|
||||
join_positions(objects_to_join, vert_ranges, world_to_active_object, *dst_mesh);
|
||||
join_normals(
|
||||
objects_to_join, vert_ranges, face_ranges, corner_ranges, world_to_active_object, *dst_mesh);
|
||||
|
||||
MutableSpan<int2> dst_edges = dst_mesh->edges_for_write();
|
||||
for (const int i : objects_to_join.index_range()) {
|
||||
|
||||
@@ -212,84 +212,6 @@ struct AllPointCloudsInfo {
|
||||
bool create_radius_attribute = false;
|
||||
};
|
||||
|
||||
static bke::AttrDomain normal_domain_to_domain(bke::MeshNormalDomain domain)
|
||||
{
|
||||
switch (domain) {
|
||||
case bke::MeshNormalDomain::Point:
|
||||
return bke::AttrDomain::Point;
|
||||
case bke::MeshNormalDomain::Face:
|
||||
return bke::AttrDomain::Face;
|
||||
case bke::MeshNormalDomain::Corner:
|
||||
return bke::AttrDomain::Corner;
|
||||
}
|
||||
BLI_assert_unreachable();
|
||||
return bke::AttrDomain::Point;
|
||||
}
|
||||
|
||||
constexpr bke::AttributeMetaData CORNER_FAN_META_DATA{bke::AttrDomain::Corner,
|
||||
bke::AttrType::Int16_2D};
|
||||
|
||||
/** Tracks the storage format for the resulting mesh based on the combination of input meshes. */
|
||||
struct MeshNormalInfo {
|
||||
enum class Output : int8_t { None, CornerFan, Free };
|
||||
Output result_type = Output::None;
|
||||
std::optional<bke::AttrDomain> result_domain;
|
||||
|
||||
void add_no_custom_normals(const bke::MeshNormalDomain domain)
|
||||
{
|
||||
this->add_domain(normal_domain_to_domain(domain));
|
||||
}
|
||||
|
||||
void add_corner_fan_normals()
|
||||
{
|
||||
this->add_domain(bke::AttrDomain::Corner);
|
||||
if (this->result_type == Output::None) {
|
||||
this->result_type = Output::CornerFan;
|
||||
}
|
||||
}
|
||||
|
||||
void add_domain(const bke::AttrDomain domain)
|
||||
{
|
||||
if (this->result_domain) {
|
||||
/* Any combination of point/face domains puts the result normals on the corner domain. */
|
||||
if (this->result_domain != domain) {
|
||||
this->result_domain = bke::AttrDomain::Corner;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this->result_domain = domain;
|
||||
}
|
||||
}
|
||||
|
||||
void add_free_normals(const bke::AttrDomain domain)
|
||||
{
|
||||
this->add_domain(domain);
|
||||
this->result_type = Output::Free;
|
||||
}
|
||||
|
||||
void add_mesh(const Mesh &mesh)
|
||||
{
|
||||
const bke::AttributeAccessor attributes = mesh.attributes();
|
||||
const std::optional<bke::AttributeMetaData> custom_normal = attributes.lookup_meta_data(
|
||||
"custom_normal");
|
||||
if (!custom_normal) {
|
||||
this->add_no_custom_normals(mesh.normals_domain());
|
||||
return;
|
||||
}
|
||||
if (custom_normal->data_type == bke::AttrType::Float3) {
|
||||
if (custom_normal->domain == bke::AttrDomain::Edge) {
|
||||
/* Skip invalid storage on the edge domain. */
|
||||
this->add_no_custom_normals(mesh.normals_domain());
|
||||
return;
|
||||
}
|
||||
this->add_free_normals(custom_normal->domain);
|
||||
}
|
||||
else if (*custom_normal == CORNER_FAN_META_DATA) {
|
||||
this->add_corner_fan_normals();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct AllMeshesInfo {
|
||||
/** Ordering of all attributes that are propagated to the output mesh generically. */
|
||||
OrderedAttributes attributes;
|
||||
@@ -301,7 +223,7 @@ struct AllMeshesInfo {
|
||||
VectorSet<Material *> materials;
|
||||
bool create_id_attribute = false;
|
||||
bool create_material_index_attribute = false;
|
||||
MeshNormalInfo custom_normal_info;
|
||||
bke::mesh::NormalJoinInfo custom_normal_info;
|
||||
|
||||
/** True if we know that there are no loose edges in any of the input meshes. */
|
||||
bool no_loose_edges_hint = false;
|
||||
@@ -1478,17 +1400,20 @@ static AllMeshesInfo preprocess_meshes(const bke::GeometrySet &geometry_set,
|
||||
"material_index", bke::AttrDomain::Face, 0);
|
||||
|
||||
switch (info.custom_normal_info.result_type) {
|
||||
case MeshNormalInfo::Output::None: {
|
||||
case bke::mesh::NormalJoinInfo::Output::None: {
|
||||
break;
|
||||
}
|
||||
case MeshNormalInfo::Output::CornerFan: {
|
||||
if (attributes.lookup_meta_data("custom_normal") == CORNER_FAN_META_DATA) {
|
||||
mesh_info.custom_normal = *attributes.lookup<short2>("custom_normal",
|
||||
bke::AttrDomain::Corner);
|
||||
case bke::mesh::NormalJoinInfo::Output::CornerFan: {
|
||||
if (const bke::GAttributeReader custom_normal = attributes.lookup("custom_normal")) {
|
||||
const bke::AttributeMetaData meta_data{
|
||||
custom_normal.domain, bke::cpp_type_to_attribute_type(custom_normal.varray.type())};
|
||||
if (bke::mesh::is_corner_fan_normals(meta_data)) {
|
||||
mesh_info.custom_normal = custom_normal.varray.typed<short2>();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MeshNormalInfo::Output::Free: {
|
||||
case bke::mesh::NormalJoinInfo::Output::Free: {
|
||||
switch (*info.custom_normal_info.result_domain) {
|
||||
case bke::AttrDomain::Point:
|
||||
mesh_info.custom_normal = VArray<float3>::from_span(mesh->vert_normals());
|
||||
@@ -1762,15 +1687,15 @@ static void execute_realize_mesh_tasks(const RealizeInstancesOptions &options,
|
||||
|
||||
GSpanAttributeWriter custom_normals;
|
||||
switch (all_meshes_info.custom_normal_info.result_type) {
|
||||
case MeshNormalInfo::Output::None: {
|
||||
case bke::mesh::NormalJoinInfo::Output::None: {
|
||||
break;
|
||||
}
|
||||
case MeshNormalInfo::Output::CornerFan: {
|
||||
case bke::mesh::NormalJoinInfo::Output::CornerFan: {
|
||||
custom_normals = dst_attributes.lookup_or_add_for_write_only_span(
|
||||
"custom_normal", bke::AttrDomain::Corner, bke::AttrType::Int16_2D);
|
||||
break;
|
||||
}
|
||||
case MeshNormalInfo::Output::Free: {
|
||||
case bke::mesh::NormalJoinInfo::Output::Free: {
|
||||
const bke::AttrDomain domain = *all_meshes_info.custom_normal_info.result_domain;
|
||||
custom_normals = dst_attributes.lookup_or_add_for_write_only_span(
|
||||
"custom_normal", domain, bke::AttrType::Float3);
|
||||
|
||||
Reference in New Issue
Block a user