IO: New C++ PLY importer/exporter

New (experimental) Stanford PLY importer and exporter written in C++.

Handles: vertices, faces, edges, vertex colors, normals, UVs. Both
binary and ASCII formats are supported.

Usually 10-20x faster than the existing Python based PLY
importer/exporter.

Additional notes compared to the previous Python addon:
- Importing point clouds with vertex colors now works
- Importing PLY files with non standard line endings
- Exporting multiple objects (previous exporter didn't take the vertex
  indices into account)
- The importer has the option to merge vertices
- The exporter supports exporting loose edges and vertices along with
  UV map data

This is squashed commit of PR #104404
Reviewed By: Hans Goudey, Aras Pranckevicius

Co-authored-by: Arjan van Diest
Co-authored-by: Lilith Houtjes
Co-authored-by: Bas Hendriks
Co-authored-by: Thomas Feijen
Co-authored-by: Yoran Huzen
This commit is contained in:
Nathan Rozendaal 2023-03-05 19:02:36 +02:00 committed by Aras Pranckevicius
parent b4d36b3efe
commit 43e9c90061
41 changed files with 3241 additions and 2 deletions

View File

@ -357,6 +357,7 @@ option(WITH_MATERIALX "Enable MaterialX Support" OFF)
# Disable opencollada when we don't have precompiled libs
option(WITH_OPENCOLLADA "Enable OpenCollada Support (http://www.opencollada.org)" ON)
option(WITH_IO_WAVEFRONT_OBJ "Enable Wavefront-OBJ 3D file format support (*.obj)" ON)
option(WITH_IO_PLY "Enable PLY 3D file format support (*.ply)" ON)
option(WITH_IO_STL "Enable STL 3D file format support (*.stl)" ON)
option(WITH_IO_GPENCIL "Enable grease-pencil file format IO (*.svg, *.pdf)" ON)

View File

@ -36,6 +36,7 @@ set(WITH_IMAGE_WEBP OFF CACHE BOOL "" FORCE)
set(WITH_INPUT_IME OFF CACHE BOOL "" FORCE)
set(WITH_INPUT_NDOF OFF CACHE BOOL "" FORCE)
set(WITH_INTERNATIONAL OFF CACHE BOOL "" FORCE)
set(WITH_IO_PLY OFF CACHE BOOL "" FORCE)
set(WITH_IO_STL OFF CACHE BOOL "" FORCE)
set(WITH_IO_WAVEFRONT_OBJ OFF CACHE BOOL "" FORCE)
set(WITH_IO_GPENCIL OFF CACHE BOOL "" FORCE)

View File

@ -475,6 +475,8 @@ class TOPBAR_MT_file_import(Menu):
if bpy.app.build_options.io_wavefront_obj:
self.layout.operator("wm.obj_import", text="Wavefront (.obj)")
if bpy.app.build_options.io_ply:
self.layout.operator("wm.ply_import", text="PLY (.ply) (experimental)")
if bpy.app.build_options.io_stl:
self.layout.operator("wm.stl_import", text="STL (.stl) (experimental)")
@ -503,6 +505,8 @@ class TOPBAR_MT_file_export(Menu):
if bpy.app.build_options.io_wavefront_obj:
self.layout.operator("wm.obj_export", text="Wavefront (.obj)")
if bpy.app.build_options.io_ply:
self.layout.operator("wm.ply_export", text="PLY (.ply) (experimental)")
class TOPBAR_MT_file_external_data(Menu):

View File

@ -11,6 +11,7 @@ set(INC
../../io/collada
../../io/common
../../io/gpencil
../../io/ply
../../io/stl
../../io/usd
../../io/wavefront_obj
@ -33,6 +34,7 @@ set(SRC
io_gpencil_utils.c
io_obj.c
io_ops.c
io_ply_ops.c
io_stl_ops.c
io_usd.c
@ -42,6 +44,7 @@ set(SRC
io_gpencil.h
io_obj.h
io_ops.h
io_ply_ops.h
io_stl_ops.h
io_usd.h
)
@ -65,6 +68,13 @@ if(WITH_IO_WAVEFRONT_OBJ)
add_definitions(-DWITH_IO_WAVEFRONT_OBJ)
endif()
if(WITH_IO_PLY)
list(APPEND LIB
bf_ply
)
add_definitions(-DWITH_IO_PLY)
endif()
if(WITH_IO_STL)
list(APPEND LIB
bf_stl

View File

@ -24,6 +24,7 @@
#include "io_cache.h"
#include "io_gpencil.h"
#include "io_obj.h"
#include "io_ply_ops.h"
#include "io_stl_ops.h"
void ED_operatortypes_io(void)
@ -64,6 +65,11 @@ void ED_operatortypes_io(void)
WM_operatortype_append(WM_OT_obj_import);
#endif
#ifdef WITH_IO_PLY
WM_operatortype_append(WM_OT_ply_export);
WM_operatortype_append(WM_OT_ply_import);
#endif
#ifdef WITH_IO_STL
WM_operatortype_append(WM_OT_stl_import);
#endif

View File

@ -0,0 +1,328 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup editor/io
*/
#ifdef WITH_IO_PLY
# include "BKE_context.h"
# include "BKE_main.h"
# include "BKE_report.h"
# include "WM_api.h"
# include "WM_types.h"
# include "DNA_space_types.h"
# include "ED_fileselect.h"
# include "ED_outliner.h"
# include "RNA_access.h"
# include "RNA_define.h"
# include "BLT_translation.h"
# include "MEM_guardedalloc.h"
# include "UI_interface.h"
# include "UI_resources.h"
# include "DEG_depsgraph.h"
# include "IO_orientation.h"
# include "IO_path_util_types.h"
# include "IO_ply.h"
# include "io_ply_ops.h"
static int wm_ply_export_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event))
{
ED_fileselect_ensure_default_filepath(C, op, ".ply");
WM_event_add_fileselect(C, op);
return OPERATOR_RUNNING_MODAL;
}
static int wm_ply_export_exec(bContext *C, wmOperator *op)
{
if (!RNA_struct_property_is_set_ex(op->ptr, "filepath", false)) {
BKE_report(op->reports, RPT_ERROR, "No filename given");
return OPERATOR_CANCELLED;
}
struct PLYExportParams export_params;
export_params.file_base_for_tests[0] = '\0';
RNA_string_get(op->ptr, "filepath", export_params.filepath);
export_params.blen_filepath = CTX_data_main(C)->filepath;
export_params.forward_axis = RNA_enum_get(op->ptr, "forward_axis");
export_params.up_axis = RNA_enum_get(op->ptr, "up_axis");
export_params.global_scale = RNA_float_get(op->ptr, "global_scale");
export_params.apply_modifiers = RNA_boolean_get(op->ptr, "apply_modifiers");
export_params.export_selected_objects = RNA_boolean_get(op->ptr, "export_selected_objects");
export_params.export_uv = RNA_boolean_get(op->ptr, "export_uv");
export_params.export_normals = RNA_boolean_get(op->ptr, "export_normals");
export_params.export_colors = RNA_boolean_get(op->ptr, "export_colors");
export_params.export_triangulated_mesh = RNA_boolean_get(op->ptr, "export_triangulated_mesh");
export_params.ascii_format = RNA_boolean_get(op->ptr, "ascii_format");
PLY_export(C, &export_params);
return OPERATOR_FINISHED;
}
static void ui_ply_export_settings(uiLayout *layout, PointerRNA *imfptr)
{
uiLayoutSetPropSep(layout, true);
uiLayoutSetPropDecorate(layout, false);
uiLayout *box, *col, *sub;
/* Object Transform options. */
box = uiLayoutBox(layout);
col = uiLayoutColumn(box, false);
sub = uiLayoutColumnWithHeading(col, false, IFACE_("Format"));
uiItemR(sub, imfptr, "ascii_format", 0, IFACE_("ASCII"), ICON_NONE);
sub = uiLayoutColumnWithHeading(col, false, IFACE_("Limit to"));
uiItemR(sub, imfptr, "export_selected_objects", 0, IFACE_("Selected Only"), ICON_NONE);
uiItemR(sub, imfptr, "global_scale", 0, NULL, ICON_NONE);
uiItemR(sub, imfptr, "forward_axis", 0, IFACE_("Forward Axis"), ICON_NONE);
uiItemR(sub, imfptr, "up_axis", 0, IFACE_("Up Axis"), ICON_NONE);
col = uiLayoutColumn(box, false);
sub = uiLayoutColumn(col, false);
sub = uiLayoutColumnWithHeading(col, false, IFACE_("Objects"));
uiItemR(sub, imfptr, "apply_modifiers", 0, IFACE_("Apply Modifiers"), ICON_NONE);
/* Geometry options. */
box = uiLayoutBox(layout);
col = uiLayoutColumn(box, false);
sub = uiLayoutColumnWithHeading(col, false, IFACE_("Geometry"));
uiItemR(sub, imfptr, "export_uv", 0, IFACE_("UV Coordinates"), ICON_NONE);
uiItemR(sub, imfptr, "export_normals", 0, IFACE_("Vertex Normals"), ICON_NONE);
uiItemR(sub, imfptr, "export_colors", 0, IFACE_("Vertex Colors"), ICON_NONE);
uiItemR(sub, imfptr, "export_triangulated_mesh", 0, IFACE_("Triangulated Mesh"), ICON_NONE);
}
static void wm_ply_export_draw(bContext *UNUSED(C), wmOperator *op)
{
PointerRNA ptr;
RNA_pointer_create(NULL, op->type->srna, op->properties, &ptr);
ui_ply_export_settings(op->layout, &ptr);
}
/**
* Return true if any property in the UI is changed.
*/
static bool wm_ply_export_check(bContext *C, wmOperator *op)
{
char filepath[FILE_MAX];
bool changed = false;
RNA_string_get(op->ptr, "filepath", filepath);
if (!BLI_path_extension_check(filepath, ".ply")) {
BLI_path_extension_ensure(filepath, FILE_MAX, ".ply");
RNA_string_set(op->ptr, "filepath", filepath);
changed = true;
}
return changed;
}
/* Both forward and up axes cannot be along the same direction. */
static void forward_axis_update(struct Main *UNUSED(main),
struct Scene *UNUSED(scene),
struct PointerRNA *ptr)
{
int forward = RNA_enum_get(ptr, "forward_axis");
int up = RNA_enum_get(ptr, "up_axis");
if ((forward % 3) == (up % 3)) {
RNA_enum_set(ptr, "up_axis", (up + 1) % 6);
}
}
static void up_axis_update(struct Main *UNUSED(main),
struct Scene *UNUSED(scene),
struct PointerRNA *ptr)
{
int forward = RNA_enum_get(ptr, "forward_axis");
int up = RNA_enum_get(ptr, "up_axis");
if ((forward % 3) == (up % 3)) {
RNA_enum_set(ptr, "forward_axis", (forward + 1) % 6);
}
}
void WM_OT_ply_export(struct wmOperatorType *ot)
{
PropertyRNA *prop;
ot->name = "Export PLY";
ot->description = "Save the scene to a PLY file";
ot->idname = "WM_OT_ply_export";
ot->invoke = wm_ply_export_invoke;
ot->exec = wm_ply_export_exec;
ot->poll = WM_operator_winactive;
ot->ui = wm_ply_export_draw;
ot->check = wm_ply_export_check;
ot->flag = OPTYPE_PRESET;
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER,
FILE_BLENDER,
FILE_SAVE,
WM_FILESEL_FILEPATH | WM_FILESEL_SHOW_PROPS,
FILE_DEFAULTDISPLAY,
FILE_SORT_DEFAULT);
/* Object transform options. */
prop = RNA_def_enum(ot->srna, "forward_axis", io_transform_axis, IO_AXIS_Y, "Forward Axis", "");
RNA_def_property_update_runtime(prop, (void *)forward_axis_update);
prop = RNA_def_enum(ot->srna, "up_axis", io_transform_axis, IO_AXIS_Z, "Up Axis", "");
RNA_def_property_update_runtime(prop, (void *)up_axis_update);
RNA_def_float(
ot->srna,
"global_scale",
1.0f,
0.0001f,
10000.0f,
"Scale",
"Value by which to enlarge or shrink the objects with respect to the world's origin",
0.0001f,
10000.0f);
/* File Writer options. */
RNA_def_boolean(
ot->srna, "apply_modifiers", true, "Apply Modifiers", "Apply modifiers to exported meshes");
RNA_def_boolean(ot->srna,
"export_selected_objects",
false,
"Export Selected Objects",
"Export only selected objects instead of all supported objects");
RNA_def_boolean(ot->srna, "export_uv", false, "Export UVs", "");
RNA_def_boolean(
ot->srna,
"export_normals",
false,
"Export Vertex Normals",
"Export specific vertex normals if available, export calculated normals otherwise");
RNA_def_boolean(
ot->srna, "export_colors", true, "Export Vertex Colors", "Export per-vertex colors");
RNA_def_boolean(ot->srna,
"export_triangulated_mesh",
false,
"Export Triangulated Mesh",
"All ngons with four or more vertices will be triangulated. Meshes in "
"the scene will not be affected. Behaves like Triangulate Modifier with "
"ngon-method: \"Beauty\", quad-method: \"Shortest Diagonal\", min vertices: 4");
RNA_def_boolean(ot->srna,
"ascii_format",
false,
"ASCII Format",
"Export file in ASCII format, export as binary otherwise");
/* Only show .ply files by default. */
prop = RNA_def_string(ot->srna, "filter_glob", "*.ply", 0, "Extension Filter", "");
RNA_def_property_flag(prop, PROP_HIDDEN);
}
static int wm_ply_import_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
return WM_operator_filesel(C, op, event);
}
static int wm_ply_import_execute(bContext *C, wmOperator *op)
{
struct PLYImportParams params;
params.forward_axis = RNA_enum_get(op->ptr, "forward_axis");
params.up_axis = RNA_enum_get(op->ptr, "up_axis");
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");
int files_len = RNA_collection_length(op->ptr, "files");
if (files_len) {
PointerRNA fileptr;
PropertyRNA *prop;
char dir_only[FILE_MAX], file_only[FILE_MAX];
RNA_string_get(op->ptr, "directory", dir_only);
prop = RNA_struct_find_property(op->ptr, "files");
for (int i = 0; i < files_len; i++) {
RNA_property_collection_lookup_int(op->ptr, prop, i, &fileptr);
RNA_string_get(&fileptr, "name", file_only);
BLI_path_join(params.filepath, sizeof(params.filepath), dir_only, file_only);
PLY_import(C, &params, op);
}
}
else if (RNA_struct_property_is_set_ex(op->ptr, "filepath", false)) {
RNA_string_get(op->ptr, "filepath", params.filepath);
PLY_import(C, &params, op);
}
else {
BKE_report(op->reports, RPT_ERROR, "No filename given");
return OPERATOR_CANCELLED;
}
Scene *scene = CTX_data_scene(C);
WM_event_add_notifier(C, NC_SCENE | ND_OB_SELECT, scene);
WM_event_add_notifier(C, NC_SCENE | ND_OB_ACTIVE, scene);
WM_event_add_notifier(C, NC_SCENE | ND_LAYER_CONTENT, scene);
ED_outliner_select_sync_from_object_tag(C);
return OPERATOR_FINISHED;
}
static bool wm_ply_import_check(bContext *UNUSED(C), wmOperator *op)
{
const int num_axes = 3;
/* Both forward and up axes cannot be the same (or same except opposite sign). */
if (RNA_enum_get(op->ptr, "forward_axis") % num_axes ==
(RNA_enum_get(op->ptr, "up_axis") % num_axes)) {
RNA_enum_set(op->ptr, "up_axis", RNA_enum_get(op->ptr, "up_axis") % num_axes + 1);
return true;
}
return false;
}
void WM_OT_ply_import(struct wmOperatorType *ot)
{
PropertyRNA *prop;
ot->name = "Import PLY";
ot->description = "Import an PLY file as an object";
ot->idname = "WM_OT_ply_import";
ot->invoke = wm_ply_import_invoke;
ot->exec = wm_ply_import_execute;
ot->poll = WM_operator_winactive;
ot->check = wm_ply_import_check;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_PRESET;
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER,
FILE_BLENDER,
FILE_OPENFILE,
WM_FILESEL_FILEPATH | WM_FILESEL_FILES | WM_FILESEL_DIRECTORY |
WM_FILESEL_SHOW_PROPS,
FILE_DEFAULTDISPLAY,
FILE_SORT_DEFAULT);
RNA_def_float(ot->srna, "global_scale", 1.0f, 1e-6f, 1e6f, "Scale", "", 0.001f, 1000.0f);
RNA_def_boolean(ot->srna,
"use_scene_unit",
false,
"Scene Unit",
"Apply current scene's unit (as defined by unit scale) to imported data");
RNA_def_enum(ot->srna, "forward_axis", io_transform_axis, IO_AXIS_Y, "Forward Axis", "");
RNA_def_enum(ot->srna, "up_axis", io_transform_axis, IO_AXIS_Z, "Up Axis", "");
RNA_def_boolean(ot->srna, "merge_verts", false, "Merge Vertices", "Merges vertices by distance");
/* Only show .ply files by default. */
prop = RNA_def_string(ot->srna, "filter_glob", "*.ply", 0, "Extension Filter", "");
RNA_def_property_flag(prop, PROP_HIDDEN);
}
#endif /* WITH_IO_PLY */

View File

@ -0,0 +1,12 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup editor/io
*/
#pragma once
struct wmOperatorType;
void WM_OT_ply_export(struct wmOperatorType *ot);
void WM_OT_ply_import(struct wmOperatorType *ot);

View File

@ -2678,7 +2678,7 @@ int ED_path_extension_type(const char *path)
return FILE_TYPE_ARCHIVE;
}
if (BLI_path_extension_check_n(
path, ".obj", ".mtl", ".3ds", ".fbx", ".glb", ".gltf", ".svg", ".stl", nullptr)) {
path, ".obj", ".mtl", ".3ds", ".fbx", ".glb", ".gltf", ".svg", ".ply", ".stl", nullptr)) {
return FILE_TYPE_OBJECT_IO;
}
if (BLI_path_extension_check_array(path, imb_ext_image)) {

View File

@ -1,7 +1,7 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2020 Blender Foundation. All rights reserved.
if(WITH_IO_WAVEFRONT_OBJ OR WITH_IO_STL OR WITH_IO_GPENCIL OR WITH_ALEMBIC OR WITH_USD)
if(WITH_IO_WAVEFRONT_OBJ OR WITH_IO_PLY OR WITH_IO_STL OR WITH_IO_GPENCIL OR WITH_ALEMBIC OR WITH_USD)
add_subdirectory(common)
endif()
@ -9,6 +9,10 @@ if(WITH_IO_WAVEFRONT_OBJ)
add_subdirectory(wavefront_obj)
endif()
if(WITH_IO_PLY)
add_subdirectory(ply)
endif()
if(WITH_IO_STL)
add_subdirectory(stl)
endif()

View File

@ -0,0 +1,82 @@
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
.
exporter
importer
intern
../common
../../blenkernel
../../blenlib
../../bmesh
../../depsgraph
../../makesdna
../../makesrna
../../windowmanager
../../geometry
../../../../extern/fmtlib/include
../../../../intern/guardedalloc
)
set(INC_SYS
)
set(SRC
exporter/ply_export_data.cc
exporter/ply_export_header.cc
exporter/ply_export_load_plydata.cc
exporter/ply_export.cc
exporter/ply_file_buffer_ascii.cc
exporter/ply_file_buffer_binary.cc
exporter/ply_file_buffer.cc
importer/ply_import_ascii.cc
importer/ply_import_binary.cc
importer/ply_import_mesh.cc
importer/ply_import.cc
IO_ply.cc
exporter/ply_export_data.hh
exporter/ply_export_header.hh
exporter/ply_export_load_plydata.hh
exporter/ply_export.hh
exporter/ply_file_buffer_ascii.hh
exporter/ply_file_buffer_binary.hh
exporter/ply_file_buffer.hh
importer/ply_import_ascii.hh
importer/ply_import_binary.hh
importer/ply_import_mesh.hh
importer/ply_import.hh
IO_ply.h
intern/ply_data.hh
intern/ply_functions.hh
intern/ply_functions.cc
)
set(LIB
bf_blenkernel
bf_io_common
)
blender_add_lib(bf_ply "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
if (WITH_GTESTS)
set(TEST_SRC
tests/io_ply_importer_test.cc
tests/io_ply_exporter_test.cc
)
set(TEST_INC
../../blenloader
../../../../tests/gtests
)
set(TEST_LIB
bf_ply
)
include(GTestTesting)
blender_add_test_lib(bf_io_ply_tests "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}")
endif()

View File

@ -0,0 +1,24 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BLI_timeit.hh"
#include "DNA_windowmanager_types.h"
#include "IO_ply.h"
#include "ply_export.hh"
#include "ply_import.hh"
void PLY_export(bContext *C, const PLYExportParams *export_params)
{
SCOPED_TIMER("PLY Export");
blender::io::ply::exporter_main(C, *export_params);
}
void PLY_import(bContext *C, const PLYImportParams *import_params, wmOperator *op)
{
SCOPED_TIMER("PLY Import");
blender::io::ply::importer_main(C, *import_params, op);
}

View File

@ -0,0 +1,64 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "BKE_context.h"
#include "BLI_path_util.h"
#include "DNA_windowmanager_types.h"
#include "IO_orientation.h"
#ifdef __cplusplus
extern "C" {
#endif
struct PLYExportParams {
/** Full path to the destination .PLY file. */
char filepath[FILE_MAX];
/** Pretend that destination file folder is this, if non-empty. Used only for tests. */
char file_base_for_tests[FILE_MAX];
/** Full path to current blender file (used for comments in output). */
const char *blen_filepath;
/** File export format, ASCII if true, binary otherwise. */
bool ascii_format;
/* Geometry Transform options. */
eIOAxis forward_axis;
eIOAxis up_axis;
float global_scale;
/* File Write Options. */
bool export_selected_objects;
bool apply_modifiers;
bool export_uv;
bool export_normals;
bool export_colors;
bool export_triangulated_mesh;
};
struct PLYImportParams {
/** Full path to the source PLY file to import. */
char filepath[FILE_MAX];
eIOAxis forward_axis;
eIOAxis up_axis;
bool use_scene_unit;
float global_scale;
bool merge_verts;
};
/**
* C-interface for the importer and exporter.
*/
void PLY_export(bContext *C, const struct PLYExportParams *export_params);
void PLY_import(bContext *C, const struct PLYImportParams *import_params, wmOperator *op);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,60 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BKE_layer.h"
#include "DNA_collection_types.h"
#include "DNA_scene_types.h"
#include "BLI_memory_utils.hh"
#include "ply_data.hh"
#include "ply_export.hh"
#include "ply_export_data.hh"
#include "ply_export_header.hh"
#include "ply_export_load_plydata.hh"
#include "ply_file_buffer_ascii.hh"
#include "ply_file_buffer_binary.hh"
namespace blender::io::ply {
void exporter_main(bContext *C, const PLYExportParams &export_params)
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
exporter_main(bmain, scene, view_layer, C, export_params);
}
void exporter_main(Main *bmain,
Scene *scene,
ViewLayer *view_layer,
bContext *C,
const PLYExportParams &export_params)
{
std::unique_ptr<blender::io::ply::PlyData> plyData = std::make_unique<PlyData>();
load_plydata(*plyData, CTX_data_ensure_evaluated_depsgraph(C), export_params);
std::unique_ptr<FileBuffer> buffer;
if (export_params.ascii_format) {
buffer = std::make_unique<FileBufferAscii>(export_params.filepath);
}
else {
buffer = std::make_unique<FileBufferBinary>(export_params.filepath);
}
write_header(*buffer.get(), *plyData.get(), export_params);
write_vertices(*buffer.get(), *plyData.get());
write_faces(*buffer.get(), *plyData.get());
write_edges(*buffer.get(), *plyData.get());
buffer->close_file();
}
} // namespace blender::io::ply

View File

@ -0,0 +1,25 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "IO_ply.h"
#include "ply_data.hh"
#include "ply_file_buffer.hh"
namespace blender::io::ply {
/* Main export function used from within Blender. */
void exporter_main(bContext *C, const PLYExportParams &export_params);
/* Used from tests, where full bContext does not exist. */
void exporter_main(Main *bmain,
Scene *scene,
ViewLayer *view_layer,
bContext *C,
const PLYExportParams &export_params);
} // namespace blender::io::ply

View File

@ -0,0 +1,51 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BLI_array.hh"
#include "ply_data.hh"
#include "ply_file_buffer.hh"
namespace blender::io::ply {
void write_vertices(FileBuffer &buffer, const PlyData &ply_data)
{
for (int i = 0; i < ply_data.vertices.size(); i++) {
buffer.write_vertex(ply_data.vertices[i].x, ply_data.vertices[i].y, ply_data.vertices[i].z);
if (!ply_data.vertex_normals.is_empty())
buffer.write_vertex_normal(ply_data.vertex_normals[i].x,
ply_data.vertex_normals[i].y,
ply_data.vertex_normals[i].z);
if (!ply_data.vertex_colors.is_empty())
buffer.write_vertex_color(uchar(ply_data.vertex_colors[i].x * 255),
uchar(ply_data.vertex_colors[i].y * 255),
uchar(ply_data.vertex_colors[i].z * 255),
uchar(ply_data.vertex_colors[i].w * 255));
if (!ply_data.UV_coordinates.is_empty())
buffer.write_UV(ply_data.UV_coordinates[i].x, ply_data.UV_coordinates[i].y);
buffer.write_vertex_end();
}
buffer.write_to_file();
}
void write_faces(FileBuffer &buffer, const PlyData &ply_data)
{
for (const Array<uint32_t> &face : ply_data.faces) {
buffer.write_face(char(face.size()), face);
}
buffer.write_to_file();
}
void write_edges(FileBuffer &buffer, const PlyData &ply_data)
{
for (const std::pair<int, int> &edge : ply_data.edges) {
buffer.write_edge(edge.first, edge.second);
}
buffer.write_to_file();
}
} // namespace blender::io::ply

View File

@ -0,0 +1,20 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "ply_data.hh"
#include "ply_file_buffer.hh"
namespace blender::io::ply {
void write_vertices(FileBuffer &buffer, const PlyData &ply_data);
void write_faces(FileBuffer &buffer, const PlyData &ply_data);
void write_edges(FileBuffer &buffer, const PlyData &ply_data);
} // namespace blender::io::ply

View File

@ -0,0 +1,66 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BKE_blender_version.h"
#include "BKE_customdata.h"
#include "IO_ply.h"
#include "ply_data.hh"
#include "ply_file_buffer.hh"
namespace blender::io::ply {
void write_header(FileBuffer &buffer,
const PlyData &ply_data,
const PLYExportParams &export_params)
{
buffer.write_string("ply");
StringRef format = export_params.ascii_format ? "ascii" : "binary_little_endian";
buffer.write_string("format " + format + " 1.0");
StringRef version = BKE_blender_version_string();
buffer.write_string("comment Created in Blender version " + version);
buffer.write_header_element("vertex", int32_t(ply_data.vertices.size()));
buffer.write_header_scalar_property("float", "x");
buffer.write_header_scalar_property("float", "y");
buffer.write_header_scalar_property("float", "z");
if (!ply_data.vertex_normals.is_empty()) {
buffer.write_header_scalar_property("float", "nx");
buffer.write_header_scalar_property("float", "ny");
buffer.write_header_scalar_property("float", "nz");
}
if (!ply_data.vertex_colors.is_empty()) {
buffer.write_header_scalar_property("uchar", "red");
buffer.write_header_scalar_property("uchar", "green");
buffer.write_header_scalar_property("uchar", "blue");
buffer.write_header_scalar_property("uchar", "alpha");
}
if (!ply_data.UV_coordinates.is_empty()) {
buffer.write_header_scalar_property("float", "s");
buffer.write_header_scalar_property("float", "t");
}
if (!ply_data.faces.is_empty()) {
buffer.write_header_element("face", int32_t(ply_data.faces.size()));
buffer.write_header_list_property("uchar", "uint", "vertex_indices");
}
if (!ply_data.edges.is_empty()) {
buffer.write_header_element("edge", int32_t(ply_data.edges.size()));
buffer.write_header_scalar_property("int", "vertex1");
buffer.write_header_scalar_property("int", "vertex2");
}
buffer.write_string("end_header");
buffer.write_to_file();
}
} // namespace blender::io::ply

View File

@ -0,0 +1,18 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "ply_data.hh"
#include "ply_file_buffer.hh"
namespace blender::io::ply {
void write_header(FileBuffer &buffer,
const PlyData &ply_data,
const PLYExportParams &export_params);
} // namespace blender::io::ply

View File

@ -0,0 +1,273 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BLI_array.hh"
#include "BLI_math.h"
#include "BKE_attribute.hh"
#include "BKE_lib_id.h"
#include "BKE_mesh.h"
#include "BKE_mesh_mapping.h"
#include "BKE_object.h"
#include "DEG_depsgraph.h"
#include "DEG_depsgraph_build.h"
#include "DEG_depsgraph_query.h"
#include "DNA_layer_types.h"
#include "IO_ply.h"
#include "bmesh.h"
#include "bmesh_tools.h"
#include <tools/bmesh_triangulate.h>
#include "ply_data.hh"
#include "ply_export_load_plydata.hh"
namespace blender::io::ply {
float world_and_axes_transform_[4][4];
float world_and_axes_normal_transform_[3][3];
bool mirrored_transform_;
Mesh *do_triangulation(const Mesh *mesh, bool force_triangulation)
{
const BMeshCreateParams bm_create_params = {false};
BMeshFromMeshParams bm_convert_params{};
bm_convert_params.calc_face_normal = true;
bm_convert_params.calc_vert_normal = true;
const int triangulation_threshold = force_triangulation ? 4 : 255;
BMesh *bmesh = BKE_mesh_to_bmesh_ex(mesh, &bm_create_params, &bm_convert_params);
BM_mesh_triangulate(bmesh, 0, 3, triangulation_threshold, false, nullptr, nullptr, nullptr);
Mesh *temp_mesh = BKE_mesh_from_bmesh_for_eval_nomain(bmesh, nullptr, mesh);
BM_mesh_free(bmesh);
return temp_mesh;
}
void set_world_axes_transform(Object *object, const eIOAxis forward, const eIOAxis up)
{
float axes_transform[3][3];
unit_m3(axes_transform);
/* +Y-forward and +Z-up are the default Blender axis settings. */
mat3_from_axis_conversion(forward, up, IO_AXIS_Y, IO_AXIS_Z, axes_transform);
mul_m4_m3m4(world_and_axes_transform_, axes_transform, object->object_to_world);
/* mul_m4_m3m4 does not transform last row of obmat, i.e. location data. */
mul_v3_m3v3(world_and_axes_transform_[3], axes_transform, object->object_to_world[3]);
world_and_axes_transform_[3][3] = object->object_to_world[3][3];
/* Normals need inverse transpose of the regular matrix to handle non-uniform scale. */
float normal_matrix[3][3];
copy_m3_m4(normal_matrix, world_and_axes_transform_);
invert_m3_m3(world_and_axes_normal_transform_, normal_matrix);
transpose_m3(world_and_axes_normal_transform_);
mirrored_transform_ = is_negative_m3(world_and_axes_normal_transform_);
}
void load_plydata(PlyData &plyData, Depsgraph *depsgraph, const PLYExportParams &export_params)
{
DEGObjectIterSettings deg_iter_settings{};
deg_iter_settings.depsgraph = depsgraph;
deg_iter_settings.flags = DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY |
DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET | DEG_ITER_OBJECT_FLAG_VISIBLE |
DEG_ITER_OBJECT_FLAG_DUPLI;
/* When exporting multiple objects, vertex indices have to be offset. */
uint32_t vertex_offset = 0;
DEG_OBJECT_ITER_BEGIN (&deg_iter_settings, object) {
if (object->type != OB_MESH) {
continue;
}
if (export_params.export_selected_objects && !(object->base_flag & BASE_SELECTED)) {
continue;
}
Object *obj_eval = DEG_get_evaluated_object(depsgraph, object);
Object export_object_eval_ = dna::shallow_copy(*obj_eval);
Mesh *mesh = export_params.apply_modifiers ?
BKE_object_get_evaluated_mesh(&export_object_eval_) :
BKE_object_get_pre_modified_mesh(&export_object_eval_);
bool force_triangulation = false;
for (const MPoly poly : mesh->polys()) {
if (poly.totloop > 255) {
force_triangulation = true;
break;
}
}
/* Triangulate */
bool manually_free_mesh = false;
if (export_params.export_triangulated_mesh || force_triangulation) {
mesh = do_triangulation(mesh, export_params.export_triangulated_mesh);
manually_free_mesh = true;
}
const float2 *uv_map = static_cast<const float2 *>(
CustomData_get_layer(&mesh->ldata, CD_PROP_FLOAT2));
Map<UV_vertex_key, int> vertex_map = generate_vertex_map(mesh, uv_map, export_params);
set_world_axes_transform(
&export_object_eval_, export_params.forward_axis, export_params.up_axis);
/* Load faces into plyData. */
int loop_offset = 0;
Span<MLoop> loops = mesh->loops();
for (const MPoly poly : mesh->polys()) {
Span<MLoop> loopSpan = loops.slice(poly.loopstart, poly.totloop);
Array<uint32_t> polyVector(loopSpan.size());
for (int i = 0; i < loopSpan.size(); ++i) {
float2 uv;
if (export_params.export_uv && uv_map != nullptr) {
uv = uv_map[i + loop_offset];
}
else {
uv = {0, 0};
}
UV_vertex_key key = UV_vertex_key(uv, loopSpan[i].v);
int ply_vertex_index = vertex_map.lookup(key);
polyVector[i] = (uint32_t(ply_vertex_index + vertex_offset));
}
loop_offset += loopSpan.size();
plyData.faces.append(polyVector);
}
Array<int> mesh_vertex_index_LUT(vertex_map.size());
Array<int> ply_vertex_index_LUT(mesh->totvert);
Array<float2> uv_coordinates(vertex_map.size());
for (auto const &[key, ply_vertex_index] : vertex_map.items()) {
mesh_vertex_index_LUT[ply_vertex_index] = key.mesh_vertex_index;
ply_vertex_index_LUT[key.mesh_vertex_index] = ply_vertex_index;
uv_coordinates[ply_vertex_index] = key.UV;
}
/* Vertices */
for (int i = 0; i < vertex_map.size(); ++i) {
float3 r_coords;
copy_v3_v3(r_coords, mesh->vert_positions()[mesh_vertex_index_LUT[i]]);
mul_m4_v3(world_and_axes_transform_, r_coords);
mul_v3_fl(r_coords, export_params.global_scale);
plyData.vertices.append(r_coords);
}
/* UV's */
if (export_params.export_uv) {
for (int i = 0; i < vertex_map.size(); ++i) {
plyData.UV_coordinates.append(uv_coordinates[i]);
}
}
/* Normals */
if (export_params.export_normals) {
const Span<float3> vert_normals = mesh->vert_normals();
for (int i = 0; i < vertex_map.size(); i++) {
mul_m3_v3(world_and_axes_normal_transform_,
float3(vert_normals[mesh_vertex_index_LUT[i]]));
plyData.vertex_normals.append(vert_normals[mesh_vertex_index_LUT[i]]);
}
}
/* Colors */
if (export_params.export_colors) {
const StringRef name = mesh->active_color_attribute;
if (!name.is_empty()) {
const bke::AttributeAccessor attributes = mesh->attributes();
const VArray<ColorGeometry4f> color_attribute =
attributes.lookup_or_default<ColorGeometry4f>(
name, ATTR_DOMAIN_POINT, {0.0f, 0.0f, 0.0f, 0.0f});
for (int i = 0; i < vertex_map.size(); i++) {
ColorGeometry4f colorGeometry = color_attribute[mesh_vertex_index_LUT[i]];
float4 vertColor(colorGeometry.r, colorGeometry.g, colorGeometry.b, colorGeometry.a);
plyData.vertex_colors.append(vertColor);
}
}
}
/* Edges */
const bke::LooseEdgeCache &loose_edges = mesh->loose_edges();
if (loose_edges.count > 0) {
Span<MEdge> edges = mesh->edges();
for (int i = 0; i < edges.size(); ++i) {
if (loose_edges.is_loose_bits[i]) {
int index_one = ply_vertex_index_LUT[edges[i].v1];
int index_two = ply_vertex_index_LUT[edges[i].v2];
plyData.edges.append({index_one, index_two});
}
}
}
vertex_offset = int(plyData.vertices.size());
if (manually_free_mesh) {
BKE_id_free(nullptr, mesh);
}
}
DEG_OBJECT_ITER_END;
}
Map<UV_vertex_key, int> generate_vertex_map(const Mesh *mesh,
const float2 *uv_map,
const PLYExportParams &export_params)
{
Map<UV_vertex_key, int> vertex_map;
const Span<MPoly> polys = mesh->polys();
const Span<MLoop> loops = mesh->loops();
const int totvert = mesh->totvert;
vertex_map.reserve(totvert);
if (uv_map == nullptr || !export_params.export_uv) {
for (int vertex_index = 0; vertex_index < totvert; ++vertex_index) {
UV_vertex_key key = UV_vertex_key({0, 0}, vertex_index);
vertex_map.add_new(key, int(vertex_map.size()));
}
return vertex_map;
}
const float limit[2] = {STD_UV_CONNECT_LIMIT, STD_UV_CONNECT_LIMIT};
UvVertMap *uv_vert_map = BKE_mesh_uv_vert_map_create(polys.data(),
nullptr,
nullptr,
loops.data(),
reinterpret_cast<const float(*)[2]>(uv_map),
uint(polys.size()),
totvert,
limit,
false,
false);
for (int vertex_index = 0; vertex_index < totvert; vertex_index++) {
const UvMapVert *uv_vert = BKE_mesh_uv_vert_map_get_vert(uv_vert_map, vertex_index);
if (uv_vert == nullptr) {
UV_vertex_key key = UV_vertex_key({0, 0}, vertex_index);
vertex_map.add_new(key, int(vertex_map.size()));
}
for (; uv_vert; uv_vert = uv_vert->next) {
/* Store UV vertex coordinates. */
const int loopstart = polys[uv_vert->poly_index].loopstart;
float2 vert_uv_coords(uv_map[loopstart + uv_vert->loop_of_poly_index]);
UV_vertex_key key = UV_vertex_key(vert_uv_coords, vertex_index);
vertex_map.add(key, int(vertex_map.size()));
}
}
BKE_mesh_uv_vert_map_free(uv_vert_map);
return vertex_map;
}
} // namespace blender::io::ply

View File

@ -0,0 +1,60 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "BKE_mesh.h"
#include "BLI_math.h"
#include "BKE_context.h"
#include "BKE_mesh.h"
#include "BKE_mesh_mapping.h"
#include "BKE_object.h"
#include "BLI_math.h"
#include "RNA_types.h"
#include "DEG_depsgraph.h"
#include "DEG_depsgraph_build.h"
#include "DEG_depsgraph_query.h"
#include "DNA_layer_types.h"
#include "ply_data.hh"
namespace blender::io::ply {
Mesh *do_triangulation(const Mesh *mesh, bool force_triangulation);
void set_world_axes_transform(Object *object, const eIOAxis forward, const eIOAxis up);
struct UV_vertex_key {
float2 UV;
int mesh_vertex_index;
UV_vertex_key(float2 UV, int vertex_index) : UV(UV), mesh_vertex_index(vertex_index)
{
}
bool operator==(const UV_vertex_key &r) const
{
return (UV == r.UV && mesh_vertex_index == r.mesh_vertex_index);
}
uint64_t hash() const
{
return ((std::hash<float>()(UV.x) ^ (std::hash<float>()(UV.y) << 1)) >> 1) ^
(std::hash<int>()(mesh_vertex_index) << 1);
}
};
blender::Map<UV_vertex_key, int> generate_vertex_map(const Mesh *mesh,
const float2 *uv_map,
const PLYExportParams &export_params);
void load_plydata(PlyData &plyData, const bContext *C, const PLYExportParams &export_params);
void load_plydata(PlyData &plyData, Depsgraph *depsgraph, const PLYExportParams &export_params);
} // namespace blender::io::ply

View File

@ -0,0 +1,82 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "ply_file_buffer.hh"
namespace blender::io::ply {
FileBuffer::FileBuffer(const char *filepath, size_t buffer_chunk_size)
: buffer_chunk_size_(buffer_chunk_size), filepath_(filepath)
{
outfile_ = BLI_fopen(filepath, "wb");
if (!outfile_) {
throw std::system_error(
errno, std::system_category(), "Cannot open file " + std::string(filepath) + ".");
}
}
void FileBuffer::write_to_file()
{
for (const VectorChar &b : blocks_)
fwrite(b.data(), 1, b.size(), this->outfile_);
blocks_.clear();
}
void FileBuffer::close_file()
{
int close_status = std::fclose(outfile_);
if (close_status == EOF) {
return;
}
if (outfile_ && close_status) {
std::cerr << "Error: could not close the file '" << this->filepath_
<< "' properly, it may be corrupted." << std::endl;
}
}
void FileBuffer::write_header_element(StringRef name, int count)
{
write_fstring("element {} {}\n", name, count);
}
void FileBuffer::write_header_scalar_property(StringRef dataType, StringRef name)
{
write_fstring("property {} {}\n", dataType, name);
}
void FileBuffer::write_header_list_property(StringRef countType,
StringRef dataType,
StringRef name)
{
write_fstring("property list {} {} {}\n", countType, dataType, name);
}
void FileBuffer::write_string(StringRef s)
{
write_fstring("{}\n", s);
}
void FileBuffer::write_newline()
{
write_fstring("\n");
}
void FileBuffer::ensure_space(size_t at_least)
{
if (blocks_.is_empty() || (blocks_.last().capacity() - blocks_.last().size() < at_least)) {
blocks_.append(VectorChar());
blocks_.reserve(std::max(at_least, buffer_chunk_size_));
}
}
void FileBuffer::write_bytes(Span<char> bytes)
{
ensure_space(bytes.size());
VectorChar &bb = blocks_.last();
bb.insert(bb.end(), bytes.begin(), bytes.end());
}
} // namespace blender::io::ply

View File

@ -0,0 +1,94 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include <string>
#include <type_traits>
#include <vector>
#include "BLI_array.hh"
#include "BLI_compiler_attrs.h"
#include "BLI_fileops.h"
#include "BLI_string_ref.hh"
#include "BLI_utility_mixins.hh"
#include "BLI_vector.hh"
/* SEP macro from BLI path utils clashes with SEP symbol in fmt headers. */
#undef SEP
#define FMT_HEADER_ONLY
#include <fmt/format.h>
namespace blender::io::ply {
/**
* File buffer writer.
* All writes are done into an internal chunked memory buffer
* (list of default 64 kilobyte blocks).
* Call write_to_file once in a while to write the memory buffer(s)
* into the given file.
*/
class FileBuffer : private NonMovable {
using VectorChar = Vector<char>;
Vector<VectorChar> blocks_;
size_t buffer_chunk_size_;
const char *filepath_;
FILE *outfile_;
public:
FileBuffer(const char *filepath, size_t buffer_chunk_size = 64 * 1024);
virtual ~FileBuffer() = default;
/* Write contents to the buffer(s) into a file, and clear the buffers. */
void write_to_file();
void close_file();
virtual void write_vertex(float x, float y, float z) = 0;
virtual void write_UV(float u, 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;
virtual void write_vertex_end() = 0;
virtual void write_face(char count, Span<uint32_t> const &vertex_indices) = 0;
virtual void write_edge(int first, int second) = 0;
void write_header_element(StringRef name, int count);
void write_header_scalar_property(StringRef dataType, StringRef name);
void write_header_list_property(StringRef countType, StringRef dataType, StringRef name);
void write_string(StringRef s);
void write_newline();
protected:
/* Ensure the last block contains at least this amount of free space.
* If not, add a new block with max of block size & the amount of space needed. */
void ensure_space(size_t at_least);
template<typename... T> void write_fstring(const char *fmt, T &&...args)
{
/* Format into a local buffer. */
fmt::memory_buffer buf;
fmt::format_to(fmt::appender(buf), fmt, std::forward<T>(args)...);
size_t len = buf.size();
ensure_space(len);
VectorChar &bb = blocks_.last();
bb.insert(bb.end(), buf.begin(), buf.end());
}
void write_bytes(Span<char> bytes);
};
} // namespace blender::io::ply

View File

@ -0,0 +1,51 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "ply_file_buffer_ascii.hh"
namespace blender::io::ply {
void FileBufferAscii::write_vertex(float x, float y, float z)
{
write_fstring("{} {} {}", x, y, z);
}
void FileBufferAscii::write_UV(float u, float v)
{
write_fstring(" {} {}", u, v);
}
void FileBufferAscii::write_vertex_normal(float nx, float ny, float nz)
{
write_fstring(" {} {} {}", nx, ny, nz);
}
void FileBufferAscii::write_vertex_color(uchar r, uchar g, uchar b, uchar a)
{
write_fstring(" {} {} {} {}", r, g, b, a);
}
void FileBufferAscii::write_vertex_end()
{
write_fstring("\n");
}
void FileBufferAscii::write_face(char count, Span<uint32_t> const &vertex_indices)
{
write_fstring("{}", int(count));
for (const uint32_t v : vertex_indices) {
write_fstring(" {}", v);
}
write_newline();
}
void FileBufferAscii::write_edge(int first, int second)
{
write_fstring("{} {}", first, second);
write_newline();
}
} // namespace blender::io::ply

View File

@ -0,0 +1,30 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "ply_file_buffer.hh"
namespace blender::io::ply {
class FileBufferAscii : public FileBuffer {
using FileBuffer::FileBuffer;
public:
void write_vertex(float x, float y, float z) override;
void write_UV(float u, 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;
void write_vertex_end() override;
void write_face(char count, Span<uint32_t> const &vertex_indices) override;
void write_edge(int first, int second) override;
};
} // namespace blender::io::ply

View File

@ -0,0 +1,66 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "ply_file_buffer_binary.hh"
namespace blender::io::ply {
void FileBufferBinary::write_vertex(float x, float y, float z)
{
float3 vector(x, y, z);
char *bits = reinterpret_cast<char *>(&vector);
Span<char> span(bits, sizeof(float3));
write_bytes(span);
}
void FileBufferBinary::write_UV(float u, float v)
{
float2 vector(u, v);
char *bits = reinterpret_cast<char *>(&vector);
Span<char> span(bits, sizeof(float2));
write_bytes(span);
}
void FileBufferBinary::write_vertex_normal(float nx, float ny, float nz)
{
float3 vector(nx, ny, nz);
char *bits = reinterpret_cast<char *>(&vector);
Span<char> span(bits, sizeof(float3));
write_bytes(span);
}
void FileBufferBinary::write_vertex_color(uchar r, uchar g, uchar b, uchar a)
{
uchar4 vector(r, g, b, a);
char *bits = reinterpret_cast<char *>(&vector);
Span<char> span(bits, sizeof(uchar4));
write_bytes(span);
}
void FileBufferBinary::write_vertex_end()
{
/* In binary, there is no end to a vertex. */
}
void FileBufferBinary::write_face(char size, Span<uint32_t> const &vertex_indices)
{
write_bytes(Span<char>({size}));
write_bytes(vertex_indices.cast<char>());
}
void FileBufferBinary::write_edge(int first, int second)
{
int2 vector(first, second);
char *bits = reinterpret_cast<char *>(&vector);
Span<char> span(bits, sizeof(int2));
write_bytes(span);
}
} // namespace blender::io::ply

View File

@ -0,0 +1,42 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include <string>
#include <type_traits>
#include "BLI_array.hh"
#include "BLI_compiler_attrs.h"
#include "BLI_fileops.h"
#include "BLI_math_vector_types.hh"
#include "BLI_string_ref.hh"
#include "BLI_utility_mixins.hh"
#include "ply_file_buffer.hh"
#include <bitset>
namespace blender::io::ply {
class FileBufferBinary : public FileBuffer {
using FileBuffer::FileBuffer;
public:
void write_vertex(float x, float y, float z) override;
void write_UV(float u, 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;
void write_vertex_end() override;
void write_face(char size, Span<uint32_t> const &vertex_indices) override;
void write_edge(int first, int second) override;
};
} // namespace blender::io::ply

View File

@ -0,0 +1,211 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BKE_layer.h"
#include "BKE_lib_id.h"
#include "BKE_mesh.h"
#include "BKE_object.h"
#include "BKE_report.h"
#include "DNA_collection_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "BLI_fileops.hh"
#include "BLI_math_vector.h"
#include "BLI_memory_utils.hh"
#include "DEG_depsgraph.h"
#include "DEG_depsgraph_build.h"
#include "ply_data.hh"
#include "ply_functions.hh"
#include "ply_import.hh"
#include "ply_import_ascii.hh"
#include "ply_import_binary.hh"
#include "ply_import_mesh.hh"
namespace blender::io::ply {
void splitstr(std::string str, Vector<std::string> &words, const StringRef &deli)
{
int pos;
while ((pos = int(str.find(deli))) != std::string::npos) {
words.append(str.substr(0, pos));
str.erase(0, pos + deli.size());
}
/* We add the final word to the vector. */
words.append(str.substr());
}
enum PlyDataTypes from_string(const StringRef &input)
{
if (input == "uchar") {
return PlyDataTypes::UCHAR;
}
if (input == "char") {
return PlyDataTypes::CHAR;
}
if (input == "ushort") {
return PlyDataTypes::USHORT;
}
if (input == "short") {
return PlyDataTypes::SHORT;
}
if (input == "uint") {
return PlyDataTypes::UINT;
}
if (input == "int") {
return PlyDataTypes::INT;
}
if (input == "float") {
return PlyDataTypes::FLOAT;
}
if (input == "double") {
return PlyDataTypes::DOUBLE;
}
return PlyDataTypes::FLOAT;
}
void importer_main(bContext *C, const PLYImportParams &import_params, wmOperator *op)
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
importer_main(bmain, scene, view_layer, import_params, op);
}
void importer_main(Main *bmain,
Scene *scene,
ViewLayer *view_layer,
const PLYImportParams &import_params,
wmOperator *op)
{
std::string line;
fstream infile(import_params.filepath, std::ios::in | std::ios::binary);
PlyHeader header;
while (true) { /* We break when end_header is encountered. */
safe_getline(infile, line);
if (header.header_size == 0 && line != "ply") {
fprintf(stderr, "PLY Importer: failed to read file. Invalid PLY header.\n");
BKE_report(op->reports, RPT_ERROR, "PLY Importer: Invalid PLY header.");
return;
}
header.header_size++;
Vector<std::string> words{};
splitstr(line, words, " ");
if (strcmp(words[0].c_str(), "format") == 0) {
if (strcmp(words[1].c_str(), "ascii") == 0) {
header.type = PlyFormatType::ASCII;
}
else if (strcmp(words[1].c_str(), "binary_big_endian") == 0) {
header.type = PlyFormatType::BINARY_BE;
}
else if (strcmp(words[1].c_str(), "binary_little_endian") == 0) {
header.type = PlyFormatType::BINARY_LE;
}
}
else if (strcmp(words[0].c_str(), "element") == 0) {
header.elements.append(std::make_pair(words[1], std::stoi(words[2])));
if (strcmp(words[1].c_str(), "vertex") == 0) {
header.vertex_count = std::stoi(words[2]);
}
else if (strcmp(words[1].c_str(), "face") == 0) {
header.face_count = std::stoi(words[2]);
}
else if (strcmp(words[1].c_str(), "edge") == 0) {
header.edge_count = std::stoi(words[2]);
}
}
else if (strcmp(words[0].c_str(), "property") == 0) {
std::pair<std::string, PlyDataTypes> property;
property.first = words[2];
property.second = from_string(words[1]);
while (header.properties.size() < header.elements.size()) {
Vector<std::pair<std::string, PlyDataTypes>> temp;
header.properties.append(temp);
}
header.properties[header.elements.size() - 1].append(property);
}
else if (words[0] == "end_header") {
break;
}
else if ((words[0][0] >= '0' && words[0][0] <= '9') || words[0][0] == '-' || line.empty() ||
infile.eof()) {
/* A value was found before we broke out of the loop. No end_header. */
BKE_report(op->reports, RPT_ERROR, "PLY Importer: No end_header");
return;
}
}
/* Name used for both mesh and object. */
char ob_name[FILE_MAX];
BLI_strncpy(ob_name, BLI_path_basename(import_params.filepath), FILE_MAX);
BLI_path_extension_replace(ob_name, FILE_MAX, "");
Mesh *mesh = BKE_mesh_add(bmain, ob_name);
BKE_view_layer_base_deselect_all(scene, view_layer);
LayerCollection *lc = BKE_layer_collection_get_active(view_layer);
Object *obj = BKE_object_add_only_object(bmain, OB_MESH, ob_name);
BKE_mesh_assign_object(bmain, obj, mesh);
BKE_collection_object_add(bmain, lc->collection, obj);
BKE_view_layer_synced_ensure(scene, view_layer);
Base *base = BKE_view_layer_base_find(view_layer, obj);
BKE_view_layer_base_select_and_set_active(view_layer, base);
try {
std::unique_ptr<PlyData> data;
if (header.type == PlyFormatType::ASCII) {
data = import_ply_ascii(infile, &header);
}
else {
data = import_ply_binary(infile, &header);
}
Mesh *temp_val = convert_ply_to_mesh(*data, mesh, import_params);
if (import_params.merge_verts && temp_val != mesh) {
BKE_mesh_nomain_to_mesh(temp_val, mesh, obj);
}
}
catch (std::exception &e) {
fprintf(stderr, "PLY Importer: failed to read file. %s.\n", e.what());
BKE_report(op->reports, RPT_ERROR, "PLY Importer: failed to parse file.");
return;
}
float global_scale = import_params.global_scale;
if ((scene->unit.system != USER_UNIT_NONE) && import_params.use_scene_unit) {
global_scale *= scene->unit.scale_length;
}
float scale_vec[3] = {global_scale, global_scale, global_scale};
float obmat3x3[3][3];
unit_m3(obmat3x3);
float obmat4x4[4][4];
unit_m4(obmat4x4);
/* +Y-forward and +Z-up are the Blender's default axis settings. */
mat3_from_axis_conversion(
IO_AXIS_Y, IO_AXIS_Z, import_params.forward_axis, import_params.up_axis, obmat3x3);
copy_m4_m3(obmat4x4, obmat3x3);
rescale_m4(obmat4x4, scale_vec);
BKE_object_apply_mat4(obj, obmat4x4, true, false);
DEG_id_tag_update(&lc->collection->id, ID_RECALC_COPY_ON_WRITE);
int flags = ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION |
ID_RECALC_BASE_FLAGS;
DEG_id_tag_update_ex(bmain, &obj->id, flags);
DEG_id_tag_update(&scene->id, ID_RECALC_BASE_FLAGS);
DEG_relations_tag_update(bmain);
infile.close();
}
} // namespace blender::io::ply

View File

@ -0,0 +1,28 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "IO_ply.h"
#include "ply_data.hh"
namespace blender::io::ply {
enum PlyDataTypes from_string(const StringRef &input);
void splitstr(std::string str, Vector<std::string> &words, const StringRef &deli);
/* Main import function used from within Blender. */
void importer_main(bContext *C, const PLYImportParams &import_params, wmOperator *op);
/* Used from tests, where full bContext does not exist. */
void importer_main(Main *bmain,
Scene *scene,
ViewLayer *view_layer,
const PLYImportParams &import_params,
wmOperator *op);
} // namespace blender::io::ply

View File

@ -0,0 +1,222 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "ply_import_ascii.hh"
#include "ply_functions.hh"
#include <algorithm>
#include <fstream>
namespace blender::io::ply {
std::unique_ptr<PlyData> import_ply_ascii(fstream &file, PlyHeader *header)
{
std::unique_ptr<PlyData> data = std::make_unique<PlyData>(load_ply_ascii(file, header));
return data;
}
PlyData load_ply_ascii(fstream &file, const PlyHeader *header)
{
PlyData data;
/* Check if header contains alpha. */
std::pair<std::string, PlyDataTypes> alpha = {"alpha", PlyDataTypes::UCHAR};
bool has_alpha = std::find(header->properties[0].begin(), header->properties[0].end(), alpha) !=
header->properties[0].end();
/* Check if header contains colors. */
std::pair<std::string, PlyDataTypes> red = {"red", PlyDataTypes::UCHAR};
bool has_color = std::find(header->properties[0].begin(), header->properties[0].end(), red) !=
header->properties[0].end();
/* Check if header contains normals. */
std::pair<std::string, PlyDataTypes> normalx = {"nx", PlyDataTypes::FLOAT};
bool has_normals = std::find(header->properties[0].begin(),
header->properties[0].end(),
normalx) != header->properties[0].end();
/* Check if header contains uv data. */
std::pair<std::string, PlyDataTypes> uv = {"s", PlyDataTypes::FLOAT};
bool has_UV = std::find(header->properties[0].begin(), header->properties[0].end(), uv) !=
header->properties[0].end();
int3 vertex_index = get_vertex_index(header);
int alpha_index;
int3 color_index;
int3 normal_index;
int2 UV_index;
if (has_alpha) {
alpha_index = get_index(header, "alpha", PlyDataTypes::UCHAR);
}
if (has_color) {
/* x=red, y=green, z=blue */
color_index = get_color_index(header);
}
if (has_normals) {
normal_index = get_normal_index(header);
}
if (has_UV) {
UV_index = get_uv_index(header);
}
for (int i = 0; i < header->vertex_count; i++) {
std::string line;
safe_getline(file, line);
Vector<std::string> value_vec = explode(line, ' ');
/* Vertex coords */
float3 vertex3;
vertex3.x = std::stof(value_vec[vertex_index.x]);
vertex3.y = std::stof(value_vec[vertex_index.y]);
vertex3.z = std::stof(value_vec[vertex_index.z]);
data.vertices.append(vertex3);
/* Vertex colors */
if (has_color) {
float4 colors4;
colors4.x = std::stof(value_vec[color_index.x]) / 255.0f;
colors4.y = std::stof(value_vec[color_index.y]) / 255.0f;
colors4.z = std::stof(value_vec[color_index.z]) / 255.0f;
if (has_alpha) {
colors4.w = std::stof(value_vec[alpha_index]) / 255.0f;
}
else {
colors4.w = 1.0f;
}
data.vertex_colors.append(colors4);
}
/* If normals */
if (has_normals) {
float3 normals3;
normals3.x = std::stof(value_vec[normal_index.x]);
normals3.y = std::stof(value_vec[normal_index.y]);
normals3.z = std::stof(value_vec[normal_index.z]);
data.vertex_normals.append(normals3);
}
/* If uv */
if (has_UV) {
float2 uvmap;
uvmap.x = std::stof(value_vec[UV_index.x]);
uvmap.y = std::stof(value_vec[UV_index.y]);
data.UV_coordinates.append(uvmap);
}
}
for (int i = 0; i < header->face_count; i++) {
std::string line;
getline(file, line);
Vector<std::string> value_vec = explode(line, ' ');
int count = std::stoi(value_vec[0]);
Array<uint> vertex_indices(count);
for (int j = 1; j <= count; j++) {
int index = std::stoi(value_vec[j]);
/* If the face has a vertex index that is outside the range. */
if (index >= data.vertices.size()) {
throw std::runtime_error("Vertex index out of bounds");
}
vertex_indices[j - 1] = index;
}
data.faces.append(vertex_indices);
}
for (int i = 0; i < header->edge_count; i++) {
std::string line;
getline(file, line);
Vector<std::string> value_vec = explode(line, ' ');
std::pair<int, int> edge = std::make_pair(stoi(value_vec[0]), stoi(value_vec[1]));
data.edges.append(edge);
}
return data;
}
int3 get_vertex_index(const PlyHeader *header)
{
int3 vertex_index;
vertex_index.x = get_index(header, "x", PlyDataTypes::FLOAT);
vertex_index.y = get_index(header, "y", PlyDataTypes::FLOAT);
vertex_index.z = get_index(header, "z", PlyDataTypes::FLOAT);
return vertex_index;
}
int3 get_color_index(const PlyHeader *header)
{
int3 color_index;
color_index.x = get_index(header, "red", PlyDataTypes::UCHAR);
color_index.y = get_index(header, "green", PlyDataTypes::UCHAR);
color_index.z = get_index(header, "blue", PlyDataTypes::UCHAR);
return color_index;
}
int3 get_normal_index(const PlyHeader *header)
{
int3 normal_index;
normal_index.x = get_index(header, "nx", PlyDataTypes::FLOAT);
normal_index.y = get_index(header, "ny", PlyDataTypes::FLOAT);
normal_index.z = get_index(header, "nz", PlyDataTypes::FLOAT);
return normal_index;
}
int2 get_uv_index(const PlyHeader *header)
{
int2 uv_index;
uv_index.x = get_index(header, "s", PlyDataTypes::FLOAT);
uv_index.y = get_index(header, "t", PlyDataTypes::FLOAT);
return uv_index;
}
int get_index(const PlyHeader *header, std::string property, PlyDataTypes datatype)
{
std::pair<std::string, PlyDataTypes> pair = {property, datatype};
const std::pair<std::string, blender::io::ply::PlyDataTypes> *it = std::find(
header->properties[0].begin(), header->properties[0].end(), pair);
return (int)(it - header->properties[0].begin());
}
Vector<std::string> explode(const StringRef str, const char &ch)
{
std::string next;
Vector<std::string> result;
/* For each character in the string. */
for (char c : str) {
/* If we've hit the terminal character. */
if (c == ch) {
/* If we have some characters accumulated. */
if (!next.empty()) {
/* Add them to the result vector. */
result.append(next);
next.clear();
}
}
else {
/* Accumulate the next character into the sequence. */
next += c;
}
}
if (!next.empty()) {
result.append(next);
}
return result;
}
} // namespace blender::io::ply

View File

@ -0,0 +1,39 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "BLI_fileops.hh"
#include "DNA_mesh_types.h"
#include "IO_ply.h"
#include "ply_data.hh"
namespace blender::io::ply {
/**
* The function that gets called from the importer.
* \param file: The PLY file that was opened.
* \param header: The information in the PLY header.
*/
std::unique_ptr<PlyData> import_ply_ascii(fstream &file, PlyHeader *header);
/**
* Loads the information from the PLY file in ASCII format to the PlyData datastructure.
* \param file: The PLY file that was opened.
* \param header: The information in the PLY header.
* \return The PlyData datastructure that can be used for conversion to a Mesh.
*/
PlyData load_ply_ascii(fstream &file, const PlyHeader *header);
int3 get_vertex_index(const PlyHeader *header);
int3 get_color_index(const PlyHeader *header);
int3 get_normal_index(const PlyHeader *header);
int2 get_uv_index(const PlyHeader *header);
int get_index(const PlyHeader *header, std::string property, PlyDataTypes datatype);
Vector<std::string> explode(const StringRef str, const char &ch);
} // namespace blender::io::ply

View File

@ -0,0 +1,215 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BLI_array.hh"
#include "ply_import_binary.hh"
#include <fstream>
namespace blender::io::ply {
std::unique_ptr<PlyData> import_ply_binary(fstream &file, const PlyHeader *header)
{
std::unique_ptr<PlyData> data = std::make_unique<PlyData>(load_ply_binary(file, header));
return data;
}
template<typename T> T read(fstream &file, bool isBigEndian)
{
T returnVal;
file.read((char *)&returnVal, sizeof(returnVal));
check_file_errors(file);
if (isBigEndian) {
returnVal = swap_bytes<T>(returnVal);
}
return returnVal;
}
template uint8_t read<uint8_t>(fstream &file, bool isBigEndian);
template int8_t read<int8_t>(fstream &file, bool isBigEndian);
template uint16_t read<uint16_t>(fstream &file, bool isBigEndian);
template int16_t read<int16_t>(fstream &file, bool isBigEndian);
template uint32_t read<uint32_t>(fstream &file, bool isBigEndian);
template int32_t read<int32_t>(fstream &file, bool isBigEndian);
template float read<float>(fstream &file, bool isBigEndian);
template double read<double>(fstream &file, bool isBigEndian);
void check_file_errors(const fstream &file)
{
if (file.bad()) {
throw std::ios_base::failure("Read/Write error on io operation");
}
if (file.fail()) {
throw std::ios_base::failure("Logical error on io operation");
}
if (file.eof()) {
throw std::ios_base::failure("Reached end of the file");
}
}
void discard_value(fstream &file, const PlyDataTypes type)
{
switch (type) {
case CHAR:
read<int8_t>(file, false);
break;
case UCHAR:
read<uint8_t>(file, false);
break;
case SHORT:
read<int16_t>(file, false);
break;
case USHORT:
read<uint16_t>(file, false);
break;
case INT:
read<int32_t>(file, false);
break;
case UINT:
read<uint32_t>(file, false);
break;
case FLOAT:
read<float>(file, false);
break;
case DOUBLE:
read<double>(file, false);
break;
}
}
PlyData load_ply_binary(fstream &file, const PlyHeader *header)
{
PlyData data;
bool isBigEndian = header->type == PlyFormatType::BINARY_BE;
for (int i = 0; i < header->elements.size(); i++) {
if (header->elements[i].first == "vertex") {
/* Import vertices. */
load_vertex_data(file, header, &data, i);
}
else if (header->elements[i].first == "edge") {
/* Import edges. */
for (int j = 0; j < header->elements[i].second; j++) {
std::pair<int, int> vertex_indices;
for (auto [name, type] : header->properties[i]) {
if (name == "vertex1") {
vertex_indices.first = int(read<int32_t>(file, isBigEndian));
}
else if (name == "vertex2") {
vertex_indices.second = int(read<int32_t>(file, isBigEndian));
}
else {
discard_value(file, type);
}
}
data.edges.append(vertex_indices);
}
}
else if (header->elements[i].first == "face") {
/* Import faces. */
for (int j = 0; j < header->elements[i].second; j++) {
/* Assume vertex_index_count_type is uchar. */
uint8_t count = read<uint8_t>(file, isBigEndian);
Array<uint> vertex_indices(count);
/* Loop over the amount of vertex indices in this face. */
for (uint8_t k = 0; k < count; k++) {
uint32_t index = read<uint32_t>(file, isBigEndian);
/* If the face has a vertex index that is outside the range. */
if (index >= data.vertices.size()) {
throw std::runtime_error("Vertex index out of bounds");
}
vertex_indices[k] = index;
}
data.faces.append(vertex_indices);
}
}
else {
/* Nothing else is supported. */
for (int j = 0; j < header->elements[i].second; j++) {
for (auto [name, type] : header->properties[i]) {
discard_value(file, type);
}
}
}
}
return data;
}
void load_vertex_data(fstream &file, const PlyHeader *header, PlyData *r_data, int index)
{
bool hasNormal = false;
bool hasColor = false;
bool hasUv = false;
bool isBigEndian = header->type == PlyFormatType::BINARY_BE;
for (int i = 0; i < header->vertex_count; i++) {
float3 coord{0};
float3 normal{0};
float4 color{1};
float2 uv{0};
for (auto [name, type] : header->properties[index]) {
if (name == "x") {
coord.x = read<float>(file, isBigEndian);
}
else if (name == "y") {
coord.y = read<float>(file, isBigEndian);
}
else if (name == "z") {
coord.z = read<float>(file, isBigEndian);
}
else if (name == "nx") {
normal.x = read<float>(file, isBigEndian);
hasNormal = true;
}
else if (name == "ny") {
normal.y = read<float>(file, isBigEndian);
}
else if (name == "nz") {
normal.z = read<float>(file, isBigEndian);
}
else if (name == "red") {
color.x = read<uint8_t>(file, isBigEndian) / 255.0f;
hasColor = true;
}
else if (name == "green") {
color.y = read<uint8_t>(file, isBigEndian) / 255.0f;
}
else if (name == "blue") {
color.z = read<uint8_t>(file, isBigEndian) / 255.0f;
}
else if (name == "alpha") {
color.w = read<uint8_t>(file, isBigEndian) / 255.0f;
}
else if (name == "s") {
uv.x = read<float>(file, isBigEndian);
hasUv = true;
}
else if (name == "t") {
uv.y = read<float>(file, isBigEndian);
}
else {
/* No other properties are supported yet. */
discard_value(file, type);
}
}
r_data->vertices.append(coord);
if (hasNormal) {
r_data->vertex_normals.append(normal);
}
if (hasColor) {
r_data->vertex_colors.append(color);
}
if (hasUv) {
r_data->UV_coordinates.append(uv);
}
}
}
} // namespace blender::io::ply

View File

@ -0,0 +1,71 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "BLI_endian_switch.h"
#include "BLI_fileops.hh"
#include "ply_data.hh"
namespace blender::io::ply {
/**
* The function that gets called from the importer.
* \param file: The PLY file that was opened.
* \param header: The information in the PLY header.
* \return The PlyData datastructure that can be used for conversion to a Mesh.
*/
std::unique_ptr<PlyData> import_ply_binary(fstream &file, const PlyHeader *header);
/**
* Loads the information from the PLY file in binary format to the PlyData datastructure.
* \param file: The PLY file that was opened.
* \param header: The information in the PLY header.
* \return The PlyData datastructure that can be used for conversion to a Mesh.
*/
PlyData load_ply_binary(fstream &file, const PlyHeader *header);
void load_vertex_data(fstream &file, const PlyHeader *header, PlyData *r_data, int index);
void check_file_errors(const fstream &file);
void discard_value(fstream &file, const PlyDataTypes type);
template<typename T> T swap_bytes(T input)
{
/* In big endian, the most-significant byte is first.
* So, we need to swap the byte order. */
/* 0xAC in LE should become 0xCA in BE. */
if (sizeof(T) == 1) {
return input;
}
if constexpr (sizeof(T) == 2) {
uint16_t value = reinterpret_cast<uint16_t &>(input);
BLI_endian_switch_uint16(&value);
return reinterpret_cast<T &>(value);
}
if constexpr (sizeof(T) == 4) {
/* Reinterpret this data as uint32 for easy rearranging of bytes. */
uint32_t value = reinterpret_cast<uint32_t &>(input);
BLI_endian_switch_uint32(&value);
return reinterpret_cast<T &>(value);
}
if constexpr (sizeof(T) == 8) {
/* Reinterpret this data as uint64 for easy rearranging of bytes. */
uint64_t value = reinterpret_cast<uint64_t &>(input);
BLI_endian_switch_uint64(&value);
return reinterpret_cast<T &>(value);
}
}
template<typename T> T read(fstream &file, bool isBigEndian);
} // namespace blender::io::ply

View File

@ -0,0 +1,117 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BKE_attribute.h"
#include "BKE_attribute.hh"
#include "BKE_customdata.h"
#include "BKE_mesh.h"
#include "BKE_mesh_runtime.h"
#include "GEO_mesh_merge_by_distance.hh"
#include "BLI_math_vector.h"
#include "ply_import_mesh.hh"
namespace blender::io::ply {
Mesh *convert_ply_to_mesh(PlyData &data, Mesh *mesh, const PLYImportParams &params)
{
/* Add vertices to the mesh. */
mesh->totvert = int(data.vertices.size());
CustomData_add_layer_named(
&mesh->vdata, CD_PROP_FLOAT3, CD_CONSTRUCT, nullptr, mesh->totvert, "position");
mesh->vert_positions_for_write().copy_from(data.vertices);
bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
if (!data.edges.is_empty()) {
mesh->totedge = int(data.edges.size());
CustomData_add_layer(&mesh->edata, CD_MEDGE, CD_SET_DEFAULT, nullptr, mesh->totedge);
MutableSpan<MEdge> edges = mesh->edges_for_write();
for (int i = 0; i < mesh->totedge; i++) {
edges[i].v1 = data.edges[i].first;
edges[i].v2 = data.edges[i].second;
}
}
/* Add faces to the mesh. */
if (!data.faces.is_empty()) {
/* Specify amount of total faces. */
mesh->totpoly = int(data.faces.size());
mesh->totloop = 0;
for (int i = 0; i < data.faces.size(); i++) {
/* Add number of loops from the vertex indices in the face. */
mesh->totloop += data.faces[i].size();
}
CustomData_add_layer(&mesh->pdata, CD_MPOLY, CD_SET_DEFAULT, nullptr, mesh->totpoly);
CustomData_add_layer(&mesh->ldata, CD_MLOOP, CD_SET_DEFAULT, nullptr, mesh->totloop);
MutableSpan<MPoly> polys = mesh->polys_for_write();
MutableSpan<MLoop> loops = mesh->loops_for_write();
int offset = 0;
/* Iterate over amount of faces. */
for (int i = 0; i < mesh->totpoly; i++) {
int size = int(data.faces[i].size());
/* Set the index from where this face starts and specify the amount of edges it has. */
polys[i].loopstart = offset;
polys[i].totloop = size;
for (int j = 0; j < size; j++) {
/* Set the vertex index of the loop to the one in PlyData. */
loops[offset + j].v = data.faces[i][j];
}
offset += size;
}
}
/* Vertex colors */
if (!data.vertex_colors.is_empty()) {
/* Create a data layer for vertex colors and set them. */
bke::SpanAttributeWriter<ColorGeometry4f> colors =
attributes.lookup_or_add_for_write_span<ColorGeometry4f>("Col", ATTR_DOMAIN_POINT);
for (int i = 0; i < data.vertex_colors.size(); i++) {
copy_v4_v4(colors.span[i], data.vertex_colors[i]);
}
colors.finish();
BKE_id_attributes_active_color_set(&mesh->id, "Col");
}
/* Uvmap */
if (!data.UV_coordinates.is_empty()) {
bke::SpanAttributeWriter<float2> uv_map = attributes.lookup_or_add_for_write_only_span<float2>(
"UVMap", ATTR_DOMAIN_CORNER);
int counter = 0;
for (int i = 0; i < data.faces.size(); i++) {
for (int j = 0; j < data.faces[i].size(); j++) {
uv_map.span[counter] = data.UV_coordinates[data.faces[i][j]];
counter++;
}
}
uv_map.finish();
}
/* Calculate edges from the rest of the mesh. */
BKE_mesh_calc_edges(mesh, true, false);
/* Note: This is important to do after initializing the loops. */
if (!data.vertex_normals.is_empty()) {
BKE_mesh_set_custom_normals_from_verts(
mesh, reinterpret_cast<float(*)[3]>(data.vertex_normals.data()));
}
/* Merge all vertices on the same location. */
if (params.merge_verts) {
std::optional<Mesh *> return_value = blender::geometry::mesh_merge_by_distance_all(
*mesh, IndexMask(mesh->totvert), 0.0001f);
if (return_value.has_value()) {
mesh = return_value.value();
}
}
return mesh;
}
} // namespace blender::io::ply

View File

@ -0,0 +1,21 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "IO_ply.h"
#include "ply_data.hh"
namespace blender::io::ply {
/**
* Converts the PlyData datastructure to a mesh.
* \param data: The PLY data.
* \return The mesh that can be used inside blender.
*/
Mesh *convert_ply_to_mesh(PlyData &data, Mesh *mesh, const PLYImportParams &params);
} // namespace blender::io::ply

View File

@ -0,0 +1,42 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "BLI_array.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_vector.hh"
namespace blender::io::ply {
enum PlyDataTypes { CHAR, UCHAR, SHORT, USHORT, INT, UINT, FLOAT, DOUBLE };
struct PlyData {
Vector<float3> vertices;
Vector<float3> vertex_normals;
/* Value between 0 and 1. */
Vector<float4> vertex_colors;
Vector<std::pair<int, int>> edges;
Vector<float3> edge_colors;
Vector<Array<uint32_t>> faces;
Vector<float2> UV_coordinates;
};
enum PlyFormatType { ASCII, BINARY_LE, BINARY_BE };
struct PlyHeader {
int vertex_count = 0;
int edge_count = 0;
int face_count = 0;
int header_size = 0;
/* List of elements in ply file with their count. */
Vector<std::pair<std::string, int>> elements;
/* List of properties (Name, type) per element. */
Vector<Vector<std::pair<std::string, PlyDataTypes>>> properties;
PlyFormatType type;
};
} // namespace blender::io::ply

View File

@ -0,0 +1,51 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "ply_functions.hh"
namespace blender::io::ply {
line_ending safe_getline(fstream &file, std::string &line)
{
line.clear();
std::streambuf *sb = file.rdbuf();
std::istream::sentry se(file, true);
line_ending possible = UNSET;
char c;
while (sb->sgetc() != std::streambuf::traits_type::eof()) {
c = char(sb->sgetc());
switch (c) {
case '\n':
if (possible == UNSET) {
possible = LF;
}
else if (possible == CR) {
possible = CR_LF;
}
break;
case '\r':
if (possible == UNSET) {
possible = CR;
}
else if (possible == LF) {
possible = LF_CR;
}
break;
default:
/* If a different character is encountered after the line ending is set, we know to return.
*/
if (possible != UNSET) {
return possible;
}
line += c;
break;
}
sb->sbumpc();
}
return possible;
}
} // namespace blender::io::ply

View File

@ -0,0 +1,26 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "BLI_fileops.hh"
#include <string>
namespace blender::io::ply {
enum line_ending { CR_LF, LF, CR, LF_CR, UNSET };
/**
* Reads a line in the ply file in a line-ending safe manner. All different line endings are
* supported. This also supports a mix of different line endings in the same file. CR (\\r), LF
* (\\n), CR/LF (\\r\\n), LF/CR (\\n\\r).
* \param file: The file stream.
* \param line: The string you want to read to.
* \return The line ending enum if you're interested.
*/
line_ending safe_getline(fstream &file, std::string &line);
} // namespace blender::io::ply

View File

@ -0,0 +1,463 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "testing/testing.h"
#include "tests/blendfile_loading_base_test.h"
#include "BKE_blender_version.h"
#include "DEG_depsgraph.h"
#include "IO_ply.h"
#include "intern/ply_data.hh"
#include "ply_export_data.hh"
#include "ply_export_header.hh"
#include "ply_export_load_plydata.hh"
#include "ply_file_buffer_ascii.hh"
#include "ply_file_buffer_binary.hh"
#include <fstream>
namespace blender::io::ply {
class PlyExportTest : public BlendfileLoadingBaseTest {
public:
bool load_file_and_depsgraph(const std::string &filepath,
const eEvaluationMode eval_mode = DAG_EVAL_VIEWPORT)
{
if (!blendfile_load(filepath.c_str())) {
return false;
}
depsgraph_create(eval_mode);
return true;
}
};
std::unique_ptr<PlyData> load_cube(PLYExportParams &params)
{
std::unique_ptr<PlyData> plyData = std::make_unique<PlyData>();
plyData->vertices = {{1.122082, 1.122082, 1.122082},
{1.122082, 1.122082, -1.122082},
{1.122082, -1.122082, 1.122082},
{1.122082, -1.122082, -1.122082},
{-1.122082, 1.122082, 1.122082},
{-1.122082, 1.122082, -1.122082},
{-1.122082, -1.122082, 1.122082},
{-1.122082, -1.122082, -1.122082}};
plyData->faces = {
{0, 2, 6, 4}, {3, 7, 6, 2}, {7, 5, 4, 6}, {5, 7, 3, 1}, {1, 3, 2, 0}, {5, 1, 0, 4}};
if (params.export_normals)
plyData->vertex_normals = {{-0.5773503, -0.5773503, -0.5773503},
{-0.5773503, -0.5773503, 0.5773503},
{-0.5773503, 0.5773503, -0.5773503},
{-0.5773503, 0.5773503, 0.5773503},
{0.5773503, -0.5773503, -0.5773503},
{0.5773503, -0.5773503, 0.5773503},
{0.5773503, 0.5773503, -0.5773503},
{0.5773503, 0.5773503, 0.5773503}};
return plyData;
}
/* The following is relative to BKE_tempdir_base.
* Use Latin Capital Letter A with Ogonek, Cyrillic Capital Letter Zhe
* at the end, to test I/O on non-English file names. */
const char *const temp_file_path = "output\xc4\x84\xd0\x96.ply";
static std::string read_temp_file_in_string(const std::string &file_path)
{
std::string res;
size_t buffer_len;
void *buffer = BLI_file_read_text_as_mem(file_path.c_str(), 0, &buffer_len);
if (buffer != nullptr) {
res.assign((const char *)buffer, buffer_len);
MEM_freeN(buffer);
}
return res;
}
char read(std::ifstream &file)
{
char returnVal;
file.read((char *)&returnVal, sizeof(returnVal));
return returnVal;
}
static std::vector<char> read_temp_file_in_vectorchar(const std::string &file_path)
{
std::vector<char> res;
std::ifstream infile(file_path, std::ios::binary);
while (true) {
uint64_t c = read(infile);
if (!infile.eof()) {
res.push_back(c);
}
else {
break;
}
}
return res;
}
TEST_F(PlyExportTest, WriteHeaderAscii)
{
std::string filePath = blender::tests::flags_test_release_dir() + "/" + temp_file_path;
PLYExportParams _params;
_params.ascii_format = true;
_params.export_normals = false;
_params.export_colors = false;
BLI_strncpy(_params.filepath, filePath.c_str(), 1024);
std::unique_ptr<PlyData> plyData = load_cube(_params);
std::unique_ptr<FileBuffer> buffer = std::make_unique<FileBufferAscii>(_params.filepath);
write_header(*buffer.get(), *plyData.get(), _params);
buffer->close_file();
std::string result = read_temp_file_in_string(filePath);
StringRef version = BKE_blender_version_string();
std::string expected =
"ply\n"
"format ascii 1.0\n"
"comment Created in Blender version " +
version +
"\n"
"element vertex 8\n"
"property float x\n"
"property float y\n"
"property float z\n"
"element face 6\n"
"property list uchar uint vertex_indices\n"
"end_header\n";
ASSERT_STREQ(result.c_str(), expected.c_str());
}
TEST_F(PlyExportTest, WriteHeaderBinary)
{
std::string filePath = blender::tests::flags_test_release_dir() + "/" + temp_file_path;
PLYExportParams _params;
_params.ascii_format = false;
_params.export_normals = false;
_params.export_colors = false;
BLI_strncpy(_params.filepath, filePath.c_str(), 1024);
std::unique_ptr<PlyData> plyData = load_cube(_params);
std::unique_ptr<FileBuffer> buffer = std::make_unique<FileBufferBinary>(_params.filepath);
write_header(*buffer.get(), *plyData.get(), _params);
buffer->close_file();
std::string result = read_temp_file_in_string(filePath);
StringRef version = BKE_blender_version_string();
std::string expected =
"ply\n"
"format binary_little_endian 1.0\n"
"comment Created in Blender version " +
version +
"\n"
"element vertex 8\n"
"property float x\n"
"property float y\n"
"property float z\n"
"element face 6\n"
"property list uchar uint vertex_indices\n"
"end_header\n";
ASSERT_STREQ(result.c_str(), expected.c_str());
}
TEST_F(PlyExportTest, WriteVerticesAscii)
{
std::string filePath = blender::tests::flags_test_release_dir() + "/" + temp_file_path;
PLYExportParams _params;
_params.ascii_format = true;
_params.export_normals = false;
_params.export_colors = false;
BLI_strncpy(_params.filepath, filePath.c_str(), 1024);
std::unique_ptr<PlyData> plyData = load_cube(_params);
std::unique_ptr<FileBuffer> buffer = std::make_unique<FileBufferAscii>(_params.filepath);
write_vertices(*buffer.get(), *plyData.get());
buffer->close_file();
std::string result = read_temp_file_in_string(filePath);
std::string expected =
"1.122082 1.122082 1.122082\n"
"1.122082 1.122082 -1.122082\n"
"1.122082 -1.122082 1.122082\n"
"1.122082 -1.122082 -1.122082\n"
"-1.122082 1.122082 1.122082\n"
"-1.122082 1.122082 -1.122082\n"
"-1.122082 -1.122082 1.122082\n"
"-1.122082 -1.122082 -1.122082\n";
ASSERT_STREQ(result.c_str(), expected.c_str());
}
TEST_F(PlyExportTest, WriteVerticesBinary)
{
std::string filePath = blender::tests::flags_test_release_dir() + "/" + temp_file_path;
PLYExportParams _params;
_params.ascii_format = false;
_params.export_normals = false;
_params.export_colors = false;
BLI_strncpy(_params.filepath, filePath.c_str(), 1024);
std::unique_ptr<PlyData> plyData = load_cube(_params);
std::unique_ptr<FileBuffer> buffer = std::make_unique<FileBufferBinary>(_params.filepath);
write_vertices(*buffer.get(), *plyData.get());
buffer->close_file();
std::vector<char> result = read_temp_file_in_vectorchar(filePath);
std::vector<char> expected(
{(char)0x62, (char)0xA0, (char)0x8F, (char)0x3F, (char)0x62, (char)0xA0, (char)0x8F,
(char)0x3F, (char)0x62, (char)0xA0, (char)0x8F, (char)0x3F, (char)0x62, (char)0xA0,
(char)0x8F, (char)0x3F, (char)0x62, (char)0xA0, (char)0x8F, (char)0x3F, (char)0x62,
(char)0xA0, (char)0x8F, (char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0x3F,
(char)0x62, (char)0xA0, (char)0x8F, (char)0xBF, (char)0x62, (char)0xA0, (char)0x8F,
(char)0x3F, (char)0x62, (char)0xA0, (char)0x8F, (char)0x3F, (char)0x62, (char)0xA0,
(char)0x8F, (char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0xBF, (char)0x62,
(char)0xA0, (char)0x8F, (char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0x3F,
(char)0x62, (char)0xA0, (char)0x8F, (char)0x3F, (char)0x62, (char)0xA0, (char)0x8F,
(char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0x3F, (char)0x62, (char)0xA0,
(char)0x8F, (char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0xBF, (char)0x62,
(char)0xA0, (char)0x8F, (char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0x3F,
(char)0x62, (char)0xA0, (char)0x8F, (char)0xBF, (char)0x62, (char)0xA0, (char)0x8F,
(char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0xBF});
ASSERT_EQ(result.size(), expected.size());
for (int i = 0; i < result.size(); i++) {
ASSERT_EQ(result[i], expected[i]);
}
}
TEST_F(PlyExportTest, WriteFacesAscii)
{
std::string filePath = blender::tests::flags_test_release_dir() + "/" + temp_file_path;
PLYExportParams _params;
_params.ascii_format = true;
_params.export_normals = false;
_params.export_colors = false;
BLI_strncpy(_params.filepath, filePath.c_str(), 1024);
std::unique_ptr<PlyData> plyData = load_cube(_params);
std::unique_ptr<FileBuffer> buffer = std::make_unique<FileBufferAscii>(_params.filepath);
write_faces(*buffer.get(), *plyData.get());
buffer->close_file();
std::string result = read_temp_file_in_string(filePath);
std::string expected =
"4 0 2 6 4\n"
"4 3 7 6 2\n"
"4 7 5 4 6\n"
"4 5 7 3 1\n"
"4 1 3 2 0\n"
"4 5 1 0 4\n";
ASSERT_STREQ(result.c_str(), expected.c_str());
}
TEST_F(PlyExportTest, WriteFacesBinary)
{
std::string filePath = blender::tests::flags_test_release_dir() + "/" + temp_file_path;
PLYExportParams _params;
_params.ascii_format = false;
_params.export_normals = false;
_params.export_colors = false;
BLI_strncpy(_params.filepath, filePath.c_str(), 1024);
std::unique_ptr<PlyData> plyData = load_cube(_params);
std::unique_ptr<FileBuffer> buffer = std::make_unique<FileBufferBinary>(_params.filepath);
write_faces(*buffer.get(), *plyData.get());
buffer->close_file();
std::vector<char> result = read_temp_file_in_vectorchar(filePath);
std::vector<char> expected(
{(char)0x04, (char)0x00, (char)0x00, (char)0x00, (char)0x00, (char)0x02, (char)0x00,
(char)0x00, (char)0x00, (char)0x06, (char)0x00, (char)0x00, (char)0x00, (char)0x04,
(char)0x00, (char)0x00, (char)0x00, (char)0x04, (char)0x03, (char)0x00, (char)0x00,
(char)0x00, (char)0x07, (char)0x00, (char)0x00, (char)0x00, (char)0x06, (char)0x00,
(char)0x00, (char)0x00, (char)0x02, (char)0x00, (char)0x00, (char)0x00, (char)0x04,
(char)0x07, (char)0x00, (char)0x00, (char)0x00, (char)0x05, (char)0x00, (char)0x00,
(char)0x00, (char)0x04, (char)0x00, (char)0x00, (char)0x00, (char)0x06, (char)0x00,
(char)0x00, (char)0x00, (char)0x04, (char)0x05, (char)0x00, (char)0x00, (char)0x00,
(char)0x07, (char)0x00, (char)0x00, (char)0x00, (char)0x03, (char)0x00, (char)0x00,
(char)0x00, (char)0x01, (char)0x00, (char)0x00, (char)0x00, (char)0x04, (char)0x01,
(char)0x00, (char)0x00, (char)0x00, (char)0x03, (char)0x00, (char)0x00, (char)0x00,
(char)0x02, (char)0x00, (char)0x00, (char)0x00, (char)0x00, (char)0x00, (char)0x00,
(char)0x00, (char)0x04, (char)0x05, (char)0x00, (char)0x00, (char)0x00, (char)0x01,
(char)0x00, (char)0x00, (char)0x00, (char)0x00, (char)0x00, (char)0x00, (char)0x00,
(char)0x04, (char)0x00, (char)0x00, (char)0x00});
ASSERT_EQ(result.size(), expected.size());
for (int i = 0; i < result.size(); i++) {
ASSERT_EQ(result[i], expected[i]);
}
}
TEST_F(PlyExportTest, WriteVertexNormalsAscii)
{
std::string filePath = blender::tests::flags_test_release_dir() + "/" + temp_file_path;
PLYExportParams _params;
_params.ascii_format = true;
_params.export_normals = true;
_params.export_colors = false;
BLI_strncpy(_params.filepath, filePath.c_str(), 1024);
std::unique_ptr<PlyData> plyData = load_cube(_params);
std::unique_ptr<FileBuffer> buffer = std::make_unique<FileBufferAscii>(_params.filepath);
write_vertices(*buffer.get(), *plyData.get());
buffer->close_file();
std::string result = read_temp_file_in_string(filePath);
std::string expected =
"1.122082 1.122082 1.122082 -0.5773503 -0.5773503 -0.5773503\n"
"1.122082 1.122082 -1.122082 -0.5773503 -0.5773503 0.5773503\n"
"1.122082 -1.122082 1.122082 -0.5773503 0.5773503 -0.5773503\n"
"1.122082 -1.122082 -1.122082 -0.5773503 0.5773503 0.5773503\n"
"-1.122082 1.122082 1.122082 0.5773503 -0.5773503 -0.5773503\n"
"-1.122082 1.122082 -1.122082 0.5773503 -0.5773503 0.5773503\n"
"-1.122082 -1.122082 1.122082 0.5773503 0.5773503 -0.5773503\n"
"-1.122082 -1.122082 -1.122082 0.5773503 0.5773503 0.5773503\n";
ASSERT_STREQ(result.c_str(), expected.c_str());
}
TEST_F(PlyExportTest, WriteVertexNormalsBinary)
{
std::string filePath = blender::tests::flags_test_release_dir() + "/" + temp_file_path;
PLYExportParams _params;
_params.ascii_format = false;
_params.export_normals = true;
_params.export_colors = false;
BLI_strncpy(_params.filepath, filePath.c_str(), 1024);
std::unique_ptr<PlyData> plyData = load_cube(_params);
std::unique_ptr<FileBuffer> buffer = std::make_unique<FileBufferBinary>(_params.filepath);
write_vertices(*buffer.get(), *plyData.get());
buffer->close_file();
std::vector<char> result = read_temp_file_in_vectorchar(filePath);
std::vector<char> expected(
{(char)0x62, (char)0xA0, (char)0x8F, (char)0x3F, (char)0x62, (char)0xA0, (char)0x8F,
(char)0x3F, (char)0x62, (char)0xA0, (char)0x8F, (char)0x3F, (char)0x3B, (char)0xCD,
(char)0x13, (char)0xBF, (char)0x3B, (char)0xCD, (char)0x13, (char)0xBF, (char)0x3B,
(char)0xCD, (char)0x13, (char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0x3F,
(char)0x62, (char)0xA0, (char)0x8F, (char)0x3F, (char)0x62, (char)0xA0, (char)0x8F,
(char)0xBF, (char)0x3B, (char)0xCD, (char)0x13, (char)0xBF, (char)0x3B, (char)0xCD,
(char)0x13, (char)0xBF, (char)0x3B, (char)0xCD, (char)0x13, (char)0x3F, (char)0x62,
(char)0xA0, (char)0x8F, (char)0x3F, (char)0x62, (char)0xA0, (char)0x8F, (char)0xBF,
(char)0x62, (char)0xA0, (char)0x8F, (char)0x3F, (char)0x3B, (char)0xCD, (char)0x13,
(char)0xBF, (char)0x3B, (char)0xCD, (char)0x13, (char)0x3F, (char)0x3B, (char)0xCD,
(char)0x13, (char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0x3F, (char)0x62,
(char)0xA0, (char)0x8F, (char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0xBF,
(char)0x3B, (char)0xCD, (char)0x13, (char)0xBF, (char)0x3B, (char)0xCD, (char)0x13,
(char)0x3F, (char)0x3B, (char)0xCD, (char)0x13, (char)0x3F, (char)0x62, (char)0xA0,
(char)0x8F, (char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0x3F, (char)0x62,
(char)0xA0, (char)0x8F, (char)0x3F, (char)0x3B, (char)0xCD, (char)0x13, (char)0x3F,
(char)0x3B, (char)0xCD, (char)0x13, (char)0xBF, (char)0x3B, (char)0xCD, (char)0x13,
(char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0xBF, (char)0x62, (char)0xA0,
(char)0x8F, (char)0x3F, (char)0x62, (char)0xA0, (char)0x8F, (char)0xBF, (char)0x3B,
(char)0xCD, (char)0x13, (char)0x3F, (char)0x3B, (char)0xCD, (char)0x13, (char)0xBF,
(char)0x3B, (char)0xCD, (char)0x13, (char)0x3F, (char)0x62, (char)0xA0, (char)0x8F,
(char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0xBF, (char)0x62, (char)0xA0,
(char)0x8F, (char)0x3F, (char)0x3B, (char)0xCD, (char)0x13, (char)0x3F, (char)0x3B,
(char)0xCD, (char)0x13, (char)0x3F, (char)0x3B, (char)0xCD, (char)0x13, (char)0xBF,
(char)0x62, (char)0xA0, (char)0x8F, (char)0xBF, (char)0x62, (char)0xA0, (char)0x8F,
(char)0xBF, (char)0x62, (char)0xA0, (char)0x8F, (char)0xBF, (char)0x3B, (char)0xCD,
(char)0x13, (char)0x3F, (char)0x3B, (char)0xCD, (char)0x13, (char)0x3F, (char)0x3B,
(char)0xCD, (char)0x13, (char)0x3F});
ASSERT_EQ(result.size(), expected.size());
for (int i = 0; i < result.size(); i++) {
ASSERT_EQ(result[i], expected[i]);
}
}
class ply_exporter_ply_data_test : public PlyExportTest {
public:
PlyData load_ply_data_from_blendfile(const std::string &blendfile, PLYExportParams &params)
{
PlyData data;
if (!load_file_and_depsgraph(blendfile)) {
return data;
}
load_plydata(data, depsgraph, params);
return data;
}
};
TEST_F(ply_exporter_ply_data_test, CubeLoadPLYDataVertices)
{
PLYExportParams params;
PlyData plyData = load_ply_data_from_blendfile("io_tests/blend_geometry/cube_all_data.blend",
params);
EXPECT_EQ(plyData.vertices.size(), 8);
}
TEST_F(ply_exporter_ply_data_test, CubeLoadPLYDataUV)
{
PLYExportParams params;
params.export_uv = true;
PlyData plyData = load_ply_data_from_blendfile("io_tests/blend_geometry/cube_all_data.blend",
params);
EXPECT_EQ(plyData.UV_coordinates.size(), 8);
}
TEST_F(ply_exporter_ply_data_test, SuzanneLoadPLYDataUV)
{
PLYExportParams params;
params.export_uv = true;
PlyData plyData = load_ply_data_from_blendfile("io_tests/blend_geometry/suzanne_all_data.blend",
params);
EXPECT_EQ(plyData.UV_coordinates.size(), 542);
}
TEST_F(ply_exporter_ply_data_test, CubeLoadPLYDataUVDisabled)
{
PLYExportParams params;
params.export_uv = false;
PlyData plyData = load_ply_data_from_blendfile("io_tests/blend_geometry/cube_all_data.blend",
params);
EXPECT_EQ(plyData.UV_coordinates.size(), 0);
}
} // namespace blender::io::ply

View File

@ -0,0 +1,248 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "tests/blendfile_loading_base_test.h"
#include "BKE_attribute.hh"
#include "BKE_mesh.h"
#include "BKE_object.h"
#include "BLO_readfile.h"
#include "DEG_depsgraph_query.h"
#include "IO_ply.h"
#include "ply_data.hh"
#include "ply_import.hh"
#include "ply_import_binary.hh"
namespace blender::io::ply {
struct Expectation {
std::string name;
PlyFormatType type;
int totvert, totpoly, totedge;
float3 vert_first, vert_last;
float3 normal_first = {0, 0, 0};
float2 uv_first;
float4 color_first = {-1, -1, -1, -1};
};
class PlyImportTest : public BlendfileLoadingBaseTest {
public:
void import_and_check(const char *path, const Expectation *expect, size_t expect_count)
{
if (!blendfile_load("io_tests/blend_geometry/all_quads.blend")) {
ADD_FAILURE();
return;
}
PLYImportParams params;
params.global_scale = 1.0f;
params.forward_axis = IO_AXIS_NEGATIVE_Z;
params.up_axis = IO_AXIS_Y;
params.merge_verts = false;
/* Import the test file. */
std::string ply_path = blender::tests::flags_test_asset_dir() + "/io_tests/ply/" + path;
strncpy(params.filepath, ply_path.c_str(), FILE_MAX - 1);
importer_main(bfile->main, bfile->curscene, bfile->cur_view_layer, params, nullptr);
depsgraph_create(DAG_EVAL_VIEWPORT);
DEGObjectIterSettings deg_iter_settings{};
deg_iter_settings.depsgraph = depsgraph;
deg_iter_settings.flags = DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY |
DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET | DEG_ITER_OBJECT_FLAG_VISIBLE |
DEG_ITER_OBJECT_FLAG_DUPLI;
size_t object_index = 0;
/* Iterate over the objects in the viewport */
DEG_OBJECT_ITER_BEGIN (&deg_iter_settings, object) {
if (object_index >= expect_count) {
ADD_FAILURE();
break;
}
const Expectation &exp = expect[object_index];
ASSERT_STREQ(object->id.name, exp.name.c_str());
EXPECT_V3_NEAR(object->loc, float3(0, 0, 0), 0.0001f);
EXPECT_V3_NEAR(object->scale, float3(1, 1, 1), 0.0001f);
if (object->type == OB_MESH) {
Mesh *mesh = BKE_object_get_evaluated_mesh(object);
/* Test if mesh has expected amount of vertices, edges, and faces. */
ASSERT_EQ(mesh->totvert, exp.totvert);
ASSERT_EQ(mesh->totedge, exp.totedge);
ASSERT_EQ(mesh->totpoly, exp.totpoly);
/* Test if first and last vertices match. */
const Span<float3> verts = mesh->vert_positions();
EXPECT_V3_NEAR(verts.first(), exp.vert_first, 0.0001f);
EXPECT_V3_NEAR(verts.last(), exp.vert_last, 0.0001f);
/* Fetch normal data from mesh and test if it matches expectation. */
if (BKE_mesh_has_custom_loop_normals(mesh)) {
const Span<float3> vertex_normals = mesh->vert_normals();
ASSERT_FALSE(vertex_normals.is_empty());
EXPECT_V3_NEAR(vertex_normals[0], exp.normal_first, 0.0001f);
}
/* Fetch UV data from mesh and test if it matches expectation. */
blender::bke::AttributeAccessor attributes = mesh->attributes();
VArray<float2> uvs = attributes.lookup<float2>("UVMap");
float2 uv_first = !uvs.is_empty() ? uvs[0] : float2(0, 0);
EXPECT_V2_NEAR(uv_first, exp.uv_first, 0.0001f);
/* Check if expected mesh has vertex colors, and tests if it matches. */
if (CustomData_has_layer(&mesh->vdata, CD_PROP_COLOR)) {
const float4 *colors = (const float4 *)CustomData_get_layer(&mesh->vdata, CD_PROP_COLOR);
ASSERT_TRUE(colors != nullptr);
EXPECT_V4_NEAR(colors[0], exp.color_first, 0.0001f);
}
}
++object_index;
}
DEG_OBJECT_ITER_END;
EXPECT_EQ(object_index, expect_count);
}
};
TEST_F(PlyImportTest, PLYImportCube)
{
Expectation expect[] = {{"OBCube",
ASCII,
8,
6,
12,
float3(1, 1, -1),
float3(-1, 1, 1),
float3(0.5773, 0.5773, -0.5773),
float2(0, 0)},
{"OBcube_ascii",
ASCII,
24,
6,
24,
float3(1, 1, -1),
float3(-1, 1, 1),
float3(0, 0, -1),
float2(0.979336, 0.844958),
float4(1, 0.8470, 0, 1)}};
import_and_check("cube_ascii.ply", expect, 2);
}
TEST_F(PlyImportTest, PLYImportASCIIEdgeTest)
{
Expectation expect[] = {{"OBCube",
ASCII,
8,
6,
12,
float3(1, 1, -1),
float3(-1, 1, 1),
float3(0.5773, 0.5773, -0.5773)},
{"OBASCII_wireframe_cube",
ASCII,
8,
0,
12,
float3(-1, -1, -1),
float3(1, 1, 1),
float3(-2, 0, -1)}};
import_and_check("ASCII_wireframe_cube.ply", expect, 2);
}
TEST_F(PlyImportTest, PLYImportBunny)
{
Expectation expect[] = {{"OBCube",
ASCII,
8,
6,
12,
float3(1, 1, -1),
float3(-1, 1, 1),
float3(0.5773, 0.5773, -0.5773)},
{"OBbunny2",
BINARY_LE,
1623,
1000,
1513,
float3(0.0380425, 0.109755, 0.0161689),
float3(-0.0722821, 0.143895, -0.0129091),
float3(-2, -2, -2)}};
import_and_check("bunny2.ply", expect, 2);
}
TEST_F(PlyImportTest, PlyImportManySmallHoles)
{
Expectation expect[] = {{"OBCube",
ASCII,
8,
6,
12,
float3(1, 1, -1),
float3(-1, 1, 1),
float3(0.5773, 0.5773, -0.5773)},
{"OBmany_small_holes",
BINARY_LE,
2004,
3524,
5564,
float3(-0.0131592, -0.0598382, 1.58958),
float3(-0.0177622, 0.0105153, 1.61977),
float3(-2, -2, -2),
float2(0, 0),
float4(0.7215, 0.6784, 0.6627, 1)}};
import_and_check("many_small_holes.ply", expect, 2);
}
TEST_F(PlyImportTest, PlyImportWireframeCube)
{
Expectation expect[] = {{"OBCube",
ASCII,
8,
6,
12,
float3(1, 1, -1),
float3(-1, 1, 1),
float3(0.5773, 0.5773, -0.5773)},
{"OBwireframe_cube",
BINARY_LE,
8,
0,
12,
float3(-1, -1, -1),
float3(1, 1, 1),
float3(-2, -2, -2)}};
import_and_check("wireframe_cube.ply", expect, 2);
}
TEST(PlyImportFunctionsTest, PlySwapBytes)
{
/* Individual bits shouldn't swap with each other. */
uint8_t val8 = 0xA8;
uint8_t exp8 = 0xA8;
uint8_t actual8 = swap_bytes<uint8_t>(val8);
ASSERT_EQ(exp8, actual8);
uint16_t val16 = 0xFEB0;
uint16_t exp16 = 0xB0FE;
uint16_t actual16 = swap_bytes<uint16_t>(val16);
ASSERT_EQ(exp16, actual16);
uint32_t val32 = 0x80A37B0A;
uint32_t exp32 = 0x0A7BA380;
uint32_t actual32 = swap_bytes<uint32_t>(val32);
ASSERT_EQ(exp32, actual32);
uint64_t val64 = 0x0102030405060708;
uint64_t exp64 = 0x0807060504030201;
uint64_t actual64 = swap_bytes<uint64_t>(val64);
ASSERT_EQ(exp64, actual64);
}
} // namespace blender::io::ply

View File

@ -311,6 +311,10 @@ if(WITH_IO_WAVEFRONT_OBJ)
add_definitions(-DWITH_IO_WAVEFRONT_OBJ)
endif()
if(WITH_IO_PLY)
add_definitions(-DWITH_IO_PLY)
endif()
if(WITH_IO_STL)
add_definitions(-DWITH_IO_STL)
endif()

View File

@ -44,6 +44,7 @@ static PyStructSequence_Field app_builtopts_info_fields[] = {
{"mod_remesh", NULL},
{"collada", NULL},
{"io_wavefront_obj", NULL},
{"io_ply",NULL},
{"io_stl", NULL},
{"io_gpencil", NULL},
{"opencolorio", NULL},
@ -260,6 +261,12 @@ static PyObject *make_builtopts_info(void)
SetObjIncref(Py_False);
#endif
#ifdef WITH_IO_PLY
SetObjIncref(Py_True);
#else
SetObjIncref(Py_False);
#endif
#ifdef WITH_IO_STL
SetObjIncref(Py_True);
#else