From 0eb6aef3b0009fc7fc0c1588097e09df8cf6aaaf Mon Sep 17 00:00:00 2001 From: Aras Pranckevicius Date: Tue, 31 Oct 2023 14:07:51 +0100 Subject: [PATCH] PLY: import/export custom vertex attributes (#108948) Implements #108948 - support for custom point domain attributes for PLY import and export. Notes: - Custom attributes are always represented as scalar floats. PLY itself has some data types that blender can't fully represent, e.g. double or uint32, or some that fit into a float just fine but blender does not have them as separate types (e.g. int16). - When importing, any PLY vertex attribute that is not one of "standard" names (position, normal, etc.) gets turned into a custom attribute. - For exporting, more complex custom attributes (e.g. 2D/3D floats, quaternions, colors) get turned into several PLY attributes, with "_x" like suffix. Custom attribute import/export is on by default in the UI. Pull Request: https://projects.blender.org/blender/blender/pulls/114320 Reviewed By: Hans Goudey --- source/blender/editors/io/io_ply_ops.cc | 13 +- source/blender/io/ply/IO_ply.hh | 2 + .../io/ply/exporter/ply_export_data.cc | 4 + .../io/ply/exporter/ply_export_header.cc | 4 + .../ply/exporter/ply_export_load_plydata.cc | 166 ++++++++++++++++++ .../io/ply/exporter/ply_file_buffer.hh | 2 + .../io/ply/exporter/ply_file_buffer_ascii.cc | 5 + .../io/ply/exporter/ply_file_buffer_ascii.hh | 2 + .../io/ply/exporter/ply_file_buffer_binary.cc | 8 + .../io/ply/exporter/ply_file_buffer_binary.hh | 2 + .../io/ply/importer/ply_import_data.cc | 19 ++ .../io/ply/importer/ply_import_mesh.cc | 9 + source/blender/io/ply/intern/ply_data.hh | 9 + 13 files changed, 243 insertions(+), 2 deletions(-) diff --git a/source/blender/editors/io/io_ply_ops.cc b/source/blender/editors/io/io_ply_ops.cc index f3372436b4c..21b920fe162 100644 --- a/source/blender/editors/io/io_ply_ops.cc +++ b/source/blender/editors/io/io_ply_ops.cc @@ -80,6 +80,7 @@ static int wm_ply_export_exec(bContext *C, wmOperator *op) export_params.export_uv = RNA_boolean_get(op->ptr, "export_uv"); export_params.export_normals = RNA_boolean_get(op->ptr, "export_normals"); export_params.vertex_colors = ePLYVertexColorMode(RNA_enum_get(op->ptr, "export_colors")); + export_params.export_attributes = RNA_boolean_get(op->ptr, "export_attributes"); export_params.export_triangulated_mesh = RNA_boolean_get(op->ptr, "export_triangulated_mesh"); export_params.ascii_format = RNA_boolean_get(op->ptr, "ascii_format"); @@ -120,6 +121,7 @@ static void ui_ply_export_settings(uiLayout *layout, PointerRNA *imfptr) uiItemR(sub, imfptr, "export_uv", UI_ITEM_NONE, IFACE_("UV Coordinates"), ICON_NONE); uiItemR(sub, imfptr, "export_normals", UI_ITEM_NONE, IFACE_("Vertex Normals"), ICON_NONE); uiItemR(sub, imfptr, "export_colors", UI_ITEM_NONE, IFACE_("Vertex Colors"), ICON_NONE); + uiItemR(sub, imfptr, "export_attributes", UI_ITEM_NONE, IFACE_("Vertex Attributes"), ICON_NONE); uiItemR(sub, imfptr, "export_triangulated_mesh", @@ -211,7 +213,11 @@ void WM_OT_ply_export(wmOperatorType *ot) PLY_VERTEX_COLOR_SRGB, "Export Vertex Colors", "Export vertex color attributes"); - + RNA_def_boolean(ot->srna, + "export_attributes", + true, + "Export Vertex Attributes", + "Export custom vertex attributes"); RNA_def_boolean(ot->srna, "export_triangulated_mesh", false, @@ -243,6 +249,7 @@ static int wm_ply_import_exec(bContext *C, wmOperator *op) params.use_scene_unit = RNA_boolean_get(op->ptr, "use_scene_unit"); params.global_scale = RNA_float_get(op->ptr, "global_scale"); params.merge_verts = RNA_boolean_get(op->ptr, "merge_verts"); + params.import_attributes = RNA_boolean_get(op->ptr, "import_attributes"); params.vertex_colors = ePLYVertexColorMode(RNA_enum_get(op->ptr, "import_colors")); int files_len = RNA_collection_length(op->ptr, "files"); @@ -316,8 +323,10 @@ void WM_OT_ply_import(wmOperatorType *ot) "import_colors", ply_vertex_colors_mode, PLY_VERTEX_COLOR_SRGB, - "Import Vertex Colors", + "Vertex Colors", "Import vertex color attributes"); + RNA_def_boolean( + ot->srna, "import_attributes", true, "Vertex Attributes", "Import custom vertex attributes"); /* Only show .ply files by default. */ prop = RNA_def_string(ot->srna, "filter_glob", "*.ply", 0, "Extension Filter", ""); diff --git a/source/blender/io/ply/IO_ply.hh b/source/blender/io/ply/IO_ply.hh index 248e7bf75ec..3d3842497f2 100644 --- a/source/blender/io/ply/IO_ply.hh +++ b/source/blender/io/ply/IO_ply.hh @@ -43,6 +43,7 @@ struct PLYExportParams { bool export_uv; bool export_normals; ePLYVertexColorMode vertex_colors; + bool export_attributes; bool export_triangulated_mesh; }; @@ -54,6 +55,7 @@ struct PLYImportParams { bool use_scene_unit; float global_scale; ePLYVertexColorMode vertex_colors; + bool import_attributes; bool merge_verts; }; diff --git a/source/blender/io/ply/exporter/ply_export_data.cc b/source/blender/io/ply/exporter/ply_export_data.cc index a644192b040..49d06a3b50e 100644 --- a/source/blender/io/ply/exporter/ply_export_data.cc +++ b/source/blender/io/ply/exporter/ply_export_data.cc @@ -34,6 +34,10 @@ void write_vertices(FileBuffer &buffer, const PlyData &ply_data) buffer.write_UV(ply_data.uv_coordinates[i].x, ply_data.uv_coordinates[i].y); } + for (const PlyCustomAttribute &attr : ply_data.vertex_custom_attr) { + buffer.write_data(attr.data[i]); + } + buffer.write_vertex_end(); } buffer.write_to_file(); diff --git a/source/blender/io/ply/exporter/ply_export_header.cc b/source/blender/io/ply/exporter/ply_export_header.cc index 50eddbe550a..f303320c9d5 100644 --- a/source/blender/io/ply/exporter/ply_export_header.cc +++ b/source/blender/io/ply/exporter/ply_export_header.cc @@ -50,6 +50,10 @@ void write_header(FileBuffer &buffer, buffer.write_header_scalar_property("float", "t"); } + for (const PlyCustomAttribute &attr : ply_data.vertex_custom_attr) { + buffer.write_header_scalar_property("float", attr.name); + } + if (!ply_data.face_sizes.is_empty()) { buffer.write_header_element("face", int(ply_data.face_sizes.size())); buffer.write_header_list_property("uchar", "uint", "vertex_indices"); diff --git a/source/blender/io/ply/exporter/ply_export_load_plydata.cc b/source/blender/io/ply/exporter/ply_export_load_plydata.cc index 707de8feead..a370673b06a 100644 --- a/source/blender/io/ply/exporter/ply_export_load_plydata.cc +++ b/source/blender/io/ply/exporter/ply_export_load_plydata.cc @@ -17,6 +17,7 @@ #include "BLI_hash.hh" #include "BLI_math_color.hh" #include "BLI_math_matrix.h" +#include "BLI_math_quaternion.hh" #include "BLI_math_rotation.h" #include "BLI_math_vector.h" #include "BLI_vector.hh" @@ -147,6 +148,166 @@ static void generate_vertex_map(const Mesh *mesh, } } +static void load_custom_attributes(const Mesh *mesh, Vector &r_attributes) +{ + const bke::AttributeAccessor attributes = mesh->attributes(); + const StringRef color_name = mesh->active_color_attribute; + const StringRef uv_name = CustomData_get_active_layer_name(&mesh->loop_data, CD_PROP_FLOAT2); + + attributes.for_all([&](const bke::AttributeIDRef &attribute_id, + const bke::AttributeMetaData &meta_data) { + /* Skip internal, standard and non-vertex domain attributes. */ + if (meta_data.domain != ATTR_DOMAIN_POINT || attribute_id.name()[0] == '.' || + attribute_id.is_anonymous() || ELEM(attribute_id.name(), "position", color_name, uv_name)) + { + return true; + } + + const GVArraySpan attribute = *mesh->attributes().lookup( + attribute_id, meta_data.domain, meta_data.data_type); + const int64_t size = attribute.size(); + if (size == 0) { + return true; + } + switch (meta_data.data_type) { + case CD_PROP_FLOAT: { + PlyCustomAttribute attr(attribute_id.name(), size); + auto typed = attribute.typed(); + for (const int64_t i : typed.index_range()) { + attr.data[i] = typed[i]; + } + r_attributes.append(attr); + break; + } + case CD_PROP_INT8: { + PlyCustomAttribute attr(attribute_id.name(), size); + auto typed = attribute.typed(); + for (const int64_t i : typed.index_range()) { + attr.data[i] = typed[i]; + } + r_attributes.append(attr); + break; + } + case CD_PROP_INT32: { + PlyCustomAttribute attr(attribute_id.name(), size); + auto typed = attribute.typed(); + for (const int64_t i : typed.index_range()) { + attr.data[i] = typed[i]; + } + r_attributes.append(attr); + break; + } + case CD_PROP_INT32_2D: { + PlyCustomAttribute attr_x(attribute_id.name() + "_x", size); + PlyCustomAttribute attr_y(attribute_id.name() + "_y", size); + auto typed = attribute.typed(); + for (const int64_t i : typed.index_range()) { + attr_x.data[i] = typed[i].x; + attr_y.data[i] = typed[i].y; + } + r_attributes.append(attr_x); + r_attributes.append(attr_y); + break; + } + case CD_PROP_FLOAT2: { + PlyCustomAttribute attr_x(attribute_id.name() + "_x", size); + PlyCustomAttribute attr_y(attribute_id.name() + "_y", size); + auto typed = attribute.typed(); + for (const int64_t i : typed.index_range()) { + attr_x.data[i] = typed[i].x; + attr_y.data[i] = typed[i].y; + } + r_attributes.append(attr_x); + r_attributes.append(attr_y); + break; + } + case CD_PROP_FLOAT3: { + PlyCustomAttribute attr_x(attribute_id.name() + "_x", size); + PlyCustomAttribute attr_y(attribute_id.name() + "_y", size); + PlyCustomAttribute attr_z(attribute_id.name() + "_z", size); + auto typed = attribute.typed(); + for (const int64_t i : typed.index_range()) { + attr_x.data[i] = typed[i].x; + attr_y.data[i] = typed[i].y; + attr_z.data[i] = typed[i].z; + } + r_attributes.append(attr_x); + r_attributes.append(attr_y); + r_attributes.append(attr_z); + break; + } + case CD_PROP_BYTE_COLOR: { + PlyCustomAttribute attr_r(attribute_id.name() + "_r", size); + PlyCustomAttribute attr_g(attribute_id.name() + "_g", size); + PlyCustomAttribute attr_b(attribute_id.name() + "_b", size); + PlyCustomAttribute attr_a(attribute_id.name() + "_a", size); + auto typed = attribute.typed(); + for (const int64_t i : typed.index_range()) { + ColorGeometry4f col = typed[i].decode(); + attr_r.data[i] = col.r; + attr_g.data[i] = col.g; + attr_b.data[i] = col.b; + attr_a.data[i] = col.a; + } + r_attributes.append(attr_r); + r_attributes.append(attr_g); + r_attributes.append(attr_b); + r_attributes.append(attr_a); + break; + } + case CD_PROP_COLOR: { + PlyCustomAttribute attr_r(attribute_id.name() + "_r", size); + PlyCustomAttribute attr_g(attribute_id.name() + "_g", size); + PlyCustomAttribute attr_b(attribute_id.name() + "_b", size); + PlyCustomAttribute attr_a(attribute_id.name() + "_a", size); + auto typed = attribute.typed(); + for (const int64_t i : typed.index_range()) { + ColorGeometry4f col = typed[i]; + attr_r.data[i] = col.r; + attr_g.data[i] = col.g; + attr_b.data[i] = col.b; + attr_a.data[i] = col.a; + } + r_attributes.append(attr_r); + r_attributes.append(attr_g); + r_attributes.append(attr_b); + r_attributes.append(attr_a); + break; + } + case CD_PROP_BOOL: { + PlyCustomAttribute attr(attribute_id.name(), size); + auto typed = attribute.typed(); + for (const int64_t i : typed.index_range()) { + attr.data[i] = typed[i] ? 1.0f : 0.0f; + } + r_attributes.append(attr); + break; + } + case CD_PROP_QUATERNION: { + PlyCustomAttribute attr_x(attribute_id.name() + "_x", size); + PlyCustomAttribute attr_y(attribute_id.name() + "_y", size); + PlyCustomAttribute attr_z(attribute_id.name() + "_z", size); + PlyCustomAttribute attr_w(attribute_id.name() + "_w", size); + auto typed = attribute.typed(); + for (const int64_t i : typed.index_range()) { + attr_x.data[i] = typed[i].x; + attr_y.data[i] = typed[i].y; + attr_z.data[i] = typed[i].z; + attr_w.data[i] = typed[i].w; + } + r_attributes.append(attr_x); + r_attributes.append(attr_y); + r_attributes.append(attr_z); + r_attributes.append(attr_w); + break; + } + default: + BLI_assert_msg(0, "Unsupported attribute type for PLY export."); + } + return true; + }); +} + void load_plydata(PlyData &plyData, Depsgraph *depsgraph, const PLYExportParams &export_params) { DEGObjectIterSettings deg_iter_settings{}; @@ -267,6 +428,11 @@ void load_plydata(PlyData &plyData, Depsgraph *depsgraph, const PLYExportParams } } + /* Custom attributes */ + if (export_params.export_attributes) { + load_custom_attributes(mesh, plyData.vertex_custom_attr); + } + /* Loose edges */ const bke::LooseEdgeCache &loose_edges = mesh->loose_edges(); if (loose_edges.count > 0) { diff --git a/source/blender/io/ply/exporter/ply_file_buffer.hh b/source/blender/io/ply/exporter/ply_file_buffer.hh index c7fe34afff5..b1fc954bade 100644 --- a/source/blender/io/ply/exporter/ply_file_buffer.hh +++ b/source/blender/io/ply/exporter/ply_file_buffer.hh @@ -50,6 +50,8 @@ class FileBuffer : private NonMovable { virtual void write_UV(float u, float v) = 0; + virtual void write_data(float v) = 0; + virtual void write_vertex_normal(float nx, float ny, float nz) = 0; virtual void write_vertex_color(uchar r, uchar g, uchar b, uchar a) = 0; diff --git a/source/blender/io/ply/exporter/ply_file_buffer_ascii.cc b/source/blender/io/ply/exporter/ply_file_buffer_ascii.cc index 99278d598e5..4815b7fbaa6 100644 --- a/source/blender/io/ply/exporter/ply_file_buffer_ascii.cc +++ b/source/blender/io/ply/exporter/ply_file_buffer_ascii.cc @@ -20,6 +20,11 @@ void FileBufferAscii::write_UV(float u, float v) write_fstring(" {} {}", u, v); } +void FileBufferAscii::write_data(float v) +{ + write_fstring(" {}", v); +} + void FileBufferAscii::write_vertex_normal(float nx, float ny, float nz) { write_fstring(" {} {} {}", nx, ny, nz); diff --git a/source/blender/io/ply/exporter/ply_file_buffer_ascii.hh b/source/blender/io/ply/exporter/ply_file_buffer_ascii.hh index a4f56217618..12a2a74a55d 100644 --- a/source/blender/io/ply/exporter/ply_file_buffer_ascii.hh +++ b/source/blender/io/ply/exporter/ply_file_buffer_ascii.hh @@ -19,6 +19,8 @@ class FileBufferAscii : public FileBuffer { void write_UV(float u, float v) override; + void write_data(float v) override; + void write_vertex_normal(float nx, float ny, float nz) override; void write_vertex_color(uchar r, uchar g, uchar b, uchar a) override; diff --git a/source/blender/io/ply/exporter/ply_file_buffer_binary.cc b/source/blender/io/ply/exporter/ply_file_buffer_binary.cc index 052e08543ab..c2d487712c8 100644 --- a/source/blender/io/ply/exporter/ply_file_buffer_binary.cc +++ b/source/blender/io/ply/exporter/ply_file_buffer_binary.cc @@ -29,6 +29,14 @@ void FileBufferBinary::write_UV(float u, float v) write_bytes(span); } +void FileBufferBinary::write_data(float v) +{ + char *bits = reinterpret_cast(&v); + Span span(bits, sizeof(float)); + + write_bytes(span); +} + void FileBufferBinary::write_vertex_normal(float nx, float ny, float nz) { float3 vector(nx, ny, nz); diff --git a/source/blender/io/ply/exporter/ply_file_buffer_binary.hh b/source/blender/io/ply/exporter/ply_file_buffer_binary.hh index 5ae98a835ea..045888faade 100644 --- a/source/blender/io/ply/exporter/ply_file_buffer_binary.hh +++ b/source/blender/io/ply/exporter/ply_file_buffer_binary.hh @@ -19,6 +19,8 @@ class FileBufferBinary : public FileBuffer { void write_UV(float u, float v) override; + void write_data(float v) override; + void write_vertex_normal(float nx, float ny, float nz) override; void write_vertex_color(uchar r, uchar g, uchar b, uchar a) override; diff --git a/source/blender/io/ply/importer/ply_import_data.cc b/source/blender/io/ply/importer/ply_import_data.cc index f8a7fc12ae9..a5f8025751f 100644 --- a/source/blender/io/ply/importer/ply_import_data.cc +++ b/source/blender/io/ply/importer/ply_import_data.cc @@ -251,6 +251,19 @@ static const char *load_vertex_element(PlyReadBuffer &file, return "Vertex positions are not present in the file"; } + Vector custom_attr_indices; + for (const int64_t prop_idx : element.properties.index_range()) { + const PlyProperty &prop = element.properties[prop_idx]; + bool is_standard = ELEM( + prop.name, "x", "y", "z", "nx", "ny", "nz", "red", "green", "blue", "alpha", "s", "t"); + if (is_standard) + continue; + + custom_attr_indices.append(prop_idx); + PlyCustomAttribute attr(prop.name, element.count); + data->vertex_custom_attr.append(attr); + } + data->vertices.reserve(element.count); if (has_color) { data->vertex_colors.reserve(element.count); @@ -329,6 +342,12 @@ static const char *load_vertex_element(PlyReadBuffer &file, uvmap.y = value_vec[uv_index.y]; data->uv_coordinates.append(uvmap); } + + /* Custom attributes */ + for (const int64_t ci : custom_attr_indices.index_range()) { + float value = value_vec[custom_attr_indices[ci]]; + data->vertex_custom_attr[ci].data[i] = value; + } } return nullptr; } diff --git a/source/blender/io/ply/importer/ply_import_mesh.cc b/source/blender/io/ply/importer/ply_import_mesh.cc index 01067d57473..1042ef04ec5 100644 --- a/source/blender/io/ply/importer/ply_import_mesh.cc +++ b/source/blender/io/ply/importer/ply_import_mesh.cc @@ -100,6 +100,15 @@ Mesh *convert_ply_to_mesh(PlyData &data, const PLYImportParams ¶ms) uv_map.finish(); } + /* Custom attributes */ + if (params.import_attributes && !data.vertex_custom_attr.is_empty()) { + for (const PlyCustomAttribute &attr : data.vertex_custom_attr) { + attributes.add(attr.name, + ATTR_DOMAIN_POINT, + bke::AttributeInitVArray(VArray::ForSpan(attr.data))); + } + } + /* Calculate edges from the rest of the mesh. */ BKE_mesh_calc_edges(mesh, true, false); diff --git a/source/blender/io/ply/intern/ply_data.hh b/source/blender/io/ply/intern/ply_data.hh index 7bd75ffefb2..e9631f86151 100644 --- a/source/blender/io/ply/intern/ply_data.hh +++ b/source/blender/io/ply/intern/ply_data.hh @@ -8,17 +8,26 @@ #pragma once +#include "BLI_array.hh" #include "BLI_math_vector_types.hh" +#include "BLI_string_ref.hh" #include "BLI_vector.hh" namespace blender::io::ply { enum PlyDataTypes { NONE, CHAR, UCHAR, SHORT, USHORT, INT, UINT, FLOAT, DOUBLE, PLY_TYPE_COUNT }; +struct PlyCustomAttribute { + PlyCustomAttribute(const StringRef name_, int64_t size) : name(name_), data(size) {} + std::string name; + Array data; /* Any custom PLY attributes are converted to floats. */ +}; + struct PlyData { Vector vertices; Vector vertex_normals; Vector vertex_colors; /* Linear space, 0..1 range colors. */ + Vector vertex_custom_attr; Vector> edges; Vector face_vertices; Vector face_sizes;