diff --git a/CMakeLists.txt b/CMakeLists.txt index 02b78fd604c..9153d7a3d13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -358,6 +358,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) diff --git a/build_files/cmake/config/blender_lite.cmake b/build_files/cmake/config/blender_lite.cmake index 79a0e5f3858..fa133de9aa6 100644 --- a/build_files/cmake/config/blender_lite.cmake +++ b/build_files/cmake/config/blender_lite.cmake @@ -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) diff --git a/scripts/startup/bl_ui/space_topbar.py b/scripts/startup/bl_ui/space_topbar.py index e235e592403..f736beafadc 100644 --- a/scripts/startup/bl_ui/space_topbar.py +++ b/scripts/startup/bl_ui/space_topbar.py @@ -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): diff --git a/source/blender/editors/io/CMakeLists.txt b/source/blender/editors/io/CMakeLists.txt index 568ece00c4c..e5368b6a792 100644 --- a/source/blender/editors/io/CMakeLists.txt +++ b/source/blender/editors/io/CMakeLists.txt @@ -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 diff --git a/source/blender/editors/io/io_ops.c b/source/blender/editors/io/io_ops.c index 0340d0598d5..4593a7c9a9b 100644 --- a/source/blender/editors/io/io_ops.c +++ b/source/blender/editors/io/io_ops.c @@ -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 diff --git a/source/blender/editors/io/io_ply_ops.c b/source/blender/editors/io/io_ply_ops.c new file mode 100644 index 00000000000..f291655af95 --- /dev/null +++ b/source/blender/editors/io/io_ply_ops.c @@ -0,0 +1,329 @@ +/* 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]; + Scene *scene = CTX_data_scene(C); + 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, ¶ms, op); + } + } + else if (RNA_struct_property_is_set_ex(op->ptr, "filepath", false)) { + RNA_string_get(op->ptr, "filepath", params.filepath); + PLY_import(C, ¶ms, 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 */ diff --git a/source/blender/editors/io/io_ply_ops.h b/source/blender/editors/io/io_ply_ops.h new file mode 100644 index 00000000000..bdd809ac7c1 --- /dev/null +++ b/source/blender/editors/io/io_ply_ops.h @@ -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); diff --git a/source/blender/editors/space_file/filelist.cc b/source/blender/editors/space_file/filelist.cc index 2ce6264e077..a6ddea14819 100644 --- a/source/blender/editors/space_file/filelist.cc +++ b/source/blender/editors/space_file/filelist.cc @@ -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)) { diff --git a/source/blender/io/CMakeLists.txt b/source/blender/io/CMakeLists.txt index 8b20b50a181..52f6543b48b 100644 --- a/source/blender/io/CMakeLists.txt +++ b/source/blender/io/CMakeLists.txt @@ -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() diff --git a/source/blender/io/ply/CMakeLists.txt b/source/blender/io/ply/CMakeLists.txt new file mode 100644 index 00000000000..12c1d842ab4 --- /dev/null +++ b/source/blender/io/ply/CMakeLists.txt @@ -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() diff --git a/source/blender/io/ply/IO_ply.cc b/source/blender/io/ply/IO_ply.cc new file mode 100644 index 00000000000..d5b0c76e627 --- /dev/null +++ b/source/blender/io/ply/IO_ply.cc @@ -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); +} diff --git a/source/blender/io/ply/IO_ply.h b/source/blender/io/ply/IO_ply.h new file mode 100644 index 00000000000..be957090490 --- /dev/null +++ b/source/blender/io/ply/IO_ply.h @@ -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 diff --git a/source/blender/io/ply/exporter/ply_export.cc b/source/blender/io/ply/exporter/ply_export.cc new file mode 100644 index 00000000000..a95f63d0e51 --- /dev/null +++ b/source/blender/io/ply/exporter/ply_export.cc @@ -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 plyData = std::make_unique(); + load_plydata(*plyData, CTX_data_ensure_evaluated_depsgraph(C), export_params); + + std::unique_ptr buffer; + + if (export_params.ascii_format) { + buffer = std::make_unique(export_params.filepath); + } + else { + buffer = std::make_unique(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 diff --git a/source/blender/io/ply/exporter/ply_export.hh b/source/blender/io/ply/exporter/ply_export.hh new file mode 100644 index 00000000000..d84b2da21c9 --- /dev/null +++ b/source/blender/io/ply/exporter/ply_export.hh @@ -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 diff --git a/source/blender/io/ply/exporter/ply_export_data.cc b/source/blender/io/ply/exporter/ply_export_data.cc new file mode 100644 index 00000000000..032ea933516 --- /dev/null +++ b/source/blender/io/ply/exporter/ply_export_data.cc @@ -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 &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 &edge : ply_data.edges) { + buffer.write_edge(edge.first, edge.second); + } + buffer.write_to_file(); +} +} // namespace blender::io::ply diff --git a/source/blender/io/ply/exporter/ply_export_data.hh b/source/blender/io/ply/exporter/ply_export_data.hh new file mode 100644 index 00000000000..376051fb32b --- /dev/null +++ b/source/blender/io/ply/exporter/ply_export_data.hh @@ -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 diff --git a/source/blender/io/ply/exporter/ply_export_header.cc b/source/blender/io/ply/exporter/ply_export_header.cc new file mode 100644 index 00000000000..fc41b764caa --- /dev/null +++ b/source/blender/io/ply/exporter/ply_export_header.cc @@ -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 diff --git a/source/blender/io/ply/exporter/ply_export_header.hh b/source/blender/io/ply/exporter/ply_export_header.hh new file mode 100644 index 00000000000..85e1361340c --- /dev/null +++ b/source/blender/io/ply/exporter/ply_export_header.hh @@ -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 diff --git a/source/blender/io/ply/exporter/ply_export_load_plydata.cc b/source/blender/io/ply/exporter/ply_export_load_plydata.cc new file mode 100644 index 00000000000..daf5a0c6ea2 --- /dev/null +++ b/source/blender/io/ply/exporter/ply_export_load_plydata.cc @@ -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 + +#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 (°_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( + CustomData_get_layer(&mesh->ldata, CD_PROP_FLOAT2)); + + Map 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 loops = mesh->loops(); + for (const MPoly poly : mesh->polys()) { + Span loopSpan = loops.slice(poly.loopstart, poly.totloop); + Array 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 mesh_vertex_index_LUT(vertex_map.size()); + Array ply_vertex_index_LUT(mesh->totvert); + Array 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 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 color_attribute = + attributes.lookup_or_default( + 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 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 generate_vertex_map(const Mesh *mesh, + const float2 *uv_map, + const PLYExportParams &export_params) +{ + + Map vertex_map; + + const Span polys = mesh->polys(); + const Span 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, 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(uv_map), + 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, 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, vertex_map.size()); + } + } + BKE_mesh_uv_vert_map_free(uv_vert_map); + return vertex_map; +} +} // namespace blender::io::ply diff --git a/source/blender/io/ply/exporter/ply_export_load_plydata.hh b/source/blender/io/ply/exporter/ply_export_load_plydata.hh new file mode 100644 index 00000000000..4b5e1fb0c09 --- /dev/null +++ b/source/blender/io/ply/exporter/ply_export_load_plydata.hh @@ -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()(UV.x) ^ (std::hash()(UV.y) << 1)) >> 1) ^ + (std::hash()(mesh_vertex_index) << 1); + } +}; + +blender::Map 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 diff --git a/source/blender/io/ply/exporter/ply_file_buffer.cc b/source/blender/io/ply/exporter/ply_file_buffer.cc new file mode 100644 index 00000000000..0e70443d395 --- /dev/null +++ b/source/blender/io/ply/exporter/ply_file_buffer.cc @@ -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 bytes) +{ + ensure_space(bytes.size()); + VectorChar &bb = blocks_.last(); + bb.insert(bb.end(), bytes.begin(), bytes.end()); +} + +} // namespace blender::io::ply diff --git a/source/blender/io/ply/exporter/ply_file_buffer.hh b/source/blender/io/ply/exporter/ply_file_buffer.hh new file mode 100644 index 00000000000..c26f9e56ed1 --- /dev/null +++ b/source/blender/io/ply/exporter/ply_file_buffer.hh @@ -0,0 +1,94 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup ply + */ + +#pragma once + +#include +#include +#include + +#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 + +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; + Vector 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 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 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(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 bytes); +}; + +} // namespace blender::io::ply diff --git a/source/blender/io/ply/exporter/ply_file_buffer_ascii.cc b/source/blender/io/ply/exporter/ply_file_buffer_ascii.cc new file mode 100644 index 00000000000..caa4b0588cc --- /dev/null +++ b/source/blender/io/ply/exporter/ply_file_buffer_ascii.cc @@ -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 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 diff --git a/source/blender/io/ply/exporter/ply_file_buffer_ascii.hh b/source/blender/io/ply/exporter/ply_file_buffer_ascii.hh new file mode 100644 index 00000000000..d40241dfd13 --- /dev/null +++ b/source/blender/io/ply/exporter/ply_file_buffer_ascii.hh @@ -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 const &vertex_indices) override; + + void write_edge(int first, int second) override; +}; +} // namespace blender::io::ply diff --git a/source/blender/io/ply/exporter/ply_file_buffer_binary.cc b/source/blender/io/ply/exporter/ply_file_buffer_binary.cc new file mode 100644 index 00000000000..6bffa4b6ffb --- /dev/null +++ b/source/blender/io/ply/exporter/ply_file_buffer_binary.cc @@ -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(&vector); + Span span(bits, sizeof(float3)); + + write_bytes(span); +} + +void FileBufferBinary::write_UV(float u, float v) +{ + float2 vector(u, v); + char *bits = reinterpret_cast(&vector); + Span 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(&vector); + Span 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(&vector); + Span 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 const &vertex_indices) +{ + write_bytes(Span({size})); + + write_bytes(vertex_indices.cast()); +} + +void FileBufferBinary::write_edge(int first, int second) +{ + int2 vector(first, second); + char *bits = reinterpret_cast(&vector); + Span span(bits, sizeof(int2)); + + write_bytes(span); +} +} // namespace blender::io::ply diff --git a/source/blender/io/ply/exporter/ply_file_buffer_binary.hh b/source/blender/io/ply/exporter/ply_file_buffer_binary.hh new file mode 100644 index 00000000000..d2493fba627 --- /dev/null +++ b/source/blender/io/ply/exporter/ply_file_buffer_binary.hh @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup ply + */ + +#pragma once + +#include +#include + +#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 + +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 const &vertex_indices) override; + + void write_edge(int first, int second) override; +}; +} // namespace blender::io::ply diff --git a/source/blender/io/ply/importer/ply_import.cc b/source/blender/io/ply/importer/ply_import.cc new file mode 100644 index 00000000000..45d07b006fd --- /dev/null +++ b/source/blender/io/ply/importer/ply_import.cc @@ -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 &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 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 property; + property.first = words[2]; + property.second = from_string(words[1]); + + while (header.properties.size() < header.elements.size()) { + Vector> 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 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 diff --git a/source/blender/io/ply/importer/ply_import.hh b/source/blender/io/ply/importer/ply_import.hh new file mode 100644 index 00000000000..a19f32cc5ad --- /dev/null +++ b/source/blender/io/ply/importer/ply_import.hh @@ -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 &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 diff --git a/source/blender/io/ply/importer/ply_import_ascii.cc b/source/blender/io/ply/importer/ply_import_ascii.cc new file mode 100644 index 00000000000..bdbebb27ba6 --- /dev/null +++ b/source/blender/io/ply/importer/ply_import_ascii.cc @@ -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 +#include + +namespace blender::io::ply { + +std::unique_ptr import_ply_ascii(fstream &file, PlyHeader *header) +{ + std::unique_ptr data = std::make_unique(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 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 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 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 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 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 value_vec = explode(line, ' '); + int count = std::stoi(value_vec[0]); + Array 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 value_vec = explode(line, ' '); + + std::pair 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 pair = {property, datatype}; + const std::pair *it = std::find( + header->properties[0].begin(), header->properties[0].end(), pair); + return (int)(it - header->properties[0].begin()); +} + +Vector explode(const StringRef str, const char &ch) +{ + std::string next; + Vector 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 diff --git a/source/blender/io/ply/importer/ply_import_ascii.hh b/source/blender/io/ply/importer/ply_import_ascii.hh new file mode 100644 index 00000000000..50f80b4f9e7 --- /dev/null +++ b/source/blender/io/ply/importer/ply_import_ascii.hh @@ -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 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 explode(const StringRef str, const char &ch); +} // namespace blender::io::ply diff --git a/source/blender/io/ply/importer/ply_import_binary.cc b/source/blender/io/ply/importer/ply_import_binary.cc new file mode 100644 index 00000000000..06b2e1d96aa --- /dev/null +++ b/source/blender/io/ply/importer/ply_import_binary.cc @@ -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 + +namespace blender::io::ply { +std::unique_ptr import_ply_binary(fstream &file, const PlyHeader *header) +{ + std::unique_ptr data = std::make_unique(load_ply_binary(file, header)); + return data; +} + +template T read(fstream &file, bool isBigEndian) +{ + T returnVal; + file.read((char *)&returnVal, sizeof(returnVal)); + check_file_errors(file); + if (isBigEndian) { + returnVal = swap_bytes(returnVal); + } + return returnVal; +} + +template uint8_t read(fstream &file, bool isBigEndian); +template int8_t read(fstream &file, bool isBigEndian); +template uint16_t read(fstream &file, bool isBigEndian); +template int16_t read(fstream &file, bool isBigEndian); +template uint32_t read(fstream &file, bool isBigEndian); +template int32_t read(fstream &file, bool isBigEndian); +template float read(fstream &file, bool isBigEndian); +template double read(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(file, false); + break; + case UCHAR: + read(file, false); + break; + case SHORT: + read(file, false); + break; + case USHORT: + read(file, false); + break; + case INT: + read(file, false); + break; + case UINT: + read(file, false); + break; + case FLOAT: + read(file, false); + break; + case DOUBLE: + read(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 vertex_indices; + for (auto [name, type] : header->properties[i]) { + if (name == "vertex1") { + vertex_indices.first = int(read(file, isBigEndian)); + } + else if (name == "vertex2") { + vertex_indices.second = int(read(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(file, isBigEndian); + Array 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(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(file, isBigEndian); + } + else if (name == "y") { + coord.y = read(file, isBigEndian); + } + else if (name == "z") { + coord.z = read(file, isBigEndian); + } + else if (name == "nx") { + normal.x = read(file, isBigEndian); + hasNormal = true; + } + else if (name == "ny") { + normal.y = read(file, isBigEndian); + } + else if (name == "nz") { + normal.z = read(file, isBigEndian); + } + else if (name == "red") { + color.x = read(file, isBigEndian) / 255.0f; + hasColor = true; + } + else if (name == "green") { + color.y = read(file, isBigEndian) / 255.0f; + } + else if (name == "blue") { + color.z = read(file, isBigEndian) / 255.0f; + } + else if (name == "alpha") { + color.w = read(file, isBigEndian) / 255.0f; + } + else if (name == "s") { + uv.x = read(file, isBigEndian); + hasUv = true; + } + else if (name == "t") { + uv.y = read(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 diff --git a/source/blender/io/ply/importer/ply_import_binary.hh b/source/blender/io/ply/importer/ply_import_binary.hh new file mode 100644 index 00000000000..870dedafbb7 --- /dev/null +++ b/source/blender/io/ply/importer/ply_import_binary.hh @@ -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 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 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(input); + BLI_endian_switch_uint16(&value); + return reinterpret_cast(value); + } + + if constexpr (sizeof(T) == 4) { + /* Reinterpret this data as uint32 for easy rearranging of bytes. */ + uint32_t value = reinterpret_cast(input); + BLI_endian_switch_uint32(&value); + return reinterpret_cast(value); + } + + if constexpr (sizeof(T) == 8) { + /* Reinterpret this data as uint64 for easy rearranging of bytes. */ + uint64_t value = reinterpret_cast(input); + BLI_endian_switch_uint64(&value); + return reinterpret_cast(value); + } +} + +template T read(fstream &file, bool isBigEndian); + +} // namespace blender::io::ply diff --git a/source/blender/io/ply/importer/ply_import_mesh.cc b/source/blender/io/ply/importer/ply_import_mesh.cc new file mode 100644 index 00000000000..ef0d8ab9eda --- /dev/null +++ b/source/blender/io/ply/importer/ply_import_mesh.cc @@ -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 ¶ms) +{ + + /* 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 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 polys = mesh->polys_for_write(); + MutableSpan 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 colors = + attributes.lookup_or_add_for_write_span("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 uv_map = attributes.lookup_or_add_for_write_only_span( + "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(data.vertex_normals.data())); + } + + /* Merge all vertices on the same location. */ + if (params.merge_verts) { + std::optional 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 diff --git a/source/blender/io/ply/importer/ply_import_mesh.hh b/source/blender/io/ply/importer/ply_import_mesh.hh new file mode 100644 index 00000000000..1e3bcc716ca --- /dev/null +++ b/source/blender/io/ply/importer/ply_import_mesh.hh @@ -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 ¶ms); + +} // namespace blender::io::ply diff --git a/source/blender/io/ply/intern/ply_data.hh b/source/blender/io/ply/intern/ply_data.hh new file mode 100644 index 00000000000..b3d7736dec6 --- /dev/null +++ b/source/blender/io/ply/intern/ply_data.hh @@ -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 vertices; + Vector vertex_normals; + /* Value between 0 and 1. */ + Vector vertex_colors; + Vector> edges; + Vector edge_colors; + Vector> faces; + Vector 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> elements; + /* List of properties (Name, type) per element. */ + Vector>> properties; + PlyFormatType type; +}; + +} // namespace blender::io::ply diff --git a/source/blender/io/ply/intern/ply_functions.cc b/source/blender/io/ply/intern/ply_functions.cc new file mode 100644 index 00000000000..5cc61547dbe --- /dev/null +++ b/source/blender/io/ply/intern/ply_functions.cc @@ -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 diff --git a/source/blender/io/ply/intern/ply_functions.hh b/source/blender/io/ply/intern/ply_functions.hh new file mode 100644 index 00000000000..e454167be87 --- /dev/null +++ b/source/blender/io/ply/intern/ply_functions.hh @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup ply + */ + +#pragma once + +#include "BLI_fileops.hh" +#include + +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 diff --git a/source/blender/io/ply/tests/io_ply_exporter_test.cc b/source/blender/io/ply/tests/io_ply_exporter_test.cc new file mode 100644 index 00000000000..f2863138388 --- /dev/null +++ b/source/blender/io/ply/tests/io_ply_exporter_test.cc @@ -0,0 +1,467 @@ +/* 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 + +namespace blender::io::ply { +/* Set this true to keep comparison-failing test output in temp file directory. */ +constexpr bool save_failing_test_output = false; + +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 load_cube(PLYExportParams ¶ms) +{ + std::unique_ptr plyData = std::make_unique(); + 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 read_temp_file_in_vectorchar(const std::string &file_path) +{ + std::vector 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 = load_cube(_params); + + std::unique_ptr buffer = std::make_unique(_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 = load_cube(_params); + + std::unique_ptr buffer = std::make_unique(_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 = load_cube(_params); + + std::unique_ptr buffer = std::make_unique(_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 = load_cube(_params); + + std::unique_ptr buffer = std::make_unique(_params.filepath); + + write_vertices(*buffer.get(), *plyData.get()); + + buffer->close_file(); + + std::vector result = read_temp_file_in_vectorchar(filePath); + + std::vector 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 = load_cube(_params); + + std::unique_ptr buffer = std::make_unique(_params.filepath); + + write_faces(*buffer.get(), *plyData.get()); + + buffer->close_file(); + + std::string result = read_temp_file_in_string(filePath); + + StringRef version = BKE_blender_version_string(); + + 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 = load_cube(_params); + + std::unique_ptr buffer = std::make_unique(_params.filepath); + + write_faces(*buffer.get(), *plyData.get()); + + buffer->close_file(); + + std::vector result = read_temp_file_in_vectorchar(filePath); + + std::vector 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 = load_cube(_params); + + std::unique_ptr buffer = std::make_unique(_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 = load_cube(_params); + + std::unique_ptr buffer = std::make_unique(_params.filepath); + + write_vertices(*buffer.get(), *plyData.get()); + + buffer->close_file(); + + std::vector result = read_temp_file_in_vectorchar(filePath); + + std::vector 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 ¶ms) + { + 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 diff --git a/source/blender/io/ply/tests/io_ply_importer_test.cc b/source/blender/io/ply/tests/io_ply_importer_test.cc new file mode 100644 index 00000000000..a3ca9608a0c --- /dev/null +++ b/source/blender/io/ply/tests/io_ply_importer_test.cc @@ -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 (°_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 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 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 uvs = attributes.lookup("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(val8); + ASSERT_EQ(exp8, actual8); + + uint16_t val16 = 0xFEB0; + uint16_t exp16 = 0xB0FE; + uint16_t actual16 = swap_bytes(val16); + ASSERT_EQ(exp16, actual16); + + uint32_t val32 = 0x80A37B0A; + uint32_t exp32 = 0x0A7BA380; + uint32_t actual32 = swap_bytes(val32); + ASSERT_EQ(exp32, actual32); + + uint64_t val64 = 0x0102030405060708; + uint64_t exp64 = 0x0807060504030201; + uint64_t actual64 = swap_bytes(val64); + ASSERT_EQ(exp64, actual64); +} + +} // namespace blender::io::ply diff --git a/source/blender/python/intern/CMakeLists.txt b/source/blender/python/intern/CMakeLists.txt index 9d2516969cf..d34db9c2548 100644 --- a/source/blender/python/intern/CMakeLists.txt +++ b/source/blender/python/intern/CMakeLists.txt @@ -314,6 +314,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() diff --git a/source/blender/python/intern/bpy_app_build_options.c b/source/blender/python/intern/bpy_app_build_options.c index 9d90b66854e..7549cd85fd5 100644 --- a/source/blender/python/intern/bpy_app_build_options.c +++ b/source/blender/python/intern/bpy_app_build_options.c @@ -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