IO: New C++ PLY importer/exporter #104404

Closed
Nathan Rozendaal wants to merge 10 commits from super_jo_nathan/blender-PLY-project:D16792 into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
41 changed files with 3246 additions and 2 deletions

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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);
super_jo_nathan marked this conversation as resolved

Minor: row is not needed for laying this out. I assume it came initially from OBJ code back when it was using axis-choices-as-inline-buttons style. It should be enough to just do:

uiItemR(sub, imfptr, "forward_axis", 0, IFACE_("Forward Axis"), ICON_NONE);
uiItemR(sub, imfptr, "up_axis", 0, IFACE_("Up Axis"), ICON_NONE);

and then *row variable above is not needed either.

Minor: `row` is not needed for laying this out. I assume it came initially from OBJ code back when it was using axis-choices-as-inline-buttons style. It should be enough to just do: ``` uiItemR(sub, imfptr, "forward_axis", 0, IFACE_("Forward Axis"), ICON_NONE); uiItemR(sub, imfptr, "up_axis", 0, IFACE_("Up Axis"), ICON_NONE); ``` and then `*row` variable above is not needed either.
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)
aras_p marked this conversation as resolved

Minor for future cleanup: forward_axis_update and up_axis_update are exactly the same between OBJ and PLY code now. Might be good idea to move it into some shared place at some point (and while at it, maybe look whether Collada could share it).

Minor for future cleanup: `forward_axis_update` and `up_axis_update` are exactly the same between OBJ and PLY code now. Might be good idea to move it into some shared place at some point (and while at it, maybe look whether Collada could share it).

Done in 08db6bf215

Done in 08db6bf215ab
{
int forward = RNA_enum_get(ptr, "forward_axis");
int up = RNA_enum_get(ptr, "up_axis");
if ((forward % 3) == (up % 3)) {
RNA_enum_set(ptr, "up_axis", (up + 1) % 6);
}
}
static void up_axis_update(struct Main *UNUSED(main),
struct Scene *UNUSED(scene),
struct PointerRNA *ptr)
{
int forward = RNA_enum_get(ptr, "forward_axis");
int up = RNA_enum_get(ptr, "up_axis");
if ((forward % 3) == (up % 3)) {
RNA_enum_set(ptr, "forward_axis", (forward + 1) % 6);
}
}
void WM_OT_ply_export(struct wmOperatorType *ot)
{
PropertyRNA *prop;
ot->name = "Export PLY";
ot->description = "Save the scene to a PLY file";
ot->idname = "WM_OT_ply_export";
ot->invoke = wm_ply_export_invoke;
ot->exec = wm_ply_export_exec;
ot->poll = WM_operator_winactive;
ot->ui = wm_ply_export_draw;
ot->check = wm_ply_export_check;
ot->flag = OPTYPE_PRESET;
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER,
FILE_BLENDER,
FILE_SAVE,
WM_FILESEL_FILEPATH | WM_FILESEL_SHOW_PROPS,
FILE_DEFAULTDISPLAY,
FILE_SORT_DEFAULT);
/* Object transform options. */
prop = RNA_def_enum(ot->srna, "forward_axis", io_transform_axis, IO_AXIS_Y, "Forward Axis", "");
RNA_def_property_update_runtime(prop, (void *)forward_axis_update);
prop = RNA_def_enum(ot->srna, "up_axis", io_transform_axis, IO_AXIS_Z, "Up Axis", "");
RNA_def_property_update_runtime(prop, (void *)up_axis_update);
RNA_def_float(
ot->srna,
"global_scale",
1.0f,
0.0001f,
10000.0f,
"Scale",
"Value by which to enlarge or shrink the objects with respect to the world's origin",
0.0001f,
10000.0f);
/* File Writer options. */
RNA_def_boolean(
ot->srna, "apply_modifiers", true, "Apply Modifiers", "Apply modifiers to exported meshes");
RNA_def_boolean(ot->srna,
"export_selected_objects",
false,
"Export Selected Objects",
"Export only selected objects instead of all supported objects");
RNA_def_boolean(ot->srna, "export_uv", false, "Export UVs", "");
RNA_def_boolean(
ot->srna,
"export_normals",
false,
"Export Vertex Normals",
"Export specific vertex normals if available, export calculated normals otherwise");
RNA_def_boolean(
ot->srna, "export_colors", true, "Export Vertex Colors", "Export per-vertex colors");
RNA_def_boolean(ot->srna,
"export_triangulated_mesh",
false,
"Export Triangulated Mesh",
"All ngons with four or more vertices will be triangulated. Meshes in "
"the scene will not be affected. Behaves like Triangulate Modifier with "
"ngon-method: \"Beauty\", quad-method: \"Shortest Diagonal\", min vertices: 4");
RNA_def_boolean(ot->srna,
"ascii_format",
false,
"ASCII Format",
"Export file in ASCII format, export as binary otherwise");
/* Only show .ply files by default. */
prop = RNA_def_string(ot->srna, "filter_glob", "*.ply", 0, "Extension Filter", "");
RNA_def_property_flag(prop, PROP_HIDDEN);
}
static int wm_ply_import_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
return WM_operator_filesel(C, op, event);
}
static int wm_ply_import_execute(bContext *C, wmOperator *op)
{
struct PLYImportParams params;
params.forward_axis = RNA_enum_get(op->ptr, "forward_axis");
params.up_axis = RNA_enum_get(op->ptr, "up_axis");
params.use_scene_unit = RNA_boolean_get(op->ptr, "use_scene_unit");
params.global_scale = RNA_float_get(op->ptr, "global_scale");
params.merge_verts = RNA_boolean_get(op->ptr, "merge_verts");
int files_len = RNA_collection_length(op->ptr, "files");
if (files_len) {
PointerRNA fileptr;
PropertyRNA *prop;
char dir_only[FILE_MAX], file_only[FILE_MAX];
RNA_string_get(op->ptr, "directory", dir_only);
prop = RNA_struct_find_property(op->ptr, "files");
for (int i = 0; i < files_len; i++) {
RNA_property_collection_lookup_int(op->ptr, prop, i, &fileptr);
RNA_string_get(&fileptr, "name", file_only);
BLI_path_join(params.filepath, sizeof(params.filepath), dir_only, file_only);
PLY_import(C, &params, op);
}
}
else if (RNA_struct_property_is_set_ex(op->ptr, "filepath", false)) {
RNA_string_get(op->ptr, "filepath", params.filepath);
PLY_import(C, &params, op);
}
else {
BKE_report(op->reports, RPT_ERROR, "No filename given");
return OPERATOR_CANCELLED;
}
Scene *scene = CTX_data_scene(C);
WM_event_add_notifier(C, NC_SCENE | ND_OB_SELECT, scene);
WM_event_add_notifier(C, NC_SCENE | ND_OB_ACTIVE, scene);
WM_event_add_notifier(C, NC_SCENE | ND_LAYER_CONTENT, scene);
ED_outliner_select_sync_from_object_tag(C);
return OPERATOR_FINISHED;
}
static bool wm_ply_import_check(bContext *UNUSED(C), wmOperator *op)
{
const int num_axes = 3;
/* Both forward and up axes cannot be the same (or same except opposite sign). */
if (RNA_enum_get(op->ptr, "forward_axis") % num_axes ==
(RNA_enum_get(op->ptr, "up_axis") % num_axes)) {
RNA_enum_set(op->ptr, "up_axis", RNA_enum_get(op->ptr, "up_axis") % num_axes + 1);
return true;
}
return false;
}
void WM_OT_ply_import(struct wmOperatorType *ot)
{
PropertyRNA *prop;
ot->name = "Import PLY";
ot->description = "Import an PLY file as an object";
ot->idname = "WM_OT_ply_import";
ot->invoke = wm_ply_import_invoke;
ot->exec = wm_ply_import_execute;
ot->poll = WM_operator_winactive;
ot->check = wm_ply_import_check;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_PRESET;
WM_operator_properties_filesel(ot,
FILE_TYPE_FOLDER,
FILE_BLENDER,
FILE_OPENFILE,
WM_FILESEL_FILEPATH | WM_FILESEL_FILES | WM_FILESEL_DIRECTORY |
WM_FILESEL_SHOW_PROPS,
FILE_DEFAULTDISPLAY,
FILE_SORT_DEFAULT);
RNA_def_float(ot->srna, "global_scale", 1.0f, 1e-6f, 1e6f, "Scale", "", 0.001f, 1000.0f);
RNA_def_boolean(ot->srna,
"use_scene_unit",
false,
"Scene Unit",
"Apply current scene's unit (as defined by unit scale) to imported data");
RNA_def_enum(ot->srna, "forward_axis", io_transform_axis, IO_AXIS_Y, "Forward Axis", "");
RNA_def_enum(ot->srna, "up_axis", io_transform_axis, IO_AXIS_Z, "Up Axis", "");
RNA_def_boolean(ot->srna, "merge_verts", false, "Merge Vertices", "Merges vertices by distance");
/* Only show .ply files by default. */
prop = RNA_def_string(ot->srna, "filter_glob", "*.ply", 0, "Extension Filter", "");
RNA_def_property_flag(prop, PROP_HIDDEN);
}
#endif /* WITH_IO_PLY */

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BLI_timeit.hh"
#include "DNA_windowmanager_types.h"
#include "IO_ply.h"
#include "ply_export.hh"
#include "ply_import.hh"
void PLY_export(bContext *C, const PLYExportParams *export_params)
super_jo_nathan marked this conversation as resolved
Review

const struct PLYExportParams -> const PLYExportParams

`const struct PLYExportParams` -> `const PLYExportParams`
{
SCOPED_TIMER("PLY Export");
blender::io::ply::exporter_main(C, *export_params);
}
void PLY_import(bContext *C, const PLYImportParams *import_params, wmOperator *op)
{
SCOPED_TIMER("PLY Import");
blender::io::ply::importer_main(C, *import_params, op);
}

View File

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

View File

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

Minor: these comments don't add much, it's very clear from the code flow what for example write_header does.

Minor: these comments don't add much, it's very clear from the code flow what for example `write_header` does.
write_faces(*buffer.get(), *plyData.get());
write_edges(*buffer.get(), *plyData.get());
buffer->close_file();
}
} // namespace blender::io::ply

View File

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

View File

@ -0,0 +1,51 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BLI_array.hh"
#include "ply_data.hh"
#include "ply_file_buffer.hh"
namespace blender::io::ply {
super_jo_nathan marked this conversation as resolved
Review

void write_vertices(std::unique_ptr<FileBuffer> &buffer, std::unique_ptr<PlyData> &plyData)
->
void write_vertices(FileBuffer &buffer, const PlyData &ply_data)

Not sure if the const is possible, but the function shouldn't need to know that the data is stored in unique pointers. Also, not a big deal, but if you're feeling motivated by consistency, the standard is snake_case for variable names.

`void write_vertices(std::unique_ptr<FileBuffer> &buffer, std::unique_ptr<PlyData> &plyData)` -> `void write_vertices(FileBuffer &buffer, const PlyData &ply_data)` Not sure if the const is possible, but the function shouldn't need to know that the data is stored in unique pointers. Also, not a big deal, but if you're feeling motivated by consistency, the standard is `snake_case` for variable names.
void write_vertices(FileBuffer &buffer, const PlyData &ply_data)
{
for (int i = 0; i < ply_data.vertices.size(); i++) {
buffer.write_vertex(ply_data.vertices[i].x, ply_data.vertices[i].y, ply_data.vertices[i].z);
if (!ply_data.vertex_normals.is_empty())
buffer.write_vertex_normal(ply_data.vertex_normals[i].x,
ply_data.vertex_normals[i].y,
ply_data.vertex_normals[i].z);
if (!ply_data.vertex_colors.is_empty())
buffer.write_vertex_color(uchar(ply_data.vertex_colors[i].x * 255),
uchar(ply_data.vertex_colors[i].y * 255),
uchar(ply_data.vertex_colors[i].z * 255),
uchar(ply_data.vertex_colors[i].w * 255));
if (!ply_data.UV_coordinates.is_empty())
buffer.write_UV(ply_data.UV_coordinates[i].x, ply_data.UV_coordinates[i].y);
buffer.write_vertex_end();
}
buffer.write_to_file();
}
void write_faces(FileBuffer &buffer, const PlyData &ply_data)
{
for (const Array<uint32_t> &face : ply_data.faces) {
buffer.write_face(char(face.size()), face);
}
buffer.write_to_file();
}
void write_edges(FileBuffer &buffer, const PlyData &ply_data)
{
for (const std::pair<int, int> &edge : ply_data.edges) {
buffer.write_edge(edge.first, edge.second);
}
buffer.write_to_file();
}
} // namespace blender::io::ply

View File

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

View File

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

View File

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

View File

@ -0,0 +1,273 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BLI_array.hh"
#include "BLI_math.h"
#include "BKE_attribute.hh"
#include "BKE_lib_id.h"
#include "BKE_mesh.h"
#include "BKE_mesh_mapping.h"
#include "BKE_object.h"
#include "DEG_depsgraph.h"
#include "DEG_depsgraph_build.h"
#include "DEG_depsgraph_query.h"
#include "DNA_layer_types.h"
#include "IO_ply.h"
#include "bmesh.h"
#include "bmesh_tools.h"
#include <tools/bmesh_triangulate.h>
#include "ply_data.hh"
#include "ply_export_load_plydata.hh"
namespace blender::io::ply {
float world_and_axes_transform_[4][4];
float world_and_axes_normal_transform_[3][3];
bool mirrored_transform_;
Mesh *do_triangulation(const Mesh *mesh, bool force_triangulation)
{
const BMeshCreateParams bm_create_params = {false};
BMeshFromMeshParams bm_convert_params{};
bm_convert_params.calc_face_normal = true;
bm_convert_params.calc_vert_normal = true;
super_jo_nathan marked this conversation as resolved
Review

false is the default for BMeshFromMeshParams, there should be no need to specify that manually

`false` is the default for `BMeshFromMeshParams`, there should be no need to specify that manually
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)

Minor for future cleanup: this is the same code as OBJ set_world_axes_transform (and possibly others?). Would be nice to share.

Minor for future cleanup: this is the same code as OBJ set_world_axes_transform (and possibly others?). Would be nice to share.
{
float axes_transform[3][3];
unit_m3(axes_transform);
/* +Y-forward and +Z-up are the default Blender axis settings. */
mat3_from_axis_conversion(forward, up, IO_AXIS_Y, IO_AXIS_Z, axes_transform);
mul_m4_m3m4(world_and_axes_transform_, axes_transform, object->object_to_world);
/* mul_m4_m3m4 does not transform last row of obmat, i.e. location data. */
mul_v3_m3v3(world_and_axes_transform_[3], axes_transform, object->object_to_world[3]);
world_and_axes_transform_[3][3] = object->object_to_world[3][3];
/* Normals need inverse transpose of the regular matrix to handle non-uniform scale. */
float normal_matrix[3][3];
copy_m3_m4(normal_matrix, world_and_axes_transform_);
invert_m3_m3(world_and_axes_normal_transform_, normal_matrix);
transpose_m3(world_and_axes_normal_transform_);
mirrored_transform_ = is_negative_m3(world_and_axes_normal_transform_);
}
void load_plydata(PlyData &plyData, Depsgraph *depsgraph, const PLYExportParams &export_params)
{
DEGObjectIterSettings deg_iter_settings{};
deg_iter_settings.depsgraph = depsgraph;
deg_iter_settings.flags = DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY |
DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET | DEG_ITER_OBJECT_FLAG_VISIBLE |
DEG_ITER_OBJECT_FLAG_DUPLI;
/* When exporting multiple objects, vertex indices have to be offset. */
uint32_t vertex_offset = 0;
DEG_OBJECT_ITER_BEGIN (&deg_iter_settings, object) {
if (object->type != OB_MESH) {
continue;
}
if (export_params.export_selected_objects && !(object->base_flag & BASE_SELECTED)) {
continue;
}
Object *obj_eval = DEG_get_evaluated_object(depsgraph, object);
Object export_object_eval_ = dna::shallow_copy(*obj_eval);
Mesh *mesh = export_params.apply_modifiers ?
BKE_object_get_evaluated_mesh(&export_object_eval_) :
BKE_object_get_pre_modified_mesh(&export_object_eval_);
bool force_triangulation = false;
for (const MPoly poly : mesh->polys()) {
super_jo_nathan marked this conversation as resolved
Review

auto &&poly -> const MPoly &poly

`auto &&poly` -> `const MPoly &poly`
if (poly.totloop > 255) {
force_triangulation = true;
break;
}
}
/* Triangulate */
bool manually_free_mesh = false;
if (export_params.export_triangulated_mesh || force_triangulation) {
mesh = do_triangulation(mesh, export_params.export_triangulated_mesh);
manually_free_mesh = true;
}
const float2 *uv_map = static_cast<const float2 *>(
CustomData_get_layer(&mesh->ldata, CD_PROP_FLOAT2));
Map<UV_vertex_key, int> vertex_map = generate_vertex_map(mesh, uv_map, export_params);
super_jo_nathan marked this conversation as resolved
Review

Specifying the blender:: namespace here isn't necessary

Specifying the `blender::` namespace here isn't necessary
set_world_axes_transform(
&export_object_eval_, export_params.forward_axis, export_params.up_axis);
/* Load faces into plyData. */
int loop_offset = 0;
Span<MLoop> loops = mesh->loops();
for (const MPoly poly : mesh->polys()) {
Span<MLoop> loopSpan = loops.slice(poly.loopstart, poly.totloop);
super_jo_nathan marked this conversation as resolved
Review

for (auto &&poly : mesh->polys()) {
->
for (const MPoly &poly : mesh->polys()) {

Don't use auto for simple cases like this

`for (auto &&poly : mesh->polys()) {` -> `for (const MPoly &poly : mesh->polys()) {` Don't use auto for simple cases like this
Array<uint32_t> polyVector(loopSpan.size());
for (int i = 0; i < loopSpan.size(); ++i) {
float2 uv;
if (export_params.export_uv && uv_map != nullptr) {
uv = uv_map[i + loop_offset];
}
else {
uv = {0, 0};
}
UV_vertex_key key = UV_vertex_key(uv, loopSpan[i].v);
int ply_vertex_index = vertex_map.lookup(key);
polyVector[i] = (uint32_t(ply_vertex_index + vertex_offset));
}
loop_offset += loopSpan.size();
plyData.faces.append(polyVector);
}
Array<int> mesh_vertex_index_LUT(vertex_map.size());
Array<int> ply_vertex_index_LUT(mesh->totvert);
Array<float2> uv_coordinates(vertex_map.size());
for (auto const &[key, ply_vertex_index] : vertex_map.items()) {
mesh_vertex_index_LUT[ply_vertex_index] = key.mesh_vertex_index;
ply_vertex_index_LUT[key.mesh_vertex_index] = ply_vertex_index;
uv_coordinates[ply_vertex_index] = key.UV;
}
/* Vertices */
for (int i = 0; i < vertex_map.size(); ++i) {
float3 r_coords;
copy_v3_v3(r_coords, mesh->vert_positions()[mesh_vertex_index_LUT[i]]);
mul_m4_v3(world_and_axes_transform_, r_coords);
mul_v3_fl(r_coords, export_params.global_scale);
plyData.vertices.append(r_coords);
}
/* UV's */
if (export_params.export_uv) {
for (int i = 0; i < vertex_map.size(); ++i) {
plyData.UV_coordinates.append(uv_coordinates[i]);
}
}
/* Normals */
if (export_params.export_normals) {
const Span<float3> vert_normals = mesh->vert_normals();
for (int i = 0; i < vertex_map.size(); i++) {
mul_m3_v3(world_and_axes_normal_transform_,
float3(vert_normals[mesh_vertex_index_LUT[i]]));
super_jo_nathan marked this conversation as resolved
Review

This (float3) cast doesn't seem necessary

This `(float3)` cast doesn't seem necessary
plyData.vertex_normals.append(vert_normals[mesh_vertex_index_LUT[i]]);
}
}
/* Colors */
if (export_params.export_colors) {
const StringRef name = mesh->active_color_attribute;
if (!name.is_empty()) {
const bke::AttributeAccessor attributes = mesh->attributes();
const VArray<ColorGeometry4f> color_attribute =
attributes.lookup_or_default<ColorGeometry4f>(
super_jo_nathan marked this conversation as resolved
Review

colorAttribute -> color_attribute

`colorAttribute` -> `color_attribute`
name, ATTR_DOMAIN_POINT, {0.0f, 0.0f, 0.0f, 0.0f});
for (int i = 0; i < vertex_map.size(); i++) {
ColorGeometry4f colorGeometry = color_attribute[mesh_vertex_index_LUT[i]];
float4 vertColor(colorGeometry.r, colorGeometry.g, colorGeometry.b, colorGeometry.a);
plyData.vertex_colors.append(vertColor);
}
}
}
/* Edges */
const bke::LooseEdgeCache &loose_edges = mesh->loose_edges();
if (loose_edges.count > 0) {
Span<MEdge> edges = mesh->edges();
for (int i = 0; i < edges.size(); ++i) {
if (loose_edges.is_loose_bits[i]) {
int index_one = ply_vertex_index_LUT[edges[i].v1];
int index_two = ply_vertex_index_LUT[edges[i].v2];
plyData.edges.append({index_one, index_two});
}
}
}
vertex_offset = int(plyData.vertices.size());
if (manually_free_mesh) {
BKE_id_free(nullptr, mesh);
}
}
DEG_OBJECT_ITER_END;
}
Map<UV_vertex_key, int> generate_vertex_map(const Mesh *mesh,
super_jo_nathan marked this conversation as resolved
Review

blender::Map -> Map

`blender::Map` -> `Map`
const float2 *uv_map,
const PLYExportParams &export_params)
{
Map<UV_vertex_key, int> vertex_map;
super_jo_nathan marked this conversation as resolved
Review

blender::Map -> Map

`blender::Map` -> `Map`
const Span<MPoly> polys = mesh->polys();
const Span<MLoop> loops = mesh->loops();
const int totvert = mesh->totvert;
vertex_map.reserve(totvert);
if (uv_map == nullptr || !export_params.export_uv) {
for (int vertex_index = 0; vertex_index < totvert; ++vertex_index) {
UV_vertex_key key = UV_vertex_key({0, 0}, vertex_index);
vertex_map.add_new(key, vertex_map.size());
}
return vertex_map;
}
const float limit[2] = {STD_UV_CONNECT_LIMIT, STD_UV_CONNECT_LIMIT};
UvVertMap *uv_vert_map = BKE_mesh_uv_vert_map_create(polys.data(),
nullptr,
nullptr,
loops.data(),
reinterpret_cast<const float(*)[2]>(uv_map),
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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,30 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "ply_file_buffer.hh"
namespace blender::io::ply {
class FileBufferAscii : public FileBuffer {
using FileBuffer::FileBuffer;
public:
void write_vertex(float x, float y, float z) override;
void write_UV(float u, float v) override;
void write_vertex_normal(float nx, float ny, float nz) override;
void write_vertex_color(uchar r, uchar g, uchar b, uchar a) override;
super_jo_nathan marked this conversation as resolved

Minor: feels like fmt/format.h things are already included via ply_file_buffer.hh above; should be no need to repeat them here. And fmt functionality is not directly used in FileBufferAscii class anyway.

Minor: feels like `fmt/format.h` things are already included via `ply_file_buffer.hh` above; should be no need to repeat them here. And fmt functionality is not directly used in `FileBufferAscii` class anyway.
void write_vertex_end() override;
void write_face(char count, Span<uint32_t> const &vertex_indices) override;
void write_edge(int first, int second) override;
};
} // namespace blender::io::ply

View File

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

Once the argument is a Span, use vertex_indices.cast<char>()

Once the argument is a `Span`, use `vertex_indices.cast<char>()`
}
void FileBufferBinary::write_edge(int first, int second)
{
int2 vector(first, second);
char *bits = reinterpret_cast<char *>(&vector);
Span<char> span(bits, sizeof(int2));
write_bytes(span);
}
} // namespace blender::io::ply

View File

@ -0,0 +1,42 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include <string>
#include <type_traits>
#include "BLI_array.hh"
#include "BLI_compiler_attrs.h"
#include "BLI_fileops.h"
#include "BLI_math_vector_types.hh"
#include "BLI_string_ref.hh"
#include "BLI_utility_mixins.hh"
#include "ply_file_buffer.hh"
#include <bitset>
namespace blender::io::ply {
class FileBufferBinary : public FileBuffer {
using FileBuffer::FileBuffer;
super_jo_nathan marked this conversation as resolved

Similar here: fmt/format.h includes feel like they are not much needed.

Similar here: `fmt/format.h` includes feel like they are not much needed.
public:
void write_vertex(float x, float y, float z) override;
void write_UV(float u, float v) override;
void write_vertex_normal(float nx, float ny, float nz) override;
void write_vertex_color(uchar r, uchar g, uchar b, uchar a) override;
void write_vertex_end() override;
void write_face(char size, Span<uint32_t> const &vertex_indices) override;
void write_edge(int first, int second) override;
};
} // namespace blender::io::ply

View File

@ -0,0 +1,211 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BKE_layer.h"
#include "BKE_lib_id.h"
#include "BKE_mesh.h"
#include "BKE_object.h"
#include "BKE_report.h"
#include "DNA_collection_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "BLI_fileops.hh"
#include "BLI_math_vector.h"
#include "BLI_memory_utils.hh"
#include "DEG_depsgraph.h"
#include "DEG_depsgraph_build.h"
#include "ply_data.hh"
#include "ply_functions.hh"
#include "ply_import.hh"
#include "ply_import_ascii.hh"
#include "ply_import_binary.hh"
#include "ply_import_mesh.hh"
namespace blender::io::ply {
void splitstr(std::string str, Vector<std::string> &words, const StringRef &deli)
{
int pos;
while ((pos = int(str.find(deli))) != std::string::npos) {
words.append(str.substr(0, pos));
str.erase(0, pos + deli.size());
}
/* We add the final word to the vector. */
words.append(str.substr());
}
enum PlyDataTypes from_string(const StringRef &input)
{
if (input == "uchar") {
return PlyDataTypes::UCHAR;
}
if (input == "char") {
return PlyDataTypes::CHAR;
}
if (input == "ushort") {
return PlyDataTypes::USHORT;
}
if (input == "short") {
return PlyDataTypes::SHORT;
}
if (input == "uint") {
return PlyDataTypes::UINT;
}
if (input == "int") {
return PlyDataTypes::INT;
}
if (input == "float") {
return PlyDataTypes::FLOAT;
}
if (input == "double") {
return PlyDataTypes::DOUBLE;
}
return PlyDataTypes::FLOAT;
}
void importer_main(bContext *C, const PLYImportParams &import_params, wmOperator *op)
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
importer_main(bmain, scene, view_layer, import_params, op);
}
void importer_main(Main *bmain,
Scene *scene,
ViewLayer *view_layer,
const PLYImportParams &import_params,
wmOperator *op)
{
std::string line;
super_jo_nathan marked this conversation as resolved

std::ifstream will fail on Windows when file path contains non-English characters. Use blender::fstream wrapper from BLI_fileops.hh instead.

`std::ifstream` will fail on Windows when file path contains non-English characters. Use `blender::fstream` wrapper from `BLI_fileops.hh` instead.
fstream infile(import_params.filepath, std::ios::in | std::ios::binary);
PlyHeader header;
super_jo_nathan marked this conversation as resolved
Review

Comment style

Comment style
while (true) { /* We break when end_header is encountered. */
safe_getline(infile, line);
if (header.header_size == 0 && line != "ply") {
fprintf(stderr, "PLY Importer: failed to read file. Invalid PLY header.\n");
BKE_report(op->reports, RPT_ERROR, "PLY Importer: Invalid PLY header.");
return;
}
header.header_size++;
Vector<std::string> words{};
splitstr(line, words, " ");
if (strcmp(words[0].c_str(), "format") == 0) {
if (strcmp(words[1].c_str(), "ascii") == 0) {
header.type = PlyFormatType::ASCII;
}
else if (strcmp(words[1].c_str(), "binary_big_endian") == 0) {
header.type = PlyFormatType::BINARY_BE;
}
else if (strcmp(words[1].c_str(), "binary_little_endian") == 0) {
header.type = PlyFormatType::BINARY_LE;
}
}
else if (strcmp(words[0].c_str(), "element") == 0) {
header.elements.append(std::make_pair(words[1], std::stoi(words[2])));
if (strcmp(words[1].c_str(), "vertex") == 0) {
header.vertex_count = std::stoi(words[2]);
}
else if (strcmp(words[1].c_str(), "face") == 0) {
header.face_count = std::stoi(words[2]);
}
else if (strcmp(words[1].c_str(), "edge") == 0) {
header.edge_count = std::stoi(words[2]);
}
}
else if (strcmp(words[0].c_str(), "property") == 0) {
std::pair<std::string, PlyDataTypes> property;
property.first = words[2];
property.second = from_string(words[1]);
while (header.properties.size() < header.elements.size()) {
Vector<std::pair<std::string, PlyDataTypes>> temp;
header.properties.append(temp);
}
header.properties[header.elements.size() - 1].append(property);
}
else if (words[0] == "end_header") {
break;
}
else if ((words[0][0] >= '0' && words[0][0] <= '9') || words[0][0] == '-' || line.empty() ||
infile.eof()) {
/* A value was found before we broke out of the loop. No end_header. */
super_jo_nathan marked this conversation as resolved
Review

Not sure it's necessary to print the error when a report is already being used, especially if the information is exactly the same.

Not sure it's necessary to print the error when a report is already being used, especially if the information is exactly the same.
BKE_report(op->reports, RPT_ERROR, "PLY Importer: No end_header");
return;
}
}
/* Name used for both mesh and object. */
char ob_name[FILE_MAX];
BLI_strncpy(ob_name, BLI_path_basename(import_params.filepath), FILE_MAX);
BLI_path_extension_replace(ob_name, FILE_MAX, "");
Mesh *mesh = BKE_mesh_add(bmain, ob_name);
BKE_view_layer_base_deselect_all(scene, view_layer);
LayerCollection *lc = BKE_layer_collection_get_active(view_layer);
Object *obj = BKE_object_add_only_object(bmain, OB_MESH, ob_name);
BKE_mesh_assign_object(bmain, obj, mesh);
BKE_collection_object_add(bmain, lc->collection, obj);
BKE_view_layer_synced_ensure(scene, view_layer);
Base *base = BKE_view_layer_base_find(view_layer, obj);
BKE_view_layer_base_select_and_set_active(view_layer, base);
try {
std::unique_ptr<PlyData> data;
if (header.type == PlyFormatType::ASCII) {
data = import_ply_ascii(infile, &header);
}
else {
data = import_ply_binary(infile, &header);
}
Mesh *temp_val = convert_ply_to_mesh(*data, mesh, import_params);
if (import_params.merge_verts && temp_val != mesh) {
BKE_mesh_nomain_to_mesh(temp_val, mesh, obj);
}
}
catch (std::exception &e) {
fprintf(stderr, "PLY Importer: failed to read file. %s.\n", e.what());
BKE_report(op->reports, RPT_ERROR, "PLY Importer: failed to parse file.");
return;
}
float global_scale = import_params.global_scale;
if ((scene->unit.system != USER_UNIT_NONE) && import_params.use_scene_unit) {
global_scale *= scene->unit.scale_length;
}
float scale_vec[3] = {global_scale, global_scale, global_scale};
float obmat3x3[3][3];
unit_m3(obmat3x3);
float obmat4x4[4][4];
unit_m4(obmat4x4);
/* +Y-forward and +Z-up are the Blender's default axis settings. */
mat3_from_axis_conversion(
IO_AXIS_Y, IO_AXIS_Z, import_params.forward_axis, import_params.up_axis, obmat3x3);
copy_m4_m3(obmat4x4, obmat3x3);
rescale_m4(obmat4x4, scale_vec);
BKE_object_apply_mat4(obj, obmat4x4, true, false);
DEG_id_tag_update(&lc->collection->id, ID_RECALC_COPY_ON_WRITE);
int flags = ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION |
ID_RECALC_BASE_FLAGS;
DEG_id_tag_update_ex(bmain, &obj->id, flags);
DEG_id_tag_update(&scene->id, ID_RECALC_BASE_FLAGS);
DEG_relations_tag_update(bmain);
infile.close();
}
} // namespace blender::io::ply

View File

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

View File

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

StringRef is small and should usually be passed by value, otherwise it's a pointer to a pointer in the end.

`StringRef` is small and should usually be passed by value, otherwise it's a pointer to a pointer in the end.
Vector<std::string> explode(const StringRef str, const char &ch)
{
std::string next;
Vector<std::string> result;
/* For each character in the string. */
for (char c : str) {
/* If we've hit the terminal character. */
if (c == ch) {
/* If we have some characters accumulated. */
if (!next.empty()) {
/* Add them to the result vector. */
result.append(next);
next.clear();
}
}
else {
/* Accumulate the next character into the sequence. */
next += c;
}
}
if (!next.empty()) {
result.append(next);
}
return result;
}
} // namespace blender::io::ply

View File

@ -0,0 +1,39 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "BLI_fileops.hh"
super_jo_nathan marked this conversation as resolved
Review

Unused includes?

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

View File

@ -0,0 +1,215 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BLI_array.hh"
#include "ply_import_binary.hh"
#include <fstream>
namespace blender::io::ply {
std::unique_ptr<PlyData> import_ply_binary(fstream &file, const PlyHeader *header)
{
std::unique_ptr<PlyData> data = std::make_unique<PlyData>(load_ply_binary(file, header));
super_jo_nathan marked this conversation as resolved
Review

The data inside unique pointers can be created directly in the constructor:

return std::make_unique<PlyData>(load_ply_binary(file, header));

The data inside unique pointers can be created directly in the constructor: `return std::make_unique<PlyData>(load_ply_binary(file, header));`
return data;
}
template<typename T> T read(fstream &file, bool isBigEndian)
{
T returnVal;
file.read((char *)&returnVal, sizeof(returnVal));
check_file_errors(file);
if (isBigEndian) {
returnVal = swap_bytes<T>(returnVal);
}
return returnVal;
}
template uint8_t read<uint8_t>(fstream &file, bool isBigEndian);
template int8_t read<int8_t>(fstream &file, bool isBigEndian);
template uint16_t read<uint16_t>(fstream &file, bool isBigEndian);
template int16_t read<int16_t>(fstream &file, bool isBigEndian);
template uint32_t read<uint32_t>(fstream &file, bool isBigEndian);
template int32_t read<int32_t>(fstream &file, bool isBigEndian);
template float read<float>(fstream &file, bool isBigEndian);
template double read<double>(fstream &file, bool isBigEndian);
void check_file_errors(const fstream &file)
{
if (file.bad()) {
throw std::ios_base::failure("Read/Write error on io operation");
}
if (file.fail()) {
throw std::ios_base::failure("Logical error on io operation");
}
if (file.eof()) {
throw std::ios_base::failure("Reached end of the file");
}
}
void discard_value(fstream &file, const PlyDataTypes type)
{
switch (type) {
case CHAR:
read<int8_t>(file, false);
break;
case UCHAR:
read<uint8_t>(file, false);
break;
case SHORT:
read<int16_t>(file, false);
break;
case USHORT:
read<uint16_t>(file, false);
break;
case INT:
read<int32_t>(file, false);
break;
case UINT:
read<uint32_t>(file, false);
break;
case FLOAT:
read<float>(file, false);
break;
case DOUBLE:
read<double>(file, false);
break;
}
}
PlyData load_ply_binary(fstream &file, const PlyHeader *header)
{
PlyData data;
bool isBigEndian = header->type == PlyFormatType::BINARY_BE;
for (int i = 0; i < header->elements.size(); i++) {
if (header->elements[i].first == "vertex") {
/* Import vertices. */
load_vertex_data(file, header, &data, i);
}
else if (header->elements[i].first == "edge") {
/* Import edges. */
for (int j = 0; j < header->elements[i].second; j++) {
std::pair<int, int> vertex_indices;
for (auto [name, type] : header->properties[i]) {
if (name == "vertex1") {
vertex_indices.first = int(read<int32_t>(file, isBigEndian));
}
else if (name == "vertex2") {
vertex_indices.second = int(read<int32_t>(file, isBigEndian));
}
else {
discard_value(file, type);
}
}
data.edges.append(vertex_indices);
}
}
else if (header->elements[i].first == "face") {
/* Import faces. */
for (int j = 0; j < header->elements[i].second; j++) {
/* Assume vertex_index_count_type is uchar. */
uint8_t count = read<uint8_t>(file, isBigEndian);
Array<uint> vertex_indices(count);
/* Loop over the amount of vertex indices in this face. */
for (uint8_t k = 0; k < count; k++) {
uint32_t index = read<uint32_t>(file, isBigEndian);
/* If the face has a vertex index that is outside the range. */
if (index >= data.vertices.size()) {
throw std::runtime_error("Vertex index out of bounds");
}
vertex_indices[k] = index;
}
data.faces.append(vertex_indices);
}
}
else {
/* Nothing else is supported. */
for (int j = 0; j < header->elements[i].second; j++) {
for (auto [name, type] : header->properties[i]) {
discard_value(file, type);
}
}
}
}
return data;
}
void load_vertex_data(fstream &file, const PlyHeader *header, PlyData *r_data, int index)
{
bool hasNormal = false;
bool hasColor = false;
bool hasUv = false;
bool isBigEndian = header->type == PlyFormatType::BINARY_BE;
for (int i = 0; i < header->vertex_count; i++) {
float3 coord{0};
float3 normal{0};
float4 color{1};
float2 uv{0};
for (auto [name, type] : header->properties[index]) {
if (name == "x") {
coord.x = read<float>(file, isBigEndian);
}
else if (name == "y") {
coord.y = read<float>(file, isBigEndian);
}
else if (name == "z") {
coord.z = read<float>(file, isBigEndian);
}
else if (name == "nx") {
normal.x = read<float>(file, isBigEndian);
hasNormal = true;
}
else if (name == "ny") {
normal.y = read<float>(file, isBigEndian);
}
else if (name == "nz") {
normal.z = read<float>(file, isBigEndian);
}
else if (name == "red") {
color.x = read<uint8_t>(file, isBigEndian) / 255.0f;
hasColor = true;
aras_p marked this conversation as resolved

What colorspace are PLY vertex colors defined in? Since they seem to be 8 bits per component, I would assume they are sRGB (linear colors would not have enough precision at just 8 bits). My impression is that Blender colors are in linear color space, when they are stored as floats. So the reading code here (and likewise, exporting code elsewhere) might need a sRGB<->Linear conversion.

What colorspace are PLY vertex colors defined in? Since they seem to be 8 bits per component, I would assume they are sRGB (linear colors would not have enough precision at just 8 bits). My impression is that Blender colors are in linear color space, when they are stored as floats. So the reading code here (and likewise, exporting code elsewhere) might need a sRGB<->Linear conversion.
Review

The PLY file format was made before the release of sRGB (1994 vs 1999). So I doubt it was made with that in mind. But the colours in the specification are defined as an integer range from 0-255 (so 8-bit colour per channel) which is why we read it as a uint8_t.

The PLY spec says nothing about colour space, and we have no way of knowing how other programmes interact with PLY files and if they use sRGB or not. But we've decided on assuming sRGB now and will make the conversion between the two.

I spotted the function srgb_to_linearrgb_v3_v3(linear, srgb); in the OBJ importer, and from the source also srgb_to_linearrgb_v4. Since we assume an alpha channel we will use that function to convert in the importer, and its opposite for the exporter.

The PLY file format was made before the release of sRGB (1994 vs 1999). So I doubt it was made with that in mind. But the colours in the specification are defined as an integer range from 0-255 (so 8-bit colour per channel) which is why we read it as a `uint8_t`. The PLY spec says nothing about colour space, and we have no way of knowing how other programmes interact with PLY files and if they use sRGB or not. But we've decided on assuming sRGB now and will make the conversion between the two. I spotted the function `srgb_to_linearrgb_v3_v3(linear, srgb);` in the OBJ importer, and from the source also `srgb_to_linearrgb_v4`. Since we assume an alpha channel we will use that function to convert in the importer, and its opposite for the exporter.
Review

On further inspection and importing a test file, we have found that using the sRGB -> linear conversion gives too dark colours compared to the original (see pictures)

Original file/website

With sRGB->Linear conversion:
With sRGB->Linear

Original implementation:
Without sRGB->Linear

On further inspection and importing a test file, we have found that using the sRGB -> linear conversion gives too dark colours compared to the original (see pictures) [Original file/website](https://www.artec3d.com/3d-models/doom-combat-scene) With sRGB->Linear conversion: ![With sRGB->Linear](https://cdn.discordapp.com/attachments/1015698649486999577/1076464344910544986/image.png) Original implementation: ![Without sRGB->Linear](https://cdn.discordapp.com/attachments/1015698649486999577/1076464979252879370/image.png)

Interesting. So it might be that various people just write linear colors in there, without realizing that at 8 bits per channel there's severe banding in the darks :) Or alternatively, the importer/exporter might need a choice between sRGB or Linear vertex colors, very similar to how FBX importer/exporter in Blender has (FBX is also in the "vertex colors are in unspecified color space, and various apps treat it differently").

Interesting. So it might be that various people just write linear colors in there, without realizing that at 8 bits per channel there's severe banding in the darks :) Or alternatively, the importer/exporter might need a choice between sRGB or Linear vertex colors, very similar to how FBX importer/exporter in Blender has (FBX is also in the "vertex colors are in unspecified color space, and various apps treat it differently").

I've implemented option for vertex colors Linear vs sRGB color space in a30abe9c2e. Defaulted to sRGB to match the current Python addon behavior.

I've implemented option for vertex colors Linear vs sRGB color space in a30abe9c2e79a01. Defaulted to sRGB to match the current Python addon behavior.
}
else if (name == "green") {
color.y = read<uint8_t>(file, isBigEndian) / 255.0f;
}
else if (name == "blue") {
color.z = read<uint8_t>(file, isBigEndian) / 255.0f;
}
else if (name == "alpha") {
color.w = read<uint8_t>(file, isBigEndian) / 255.0f;
}
else if (name == "s") {
uv.x = read<float>(file, isBigEndian);
hasUv = true;
}
else if (name == "t") {
uv.y = read<float>(file, isBigEndian);
}
else {
/* No other properties are supported yet. */
discard_value(file, type);
}
}
r_data->vertices.append(coord);
if (hasNormal) {
r_data->vertex_normals.append(normal);
}
if (hasColor) {
r_data->vertex_colors.append(color);
}
if (hasUv) {
r_data->UV_coordinates.append(uv);
}
}
}
} // namespace blender::io::ply

View File

@ -0,0 +1,71 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "BLI_endian_switch.h"
super_jo_nathan marked this conversation as resolved
Review

Unused includes?

Unused includes?
#include "BLI_fileops.hh"
#include "ply_data.hh"
namespace blender::io::ply {
/**
* The function that gets called from the importer.
* \param file: The PLY file that was opened.
* \param header: The information in the PLY header.
* \return The PlyData datastructure that can be used for conversion to a Mesh.
*/
std::unique_ptr<PlyData> import_ply_binary(fstream &file, const PlyHeader *header);
/**
* Loads the information from the PLY file in binary format to the PlyData datastructure.
* \param file: The PLY file that was opened.
* \param header: The information in the PLY header.
* \return The PlyData datastructure that can be used for conversion to a Mesh.
*/
PlyData load_ply_binary(fstream &file, const PlyHeader *header);
void load_vertex_data(fstream &file, const PlyHeader *header, PlyData *r_data, int index);
void check_file_errors(const fstream &file);
void discard_value(fstream &file, const PlyDataTypes type);
super_jo_nathan marked this conversation as resolved
Review

I'm a little skeptical about seeing this endian swapping implemented just for the ply importer, since it's a fairly generic thing that Blender already does.

This function itself seems fine, since it's templated which we don't do elsewhere. However, I'd suggest implementing each size case with the functions from BLI_endian_switch.h, so the logic isn't duplicated.

Two other suggestions:

  • Replace if with if constexpr
  • Use reinterpret_cast instead of C-style casts.
I'm a little skeptical about seeing this endian swapping implemented just for the ply importer, since it's a fairly generic thing that Blender already does. This function itself seems fine, since it's templated which we don't do elsewhere. However, I'd suggest implementing each size case with the functions from `BLI_endian_switch.h`, so the logic isn't duplicated. Two other suggestions: - Replace `if` with `if constexpr` - Use `reinterpret_cast` instead of C-style casts.
template<typename T> T swap_bytes(T input)
{
/* In big endian, the most-significant byte is first.
* So, we need to swap the byte order. */
/* 0xAC in LE should become 0xCA in BE. */
if (sizeof(T) == 1) {
return input;
}
if constexpr (sizeof(T) == 2) {
uint16_t value = reinterpret_cast<uint16_t &>(input);
BLI_endian_switch_uint16(&value);
return reinterpret_cast<T &>(value);
}
if constexpr (sizeof(T) == 4) {
/* Reinterpret this data as uint32 for easy rearranging of bytes. */
uint32_t value = reinterpret_cast<uint32_t &>(input);
BLI_endian_switch_uint32(&value);
return reinterpret_cast<T &>(value);
}
if constexpr (sizeof(T) == 8) {
/* Reinterpret this data as uint64 for easy rearranging of bytes. */
uint64_t value = reinterpret_cast<uint64_t &>(input);
BLI_endian_switch_uint64(&value);
return reinterpret_cast<T &>(value);
}
}
template<typename T> T read(fstream &file, bool isBigEndian);
} // namespace blender::io::ply

View File

@ -0,0 +1,117 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#include "BKE_attribute.h"
#include "BKE_attribute.hh"
#include "BKE_customdata.h"
#include "BKE_mesh.h"
#include "BKE_mesh_runtime.h"
#include "GEO_mesh_merge_by_distance.hh"
#include "BLI_math_vector.h"
#include "ply_import_mesh.hh"
namespace blender::io::ply {
Mesh *convert_ply_to_mesh(PlyData &data, Mesh *mesh, const PLYImportParams &params)
{
/* Add vertices to the mesh. */
mesh->totvert = int(data.vertices.size());
CustomData_add_layer_named(
&mesh->vdata, CD_PROP_FLOAT3, CD_CONSTRUCT, nullptr, mesh->totvert, "position");
mesh->vert_positions_for_write().copy_from(data.vertices);
super_jo_nathan marked this conversation as resolved
Review

How about simply mesh->vert_positions_for_write().copy_from(data.vertices);?

How about simply `mesh->vert_positions_for_write().copy_from(data.vertices);`?
bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
if (!data.edges.is_empty()) {
mesh->totedge = int(data.edges.size());
CustomData_add_layer(&mesh->edata, CD_MEDGE, CD_SET_DEFAULT, nullptr, mesh->totedge);
MutableSpan<MEdge> edges = mesh->edges_for_write();
for (int i = 0; i < mesh->totedge; i++) {
edges[i].v1 = data.edges[i].first;
edges[i].v2 = data.edges[i].second;
}
}
/* Add faces to the mesh. */
if (!data.faces.is_empty()) {
/* Specify amount of total faces. */
mesh->totpoly = int(data.faces.size());
mesh->totloop = 0;
for (int i = 0; i < data.faces.size(); i++) {
/* Add number of loops from the vertex indices in the face. */
super_jo_nathan marked this conversation as resolved
Review

This comment probably means "loops" or "corners" rather than edges.

This comment probably means "loops" or "corners" rather than edges.
mesh->totloop += data.faces[i].size();
}
CustomData_add_layer(&mesh->pdata, CD_MPOLY, CD_SET_DEFAULT, nullptr, mesh->totpoly);
CustomData_add_layer(&mesh->ldata, CD_MLOOP, CD_SET_DEFAULT, nullptr, mesh->totloop);
MutableSpan<MPoly> polys = mesh->polys_for_write();
MutableSpan<MLoop> loops = mesh->loops_for_write();
int offset = 0;
/* Iterate over amount of faces. */
for (int i = 0; i < mesh->totpoly; i++) {
int size = int(data.faces[i].size());
/* Set the index from where this face starts and specify the amount of edges it has. */
polys[i].loopstart = offset;
polys[i].totloop = size;
for (int j = 0; j < size; j++) {
/* Set the vertex index of the loop to the one in PlyData. */
loops[offset + j].v = data.faces[i][j];
}
offset += size;
}
}
/* Vertex colors */
if (!data.vertex_colors.is_empty()) {
/* Create a data layer for vertex colors and set them. */
bke::SpanAttributeWriter<ColorGeometry4f> colors =
attributes.lookup_or_add_for_write_span<ColorGeometry4f>("Col", ATTR_DOMAIN_POINT);
for (int i = 0; i < data.vertex_colors.size(); i++) {
copy_v4_v4(colors.span[i], data.vertex_colors[i]);
}
colors.finish();
BKE_id_attributes_active_color_set(&mesh->id, "Col");
}
/* Uvmap */
if (!data.UV_coordinates.is_empty()) {
bke::SpanAttributeWriter<float2> uv_map = attributes.lookup_or_add_for_write_only_span<float2>(
"UVMap", ATTR_DOMAIN_CORNER);
super_jo_nathan marked this conversation as resolved
Review

Uv looks like a type, how about uv_map instead?

`Uv` looks like a type, how about `uv_map` instead?
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]];
super_jo_nathan marked this conversation as resolved
Review

Should be possible to just use assignment rather than copy_v2_v2:

uv_map.span[counter] = ...

Should be possible to just use assignment rather than `copy_v2_v2`: `uv_map.span[counter] = ...`
counter++;
}
}
uv_map.finish();
}
/* Calculate edges from the rest of the mesh. */
BKE_mesh_calc_edges(mesh, true, false);
/* Note: This is important to do after initializing the loops. */
if (!data.vertex_normals.is_empty()) {
BKE_mesh_set_custom_normals_from_verts(
mesh, reinterpret_cast<float(*)[3]>(data.vertex_normals.data()));
}
/* Merge all vertices on the same location. */
if (params.merge_verts) {
std::optional<Mesh *> return_value = blender::geometry::mesh_merge_by_distance_all(
super_jo_nathan marked this conversation as resolved
Review

Looking at this again, since the custom normals are set already, I don't think creating an attribute with normals should be included in this patch. We're planning on adding nodes to access custom normals to geometry nodes in 3.6, so this workaround wouldn't even really be necessary anyway.

Looking at this again, since the custom normals are set already, I don't think creating an attribute with normals should be included in this patch. We're planning on adding nodes to access custom normals to geometry nodes in 3.6, so this workaround wouldn't even really be necessary anyway.
*mesh, IndexMask(mesh->totvert), 0.0001f);
if (return_value.has_value()) {
mesh = return_value.value();
}
}
return mesh;
}
} // namespace blender::io::ply

View File

@ -0,0 +1,21 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "IO_ply.h"
#include "ply_data.hh"
namespace blender::io::ply {
/**
* Converts the PlyData datastructure to a mesh.
* \param data: The PLY data.
super_jo_nathan marked this conversation as resolved
Review

\param data -> \param data:

`\param data` -> `\param data:`
* \return The mesh that can be used inside blender.
*/
Mesh *convert_ply_to_mesh(PlyData &data, Mesh *mesh, const PLYImportParams &params);
} // namespace blender::io::ply

View File

@ -0,0 +1,42 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "BLI_array.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_vector.hh"
namespace blender::io::ply {
enum PlyDataTypes { CHAR, UCHAR, SHORT, USHORT, INT, UINT, FLOAT, DOUBLE };
struct PlyData {
Vector<float3> vertices;
Vector<float3> vertex_normals;
/* Value between 0 and 1. */
Vector<float4> vertex_colors;
Vector<std::pair<int, int>> edges;
Vector<float3> edge_colors;
super_jo_nathan marked this conversation as resolved
Review

Unless these faces are resized after they're added, it would be more efficient and clear to use Vector<Array<uint32_t>> faces

Unless these faces are resized after they're added, it would be more efficient and clear to use `Vector<Array<uint32_t>>` faces
Vector<Array<uint32_t>> faces;
Vector<float2> UV_coordinates;
};
enum PlyFormatType { ASCII, BINARY_LE, BINARY_BE };
struct PlyHeader {
int vertex_count = 0;
int edge_count = 0;
int face_count = 0;
int header_size = 0;
/* List of elements in ply file with their count. */
Vector<std::pair<std::string, int>> elements;
/* List of properties (Name, type) per element. */
Vector<Vector<std::pair<std::string, PlyDataTypes>>> properties;
PlyFormatType type;
};
} // namespace blender::io::ply

View File

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

View File

@ -0,0 +1,26 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup ply
*/
#pragma once
#include "BLI_fileops.hh"
#include <string>
namespace blender::io::ply {
enum line_ending { CR_LF, LF, CR, LF_CR, UNSET };
/**
* Reads a line in the ply file in a line-ending safe manner. All different line endings are
* supported. This also supports a mix of different line endings in the same file. CR (\\r), LF
* (\\n), CR/LF (\\r\\n), LF/CR (\\n\\r).
* \param file: The file stream.
super_jo_nathan marked this conversation as resolved
Review

Convention in Blender is to use \param file: instead of @param file

Convention in Blender is to use `\param file:` instead of `@param file`
* \param line: The string you want to read to.
* \return The line ending enum if you're interested.
*/
line_ending safe_getline(fstream &file, std::string &line);
} // namespace blender::io::ply

View File

@ -0,0 +1,467 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
super_jo_nathan marked this conversation as resolved
Review

Missing license header

Missing license header
#include "testing/testing.h"
#include "tests/blendfile_loading_base_test.h"
super_jo_nathan marked this conversation as resolved
Review

More unused includes (curve, material, more?)

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

View File

@ -0,0 +1,248 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
super_jo_nathan marked this conversation as resolved
Review

Missing license header

Missing license header
#include "tests/blendfile_loading_base_test.h"
#include "BKE_attribute.hh"
super_jo_nathan marked this conversation as resolved
Review

At least a few unused includes here (I'm noticing the curve ones right now, but there are probably others)

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

View File

@ -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()

View File

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