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
This commit is contained in:
Aras Pranckevicius
2023-10-31 14:07:51 +01:00
committed by Aras Pranckevicius
parent 874029fd5c
commit 0eb6aef3b0
13 changed files with 243 additions and 2 deletions
+11 -2
View File
@@ -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", "");
+2
View File
@@ -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;
};
@@ -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();
@@ -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");
@@ -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<PlyCustomAttribute> &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<float>();
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<int8_t>();
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<int32_t>();
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<int2>();
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<float2>();
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<float3>();
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<ColorGeometry4b>();
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<ColorGeometry4f>();
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<bool>();
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<math::Quaternion>();
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) {
@@ -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;
@@ -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);
@@ -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;
@@ -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<char *>(&v);
Span<char> span(bits, sizeof(float));
write_bytes(span);
}
void FileBufferBinary::write_vertex_normal(float nx, float ny, float nz)
{
float3 vector(nx, ny, nz);
@@ -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;
@@ -251,6 +251,19 @@ static const char *load_vertex_element(PlyReadBuffer &file,
return "Vertex positions are not present in the file";
}
Vector<int64_t> 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;
}
@@ -100,6 +100,15 @@ Mesh *convert_ply_to_mesh(PlyData &data, const PLYImportParams &params)
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<float>(attr.name,
ATTR_DOMAIN_POINT,
bke::AttributeInitVArray(VArray<float>::ForSpan(attr.data)));
}
}
/* Calculate edges from the rest of the mesh. */
BKE_mesh_calc_edges(mesh, true, false);
+9
View File
@@ -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<float> data; /* Any custom PLY attributes are converted to floats. */
};
struct PlyData {
Vector<float3> vertices;
Vector<float3> vertex_normals;
Vector<float4> vertex_colors; /* Linear space, 0..1 range colors. */
Vector<PlyCustomAttribute> vertex_custom_attr;
Vector<std::pair<int, int>> edges;
Vector<uint32_t> face_vertices;
Vector<uint32_t> face_sizes;