IO: New C++ PLY importer/exporter #104404
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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
Aras Pranckevicius
commented
Minor for future cleanup: 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).
Aras Pranckevicius
commented
Done in 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, ¶ms, op);
|
||||
}
|
||||
}
|
||||
else if (RNA_struct_property_is_set_ex(op->ptr, "filepath", false)) {
|
||||
RNA_string_get(op->ptr, "filepath", params.filepath);
|
||||
PLY_import(C, ¶ms, op);
|
||||
}
|
||||
else {
|
||||
BKE_report(op->reports, RPT_ERROR, "No filename given");
|
||||
return OPERATOR_CANCELLED;
|
||||
}
|
||||
|
||||
Scene *scene = CTX_data_scene(C);
|
||||
WM_event_add_notifier(C, NC_SCENE | ND_OB_SELECT, scene);
|
||||
WM_event_add_notifier(C, NC_SCENE | ND_OB_ACTIVE, scene);
|
||||
WM_event_add_notifier(C, NC_SCENE | ND_LAYER_CONTENT, scene);
|
||||
ED_outliner_select_sync_from_object_tag(C);
|
||||
|
||||
return OPERATOR_FINISHED;
|
||||
}
|
||||
|
||||
static bool wm_ply_import_check(bContext *UNUSED(C), wmOperator *op)
|
||||
{
|
||||
const int num_axes = 3;
|
||||
/* Both forward and up axes cannot be the same (or same except opposite sign). */
|
||||
if (RNA_enum_get(op->ptr, "forward_axis") % num_axes ==
|
||||
(RNA_enum_get(op->ptr, "up_axis") % num_axes)) {
|
||||
RNA_enum_set(op->ptr, "up_axis", RNA_enum_get(op->ptr, "up_axis") % num_axes + 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void WM_OT_ply_import(struct wmOperatorType *ot)
|
||||
{
|
||||
PropertyRNA *prop;
|
||||
|
||||
ot->name = "Import PLY";
|
||||
ot->description = "Import an PLY file as an object";
|
||||
ot->idname = "WM_OT_ply_import";
|
||||
|
||||
ot->invoke = wm_ply_import_invoke;
|
||||
ot->exec = wm_ply_import_execute;
|
||||
ot->poll = WM_operator_winactive;
|
||||
ot->check = wm_ply_import_check;
|
||||
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_PRESET;
|
||||
|
||||
WM_operator_properties_filesel(ot,
|
||||
FILE_TYPE_FOLDER,
|
||||
FILE_BLENDER,
|
||||
FILE_OPENFILE,
|
||||
WM_FILESEL_FILEPATH | WM_FILESEL_FILES | WM_FILESEL_DIRECTORY |
|
||||
WM_FILESEL_SHOW_PROPS,
|
||||
FILE_DEFAULTDISPLAY,
|
||||
FILE_SORT_DEFAULT);
|
||||
|
||||
RNA_def_float(ot->srna, "global_scale", 1.0f, 1e-6f, 1e6f, "Scale", "", 0.001f, 1000.0f);
|
||||
RNA_def_boolean(ot->srna,
|
||||
"use_scene_unit",
|
||||
false,
|
||||
"Scene Unit",
|
||||
"Apply current scene's unit (as defined by unit scale) to imported data");
|
||||
RNA_def_enum(ot->srna, "forward_axis", io_transform_axis, IO_AXIS_Y, "Forward Axis", "");
|
||||
RNA_def_enum(ot->srna, "up_axis", io_transform_axis, IO_AXIS_Z, "Up Axis", "");
|
||||
RNA_def_boolean(ot->srna, "merge_verts", false, "Merge Vertices", "Merges vertices by distance");
|
||||
|
||||
/* Only show .ply files by default. */
|
||||
prop = RNA_def_string(ot->srna, "filter_glob", "*.ply", 0, "Extension Filter", "");
|
||||
RNA_def_property_flag(prop, PROP_HIDDEN);
|
||||
}
|
||||
|
||||
#endif /* WITH_IO_PLY */
|
|
@ -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);
|
|
@ -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)) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -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
Hans Goudey
commented
`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);
|
||||
}
|
|
@ -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
|
|
@ -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
Aras Pranckevicius
commented
Minor: these comments don't add much, it's very clear from the code flow what for example 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
|
|
@ -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
|
|
@ -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
Hans Goudey
commented
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 `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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
Hans Goudey
commented
`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)
|
||||
Aras Pranckevicius
commented
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 (°_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
Hans Goudey
commented
`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
Hans Goudey
commented
Specifying the 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
Hans Goudey
commented
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
Hans Goudey
commented
This 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
Hans Goudey
commented
`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
Hans Goudey
commented
`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
Hans Goudey
commented
`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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
Aras Pranckevicius
commented
Minor: feels like 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
|
|
@ -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
Hans Goudey
commented
Once the argument is a 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
|
|
@ -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
Aras Pranckevicius
commented
Similar here: 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
|
|
@ -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
Aras Pranckevicius
commented
`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
Hans Goudey
commented
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
Hans Goudey
commented
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
|
|
@ -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
|
|
@ -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
Hans Goudey
commented
`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
|
|
@ -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
Hans Goudey
commented
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
|
|
@ -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
Hans Goudey
commented
The data inside unique pointers can be created directly in the constructor:
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
Aras Pranckevicius
commented
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.
Lilith Houtjes
commented
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 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 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.
Lilith Houtjes
commented
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) 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)
Aras Pranckevicius
commented
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").
Aras Pranckevicius
commented
I've implemented option for vertex colors Linear vs sRGB color space in 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
|
|
@ -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
Hans Goudey
commented
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
Hans Goudey
commented
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 Two other suggestions:
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
|
|
@ -0,0 +1,117 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup ply
|
||||
*/
|
||||
|
||||
#include "BKE_attribute.h"
|
||||
#include "BKE_attribute.hh"
|
||||
#include "BKE_customdata.h"
|
||||
#include "BKE_mesh.h"
|
||||
#include "BKE_mesh_runtime.h"
|
||||
|
||||
#include "GEO_mesh_merge_by_distance.hh"
|
||||
|
||||
#include "BLI_math_vector.h"
|
||||
|
||||
#include "ply_import_mesh.hh"
|
||||
|
||||
namespace blender::io::ply {
|
||||
Mesh *convert_ply_to_mesh(PlyData &data, Mesh *mesh, const PLYImportParams ¶ms)
|
||||
{
|
||||
|
||||
/* Add vertices to the mesh. */
|
||||
mesh->totvert = int(data.vertices.size());
|
||||
CustomData_add_layer_named(
|
||||
&mesh->vdata, CD_PROP_FLOAT3, CD_CONSTRUCT, nullptr, mesh->totvert, "position");
|
||||
mesh->vert_positions_for_write().copy_from(data.vertices);
|
||||
|
||||
super_jo_nathan marked this conversation as resolved
Hans Goudey
commented
How about simply 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
Hans Goudey
commented
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
Hans Goudey
commented
`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
Hans Goudey
commented
Should be possible to just use assignment rather than
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
Hans Goudey
commented
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
|
|
@ -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
Hans Goudey
commented
`\param data` -> `\param data:`
|
||||
* \return The mesh that can be used inside blender.
|
||||
*/
|
||||
Mesh *convert_ply_to_mesh(PlyData &data, Mesh *mesh, const PLYImportParams ¶ms);
|
||||
|
||||
} // namespace blender::io::ply
|
|
@ -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
Hans Goudey
commented
Unless these faces are resized after they're added, it would be more efficient and clear to use 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
|
|
@ -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
|
|
@ -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
Hans Goudey
commented
Convention in Blender is to use 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
|
|
@ -0,0 +1,467 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
super_jo_nathan marked this conversation as resolved
Hans Goudey
commented
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
Hans Goudey
commented
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 ¶ms)
|
||||
{
|
||||
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 ¶ms)
|
||||
{
|
||||
PlyData data;
|
||||
if (!load_file_and_depsgraph(blendfile)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
load_plydata(data, depsgraph, params);
|
||||
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ply_exporter_ply_data_test, CubeLoadPLYDataVertices)
|
||||
{
|
||||
PLYExportParams params;
|
||||
PlyData plyData = load_ply_data_from_blendfile("io_tests/blend_geometry/cube_all_data.blend",
|
||||
params);
|
||||
EXPECT_EQ(plyData.vertices.size(), 8);
|
||||
}
|
||||
TEST_F(ply_exporter_ply_data_test, CubeLoadPLYDataUV)
|
||||
{
|
||||
PLYExportParams params;
|
||||
params.export_uv = true;
|
||||
PlyData plyData = load_ply_data_from_blendfile("io_tests/blend_geometry/cube_all_data.blend",
|
||||
params);
|
||||
EXPECT_EQ(plyData.UV_coordinates.size(), 8);
|
||||
}
|
||||
TEST_F(ply_exporter_ply_data_test, SuzanneLoadPLYDataUV)
|
||||
{
|
||||
PLYExportParams params;
|
||||
params.export_uv = true;
|
||||
PlyData plyData = load_ply_data_from_blendfile("io_tests/blend_geometry/suzanne_all_data.blend",
|
||||
params);
|
||||
EXPECT_EQ(plyData.UV_coordinates.size(), 542);
|
||||
}
|
||||
|
||||
TEST_F(ply_exporter_ply_data_test, CubeLoadPLYDataUVDisabled)
|
||||
{
|
||||
PLYExportParams params;
|
||||
params.export_uv = false;
|
||||
PlyData plyData = load_ply_data_from_blendfile("io_tests/blend_geometry/cube_all_data.blend",
|
||||
params);
|
||||
EXPECT_EQ(plyData.UV_coordinates.size(), 0);
|
||||
}
|
||||
|
||||
} // namespace blender::io::ply
|
|
@ -0,0 +1,248 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
super_jo_nathan marked this conversation as resolved
Hans Goudey
commented
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
Hans Goudey
commented
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 (°_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
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
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:and then
*row
variable above is not needed either.