diff options
42 files changed, 7256 insertions, 0 deletions
diff --git a/release/scripts/startup/bl_ui/space_topbar.py b/release/scripts/startup/bl_ui/space_topbar.py index 518979a5ef3..a845ca6a4bd 100644 --- a/release/scripts/startup/bl_ui/space_topbar.py +++ b/release/scripts/startup/bl_ui/space_topbar.py @@ -463,6 +463,7 @@ class TOPBAR_MT_file_import(Menu): bl_owner_use_filter = False def draw(self, _context): + self.layout.operator("wm.obj_import", text="Wavefront OBJ (.obj) - New") if bpy.app.build_options.collada: self.layout.operator("wm.collada_import", text="Collada (Default) (.dae)") @@ -481,6 +482,7 @@ class TOPBAR_MT_file_export(Menu): bl_owner_use_filter = False def draw(self, _context): + self.layout.operator("wm.obj_export", text="Wavefront OBJ (.obj) - New") if bpy.app.build_options.collada: self.layout.operator("wm.collada_export", text="Collada (Default) (.dae)") diff --git a/source/blender/blenloader/CMakeLists.txt b/source/blender/blenloader/CMakeLists.txt index 89631588ed0..ca4b7cc2a32 100644 --- a/source/blender/blenloader/CMakeLists.txt +++ b/source/blender/blenloader/CMakeLists.txt @@ -26,6 +26,7 @@ set(INC ../blentranslation ../depsgraph ../draw + ../editors/include ../imbuf ../makesdna ../makesrna diff --git a/source/blender/blenloader/tests/blendfile_loading_base_test.cc b/source/blender/blenloader/tests/blendfile_loading_base_test.cc index 8afa631ffc5..ebafab9cd93 100644 --- a/source/blender/blenloader/tests/blendfile_loading_base_test.cc +++ b/source/blender/blenloader/tests/blendfile_loading_base_test.cc @@ -26,9 +26,11 @@ #include "BKE_idtype.h" #include "BKE_image.h" #include "BKE_main.h" +#include "BKE_mball_tessellate.h" #include "BKE_modifier.h" #include "BKE_node.h" #include "BKE_scene.h" +#include "BKE_vfont.h" #include "BLI_path_util.h" #include "BLI_threads.h" @@ -43,6 +45,8 @@ #include "IMB_imbuf.h" +#include "ED_datafiles.h" + #include "RNA_define.h" #include "WM_api.h" @@ -70,6 +74,7 @@ void BlendfileLoadingBaseTest::SetUpTestCase() DEG_register_node_types(); RNA_init(); BKE_node_system_init(); + BKE_vfont_builtin_register(datatoc_bfont_pfb, datatoc_bfont_pfb_size); G.background = true; G.factory_startup = true; @@ -107,6 +112,7 @@ void BlendfileLoadingBaseTest::TearDownTestCase() void BlendfileLoadingBaseTest::TearDown() { + BKE_mball_cubeTable_free(); depsgraph_free(); blendfile_free(); diff --git a/source/blender/editors/io/CMakeLists.txt b/source/blender/editors/io/CMakeLists.txt index 44b5f85050f..f4da114159f 100644 --- a/source/blender/editors/io/CMakeLists.txt +++ b/source/blender/editors/io/CMakeLists.txt @@ -25,6 +25,20 @@ set(INC ../../io/alembic ../../io/collada ../../io/gpencil + ../../io/wavefront_obj + ../../io/usd + ../../makesdna + ../../makesrna + ../../windowmanager + ../../../../intern/guardedalloc +) + +set(INC_SYS + +) + +set(SRC + io_alembic.c ../../io/usd ../../makesdna ../../makesrna @@ -43,6 +57,7 @@ set(SRC io_gpencil_export.c io_gpencil_import.c io_gpencil_utils.c + io_obj.c io_ops.c io_usd.c @@ -57,6 +72,7 @@ set(SRC set(LIB bf_blenkernel bf_blenlib + bf_wavefront_obj ) if(WITH_OPENCOLLADA) diff --git a/source/blender/editors/io/io_obj.c b/source/blender/editors/io/io_obj.c new file mode 100644 index 00000000000..e97770b93f6 --- /dev/null +++ b/source/blender/editors/io/io_obj.c @@ -0,0 +1,456 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup editor/io + */ + +#include "DNA_space_types.h" + +#include "BKE_context.h" +#include "BKE_main.h" +#include "BKE_report.h" + +#include "BLI_path_util.h" +#include "BLI_string.h" +#include "BLI_utildefines.h" + +#include "BLT_translation.h" + +#include "MEM_guardedalloc.h" + +#include "RNA_access.h" +#include "RNA_define.h" + +#include "UI_interface.h" +#include "UI_resources.h" + +#include "WM_api.h" +#include "WM_types.h" + +#include "DEG_depsgraph.h" + +#include "IO_wavefront_obj.h" +#include "io_obj.h" + +const EnumPropertyItem io_obj_transform_axis_forward[] = { + {OBJ_AXIS_X_FORWARD, "X_FORWARD", 0, "X", "Positive X axis"}, + {OBJ_AXIS_Y_FORWARD, "Y_FORWARD", 0, "Y", "Positive Y axis"}, + {OBJ_AXIS_Z_FORWARD, "Z_FORWARD", 0, "Z", "Positive Z axis"}, + {OBJ_AXIS_NEGATIVE_X_FORWARD, "NEGATIVE_X_FORWARD", 0, "-X", "Negative X axis"}, + {OBJ_AXIS_NEGATIVE_Y_FORWARD, "NEGATIVE_Y_FORWARD", 0, "-Y", "Negative Y axis"}, + {OBJ_AXIS_NEGATIVE_Z_FORWARD, "NEGATIVE_Z_FORWARD", 0, "-Z (Default)", "Negative Z axis"}, + {0, NULL, 0, NULL, NULL}}; + +const EnumPropertyItem io_obj_transform_axis_up[] = { + {OBJ_AXIS_X_UP, "X_UP", 0, "X", "Positive X axis"}, + {OBJ_AXIS_Y_UP, "Y_UP", 0, "Y (Default)", "Positive Y axis"}, + {OBJ_AXIS_Z_UP, "Z_UP", 0, "Z", "Positive Z axis"}, + {OBJ_AXIS_NEGATIVE_X_UP, "NEGATIVE_X_UP", 0, "-X", "Negative X axis"}, + {OBJ_AXIS_NEGATIVE_Y_UP, "NEGATIVE_Y_UP", 0, "-Y", "Negative Y axis"}, + {OBJ_AXIS_NEGATIVE_Z_UP, "NEGATIVE_Z_UP", 0, "-Z", "Negative Z axis"}, + {0, NULL, 0, NULL, NULL}}; + +const EnumPropertyItem io_obj_export_evaluation_mode[] = { + {DAG_EVAL_RENDER, "DAG_EVAL_RENDER", 0, "Render", "Export objects as they appear in render"}, + {DAG_EVAL_VIEWPORT, + "DAG_EVAL_VIEWPORT", + 0, + "Viewport (Default)", + "Export objects as they appear in the viewport"}, + {0, NULL, 0, NULL, NULL}}; + +static int wm_obj_export_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event)) +{ + if (!RNA_struct_property_is_set(op->ptr, "filepath")) { + Main *bmain = CTX_data_main(C); + char filepath[FILE_MAX]; + + if (BKE_main_blendfile_path(bmain)[0] == '\0') { + BLI_strncpy(filepath, "untitled", sizeof(filepath)); + } + else { + BLI_strncpy(filepath, BKE_main_blendfile_path(bmain), sizeof(filepath)); + } + + BLI_path_extension_replace(filepath, sizeof(filepath), ".obj"); + RNA_string_set(op->ptr, "filepath", filepath); + } + + WM_event_add_fileselect(C, op); + return OPERATOR_RUNNING_MODAL; +} + +static int wm_obj_export_exec(bContext *C, wmOperator *op) +{ + if (!RNA_struct_property_is_set(op->ptr, "filepath")) { + BKE_report(op->reports, RPT_ERROR, "No filename given"); + return OPERATOR_CANCELLED; + } + struct OBJExportParams export_params; + RNA_string_get(op->ptr, "filepath", export_params.filepath); + export_params.blen_filepath = CTX_data_main(C)->name; + export_params.export_animation = RNA_boolean_get(op->ptr, "export_animation"); + export_params.start_frame = RNA_int_get(op->ptr, "start_frame"); + export_params.end_frame = RNA_int_get(op->ptr, "end_frame"); + + export_params.forward_axis = RNA_enum_get(op->ptr, "forward_axis"); + export_params.up_axis = RNA_enum_get(op->ptr, "up_axis"); + export_params.scaling_factor = RNA_float_get(op->ptr, "scaling_factor"); + export_params.export_eval_mode = RNA_enum_get(op->ptr, "export_eval_mode"); + + 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_materials = RNA_boolean_get(op->ptr, "export_materials"); + export_params.export_triangulated_mesh = RNA_boolean_get(op->ptr, "export_triangulated_mesh"); + export_params.export_curves_as_nurbs = RNA_boolean_get(op->ptr, "export_curves_as_nurbs"); + + export_params.export_object_groups = RNA_boolean_get(op->ptr, "export_object_groups"); + export_params.export_material_groups = RNA_boolean_get(op->ptr, "export_material_groups"); + export_params.export_vertex_groups = RNA_boolean_get(op->ptr, "export_vertex_groups"); + export_params.export_smooth_groups = RNA_boolean_get(op->ptr, "export_smooth_groups"); + export_params.smooth_groups_bitflags = RNA_boolean_get(op->ptr, "smooth_group_bitflags"); + + OBJ_export(C, &export_params); + + return OPERATOR_FINISHED; +} + +static void ui_obj_export_settings(uiLayout *layout, PointerRNA *imfptr) +{ + + const bool export_animation = RNA_boolean_get(imfptr, "export_animation"); + const bool export_smooth_groups = RNA_boolean_get(imfptr, "export_smooth_groups"); + + uiLayoutSetPropSep(layout, true); + uiLayoutSetPropDecorate(layout, false); + + /* Animation options. */ + uiLayout *box = uiLayoutBox(layout); + uiItemL(box, IFACE_("Animation"), ICON_ANIM); + uiLayout *col = uiLayoutColumn(box, false); + uiLayout *sub = uiLayoutColumn(col, false); + uiItemR(sub, imfptr, "export_animation", 0, NULL, ICON_NONE); + sub = uiLayoutColumn(sub, true); + uiItemR(sub, imfptr, "start_frame", 0, IFACE_("Frame Start"), ICON_NONE); + uiItemR(sub, imfptr, "end_frame", 0, IFACE_("End"), ICON_NONE); + uiLayoutSetEnabled(sub, export_animation); + + /* Object Transform options. */ + box = uiLayoutBox(layout); + uiItemL(box, IFACE_("Object Properties"), ICON_OBJECT_DATA); + col = uiLayoutColumn(box, false); + sub = uiLayoutColumn(col, false); + uiItemR(sub, imfptr, "forward_axis", 0, IFACE_("Axis Forward"), ICON_NONE); + uiItemR(sub, imfptr, "up_axis", 0, IFACE_("Up"), ICON_NONE); + sub = uiLayoutColumn(col, false); + uiItemR(sub, imfptr, "scaling_factor", 0, NULL, ICON_NONE); + sub = uiLayoutColumnWithHeading(col, false, IFACE_("Objects")); + uiItemR(sub, imfptr, "export_selected_objects", 0, IFACE_("Selected Only"), ICON_NONE); + uiItemR(sub, imfptr, "export_eval_mode", 0, IFACE_("Properties"), ICON_NONE); + + /* Options for what to write. */ + box = uiLayoutBox(layout); + uiItemL(box, IFACE_("Geometry Export"), ICON_EXPORT); + col = uiLayoutColumn(box, false); + sub = uiLayoutColumnWithHeading(col, false, IFACE_("Export")); + uiItemR(sub, imfptr, "export_uv", 0, IFACE_("UV Coordinates"), ICON_NONE); + uiItemR(sub, imfptr, "export_normals", 0, IFACE_("Normals"), ICON_NONE); + uiItemR(sub, imfptr, "export_materials", 0, IFACE_("Materials"), ICON_NONE); + uiItemR(sub, imfptr, "export_triangulated_mesh", 0, IFACE_("Triangulated Mesh"), ICON_NONE); + uiItemR(sub, imfptr, "export_curves_as_nurbs", 0, IFACE_("Curves as NURBS"), ICON_NONE); + + box = uiLayoutBox(layout); + uiItemL(box, IFACE_("Grouping"), ICON_GROUP); + col = uiLayoutColumn(box, false); + sub = uiLayoutColumnWithHeading(col, false, IFACE_("Export")); + uiItemR(sub, imfptr, "export_object_groups", 0, IFACE_("Object Groups"), ICON_NONE); + uiItemR(sub, imfptr, "export_material_groups", 0, IFACE_("Material Groups"), ICON_NONE); + uiItemR(sub, imfptr, "export_vertex_groups", 0, IFACE_("Vertex Groups"), ICON_NONE); + uiItemR(sub, imfptr, "export_smooth_groups", 0, IFACE_("Smooth Groups"), ICON_NONE); + sub = uiLayoutColumn(sub, false); + uiLayoutSetEnabled(sub, export_smooth_groups); + uiItemR(sub, imfptr, "smooth_group_bitflags", 0, IFACE_("Smooth Group Bitflags"), ICON_NONE); +} + +static void wm_obj_export_draw(bContext *UNUSED(C), wmOperator *op) +{ + PointerRNA ptr; + RNA_pointer_create(NULL, op->type->srna, op->properties, &ptr); + ui_obj_export_settings(op->layout, &ptr); +} + +/** + * Return true if any property in the UI is changed. + */ +static bool wm_obj_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, ".obj")) { + BLI_path_extension_ensure(filepath, FILE_MAX, ".obj"); + RNA_string_set(op->ptr, "filepath", filepath); + changed = true; + } + + { + int start = RNA_int_get(op->ptr, "start_frame"); + int end = RNA_int_get(op->ptr, "end_frame"); + /* Set the defaults. */ + if (start == INT_MIN) { + start = SFRA; + changed = true; + } + if (end == INT_MAX) { + end = EFRA; + changed = true; + } + /* Fix user errors. */ + if (end < start) { + end = start; + changed = true; + } + RNA_int_set(op->ptr, "start_frame", start); + RNA_int_set(op->ptr, "end_frame", end); + } + + /* Both forward and up axes cannot be the same (or same except opposite sign). */ + if (RNA_enum_get(op->ptr, "forward_axis") % TOTAL_AXES == + (RNA_enum_get(op->ptr, "up_axis") % TOTAL_AXES)) { + /* TODO (ankitm) Show a warning here. */ + RNA_enum_set(op->ptr, "up_axis", RNA_enum_get(op->ptr, "up_axis") % TOTAL_AXES + 1); + changed = true; + } + return changed; +} + +void WM_OT_obj_export(struct wmOperatorType *ot) +{ + ot->name = "Export Wavefront OBJ"; + ot->description = "Save the scene to a Wavefront OBJ file"; + ot->idname = "WM_OT_obj_export"; + + ot->invoke = wm_obj_export_invoke; + ot->exec = wm_obj_export_exec; + ot->poll = WM_operator_winactive; + ot->ui = wm_obj_export_draw; + ot->check = wm_obj_export_check; + + WM_operator_properties_filesel(ot, + FILE_TYPE_FOLDER | FILE_TYPE_OBJECT_IO, + FILE_BLENDER, + FILE_SAVE, + WM_FILESEL_FILEPATH | WM_FILESEL_SHOW_PROPS, + FILE_DEFAULTDISPLAY, + FILE_SORT_ALPHA); + + /* Animation options. */ + RNA_def_boolean(ot->srna, + "export_animation", + false, + "Export Animation", + "Export multiple frames instead of the current frame only"); + RNA_def_int(ot->srna, + "start_frame", + INT_MIN, /* wm_obj_export_check uses this to set SFRA. */ + INT_MIN, + INT_MAX, + "Start Frame", + "The first frame to be exported", + INT_MIN, + INT_MAX); + RNA_def_int(ot->srna, + "end_frame", + INT_MAX, /* wm_obj_export_check uses this to set EFRA. */ + INT_MIN, + INT_MAX, + "End Frame", + "The last frame to be exported", + INT_MIN, + INT_MAX); + /* Object transform options. */ + RNA_def_enum(ot->srna, + "forward_axis", + io_obj_transform_axis_forward, + OBJ_AXIS_NEGATIVE_Z_FORWARD, + "Forward Axis", + ""); + RNA_def_enum(ot->srna, "up_axis", io_obj_transform_axis_up, OBJ_AXIS_Y_UP, "Up Axis", ""); + RNA_def_float(ot->srna, + "scaling_factor", + 1.0f, + 0.001f, + 10000.0f, + "Scale", + "Upscale the object by this factor", + 0.01, + 1000.0f); + /* File Writer options. */ + RNA_def_enum(ot->srna, + "export_eval_mode", + io_obj_export_evaluation_mode, + DAG_EVAL_VIEWPORT, + "Object Properties", + "Determines properties like object visibility, modifiers etc., where they differ " + "for Render and Viewport"); + 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", true, "Export UVs", ""); + RNA_def_boolean(ot->srna, + "export_normals", + true, + "Export Normals", + "Export per-face normals if the face is flat-shaded, per-face-per-loop " + "normals if smooth-shaded"); + RNA_def_boolean(ot->srna, + "export_materials", + true, + "Export Materials", + "Export MTL library. There must be a Principled-BSDF node for image textures to " + "be exported to the MTL file"); + 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, + "export_curves_as_nurbs", + false, + "Export Curves as NURBS", + "Export curves in parametric form instead of exporting as mesh"); + + RNA_def_boolean(ot->srna, + "export_object_groups", + false, + "Export Object Groups", + "Append mesh name to object name, separated by a '_'"); + RNA_def_boolean(ot->srna, + "export_material_groups", + false, + "Export Material Groups", + "Append mesh name and material name to object name, separated by a '_'"); + RNA_def_boolean( + ot->srna, + "export_vertex_groups", + false, + "Export Vertex Groups", + "Export the name of the vertex group of a face. It is approximated " + "by choosing the vertex group with the most members among the vertices of a face"); + RNA_def_boolean( + ot->srna, + "export_smooth_groups", + false, + "Export Smooth Groups", + "Every smooth-shaded face is assigned group \"1\" and every flat-shaded face \"off\""); + RNA_def_boolean( + ot->srna, "smooth_group_bitflags", false, "Generate Bitflags for Smooth Groups", ""); +} + +static int wm_obj_import_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event)) +{ + WM_event_add_fileselect(C, op); + return OPERATOR_RUNNING_MODAL; +} + +static int wm_obj_import_exec(bContext *C, wmOperator *op) +{ + if (!RNA_struct_property_is_set(op->ptr, "filepath")) { + BKE_report(op->reports, RPT_ERROR, "No filename given"); + return OPERATOR_CANCELLED; + } + + struct OBJImportParams import_params; + RNA_string_get(op->ptr, "filepath", import_params.filepath); + import_params.clamp_size = RNA_float_get(op->ptr, "clamp_size"); + import_params.forward_axis = RNA_enum_get(op->ptr, "forward_axis"); + import_params.up_axis = RNA_enum_get(op->ptr, "up_axis"); + + OBJ_import(C, &import_params); + + return OPERATOR_FINISHED; +} + +static void ui_obj_import_settings(uiLayout *layout, PointerRNA *imfptr) +{ + uiLayoutSetPropSep(layout, true); + uiLayoutSetPropDecorate(layout, false); + uiLayout *box = uiLayoutBox(layout); + + uiItemL(box, IFACE_("Transform"), ICON_OBJECT_DATA); + uiLayout *col = uiLayoutColumn(box, false); + uiLayout *sub = uiLayoutColumn(col, false); + uiItemR(sub, imfptr, "clamp_size", 0, NULL, ICON_NONE); + sub = uiLayoutColumn(col, false); + uiItemR(sub, imfptr, "forward_axis", 0, IFACE_("Axis Forward"), ICON_NONE); + uiItemR(sub, imfptr, "up_axis", 0, IFACE_("Up"), ICON_NONE); +} + +static void wm_obj_import_draw(bContext *C, wmOperator *op) +{ + PointerRNA ptr; + wmWindowManager *wm = CTX_wm_manager(C); + RNA_pointer_create(&wm->id, op->type->srna, op->properties, &ptr); + ui_obj_import_settings(op->layout, &ptr); +} + +void WM_OT_obj_import(struct wmOperatorType *ot) +{ + ot->name = "Import Wavefront OBJ"; + ot->description = "Load a Wavefront OBJ scene"; + ot->idname = "WM_OT_obj_import"; + + ot->invoke = wm_obj_import_invoke; + ot->exec = wm_obj_import_exec; + ot->poll = WM_operator_winactive; + ot->ui = wm_obj_import_draw; + + WM_operator_properties_filesel(ot, + FILE_TYPE_FOLDER | FILE_TYPE_OBJECT_IO, + FILE_BLENDER, + FILE_OPENFILE, + WM_FILESEL_FILEPATH | WM_FILESEL_SHOW_PROPS, + FILE_DEFAULTDISPLAY, + FILE_SORT_ALPHA); + RNA_def_float( + ot->srna, + "clamp_size", + 0.0f, + 0.0f, + 1000.0f, + "Clamp Bounding Box", + "Resize the objects to keep bounding box under this value. Value 0 diables clamping", + 0.0f, + 1000.0f); + RNA_def_enum(ot->srna, + "forward_axis", + io_obj_transform_axis_forward, + OBJ_AXIS_NEGATIVE_Z_FORWARD, + "Forward Axis", + ""); + RNA_def_enum(ot->srna, "up_axis", io_obj_transform_axis_up, OBJ_AXIS_Y_UP, "Up Axis", ""); +} diff --git a/source/blender/editors/io/io_obj.h b/source/blender/editors/io/io_obj.h new file mode 100644 index 00000000000..ff8747879f5 --- /dev/null +++ b/source/blender/editors/io/io_obj.h @@ -0,0 +1,29 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup editor/io + */ + +#pragma once + +struct wmOperatorType; + +void WM_OT_obj_export(struct wmOperatorType *ot); +void WM_OT_obj_import(struct wmOperatorType *ot); diff --git a/source/blender/editors/io/io_ops.c b/source/blender/editors/io/io_ops.c index b2788ee49a2..90eba4ad700 100644 --- a/source/blender/editors/io/io_ops.c +++ b/source/blender/editors/io/io_ops.c @@ -39,6 +39,7 @@ #include "io_cache.h" #include "io_gpencil.h" +#include "io_obj.h" void ED_operatortypes_io(void) { @@ -68,4 +69,6 @@ void ED_operatortypes_io(void) WM_operatortype_append(CACHEFILE_OT_open); WM_operatortype_append(CACHEFILE_OT_reload); + WM_operatortype_append(WM_OT_obj_import); + WM_operatortype_append(WM_OT_obj_export); } diff --git a/source/blender/editors/space_file/filesel.c b/source/blender/editors/space_file/filesel.c index 11757975a62..ce76fd65a86 100644 --- a/source/blender/editors/space_file/filesel.c +++ b/source/blender/editors/space_file/filesel.c @@ -275,6 +275,9 @@ static FileSelectParams *fileselect_ensure_updated_file_params(SpaceFile *sfile) if ((prop = RNA_struct_find_property(op->ptr, "filter_usd"))) { params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_USD : 0; } + if ((prop = RNA_struct_find_property(op->ptr, "filter_obj"))) { + params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_OBJECT_IO : 0; + } if ((prop = RNA_struct_find_property(op->ptr, "filter_volume"))) { params->filter |= RNA_property_boolean_get(op->ptr, prop) ? FILE_TYPE_VOLUME : 0; } diff --git a/source/blender/io/CMakeLists.txt b/source/blender/io/CMakeLists.txt index f11ad7627b9..b97b3ef97de 100644 --- a/source/blender/io/CMakeLists.txt +++ b/source/blender/io/CMakeLists.txt @@ -19,6 +19,7 @@ # ***** END GPL LICENSE BLOCK ***** add_subdirectory(common) +add_subdirectory(wavefront_obj) if(WITH_ALEMBIC) add_subdirectory(alembic) diff --git a/source/blender/io/wavefront_obj/CMakeLists.txt b/source/blender/io/wavefront_obj/CMakeLists.txt new file mode 100644 index 00000000000..8e49f4fe2b6 --- /dev/null +++ b/source/blender/io/wavefront_obj/CMakeLists.txt @@ -0,0 +1,104 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# The Original Code is Copyright (C) 2020, Blender Foundation +# All rights reserved. +# ***** END GPL LICENSE BLOCK ***** + +set(INC + . + ./exporter + ./importer + ../../blenkernel + ../../blenlib + ../../bmesh + ../../bmesh/intern + ../../depsgraph + ../../editors/include + ../../makesdna + ../../makesrna + ../../nodes + ../../windowmanager + ../../../../intern/guardedalloc +) + +set(INC_SYS + +) + +set(SRC + IO_wavefront_obj.cc + exporter/obj_exporter.cc + exporter/obj_export_file_writer.cc + exporter/obj_export_mesh.cc + exporter/obj_export_mtl.cc + exporter/obj_export_nurbs.cc + importer/importer_mesh_utils.cc + importer/obj_import_file_reader.cc + importer/obj_importer.cc + importer/obj_import_mesh.cc + importer/obj_import_mtl.cc + importer/obj_import_nurbs.cc + importer/obj_import_objects.cc + importer/parser_string_utils.cc + + IO_wavefront_obj.h + exporter/obj_exporter.hh + exporter/obj_export_file_writer.hh + exporter/obj_export_io.hh + exporter/obj_export_mesh.hh + exporter/obj_export_mtl.hh + exporter/obj_export_nurbs.hh + importer/importer_mesh_utils.hh + importer/obj_import_file_reader.hh + importer/obj_importer.hh + importer/obj_import_mesh.hh + importer/obj_import_mtl.hh + importer/obj_import_nurbs.hh + importer/obj_import_objects.hh + importer/parser_string_utils.hh +) + +set(LIB + bf_blenkernel +) + +blender_add_lib(bf_wavefront_obj "${SRC}" "${INC}" "${INC_SYS}" "${LIB}") + +if(WITH_GTESTS) + set(TEST_SRC + tests/obj_exporter_tests.cc + tests/obj_exporter_tests.hh + ) + + set(TEST_INC + ${INC} + + ../../blenloader + ../../../../tests/gtests + ) + + set(TEST_LIB + ${LIB} + + bf_blenloader_tests + bf_wavefront_obj + ) + + include(GTestTesting) + blender_add_test_lib(bf_wavefront_obj_tests "${TEST_SRC}" "${TEST_INC}" "${INC_SYS}" "${TEST_LIB}") + add_dependencies(bf_wavefront_obj_tests bf_wavefront_obj) +endif() diff --git a/source/blender/io/wavefront_obj/IO_wavefront_obj.cc b/source/blender/io/wavefront_obj/IO_wavefront_obj.cc new file mode 100644 index 00000000000..9db2a1bee98 --- /dev/null +++ b/source/blender/io/wavefront_obj/IO_wavefront_obj.cc @@ -0,0 +1,47 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include "BLI_timeit.hh" + +#include "IO_wavefront_obj.h" + +#include "obj_exporter.hh" +#include "obj_importer.hh" + +/** + * C-interface for the exporter. + */ +void OBJ_export(bContext *C, const OBJExportParams *export_params) +{ + SCOPED_TIMER("OBJ export"); + blender::io::obj::exporter_main(C, *export_params); +} + +/** + * Time the full import process. + */ +void OBJ_import(bContext *C, const OBJImportParams *import_params) +{ + SCOPED_TIMER(__func__); + blender::io::obj::importer_main(C, *import_params); +} diff --git a/source/blender/io/wavefront_obj/IO_wavefront_obj.h b/source/blender/io/wavefront_obj/IO_wavefront_obj.h new file mode 100644 index 00000000000..49ca6a754da --- /dev/null +++ b/source/blender/io/wavefront_obj/IO_wavefront_obj.h @@ -0,0 +1,111 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include "BKE_context.h" +#include "BLI_path_util.h" +#include "DEG_depsgraph.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + OBJ_AXIS_X_UP = 0, + OBJ_AXIS_Y_UP = 1, + OBJ_AXIS_Z_UP = 2, + OBJ_AXIS_NEGATIVE_X_UP = 3, + OBJ_AXIS_NEGATIVE_Y_UP = 4, + OBJ_AXIS_NEGATIVE_Z_UP = 5, +} eTransformAxisUp; + +typedef enum { + OBJ_AXIS_X_FORWARD = 0, + OBJ_AXIS_Y_FORWARD = 1, + OBJ_AXIS_Z_FORWARD = 2, + OBJ_AXIS_NEGATIVE_X_FORWARD = 3, + OBJ_AXIS_NEGATIVE_Y_FORWARD = 4, + OBJ_AXIS_NEGATIVE_Z_FORWARD = 5, +} eTransformAxisForward; + +const int TOTAL_AXES = 3; + +struct OBJExportParams { + /** Full path to the destination .OBJ file. */ + char filepath[FILE_MAX]; + + /** Full path to current blender file (used for comments in output). */ + const char *blen_filepath; + + /** Whether multiple frames should be exported. */ + bool export_animation; + /** The first frame to be exported. */ + int start_frame; + /** The last frame to be exported. */ + int end_frame; + + /* Geometry Transform options. */ + eTransformAxisForward forward_axis; + eTransformAxisUp up_axis; + float scaling_factor; + + /* File Write Options. */ + bool export_selected_objects; + eEvaluationMode export_eval_mode; + bool export_uv; + bool export_normals; + bool export_materials; + bool export_triangulated_mesh; + bool export_curves_as_nurbs; + + /* Grouping options. */ + bool export_object_groups; + bool export_material_groups; + bool export_vertex_groups; + /** + * Calculate smooth groups from sharp edges. + */ + bool export_smooth_groups; + /** + * Create bitflags instead of the default "0"/"1" group IDs. + */ + bool smooth_groups_bitflags; +}; + +struct OBJImportParams { + /** Full path to the source OBJ file to import. */ + char filepath[FILE_MAX]; + /* Value 0 disables clamping. */ + float clamp_size; + eTransformAxisForward forward_axis; + eTransformAxisUp up_axis; +}; + +void OBJ_import(bContext *C, const struct OBJImportParams *import_params); + +void OBJ_export(bContext *C, const struct OBJExportParams *export_params); + +#ifdef __cplusplus +} +#endif diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc new file mode 100644 index 00000000000..d2faccfee0e --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc @@ -0,0 +1,629 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include <algorithm> +#include <cstdio> + +#include "BKE_blender_version.h" + +#include "BLI_path_util.h" + +#include "obj_export_mesh.hh" +#include "obj_export_mtl.hh" +#include "obj_export_nurbs.hh" + +#include "obj_export_file_writer.hh" + +namespace blender::io::obj { +/** + * "To turn off smoothing + * groups, use a value of 0 or off. Polygonal elements use group + * numbers to put elements in different smoothing groups. For + * free-form surfaces, smoothing groups are either turned on or off; + * there is no difference between values greater than 0." + * http://www.martinreddy.net/gfx/3d/OBJ.spec + */ +const int SMOOTH_GROUP_DISABLED = 0; +const int SMOOTH_GROUP_DEFAULT = 1; + +const char *DEFORM_GROUP_DISABLED = "off"; +/* There is no deform group default name. Use what the user set in the UI. */ + +/* "Once a material is assigned, it cannot be turned off; it can only be changed. + * If a material name is not specified, a white material is used." + * http://www.martinreddy.net/gfx/3d/OBJ.spec + * So an empty material name is written. */ +const char *MATERIAL_GROUP_DISABLED = ""; + +/** + * Write one line of polygon indices as "f v1/vt1/vn1 v2/vt2/vn2 ...". + */ +void OBJWriter::write_vert_uv_normal_indices(Span<int> vert_indices, + Span<int> uv_indices, + Span<int> normal_indices) const +{ + BLI_assert(vert_indices.size() == uv_indices.size() && + vert_indices.size() == normal_indices.size()); + file_handler_->write<eOBJSyntaxElement::poly_element_begin>(); + for (int j = 0; j < vert_indices.size(); j++) { + file_handler_->write<eOBJSyntaxElement::vertex_uv_normal_indices>( + vert_indices[j] + index_offsets_.vertex_offset + 1, + uv_indices[j] + index_offsets_.uv_vertex_offset + 1, + normal_indices[j] + index_offsets_.normal_offset + 1); + } + file_handler_->write<eOBJSyntaxElement::poly_element_end>(); +} + +/** + * Write one line of polygon indices as "f v1//vn1 v2//vn2 ...". + */ +void OBJWriter::write_vert_normal_indices(Span<int> vert_indices, + Span<int> /*uv_indices*/, + Span<int> normal_indices) const +{ + BLI_assert(vert_indices.size() == normal_indices.size()); + file_handler_->write<eOBJSyntaxElement::poly_element_begin>(); + for (int j = 0; j < vert_indices.size(); j++) { + file_handler_->write<eOBJSyntaxElement::vertex_normal_indices>( + vert_indices[j] + index_offsets_.vertex_offset + 1, + normal_indices[j] + index_offsets_.normal_offset + 1); + } + file_handler_->write<eOBJSyntaxElement::poly_element_end>(); +} + +/** + * Write one line of polygon indices as "f v1/vt1 v2/vt2 ...". + */ +void OBJWriter::write_vert_uv_indices(Span<int> vert_indices, + Span<int> uv_indices, + Span<int> /*normal_indices*/) const +{ + BLI_assert(vert_indices.size() == uv_indices.size()); + file_handler_->write<eOBJSyntaxElement::poly_element_begin>(); + for (int j = 0; j < vert_indices.size(); j++) { + file_handler_->write<eOBJSyntaxElement::vertex_uv_indices>( + vert_indices[j] + index_offsets_.vertex_offset + 1, + uv_indices[j] + index_offsets_.uv_vertex_offset + 1); + } + file_handler_->write<eOBJSyntaxElement::poly_element_end>(); +} + +/** + * Write one line of polygon indices as "f v1 v2 ...". + */ +void OBJWriter::write_vert_indices(Span<int> vert_indices, + Span<int> /*uv_indices*/, + Span<int> /*normal_indices*/) const +{ + file_handler_->write<eOBJSyntaxElement::poly_element_begin>(); + for (const int vert_index : vert_indices) { + file_handler_->write<eOBJSyntaxElement::vertex_indices>(vert_index + + index_offsets_.vertex_offset + 1); + } + file_handler_->write<eOBJSyntaxElement::poly_element_end>(); +} + +void OBJWriter::write_header() const +{ + using namespace std::string_literals; + file_handler_->write<eOBJSyntaxElement::string>("# Blender "s + BKE_blender_version_string() + + "\n"); + file_handler_->write<eOBJSyntaxElement::string>("# www.blender.org\n"); +} + +/** + * Write file name of Material Library in .OBJ file. + */ +void OBJWriter::write_mtllib_name(const StringRefNull mtl_filepath) const +{ + /* Split .MTL file path into parent directory and filename. */ + char mtl_file_name[FILE_MAXFILE]; + char mtl_dir_name[FILE_MAXDIR]; + BLI_split_dirfile(mtl_filepath.data(), mtl_dir_name, mtl_file_name, FILE_MAXDIR, FILE_MAXFILE); + file_handler_->write<eOBJSyntaxElement::mtllib>(mtl_file_name); +} + +/** + * Write an object's group with mesh and/or material name appended conditionally. + */ +void OBJWriter::write_object_group(const OBJMesh &obj_mesh_data) const +{ + /* "o object_name" is not mandatory. A valid .OBJ file may contain neither + * "o name" nor "g group_name". */ + BLI_assert(export_params_.export_object_groups); + if (!export_params_.export_object_groups) { + return; + } + const std::string object_name = obj_mesh_data.get_object_name(); + const char *object_mesh_name = obj_mesh_data.get_object_mesh_name(); + const char *object_material_name = obj_mesh_data.get_object_material_name(0); + if (export_params_.export_materials && export_params_.export_material_groups && + object_material_name) { + file_handler_->write<eOBJSyntaxElement::object_group>(object_name + "_" + object_mesh_name + + "_" + object_material_name); + return; + } + file_handler_->write<eOBJSyntaxElement::object_group>(object_name + "_" + object_mesh_name); +} + +/** + * Write object's name or group. + */ +void OBJWriter::write_object_name(const OBJMesh &obj_mesh_data) const +{ + const char *object_name = obj_mesh_data.get_object_name(); + if (export_params_.export_object_groups) { + write_object_group(obj_mesh_data); + return; + } + file_handler_->write<eOBJSyntaxElement::object_name>(object_name); +} + +/** + * Write vertex coordinates for all vertices as "v x y z". + */ +void OBJWriter::write_vertex_coords(const OBJMesh &obj_mesh_data) const +{ + const int tot_vertices = obj_mesh_data.tot_vertices(); + for (int i = 0; i < tot_vertices; i++) { + float3 vertex = obj_mesh_data.calc_vertex_coords(i, export_params_.scaling_factor); + file_handler_->write<eOBJSyntaxElement::vertex_coords>(vertex[0], vertex[1], vertex[2]); + } +} + +/** + * Write UV vertex coordinates for all vertices as "vt u v". + * \note UV indices are stored here, but written later. + */ +void OBJWriter::write_uv_coords(OBJMesh &r_obj_mesh_data) const +{ + Vector<std::array<float, 2>> uv_coords; + /* UV indices are calculated and stored in an OBJMesh member here. */ + r_obj_mesh_data.store_uv_coords_and_indices(uv_coords); + + for (const std::array<float, 2> &uv_vertex : uv_coords) { + file_handler_->write<eOBJSyntaxElement::uv_vertex_coords>(uv_vertex[0], uv_vertex[1]); + } +} + +/** + * Write loop normals for smooth-shaded polygons, and polygon normals otherwise, as "vn x y z". + */ +void OBJWriter::write_poly_normals(const OBJMesh &obj_mesh_data) const +{ + obj_mesh_data.ensure_mesh_normals(); + Vector<float3> lnormals; + const int tot_polygons = obj_mesh_data.tot_polygons(); + for (int i = 0; i < tot_polygons; i++) { + if (obj_mesh_data.is_ith_poly_smooth(i)) { + obj_mesh_data.calc_loop_normals(i, lnormals); + for (const float3 &lnormal : lnormals) { + file_handler_->write<eOBJSyntaxElement::normal>(lnormal[0], lnormal[1], lnormal[2]); + } + } + else { + float3 poly_normal = obj_mesh_data.calc_poly_normal(i); + file_handler_->write<eOBJSyntaxElement::normal>( + poly_normal[0], poly_normal[1], poly_normal[2]); + } + } +} + +/** + * Write smooth group if polygon at the given index is shaded smooth else "s 0" + */ +int OBJWriter::write_smooth_group(const OBJMesh &obj_mesh_data, + const int poly_index, + const int last_poly_smooth_group) const +{ + int current_group = SMOOTH_GROUP_DISABLED; + if (!export_params_.export_smooth_groups && obj_mesh_data.is_ith_poly_smooth(poly_index)) { + /* Smooth group calculation is disabled, but polygon is smooth-shaded. */ + current_group = SMOOTH_GROUP_DEFAULT; + } + else if (obj_mesh_data.is_ith_poly_smooth(poly_index)) { + /* Smooth group calc is enabled and polygon is smooth–shaded, so find the group. */ + current_group = obj_mesh_data.ith_smooth_group(poly_index); + } + + if (current_group == last_poly_smooth_group) { + /* Group has already been written, even if it is "s 0". */ + return current_group; + } + file_handler_->write<eOBJSyntaxElement::smooth_group>(current_group); + return current_group; +} + +/** + * Write material name and material group of a polygon in the .OBJ file. + * \return #mat_nr of the polygon at the given index. + * \note It doesn't write to the material library. + */ +int16_t OBJWriter::write_poly_material(const OBJMesh &obj_mesh_data, + const int poly_index, + const int16_t last_poly_mat_nr, + std::function<const char *(int)> matname_fn) const +{ + if (!export_params_.export_materials || obj_mesh_data.tot_materials() <= 0) { + return last_poly_mat_nr; + } + const int16_t current_mat_nr = obj_mesh_data.ith_poly_matnr(poly_index); + /* Whenever a polygon with a new material is encountered, write its material + * and/or group, otherwise pass. */ + if (last_poly_mat_nr == current_mat_nr) { + return current_mat_nr; + } + if (current_mat_nr == NOT_FOUND) { + file_handler_->write<eOBJSyntaxElement::poly_usemtl>(MATERIAL_GROUP_DISABLED); + return current_mat_nr; + } + if (export_params_.export_object_groups) { + write_object_group(obj_mesh_data); + } + const char *mat_name = matname_fn(current_mat_nr); + if (!mat_name) { + mat_name = MATERIAL_GROUP_DISABLED; + } + file_handler_->write<eOBJSyntaxElement::poly_usemtl>(mat_name); + + return current_mat_nr; +} + +/** + * Write the name of the deform group of a polygon. + */ +int16_t OBJWriter::write_vertex_group(const OBJMesh &obj_mesh_data, + const int poly_index, + const int16_t last_poly_vertex_group) const +{ + if (!export_params_.export_vertex_groups) { + return last_poly_vertex_group; + } + const int16_t current_group = obj_mesh_data.get_poly_deform_group_index(poly_index); + + if (current_group == last_poly_vertex_group) { + /* No vertex group found in this polygon, just like in the last iteration. */ + return current_group; + } + if (current_group == NOT_FOUND) { + file_handler_->write<eOBJSyntaxElement::object_group>(DEFORM_GROUP_DISABLED); + return current_group; + } + file_handler_->write<eOBJSyntaxElement::object_group>( + obj_mesh_data.get_poly_deform_group_name(current_group)); + return current_group; +} + +/** + * \return Writer function with appropriate polygon-element syntax. + */ +OBJWriter::func_vert_uv_normal_indices OBJWriter::get_poly_element_writer( + const int total_uv_vertices) const +{ + if (export_params_.export_normals) { + if (export_params_.export_uv && (total_uv_vertices > 0)) { + /* Write both normals and UV indices. */ + return &OBJWriter::write_vert_uv_normal_indices; + } + /* Write normals indices. */ + return &OBJWriter::write_vert_normal_indices; + } + /* Write UV indices. */ + if (export_params_.export_uv && (total_uv_vertices > 0)) { + return &OBJWriter::write_vert_uv_indices; + } + /* Write neither normals nor UV indices. */ + return &OBJWriter::write_vert_indices; +} + +/** + * Write polygon elements with at least vertex indices, and conditionally with UV vertex + * indices and polygon normal indices. Also write groups: smooth, vertex, material. + * The matname_fn turns a 0-indexed material slot number in an Object into the + * name used in the .obj file. + * \note UV indices were stored while writing UV vertices. + */ +void OBJWriter::write_poly_elements(const OBJMesh &obj_mesh_data, + std::function<const char *(int)> matname_fn) +{ + int last_poly_smooth_group = NEGATIVE_INIT; + int16_t last_poly_vertex_group = NEGATIVE_INIT; + int16_t last_poly_mat_nr = NEGATIVE_INIT; + + const func_vert_uv_normal_indices poly_element_writer = get_poly_element_writer( + obj_mesh_data.tot_uv_vertices()); + + /* Number of normals may not be equal to number of polygons due to smooth shading. */ + int per_object_tot_normals = 0; + const int tot_polygons = obj_mesh_data.tot_polygons(); + for (int i = 0; i < tot_polygons; i++) { + Vector<int> poly_vertex_indices = obj_mesh_data.calc_poly_vertex_indices(i); + Span<int> poly_uv_indices = obj_mesh_data.calc_poly_uv_indices(i); + /* For an Object, a normal index depends on how many of its normals have been written before + * it. This is unknown because of smooth shading. So pass "per object total normals" + * and update it after each call. */ + int new_normals = 0; + Vector<int> poly_normal_indices; + std::tie(new_normals, poly_normal_indices) = obj_mesh_data.calc_poly_normal_indices( + i, per_object_tot_normals); + per_object_tot_normals += new_normals; + + last_poly_smooth_group = write_smooth_group(obj_mesh_data, i, last_poly_smooth_group); + last_poly_vertex_group = write_vertex_group(obj_mesh_data, i, last_poly_vertex_group); + last_poly_mat_nr = write_poly_material(obj_mesh_data, i, last_poly_mat_nr, matname_fn); + (this->*poly_element_writer)(poly_vertex_indices, poly_uv_indices, poly_normal_indices); + } + /* Unusual: Other indices are updated in #OBJWriter::update_index_offsets. */ + index_offsets_.normal_offset += per_object_tot_normals; +} + +/** + * Write loose edges of a mesh as "l v1 v2". + */ +void OBJWriter::write_edges_indices(const OBJMesh &obj_mesh_data) const +{ + obj_mesh_data.ensure_mesh_edges(); + const int tot_edges = obj_mesh_data.tot_edges(); + for (int edge_index = 0; edge_index < tot_edges; edge_index++) { + const std::optional<std::array<int, 2>> vertex_indices = + obj_mesh_data.calc_loose_edge_vert_indices(edge_index); + if (!vertex_indices) { + continue; + } + file_handler_->write<eOBJSyntaxElement::edge>( + (*vertex_indices)[0] + index_offsets_.vertex_offset + 1, + (*vertex_indices)[1] + index_offsets_.vertex_offset + 1); + } +} + +/** + * Write a NURBS curve to the .OBJ file in parameter form. + */ +void OBJWriter::write_nurbs_curve(const OBJCurve &obj_nurbs_data) const +{ + const int total_splines = obj_nurbs_data.total_splines(); + for (int spline_idx = 0; spline_idx < total_splines; spline_idx++) { + const int total_vertices = obj_nurbs_data.total_spline_vertices(spline_idx); + for (int vertex_idx = 0; vertex_idx < total_vertices; vertex_idx++) { + const float3 vertex_coords = obj_nurbs_data.vertex_coordinates( + spline_idx, vertex_idx, export_params_.scaling_factor); + file_handler_->write<eOBJSyntaxElement::vertex_coords>( + vertex_coords[0], vertex_coords[1], vertex_coords[2]); + } + + const char *nurbs_name = obj_nurbs_data.get_curve_name(); + const int nurbs_degree = obj_nurbs_data.get_nurbs_degree(spline_idx); + file_handler_->write<eOBJSyntaxElement::object_group>(nurbs_name); + file_handler_->write<eOBJSyntaxElement::cstype>(); + file_handler_->write<eOBJSyntaxElement::nurbs_degree>(nurbs_degree); + /** + * The numbers written here are indices into the vertex coordinates written + * earlier, relative to the line that is going to be written. + * [0.0 - 1.0] is the curve parameter range. + * 0.0 1.0 -1 -2 -3 -4 for a non-cyclic curve with 4 vertices. + * 0.0 1.0 -1 -2 -3 -4 -1 -2 -3 for a cyclic curve with 4 vertices. + */ + const int total_control_points = obj_nurbs_data.total_spline_control_points(spline_idx); + file_handler_->write<eOBJSyntaxElement::curve_element_begin>(); + for (int i = 0; i < total_control_points; i++) { + /* "+1" to keep indices one-based, even if they're negative: i.e., -1 refers to the + * last vertex coordinate, -2 second last. */ + file_handler_->write<eOBJSyntaxElement::vertex_indices>(-((i % total_vertices) + 1)); + } + file_handler_->write<eOBJSyntaxElement::curve_element_end>(); + + /** + * In "parm u 0 0.1 .." line:, (total control points + 2) equidistant numbers in the + * parameter range are inserted. + */ + file_handler_->write<eOBJSyntaxElement::nurbs_parameter_begin>(); + for (int i = 1; i <= total_control_points + 2; i++) { + file_handler_->write<eOBJSyntaxElement::nurbs_parameters>(1.0f * i / + (total_control_points + 2 + 1)); + } + file_handler_->write<eOBJSyntaxElement::nurbs_parameter_end>(); + + file_handler_->write<eOBJSyntaxElement::nurbs_group_end>(); + } +} + +/** + * When there are multiple objects in a frame, the indices of previous objects' coordinates or + * normals add up. + */ +void OBJWriter::update_index_offsets(const OBJMesh &obj_mesh_data) +{ + index_offsets_.vertex_offset += obj_mesh_data.tot_vertices(); + index_offsets_.uv_vertex_offset += obj_mesh_data.tot_uv_vertices(); + /* Normal index is updated right after writing the normals. */ +} + +/* -------------------------------------------------------------------- */ +/** \name .MTL writers. + * \{ */ + +/** + * Convert #float3 to string of space-separated numbers, with no leading or trailing space. + * Only to be used in NON-performance-critical code. + */ +static std::string float3_to_string(const float3 &numbers) +{ + std::ostringstream r_string; + r_string << numbers[0] << " " << numbers[1] << " " << numbers[2]; + return r_string.str(); +}; + +/* + * Create the .MTL file. + */ +MTLWriter::MTLWriter(const char *obj_filepath) noexcept(false) +{ + mtl_filepath_ = obj_filepath; + const bool ok = BLI_path_extension_replace(mtl_filepath_.data(), FILE_MAX, ".mtl"); + if (!ok) { + throw std::system_error(ENAMETOOLONG, std::system_category(), ""); + } + file_handler_ = std::make_unique<FileHandler<eFileType::MTL>>(mtl_filepath_); +} + +void MTLWriter::write_header(const char *blen_filepath) const +{ + using namespace std::string_literals; + const char *blen_basename = (blen_filepath && blen_filepath[0] != '\0') ? + BLI_path_basename(blen_filepath) : + "None"; + file_handler_->write<eMTLSyntaxElement::string>("# Blender "s + BKE_blender_version_string() + + " MTL File: '" + blen_basename + "'\n"); + file_handler_->write<eMTLSyntaxElement::string>("# www.blender.org\n"); +} + +StringRefNull MTLWriter::mtl_file_path() const +{ + return mtl_filepath_; +} + +/** + * Write properties sourced from p-BSDF node or #Object.Material. + */ +void MTLWriter::write_bsdf_properties(const MTLMaterial &mtl_material) +{ + file_handler_->write<eMTLSyntaxElement::Ns>(mtl_material.Ns); + file_handler_->write<eMTLSyntaxElement::Ka>( + mtl_material.Ka.x, mtl_material.Ka.y, mtl_material.Ka.z); + file_handler_->write<eMTLSyntaxElement::Kd>( + mtl_material.Kd.x, mtl_material.Kd.y, mtl_material.Kd.z); + file_handler_->write<eMTLSyntaxElement::Ks>( + mtl_material.Ks.x, mtl_material.Ks.y, mtl_material.Ks.z); + file_handler_->write<eMTLSyntaxElement::Ke>( + mtl_material.Ke.x, mtl_material.Ke.y, mtl_material.Ke.z); + file_handler_->write<eMTLSyntaxElement::Ni>(mtl_material.Ni); + file_handler_->write<eMTLSyntaxElement::d>(mtl_material.d); + file_handler_->write<eMTLSyntaxElement::illum>(mtl_material.illum); +} + +/** + * Write a texture map in the form "map_XX -s 1. 1. 1. -o 0. 0. 0. [-bm 1.] path/to/image". + */ +void MTLWriter::write_texture_map( + const MTLMaterial &mtl_material, + const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map) +{ + std::string translation; + std::string scale; + std::string map_bump_strength; + /* Optional strings should have their own leading spaces. */ + if (texture_map.value.translation != float3{0.0f, 0.0f, 0.0f}) { + translation.append(" -s ").append(float3_to_string(texture_map.value.translation)); + } + if (texture_map.value.scale != float3{1.0f, 1.0f, 1.0f}) { + scale.append(" -o ").append(float3_to_string(texture_map.value.scale)); + } + if (texture_map.key == eMTLSyntaxElement::map_Bump && mtl_material.map_Bump_strength > 0.0001f) { + map_bump_strength.append(" -bm ").append(std::to_string(mtl_material.map_Bump_strength)); + } + +#define SYNTAX_DISPATCH(eMTLSyntaxElement) \ + if (texture_map.key == eMTLSyntaxElement) { \ + file_handler_->write<eMTLSyntaxElement>(translation + scale + map_bump_strength, \ + texture_map.value.image_path); \ + return; \ + } + + SYNTAX_DISPATCH(eMTLSyntaxElement::map_Kd); + SYNTAX_DISPATCH(eMTLSyntaxElement::map_Ks); + SYNTAX_DISPATCH(eMTLSyntaxElement::map_Ns); + SYNTAX_DISPATCH(eMTLSyntaxElement::map_d); + SYNTAX_DISPATCH(eMTLSyntaxElement::map_refl); + SYNTAX_DISPATCH(eMTLSyntaxElement::map_Ke); + SYNTAX_DISPATCH(eMTLSyntaxElement::map_Bump); + + BLI_assert(!"This map type was not written to the file."); +} + +/** + * Write all of the material specifications to the MTL file. + * For consistency of output from run to run (useful for testing), + * the materials are sorted by name before writing. + */ +void MTLWriter::write_materials() +{ + if (mtlmaterials_.size() == 0) { + return; + } + std::sort(mtlmaterials_.begin(), + mtlmaterials_.end(), + [](const MTLMaterial &a, const MTLMaterial &b) { return a.name < b.name; }); + for (const MTLMaterial &mtlmat : mtlmaterials_) { + file_handler_->write<eMTLSyntaxElement::string>("\n"); + file_handler_->write<eMTLSyntaxElement::newmtl>(mtlmat.name); + write_bsdf_properties(mtlmat); + for (const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map : + mtlmat.texture_maps.items()) { + if (!texture_map.value.image_path.empty()) { + write_texture_map(mtlmat, texture_map); + } + } + } +} + +/** + * Add the materials of the given object to MTLWriter, deduping + * against ones that are already there. + * Return a Vector of indices into mtlmaterials_ that hold the MTLMaterial + * that corresponds to each material slot, in order, of the given Object. + * Indexes are returned rather than pointers to the MTLMaterials themselves + * because the mtlmaterials_ Vector may move around when resized. + */ +Vector<int> MTLWriter::add_materials(const OBJMesh &mesh_to_export) +{ + Vector<int> r_mtl_indices; + r_mtl_indices.resize(mesh_to_export.tot_materials()); + for (int16_t i = 0; i < mesh_to_export.tot_materials(); i++) { + const Material *material = mesh_to_export.get_object_material(i); + if (!material) { + r_mtl_indices[i] = -1; + continue; + } + int mtlmat_index = material_map_.lookup_default(material, -1); + if (mtlmat_index != -1) { + r_mtl_indices[i] = mtlmat_index; + } + else { + mtlmaterials_.append(mtlmaterial_for_material(material)); + r_mtl_indices[i] = mtlmaterials_.size() - 1; + material_map_.add_new(material, r_mtl_indices[i]); + } + } + return r_mtl_indices; +} + +const char *MTLWriter::mtlmaterial_name(int index) +{ + if (index < 0 || index >= mtlmaterials_.size()) { + return nullptr; + } + return mtlmaterials_[index].name.c_str(); +} +/** \} */ + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh new file mode 100644 index 00000000000..18c9ddb85c4 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh @@ -0,0 +1,135 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include "DNA_meshdata_types.h" + +#include "BLI_map.hh" +#include "BLI_vector.hh" + +#include "IO_wavefront_obj.h" +#include "obj_export_io.hh" +#include "obj_export_mtl.hh" + +namespace blender::io::obj { + +class OBJCurve; +class OBJMesh; +/** + * Total vertices/ UV vertices/ normals of previous Objects + * should be added to the current Object's indices. + */ +struct IndexOffsets { + int vertex_offset; + int uv_vertex_offset; + int normal_offset; +}; + +/** + * Responsible for writing a .OBJ file. + */ +class OBJWriter : NonMovable, NonCopyable { + private: + const OBJExportParams &export_params_; + std::unique_ptr<FileHandler<eFileType::OBJ>> file_handler_ = nullptr; + IndexOffsets index_offsets_{0, 0, 0}; + + public: + OBJWriter(const char *filepath, const OBJExportParams &export_params) noexcept(false) + : export_params_(export_params) + { + file_handler_ = std::make_unique<FileHandler<eFileType::OBJ>>(filepath); + } + + void write_header() const; + + void write_object_name(const OBJMesh &obj_mesh_data) const; + void write_object_group(const OBJMesh &obj_mesh_data) const; + void write_mtllib_name(const StringRefNull mtl_filepath) const; + void write_vertex_coords(const OBJMesh &obj_mesh_data) const; + void write_uv_coords(OBJMesh &obj_mesh_data) const; + void write_poly_normals(const OBJMesh &obj_mesh_data) const; + int write_smooth_group(const OBJMesh &obj_mesh_data, + int poly_index, + const int last_poly_smooth_group) const; + int16_t write_poly_material(const OBJMesh &obj_mesh_data, + const int poly_index, + const int16_t last_poly_mat_nr, + std::function<const char *(int)> matname_fn) const; + int16_t write_vertex_group(const OBJMesh &obj_mesh_data, + const int poly_index, + const int16_t last_poly_vertex_group) const; + void write_poly_elements(const OBJMesh &obj_mesh_data, + std::function<const char *(int)> matname_fn); + void write_edges_indices(const OBJMesh &obj_mesh_data) const; + void write_nurbs_curve(const OBJCurve &obj_nurbs_data) const; + + void update_index_offsets(const OBJMesh &obj_mesh_data); + + private: + using func_vert_uv_normal_indices = void (OBJWriter::*)(Span<int> vert_indices, + Span<int> uv_indices, + Span<int> normal_indices) const; + func_vert_uv_normal_indices get_poly_element_writer(const int total_uv_vertices) const; + + void write_vert_uv_normal_indices(Span<int> vert_indices, + Span<int> uv_indices, + Span<int> normal_indices) const; + void write_vert_normal_indices(Span<int> vert_indices, + Span<int> /*uv_indices*/, + Span<int> normal_indices) const; + void write_vert_uv_indices(Span<int> vert_indices, + Span<int> uv_indices, + Span<int> /*normal_indices*/) const; + void write_vert_indices(Span<int> vert_indices, + Span<int> /*uv_indices*/, + Span<int> /*normal_indices*/) const; +}; + +/** + * Responsible for writing a .MTL file. + */ +class MTLWriter : NonMovable, NonCopyable { + private: + std::unique_ptr<FileHandler<eFileType::MTL>> file_handler_ = nullptr; + std::string mtl_filepath_; + Vector<MTLMaterial> mtlmaterials_; + /* Map from a Material* to an index into mtlmaterials_. */ + Map<const Material *, int> material_map_; + + public: + MTLWriter(const char *obj_filepath) noexcept(false); + + void write_header(const char *blen_filepath) const; + void write_materials(); + StringRefNull mtl_file_path() const; + Vector<int> add_materials(const OBJMesh &mesh_to_export); + const char *mtlmaterial_name(int index); + + private: + void write_bsdf_properties(const MTLMaterial &mtl_material); + void write_texture_map(const MTLMaterial &mtl_material, + const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map); +}; +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_io.hh b/source/blender/io/wavefront_obj/exporter/obj_export_io.hh new file mode 100644 index 00000000000..a173fe4b268 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_io.hh @@ -0,0 +1,343 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include <cstdio> +#include <string> +#include <system_error> +#include <type_traits> + +#include "BLI_compiler_attrs.h" +#include "BLI_string_ref.hh" +#include "BLI_utility_mixins.hh" + +namespace blender::io::obj { + +enum class eFileType { + OBJ, + MTL, +}; + +enum class eOBJSyntaxElement { + vertex_coords, + uv_vertex_coords, + normal, + poly_element_begin, + vertex_uv_normal_indices, + vertex_normal_indices, + vertex_uv_indices, + vertex_indices, + poly_element_end, + poly_usemtl, + edge, + cstype, + nurbs_degree, + curve_element_begin, + curve_element_end, + nurbs_parameter_begin, + nurbs_parameters, + nurbs_parameter_end, + nurbs_group_end, + new_line, + mtllib, + smooth_group, + object_group, + object_name, + /* Use rarely. New line is NOT included for string. */ + string, +}; + +enum class eMTLSyntaxElement { + newmtl, + Ni, + d, + Ns, + illum, + Ka, + Kd, + Ks, + Ke, + map_Kd, + map_Ks, + map_Ns, + map_d, + map_refl, + map_Ke, + map_Bump, + /* Use rarely. New line is NOT included for string. */ + string, +}; + +template<eFileType filetype> struct FileTypeTraits; + +template<> struct FileTypeTraits<eFileType::OBJ> { + using SyntaxType = eOBJSyntaxElement; +}; + +template<> struct FileTypeTraits<eFileType::MTL> { + using SyntaxType = eMTLSyntaxElement; +}; + +template<eFileType type> struct Formatting { + const char *fmt = nullptr; + const int total_args = 0; + /* Fail to compile by default. */ + const bool is_type_valid = false; +}; + +/** + * Type dependent but always false. Use to add a conditional compile-time error. + */ +template<typename T> struct always_false : std::false_type { +}; + +template<typename... T> +constexpr bool is_type_float = (... && std::is_floating_point_v<std::decay_t<T>>); + +template<typename... T> +constexpr bool is_type_integral = (... && std::is_integral_v<std::decay_t<T>>); + +template<typename... T> +constexpr bool is_type_string_related = (... && std::is_constructible_v<std::string, T>); + +template<eFileType filetype, typename... T> +constexpr std::enable_if_t<filetype == eFileType::OBJ, Formatting<filetype>> +syntax_elem_to_formatting(const eOBJSyntaxElement key) +{ + switch (key) { + case eOBJSyntaxElement::vertex_coords: { + return {"v %f %f %f\n", 3, is_type_float<T...>}; + } + case eOBJSyntaxElement::uv_vertex_coords: { + return {"vt %f %f\n", 2, is_type_float<T...>}; + } + case eOBJSyntaxElement::normal: { + return {"vn %f %f %f\n", 3, is_type_float<T...>}; + } + case eOBJSyntaxElement::poly_element_begin: { + return {"f", 0, is_type_string_related<T...>}; + } + case eOBJSyntaxElement::vertex_uv_normal_indices: { + return {" %d/%d/%d", 3, is_type_integral<T...>}; + } + case eOBJSyntaxElement::vertex_normal_indices: { + return {" %d//%d", 2, is_type_integral<T...>}; + } + case eOBJSyntaxElement::vertex_uv_indices: { + return {" %d/%d", 2, is_type_integral<T...>}; + } + case eOBJSyntaxElement::vertex_indices: { + return {" %d", 1, is_type_integral<T...>}; + } + case eOBJSyntaxElement::poly_usemtl: { + return {"usemtl %s\n", 1, is_type_string_related<T...>}; + } + case eOBJSyntaxElement::edge: { + return {"l %d %d\n", 2, is_type_integral<T...>}; + } + case eOBJSyntaxElement::cstype: { + return {"cstype bspline\n", 0, is_type_string_related<T...>}; + } + case eOBJSyntaxElement::nurbs_degree: { + return {"deg %d\n", 1, is_type_integral<T...>}; + } + case eOBJSyntaxElement::curve_element_begin: { + return {"curv 0.0 1.0", 0, is_type_string_related<T...>}; + } + case eOBJSyntaxElement::nurbs_parameter_begin: { + return {"parm 0.0", 0, is_type_string_related<T...>}; + } + case eOBJSyntaxElement::nurbs_parameters: { + return {" %f", 1, is_type_float<T...>}; + } + case eOBJSyntaxElement::nurbs_parameter_end: { + return {" 1.0\n", 0, is_type_string_related<T...>}; + } + case eOBJSyntaxElement::nurbs_group_end: { + return {"end\n", 0, is_type_string_related<T...>}; + } + case eOBJSyntaxElement::poly_element_end: { + ATTR_FALLTHROUGH; + } + case eOBJSyntaxElement::curve_element_end: { + ATTR_FALLTHROUGH; + } + case eOBJSyntaxElement::new_line: { + return {"\n", 0, is_type_string_related<T...>}; + } + case eOBJSyntaxElement::mtllib: { + return {"mtllib %s\n", 1, is_type_string_related<T...>}; + } + case eOBJSyntaxElement::smooth_group: { + return {"s %d\n", 1, is_type_integral<T...>}; + } + case eOBJSyntaxElement::object_group: { + return {"g %s\n", 1, is_type_string_related<T...>}; + } + case eOBJSyntaxElement::object_name: { + return {"o %s\n", 1, is_type_string_related<T...>}; + } + case eOBJSyntaxElement::string: { + return {"%s", 1, is_type_string_related<T...>}; + } + } +} + +template<eFileType filetype, typename... T> +constexpr std::enable_if_t<filetype == eFileType::MTL, Formatting<filetype>> +syntax_elem_to_formatting(const eMTLSyntaxElement key) +{ + switch (key) { + case eMTLSyntaxElement::newmtl: { + return {"newmtl %s\n", 1, is_type_string_related<T...>}; + } + case eMTLSyntaxElement::Ni: { + return {"Ni %.6f\n", 1, is_type_float<T...>}; + } + case eMTLSyntaxElement::d: { + return {"d %.6f\n", 1, is_type_float<T...>}; + } + case eMTLSyntaxElement::Ns: { + return {"Ns %.6f\n", 1, is_type_float<T...>}; + } + case eMTLSyntaxElement::illum: { + return {"illum %d\n", 1, is_type_integral<T...>}; + } + case eMTLSyntaxElement::Ka: { + return {"Ka %.6f %.6f %.6f\n", 3, is_type_float<T...>}; + } + case eMTLSyntaxElement::Kd: { + return {"Kd %.6f %.6f %.6f\n", 3, is_type_float<T...>}; + } + case eMTLSyntaxElement::Ks: { + return {"Ks %.6f %.6f %.6f\n", 3, is_type_float<T...>}; + } + case eMTLSyntaxElement::Ke: { + return {"Ke %.6f %.6f %.6f\n", 3, is_type_float<T...>}; + } + /* Keep only one space between options since filepaths may have leading spaces too. */ + case eMTLSyntaxElement::map_Kd: { + return {"map_Kd %s %s\n", 2, is_type_string_related<T...>}; + } + case eMTLSyntaxElement::map_Ks: { + return {"map_Ks %s %s\n", 2, is_type_string_related<T...>}; + } + case eMTLSyntaxElement::map_Ns: { + return {"map_Ns %s %s\n", 2, is_type_string_related<T...>}; + } + case eMTLSyntaxElement::map_d: { + return {"map_d %s %s\n", 2, is_type_string_related<T...>}; + } + case eMTLSyntaxElement::map_refl: { + return {"map_refl %s %s\n", 2, is_type_string_related<T...>}; + } + case eMTLSyntaxElement::map_Ke: { + return {"map_Ke %s %s\n", 2, is_type_string_related<T...>}; + } + case eMTLSyntaxElement::map_Bump: { + return {"map_Bump %s %s\n", 2, is_type_string_related<T...>}; + } + case eMTLSyntaxElement::string: { + return {"%s", 1, is_type_string_related<T...>}; + } + } +} + +template<eFileType filetype> class FileHandler : NonCopyable, NonMovable { + private: + FILE *outfile_ = nullptr; + std::string outfile_path_; + + public: + FileHandler(std::string outfile_path) noexcept(false) : outfile_path_(std::move(outfile_path)) + { + outfile_ = std::fopen(outfile_path_.c_str(), "w"); + if (!outfile_) { + throw std::system_error(errno, std::system_category(), "Cannot open file"); + } + } + + ~FileHandler() + { + if (outfile_ && std::fclose(outfile_)) { + std::cerr << "Error: could not close the file '" << outfile_path_ + << "' properly, it may be corrupted." << std::endl; + } + } + + template<typename FileTypeTraits<filetype>::SyntaxType key, typename... T> + constexpr void write(T &&...args) const + { + constexpr Formatting<filetype> fmt_nargs_valid = syntax_elem_to_formatting<filetype, T...>( + key); + write__impl<fmt_nargs_valid.total_args>(fmt_nargs_valid.fmt, std::forward<T>(args)...); + /* Types of all arguments and the number of arguments should match + * what the formatting specifies. */ + return std::enable_if_t < fmt_nargs_valid.is_type_valid && + (sizeof...(T) == fmt_nargs_valid.total_args), + void > (); + } + + private: + /* Remove this after upgrading to C++20. */ + template<typename T> using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>; + + /** + * Make #std::string etc., usable for fprintf-family. + * \return: `const char *` or the original argument if the argument is + * not related to #std::string. + */ + template<typename T> constexpr auto string_to_primitive(T &&arg) const + { + if constexpr (std::is_same_v<remove_cvref_t<T>, std::string> || + std::is_same_v<remove_cvref_t<T>, blender::StringRefNull>) { + return arg.c_str(); + } + else if constexpr (std::is_same_v<remove_cvref_t<T>, blender::StringRef>) { + BLI_STATIC_ASSERT( + (always_false<T>::value), + "Null-terminated string not present. Please use blender::StringRefNull instead."); + /* Another trick to cause a compile-time error: returning nothing to #std::printf. */ + return; + } + else { + return std::forward<T>(arg); + } + } + + template<int total_args, typename... T> + constexpr std::enable_if_t<(total_args != 0), void> write__impl(const char *fmt, + T &&...args) const + { + std::fprintf(outfile_, fmt, string_to_primitive(std::forward<T>(args))...); + } + template<int total_args, typename... T> + constexpr std::enable_if_t<(total_args == 0), void> write__impl(const char *fmt, + T &&...args) const + { + std::fputs(fmt, outfile_); + } +}; + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc new file mode 100644 index 00000000000..7b7c5a7c4f1 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc @@ -0,0 +1,490 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include "BKE_customdata.h" +#include "BKE_lib_id.h" +#include "BKE_material.h" +#include "BKE_mesh.h" +#include "BKE_mesh_mapping.h" +#include "BKE_object.h" + +#include "BLI_listbase.h" +#include "BLI_math.h" + +#include "DEG_depsgraph_query.h" + +#include "DNA_material_types.h" +#include "DNA_mesh_types.h" +#include "DNA_modifier_types.h" +#include "DNA_object_types.h" + +#include "obj_export_mesh.hh" + +namespace blender::io::obj { +/** + * Store evaluated Object and Mesh pointers. Conditionally triangulate a mesh, or + * create a new Mesh from a Curve. + */ +OBJMesh::OBJMesh(Depsgraph *depsgraph, const OBJExportParams &export_params, Object *mesh_object) +{ + export_object_eval_ = DEG_get_evaluated_object(depsgraph, mesh_object); + export_mesh_eval_ = BKE_object_get_evaluated_mesh(export_object_eval_); + mesh_eval_needs_free_ = false; + + if (!export_mesh_eval_) { + /* Curves and NURBS surfaces need a new mesh when they're + * exported in the form of vertices and edges. + */ + export_mesh_eval_ = BKE_mesh_new_from_object(depsgraph, export_object_eval_, true, true); + /* Since a new mesh been allocated, it needs to be freed in the destructor. */ + mesh_eval_needs_free_ = true; + } + if (export_params.export_triangulated_mesh && + ELEM(export_object_eval_->type, OB_MESH, OB_SURF)) { + std::tie(export_mesh_eval_, mesh_eval_needs_free_) = triangulate_mesh_eval(); + } + set_world_axes_transform(export_params.forward_axis, export_params.up_axis); +} + +/** + * Free new meshes allocated for triangulated meshes, or Curve converted to Mesh. + */ +OBJMesh::~OBJMesh() +{ + free_mesh_if_needed(); + if (poly_smooth_groups_) { + MEM_freeN(poly_smooth_groups_); + } +} + +/** + * Free the mesh if _the exporter_ created it. + */ +void OBJMesh::free_mesh_if_needed() +{ + if (mesh_eval_needs_free_ && export_mesh_eval_) { + BKE_id_free(nullptr, export_mesh_eval_); + } +} + +/** + * Allocate a new Mesh with triangulated polygons. + * + * The returned mesh can be the same as the old one. + * \return Owning pointer to the new Mesh, and whether a new Mesh was created. + */ +std::pair<Mesh *, bool> OBJMesh::triangulate_mesh_eval() +{ + if (export_mesh_eval_->totpoly <= 0) { + return {export_mesh_eval_, false}; + } + const struct BMeshCreateParams bm_create_params = {0u}; + const struct BMeshFromMeshParams bm_convert_params = {1u, 0, 0, 0}; + /* Lower threshold where triangulation of a polygon starts, i.e. a quadrilateral will be + * triangulated here. */ + const int triangulate_min_verts = 4; + + unique_bmesh_ptr bmesh( + BKE_mesh_to_bmesh_ex(export_mesh_eval_, &bm_create_params, &bm_convert_params)); + BM_mesh_triangulate(bmesh.get(), + MOD_TRIANGULATE_NGON_BEAUTY, + MOD_TRIANGULATE_QUAD_SHORTEDGE, + triangulate_min_verts, + false, + nullptr, + nullptr, + nullptr); + + Mesh *triangulated = BKE_mesh_from_bmesh_for_eval_nomain( + bmesh.get(), nullptr, export_mesh_eval_); + free_mesh_if_needed(); + return {triangulated, true}; +} + +/** + * Set the final transform after applying axes settings and an Object's world transform. + */ +void OBJMesh::set_world_axes_transform(const eTransformAxisForward forward, + const eTransformAxisUp up) +{ + float axes_transform[3][3]; + unit_m3(axes_transform); + /* +Y-forward and +Z-up are the default Blender axis settings. */ + mat3_from_axis_conversion(OBJ_AXIS_Y_FORWARD, OBJ_AXIS_Z_UP, forward, up, axes_transform); + /* mat3_from_axis_conversion returns a transposed matrix! */ + transpose_m3(axes_transform); + mul_m4_m3m4(world_and_axes_transform_, axes_transform, export_object_eval_->obmat); + /* mul_m4_m3m4 does not transform last row of obmat, i.e. location data. */ + mul_v3_m3v3(world_and_axes_transform_[3], axes_transform, export_object_eval_->obmat[3]); + world_and_axes_transform_[3][3] = export_object_eval_->obmat[3][3]; +} + +int OBJMesh::tot_vertices() const +{ + return export_mesh_eval_->totvert; +} + +int OBJMesh::tot_polygons() const +{ + return export_mesh_eval_->totpoly; +} + +int OBJMesh::tot_uv_vertices() const +{ + return tot_uv_vertices_; +} + +int OBJMesh::tot_edges() const +{ + return export_mesh_eval_->totedge; +} + +/** + * \return Total materials in the object. + */ +int16_t OBJMesh::tot_materials() const +{ + return export_mesh_eval_->totcol; +} + +/** + * \return Smooth group of the polygon at the given index. + */ +int OBJMesh::ith_smooth_group(const int poly_index) const +{ + /* Calculate smooth groups first: #OBJMesh::calc_smooth_groups. */ + BLI_assert(tot_smooth_groups_ != -NEGATIVE_INIT); + BLI_assert(poly_smooth_groups_); + return poly_smooth_groups_[poly_index]; +} + +void OBJMesh::ensure_mesh_normals() const +{ + BKE_mesh_ensure_normals(export_mesh_eval_); + BKE_mesh_calc_normals_split(export_mesh_eval_); +} + +void OBJMesh::ensure_mesh_edges() const +{ + BKE_mesh_calc_edges(export_mesh_eval_, true, false); + BKE_mesh_calc_edges_loose(export_mesh_eval_); +} + +/** + * Calculate smooth groups of a smooth-shaded object. + * \return A polygon aligned array of smooth group numbers. + */ +void OBJMesh::calc_smooth_groups(const bool use_bitflags) +{ + poly_smooth_groups_ = BKE_mesh_calc_smoothgroups(export_mesh_eval_->medge, + export_mesh_eval_->totedge, + export_mesh_eval_->mpoly, + export_mesh_eval_->totpoly, + export_mesh_eval_->mloop, + export_mesh_eval_->totloop, + &tot_smooth_groups_, + use_bitflags); +} + +/** + * Return mat_nr-th material of the object. The given index should be zero-based. + */ +const Material *OBJMesh::get_object_material(const int16_t mat_nr) const +{ + /* "+ 1" as material getter needs one-based indices. */ + const Material *r_mat = BKE_object_material_get(export_object_eval_, mat_nr + 1); +#ifdef DEBUG + if (!r_mat) { + std::cerr << "Material not found for mat_nr = " << mat_nr << std::endl; + } +#endif + return r_mat; +} + +bool OBJMesh::is_ith_poly_smooth(const int poly_index) const +{ + return export_mesh_eval_->mpoly[poly_index].flag & ME_SMOOTH; +} + +/** + * Returns a zero-based index of a polygon's material indexing into + * the Object's material slots. + */ +int16_t OBJMesh::ith_poly_matnr(const int poly_index) const +{ + BLI_assert(poly_index < export_mesh_eval_->totpoly); + const int16_t r_mat_nr = export_mesh_eval_->mpoly[poly_index].mat_nr; + return r_mat_nr >= 0 ? r_mat_nr : NOT_FOUND; +} + +/** + * Get object name as it appears in the outliner. + */ +const char *OBJMesh::get_object_name() const +{ + return export_object_eval_->id.name + 2; +} + +/** + * Get Object's Mesh's name. + */ +const char *OBJMesh::get_object_mesh_name() const +{ + return export_mesh_eval_->id.name + 2; +} + +/** + * Get object's material (at the given index) name. The given index should be zero-based. + */ +const char *OBJMesh::get_object_material_name(const int16_t mat_nr) const +{ + const Material *mat = get_object_material(mat_nr); + if (!mat) { + return nullptr; + } + return mat->id.name + 2; +} + +/** + * Calculate coordinates of the vertex at the given index. + */ +float3 OBJMesh::calc_vertex_coords(const int vert_index, const float scaling_factor) const +{ + float3 r_coords; + copy_v3_v3(r_coords, export_mesh_eval_->mvert[vert_index].co); + mul_v3_fl(r_coords, scaling_factor); + mul_m4_v3(world_and_axes_transform_, r_coords); + return r_coords; +} + +/** + * Calculate vertex indices of all vertices of the polygon at the given index. + */ +Vector<int> OBJMesh::calc_poly_vertex_indices(const int poly_index) const +{ + const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index]; + const MLoop *mloop = &export_mesh_eval_->mloop[mpoly.loopstart]; + const int totloop = mpoly.totloop; + Vector<int> r_poly_vertex_indices(totloop); + for (int loop_index = 0; loop_index < totloop; loop_index++) { + r_poly_vertex_indices[loop_index] = mloop[loop_index].v; + } + return r_poly_vertex_indices; +} + +/** + * Calculate UV vertex coordinates of an Object. + * + * \note Also store the UV vertex indices in the member variable. + */ +void OBJMesh::store_uv_coords_and_indices(Vector<std::array<float, 2>> &r_uv_coords) +{ + const MPoly *mpoly = export_mesh_eval_->mpoly; + const MLoop *mloop = export_mesh_eval_->mloop; + const int totpoly = export_mesh_eval_->totpoly; + const int totvert = export_mesh_eval_->totvert; + const MLoopUV *mloopuv = static_cast<MLoopUV *>( + CustomData_get_layer(&export_mesh_eval_->ldata, CD_MLOOPUV)); + if (!mloopuv) { + tot_uv_vertices_ = 0; + return; + } + const float limit[2] = {STD_UV_CONNECT_LIMIT, STD_UV_CONNECT_LIMIT}; + + UvVertMap *uv_vert_map = BKE_mesh_uv_vert_map_create( + mpoly, mloop, mloopuv, totpoly, totvert, limit, false, false); + + uv_indices_.resize(totpoly); + /* At least total vertices of a mesh will be present in its texture map. So + * reserve minimum space early. */ + r_uv_coords.reserve(totvert); + + tot_uv_vertices_ = 0; + 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); + for (; uv_vert; uv_vert = uv_vert->next) { + if (uv_vert->separate) { + tot_uv_vertices_ += 1; + } + const int vertices_in_poly = mpoly[uv_vert->poly_index].totloop; + + /* Store UV vertex coordinates. */ + r_uv_coords.resize(tot_uv_vertices_); + const int loopstart = mpoly[uv_vert->poly_index].loopstart; + Span<float> vert_uv_coords(mloopuv[loopstart + uv_vert->loop_of_poly_index].uv, 2); + r_uv_coords[tot_uv_vertices_ - 1][0] = vert_uv_coords[0]; + r_uv_coords[tot_uv_vertices_ - 1][1] = vert_uv_coords[1]; + + /* Store UV vertex indices. */ + uv_indices_[uv_vert->poly_index].resize(vertices_in_poly); + /* Keep indices zero-based and let the writer handle the "+ 1" as per OBJ spec. */ + uv_indices_[uv_vert->poly_index][uv_vert->loop_of_poly_index] = tot_uv_vertices_ - 1; + } + } + BKE_mesh_uv_vert_map_free(uv_vert_map); +} + +Span<int> OBJMesh::calc_poly_uv_indices(const int poly_index) const +{ + if (uv_indices_.size() <= 0) { + return {}; + } + BLI_assert(poly_index < export_mesh_eval_->totpoly); + BLI_assert(poly_index < uv_indices_.size()); + return uv_indices_[poly_index]; +} +/** + * Calculate polygon normal of a polygon at given index. + * + * Should be used for flat-shaded polygons. + */ +float3 OBJMesh::calc_poly_normal(const int poly_index) const +{ + float3 r_poly_normal; + const MPoly &poly = export_mesh_eval_->mpoly[poly_index]; + const MLoop &mloop = export_mesh_eval_->mloop[poly.loopstart]; + const MVert &mvert = *(export_mesh_eval_->mvert); + BKE_mesh_calc_poly_normal(&poly, &mloop, &mvert, r_poly_normal); + mul_mat3_m4_v3(world_and_axes_transform_, r_poly_normal); + return r_poly_normal; +} + +/** + * Calculate loop normals of a polygon at the given index. + * + * Should be used for smooth-shaded polygons. + */ +void OBJMesh::calc_loop_normals(const int poly_index, Vector<float3> &r_loop_normals) const +{ + r_loop_normals.clear(); + const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index]; + const float( + *lnors)[3] = (const float(*)[3])(CustomData_get_layer(&export_mesh_eval_->ldata, CD_NORMAL)); + for (int loop_of_poly = 0; loop_of_poly < mpoly.totloop; loop_of_poly++) { + float3 loop_normal; + copy_v3_v3(loop_normal, lnors[mpoly.loopstart + loop_of_poly]); + mul_mat3_m4_v3(world_and_axes_transform_, loop_normal); + r_loop_normals.append(loop_normal); + } +} + +/** + * Calculate a polygon's polygon/loop normal indices. + * \param object_tot_prev_normals Number of normals of this Object written so far. + * \return Number of distinct normal indices. + */ +std::pair<int, Vector<int>> OBJMesh::calc_poly_normal_indices( + const int poly_index, const int object_tot_prev_normals) const +{ + const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index]; + const int totloop = mpoly.totloop; + Vector<int> r_poly_normal_indices(totloop); + + if (is_ith_poly_smooth(poly_index)) { + for (int poly_loop_index = 0; poly_loop_index < totloop; poly_loop_index++) { + /* Using polygon loop index is fine because polygon/loop normals and their normal indices are + * written by looping over #Mesh.mpoly /#Mesh.mloop in the same order. */ + r_poly_normal_indices[poly_loop_index] = object_tot_prev_normals + poly_loop_index; + } + /* For a smooth-shaded polygon, #Mesh.totloop -many loop normals are written. */ + return {totloop, r_poly_normal_indices}; + } + for (int poly_loop_index = 0; poly_loop_index < totloop; poly_loop_index++) { + r_poly_normal_indices[poly_loop_index] = object_tot_prev_normals; + } + /* For a flat-shaded polygon, one polygon normal is written. */ + return {1, r_poly_normal_indices}; +} + +/** + * Find the index of the vertex group with the maximum number of vertices in a polygon. + * The index indices into the #Object.defbase. + * + * If two or more groups have the same number of vertices (maximum), group name depends on the + * implementation of #std::max_element. + */ +int16_t OBJMesh::get_poly_deform_group_index(const int poly_index) const +{ + BLI_assert(poly_index < export_mesh_eval_->totpoly); + const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index]; + const MLoop *mloop = &export_mesh_eval_->mloop[mpoly.loopstart]; + const int tot_deform_groups = BLI_listbase_count(&export_object_eval_->defbase); + /* Indices of the vector index into deform groups of an object; values are the] + * number of vertex members in one deform group. */ + Vector<int16_t> deform_group_members(tot_deform_groups, 0); + /* Whether at least one vertex in the polygon belongs to any group. */ + bool found_group = false; + + const MDeformVert *dvert_orig = static_cast<MDeformVert *>( + CustomData_get_layer(&export_mesh_eval_->vdata, CD_MDEFORMVERT)); + if (!dvert_orig) { + return NOT_FOUND; + } + + const MDeformWeight *curr_weight = nullptr; + const MDeformVert *dvert = nullptr; + for (int loop_index = 0; loop_index < mpoly.totloop; loop_index++) { + dvert = &dvert_orig[(mloop + loop_index)->v]; + curr_weight = dvert->dw; + if (curr_weight) { + bDeformGroup *vertex_group = static_cast<bDeformGroup *>( + BLI_findlink((&export_object_eval_->defbase), curr_weight->def_nr)); + if (vertex_group) { + deform_group_members[curr_weight->def_nr] += 1; + found_group = true; + } + } + } + + if (!found_group) { + return NOT_FOUND; + } + /* Index of the group with maximum vertices. */ + int16_t max_idx = std::max_element(deform_group_members.begin(), deform_group_members.end()) - + deform_group_members.begin(); + return max_idx; +} + +/** + * Find the name of the vertex deform group at the given index. + * The index indices into the #Object.defbase. + */ +const char *OBJMesh::get_poly_deform_group_name(const int16_t def_group_index) const +{ + const bDeformGroup &vertex_group = *( + static_cast<bDeformGroup *>(BLI_findlink(&export_object_eval_->defbase, def_group_index))); + return vertex_group.name; +} + +/** + * Calculate vertex indices of an edge's corners if it is a loose edge. + */ +std::optional<std::array<int, 2>> OBJMesh::calc_loose_edge_vert_indices(const int edge_index) const +{ + const MEdge &edge = export_mesh_eval_->medge[edge_index]; + if (edge.flag & ME_LOOSEEDGE) { + return std::array<int, 2>{static_cast<int>(edge.v1), static_cast<int>(edge.v2)}; + } + return std::nullopt; +} +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh new file mode 100644 index 00000000000..6e6cf6383a9 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh @@ -0,0 +1,134 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include <optional> + +#include "BLI_float3.hh" +#include "BLI_utility_mixins.hh" +#include "BLI_vector.hh" + +#include "bmesh.h" +#include "bmesh_tools.h" + +#include "DNA_material_types.h" +#include "DNA_mesh_types.h" +#include "DNA_meshdata_types.h" + +#include "IO_wavefront_obj.h" + +namespace blender::io::obj { +/* Denote absence for usually non-negative numbers. */ +const int NOT_FOUND = -1; +/* Any negative number other than `NOT_FOUND` to initialise usually non-negative numbers. */ +const int NEGATIVE_INIT = -10; + +/** + * #std::unique_ptr deleter for BMesh. + */ +struct CustomBMeshDeleter { + void operator()(BMesh *bmesh) + { + if (bmesh) { + BM_mesh_free(bmesh); + } + } +}; + +using unique_bmesh_ptr = std::unique_ptr<BMesh, CustomBMeshDeleter>; + +class OBJMesh : NonCopyable { + private: + Object *export_object_eval_; + Mesh *export_mesh_eval_; + /** + * For curves which are converted to mesh, and triangulated meshes, a new mesh is allocated. + */ + bool mesh_eval_needs_free_ = false; + /** + * Final transform of an object obtained from export settings (up_axis, forward_axis) and the + * object's world transform matrix. + */ + float world_and_axes_transform_[4][4]; + + /** + * Total UV vertices in a mesh's texture map. + */ + int tot_uv_vertices_ = 0; + /** + * Per-polygon-per-vertex UV vertex indices. + */ + Vector<Vector<int>> uv_indices_; + /** + * Total smooth groups in an object. + */ + int tot_smooth_groups_ = NEGATIVE_INIT; + /** + * Polygon aligned array of their smooth groups. + */ + int *poly_smooth_groups_ = nullptr; + + public: + OBJMesh(Depsgraph *depsgraph, const OBJExportParams &export_params, Object *mesh_object); + ~OBJMesh(); + + int tot_vertices() const; + int tot_polygons() const; + int tot_uv_vertices() const; + int tot_edges() const; + + int16_t tot_materials() const; + const Material *get_object_material(const int16_t mat_nr) const; + int16_t ith_poly_matnr(const int poly_index) const; + + void ensure_mesh_normals() const; + void ensure_mesh_edges() const; + + void calc_smooth_groups(const bool use_bitflags); + int ith_smooth_group(const int poly_index) const; + bool is_ith_poly_smooth(const int poly_index) const; + + const char *get_object_name() const; + const char *get_object_mesh_name() const; + const char *get_object_material_name(const int16_t mat_nr) const; + + float3 calc_vertex_coords(const int vert_index, const float scaling_factor) const; + Vector<int> calc_poly_vertex_indices(const int poly_index) const; + void store_uv_coords_and_indices(Vector<std::array<float, 2>> &r_uv_coords); + Span<int> calc_poly_uv_indices(const int poly_index) const; + float3 calc_poly_normal(const int poly_index) const; + std::pair<int, Vector<int>> calc_poly_normal_indices(const int poly_index, + const int object_tot_prev_normals) const; + void calc_loop_normals(const int poly_index, Vector<float3> &r_loop_normals) const; + int16_t get_poly_deform_group_index(const int poly_index) const; + const char *get_poly_deform_group_name(const int16_t def_group_index) const; + + std::optional<std::array<int, 2>> calc_loose_edge_vert_indices(const int edge_index) const; + + private: + void free_mesh_if_needed(); + std::pair<Mesh *, bool> triangulate_mesh_eval(); + void set_world_axes_transform(const eTransformAxisForward forward, const eTransformAxisUp up); +}; +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc b/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc new file mode 100644 index 00000000000..1732fd5766c --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc @@ -0,0 +1,365 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include "BKE_image.h" +#include "BKE_node.h" + +#include "BLI_float3.hh" +#include "BLI_map.hh" +#include "BLI_path_util.h" + +#include "DNA_material_types.h" +#include "DNA_node_types.h" + +#include "NOD_node_tree_ref.hh" + +#include "obj_export_mesh.hh" +#include "obj_export_mtl.hh" + +namespace blender::io::obj { + +/** + * Copy a float property of the given type from the bNode to given buffer. + */ +static void copy_property_from_node(const eNodeSocketDatatype property_type, + const bNode *node, + const char *identifier, + MutableSpan<float> r_property) +{ + if (!node) { + return; + } + bNodeSocket *socket{nodeFindSocket(node, SOCK_IN, identifier)}; + BLI_assert(socket && socket->type == property_type); + if (!socket) { + return; + } + switch (property_type) { + case SOCK_FLOAT: { + BLI_assert(r_property.size() == 1); + bNodeSocketValueFloat *socket_def_value = static_cast<bNodeSocketValueFloat *>( + socket->default_value); + r_property[0] = socket_def_value->value; + break; + } + case SOCK_RGBA: { + BLI_assert(r_property.size() == 3); + bNodeSocketValueRGBA *socket_def_value = static_cast<bNodeSocketValueRGBA *>( + socket->default_value); + copy_v3_v3(r_property.data(), socket_def_value->value); + break; + } + case SOCK_VECTOR: { + BLI_assert(r_property.size() == 3); + bNodeSocketValueVector *socket_def_value = static_cast<bNodeSocketValueVector *>( + socket->default_value); + copy_v3_v3(r_property.data(), socket_def_value->value); + break; + } + default: { + /* Other socket types are not handled here. */ + BLI_assert(0); + break; + } + } +} + +/** + * Collect all the source sockets linked to the destination socket in a destination node. + */ +static void linked_sockets_to_dest_id(const bNode *dest_node, + const nodes::NodeTreeRef &node_tree, + StringRefNull dest_socket_id, + Vector<const nodes::OutputSocketRef *> &r_linked_sockets) +{ + r_linked_sockets.clear(); + if (!dest_node) { + return; + } + Span<const nodes::NodeRef *> object_dest_nodes = node_tree.nodes_by_type(dest_node->idname); + Span<const nodes::InputSocketRef *> dest_inputs = object_dest_nodes.first()->inputs(); + const nodes::InputSocketRef *dest_socket = nullptr; + for (const nodes::InputSocketRef *curr_socket : dest_inputs) { + if (STREQ(curr_socket->bsocket()->identifier, dest_socket_id.c_str())) { + dest_socket = curr_socket; + break; + } + } + if (dest_socket) { + Span<const nodes::OutputSocketRef *> linked_sockets = dest_socket->directly_linked_sockets(); + r_linked_sockets.resize(linked_sockets.size()); + r_linked_sockets = linked_sockets; + } +} + +/** + * From a list of sockets, get the parent node which is of the given node type. + */ +static const bNode *get_node_of_type(Span<const nodes::OutputSocketRef *> sockets_list, + const int node_type) +{ + for (const nodes::SocketRef *socket : sockets_list) { + const bNode *parent_node = socket->bnode(); + if (parent_node->typeinfo->type == node_type) { + return parent_node; + } + } + return nullptr; +} + +/** + * From a texture image shader node, get the image's filepath. + * Returned filepath is stripped of initial "//". If packed image is found, + * only the file "name" is returned. + */ +static const char *get_image_filepath(const bNode *tex_node) +{ + if (!tex_node) { + return nullptr; + } + Image *tex_image = reinterpret_cast<Image *>(tex_node->id); + if (!tex_image || !BKE_image_has_filepath(tex_image)) { + return nullptr; + } + const char *path = tex_image->filepath; + if (BKE_image_has_packedfile(tex_image)) { + /* Put image in the same directory as the .MTL file. */ + path = BLI_path_slash_rfind(path) + 1; + fprintf(stderr, + "Packed image found:'%s'. Unpack and place the image in the same " + "directory as the .MTL file.\n", + path); + } + if (path[0] == '/' && path[1] == '/') { + path += 2; + } + return path; +} + +/** + * Find the Principled-BSDF Node in nodetree. + * We only want one that feeds directly into a Material Output node + * (that is the behavior of the legacy Python exporter). + */ +static const nodes::NodeRef *find_bsdf_node(const nodes::NodeTreeRef *nodetree) +{ + if (!nodetree) { + return nullptr; + } + for (const nodes::NodeRef *node : nodetree->nodes_by_type("ShaderNodeOutputMaterial")) { + const nodes::InputSocketRef *node_input_socket0 = node->inputs()[0]; + for (const nodes::OutputSocketRef *out_sock : node_input_socket0->directly_linked_sockets()) { + const nodes::NodeRef &in_node = out_sock->node(); + if (in_node.typeinfo()->type == SH_NODE_BSDF_PRINCIPLED) { + return &in_node; + } + } + } + return nullptr; +} + +/** + * Store properties found either in bNode or material into r_mtl_mat. + */ +static void store_bsdf_properties(const nodes::NodeRef *bsdf_node, + const Material *material, + MTLMaterial &r_mtl_mat) +{ + const bNode *bnode = nullptr; + if (bsdf_node) { + bnode = bsdf_node->bnode(); + } + + /* If p-BSDF is not present, fallback to #Object.Material. */ + float roughness = material->roughness; + if (bnode) { + copy_property_from_node(SOCK_FLOAT, bnode, "Roughness", {&roughness, 1}); + } + /* Emperical approximation. Importer should use the inverse of this method. */ + float spec_exponent = (1.0f - roughness) * 30; + spec_exponent *= spec_exponent; + + float specular = material->spec; + if (bnode) { + copy_property_from_node(SOCK_FLOAT, bnode, "Specular", {&specular, 1}); + } + + float metallic = material->metallic; + if (bnode) { + copy_property_from_node(SOCK_FLOAT, bnode, "Metallic", {&metallic, 1}); + } + + float refraction_index = 1.0f; + if (bnode) { + copy_property_from_node(SOCK_FLOAT, bnode, "IOR", {&refraction_index, 1}); + } + + float dissolved = material->a; + if (bnode) { + copy_property_from_node(SOCK_FLOAT, bnode, "Alpha", {&dissolved, 1}); + } + const bool transparent = dissolved != 1.0f; + + float3 diffuse_col = {material->r, material->g, material->b}; + if (bnode) { + copy_property_from_node(SOCK_RGBA, bnode, "Base Color", {diffuse_col, 3}); + } + + float3 emission_col{0.0f}; + float emission_strength = 0.0f; + if (bnode) { + copy_property_from_node(SOCK_FLOAT, bnode, "Emission Strength", {&emission_strength, 1}); + copy_property_from_node(SOCK_RGBA, bnode, "Emission", {emission_col, 3}); + } + mul_v3_fl(emission_col, emission_strength); + + /* See https://wikipedia.org/wiki/Wavefront_.obj_file for all possible values of illum. */ + /* Highlight on. */ + int illum = 2; + if (specular == 0.0f) { + /* Color on and Ambient on. */ + illum = 1; + } + else if (metallic > 0.0f) { + /* Metallic ~= Reflection. */ + if (transparent) { + /* Transparency: Refraction on, Reflection: ~~Fresnel off and Ray trace~~ on. */ + illum = 6; + } + else { + /* Reflection on and Ray trace on. */ + illum = 3; + } + } + else if (transparent) { + /* Transparency: Glass on, Reflection: Ray trace off */ + illum = 9; + } + r_mtl_mat.Ns = spec_exponent; + if (metallic != 0.0f) { + r_mtl_mat.Ka = {metallic, metallic, metallic}; + } + else { + r_mtl_mat.Ka = {1.0f, 1.0f, 1.0f}; + } + r_mtl_mat.Kd = diffuse_col; + r_mtl_mat.Ks = {specular, specular, specular}; + r_mtl_mat.Ke = emission_col; + r_mtl_mat.Ni = refraction_index; + r_mtl_mat.d = dissolved; + r_mtl_mat.illum = illum; +} + +/** + * Store image texture options and filepaths in r_mtl_mat. + */ +static void store_image_textures(const nodes::NodeRef *bsdf_node, + const nodes::NodeTreeRef *node_tree, + const Material *material, + MTLMaterial &r_mtl_mat) +{ + if (!material || !node_tree || !bsdf_node) { + /* No nodetree, no images, or no Principled BSDF node. */ + return; + } + const bNode *bnode = bsdf_node->bnode(); + + /* Normal Map Texture has two extra tasks of: + * - finding a Normal Map node before finding a texture node. + * - finding "Strength" property of the node for `-bm` option. + */ + + for (Map<const eMTLSyntaxElement, tex_map_XX>::MutableItem texture_map : + r_mtl_mat.texture_maps.items()) { + Vector<const nodes::OutputSocketRef *> linked_sockets; + const bNode *normal_map_node{nullptr}; + + if (texture_map.key == eMTLSyntaxElement::map_Bump) { + /* Find sockets linked to destination "Normal" socket in p-bsdf node. */ + linked_sockets_to_dest_id(bnode, *node_tree, "Normal", linked_sockets); + /* Among the linked sockets, find Normal Map shader node. */ + normal_map_node = get_node_of_type(linked_sockets, SH_NODE_NORMAL_MAP); + + /* Find sockets linked to "Color" socket in normal map node. */ + linked_sockets_to_dest_id(normal_map_node, *node_tree, "Color", linked_sockets); + } + else if (texture_map.key == eMTLSyntaxElement::map_Ke) { + float emission_strength = 0.0f; + copy_property_from_node(SOCK_FLOAT, bnode, "Emission Strength", {&emission_strength, 1}); + if (emission_strength == 0.0f) { + continue; + } + } + else { + /* Find sockets linked to the destination socket of interest, in p-bsdf node. */ + linked_sockets_to_dest_id( + bnode, *node_tree, texture_map.value.dest_socket_id, linked_sockets); + } + + /* Among the linked sockets, find Image Texture shader node. */ + const bNode *tex_node{get_node_of_type(linked_sockets, SH_NODE_TEX_IMAGE)}; + if (!tex_node) { + continue; + } + const char *tex_image_filepath = get_image_filepath(tex_node); + if (!tex_image_filepath) { + continue; + } + + /* Find "Mapping" node if connected to texture node. */ + linked_sockets_to_dest_id(tex_node, *node_tree, "Vector", linked_sockets); + const bNode *mapping = get_node_of_type(linked_sockets, SH_NODE_MAPPING); + + if (normal_map_node) { + copy_property_from_node( + SOCK_FLOAT, normal_map_node, "Strength", {&r_mtl_mat.map_Bump_strength, 1}); + } + /* Texture transform options. Only translation (origin offset, "-o") and scale + * ("-o") are supported. */ + copy_property_from_node(SOCK_VECTOR, mapping, "Location", {texture_map.value.translation, 3}); + copy_property_from_node(SOCK_VECTOR, mapping, "Scale", {texture_map.value.scale, 3}); + + texture_map.value.image_path = tex_image_filepath; + } +} + +MTLMaterial mtlmaterial_for_material(const Material *material) +{ + BLI_assert(material != nullptr); + MTLMaterial mtlmat; + mtlmat.name = std::string(material->id.name + 2); + std::replace(mtlmat.name.begin(), mtlmat.name.end(), ' ', '_'); + const nodes::NodeTreeRef *nodetree = nullptr; + if (material->nodetree) { + nodetree = new nodes::NodeTreeRef(material->nodetree); + } + const nodes::NodeRef *bsdf_node = find_bsdf_node(nodetree); + store_bsdf_properties(bsdf_node, material, mtlmat); + store_image_textures(bsdf_node, nodetree, material, mtlmat); + if (nodetree) { + delete nodetree; + } + return mtlmat; +} + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_mtl.hh b/source/blender/io/wavefront_obj/exporter/obj_export_mtl.hh new file mode 100644 index 00000000000..c95491bf41d --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mtl.hh @@ -0,0 +1,107 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include "BLI_float3.hh" +#include "BLI_map.hh" +#include "BLI_string_ref.hh" +#include "BLI_vector.hh" + +#include "DNA_node_types.h" +#include "obj_export_io.hh" + +namespace blender { +template<> struct DefaultHash<io::obj::eMTLSyntaxElement> { + uint64_t operator()(const io::obj::eMTLSyntaxElement value) const + { + return static_cast<uint64_t>(value); + } +}; + +} // namespace blender + +namespace blender::io::obj { +class OBJMesh; + +/** + * Generic container for texture node properties. + */ +struct tex_map_XX { + tex_map_XX(StringRef to_socket_id) : dest_socket_id(to_socket_id){}; + + /** Target socket which this texture node connects to. */ + const std::string dest_socket_id; + float3 translation{0.0f}; + float3 scale{1.0f}; + /* Only Flat and Smooth projections are supported. */ + int projection_type = SHD_PROJ_FLAT; + std::string image_path; + std::string mtl_dir_path; +}; + +/** + * Container suited for storing Material data for/from a .MTL file. + */ +struct MTLMaterial { + MTLMaterial() + { + texture_maps.add(eMTLSyntaxElement::map_Kd, tex_map_XX("Base Color")); + texture_maps.add(eMTLSyntaxElement::map_Ks, tex_map_XX("Specular")); + texture_maps.add(eMTLSyntaxElement::map_Ns, tex_map_XX("Roughness")); + texture_maps.add(eMTLSyntaxElement::map_d, tex_map_XX("Alpha")); + texture_maps.add(eMTLSyntaxElement::map_refl, tex_map_XX("Metallic")); + texture_maps.add(eMTLSyntaxElement::map_Ke, tex_map_XX("Emission")); + texture_maps.add(eMTLSyntaxElement::map_Bump, tex_map_XX("Normal")); + } + + /** + * Caller must ensure that the given lookup key exists in the Map. + * \return Texture map corresponding to the given ID. + */ + tex_map_XX &tex_map_of_type(const eMTLSyntaxElement key) + { + { + BLI_assert(texture_maps.contains_as(key)); + return texture_maps.lookup_as(key); + } + } + + std::string name; + /* Always check for negative values while importing or exporting. Use defaults if + * any value is negative. */ + float Ns{-1.0f}; + float3 Ka{-1.0f}; + float3 Kd{-1.0f}; + float3 Ks{-1.0f}; + float3 Ke{-1.0f}; + float Ni{-1.0f}; + float d{-1.0f}; + int illum{-1}; + Map<const eMTLSyntaxElement, tex_map_XX> texture_maps; + /** Only used for Normal Map node: "map_Bump". */ + float map_Bump_strength{-1.0f}; +}; + +MTLMaterial mtlmaterial_for_material(const Material *material); +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc new file mode 100644 index 00000000000..d21665ba040 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc @@ -0,0 +1,125 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include "BLI_float3.hh" +#include "BLI_listbase.h" +#include "BLI_math.h" + +#include "DEG_depsgraph.h" +#include "DEG_depsgraph_query.h" + +#include "IO_wavefront_obj.h" +#include "obj_export_nurbs.hh" + +namespace blender::io::obj { +OBJCurve::OBJCurve(const Depsgraph *depsgraph, + const OBJExportParams &export_params, + Object *curve_object) + : export_object_eval_(curve_object) +{ + export_object_eval_ = DEG_get_evaluated_object(depsgraph, curve_object); + export_curve_ = static_cast<Curve *>(export_object_eval_->data); + set_world_axes_transform(export_params.forward_axis, export_params.up_axis); +} + +/** + * Set the final transform after applying axes settings and an Object's world transform. + */ +void OBJCurve::set_world_axes_transform(const eTransformAxisForward forward, + const eTransformAxisUp up) +{ + float axes_transform[3][3]; + unit_m3(axes_transform); + /* +Y-forward and +Z-up are the Blender's default axis settings. */ + mat3_from_axis_conversion(OBJ_AXIS_Y_FORWARD, OBJ_AXIS_Z_UP, forward, up, axes_transform); + /* mat3_from_axis_conversion returns a transposed matrix! */ + transpose_m3(axes_transform); + mul_m4_m3m4(world_axes_transform_, axes_transform, export_object_eval_->obmat); + /* #mul_m4_m3m4 does not transform last row of #Object.obmat, i.e. location data. */ + mul_v3_m3v3(world_axes_transform_[3], axes_transform, export_object_eval_->obmat[3]); + world_axes_transform_[3][3] = export_object_eval_->obmat[3][3]; +} + +const char *OBJCurve::get_curve_name() const +{ + return export_object_eval_->id.name + 2; +} + +int OBJCurve::total_splines() const +{ + return BLI_listbase_count(&export_curve_->nurb); +} + +/** + * \param spline_index: Zero-based index of spline of interest. + * \return: Total vertices in a spline. + */ +int OBJCurve::total_spline_vertices(const int spline_index) const +{ + const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index)); + return nurb->pntsu * nurb->pntsv; +} + +/** + * Get coordinates of the vertex at the given index on the given spline. + */ +float3 OBJCurve::vertex_coordinates(const int spline_index, + const int vertex_index, + const float scaling_factor) const +{ + const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index)); + float3 r_coord; + const BPoint &bpoint = nurb->bp[vertex_index]; + copy_v3_v3(r_coord, bpoint.vec); + mul_m4_v3(world_axes_transform_, r_coord); + mul_v3_fl(r_coord, scaling_factor); + return r_coord; +} + +/** + * Get total control points of the NURBS spline at the given index. This is different than total + * vertices of a spline. + */ +int OBJCurve::total_spline_control_points(const int spline_index) const +{ + const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index)); + const int r_nurbs_degree = nurb->orderu - 1; + /* Total control points = Number of points in the curve (+ degree of the + * curve if it is cyclic). */ + int r_tot_control_points = nurb->pntsv * nurb->pntsu; + if (nurb->flagu & CU_NURB_CYCLIC) { + r_tot_control_points += r_nurbs_degree; + } + return r_tot_control_points; +} + +/** + * Get the degree of the NURBS spline at the given index. + */ +int OBJCurve::get_nurbs_degree(const int spline_index) const +{ + const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index)); + return nurb->orderu - 1; +} + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh new file mode 100644 index 00000000000..6a34891ca8a --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh @@ -0,0 +1,60 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include "BLI_utility_mixins.hh" + +#include "DNA_curve_types.h" + +namespace blender::io::obj { + +/** + * Provides access to the a Curve Object's properties. + * Only #CU_NURBS type is supported. + * + * \note Used for Curves to be exported in parameter form, and not converted to meshes. + */ +class OBJCurve : NonCopyable { + private: + const Object *export_object_eval_; + const Curve *export_curve_; + float world_axes_transform_[4][4]; + + public: + OBJCurve(const Depsgraph *depsgraph, const OBJExportParams &export_params, Object *curve_object); + + const char *get_curve_name() const; + int total_splines() const; + int total_spline_vertices(const int spline_index) const; + float3 vertex_coordinates(const int spline_index, + const int vertex_index, + const float scaling_factor) const; + int total_spline_control_points(const int spline_index) const; + int get_nurbs_degree(const int spline_index) const; + + private: + void set_world_axes_transform(const eTransformAxisForward forward, const eTransformAxisUp up); +}; + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_exporter.cc b/source/blender/io/wavefront_obj/exporter/obj_exporter.cc new file mode 100644 index 00000000000..1c59bd43aab --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_exporter.cc @@ -0,0 +1,310 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include <cstdio> +#include <exception> +#include <memory> + +#include "BKE_scene.h" + +#include "BLI_path_util.h" +#include "BLI_vector.hh" + +#include "DEG_depsgraph_query.h" + +#include "DNA_scene_types.h" + +#include "ED_object.h" + +#include "obj_export_mesh.hh" +#include "obj_export_nurbs.hh" +#include "obj_exporter.hh" + +#include "obj_export_file_writer.hh" + +namespace blender::io::obj { + +OBJDepsgraph::OBJDepsgraph(const bContext *C, const eEvaluationMode eval_mode) +{ + Scene *scene = CTX_data_scene(C); + Main *bmain = CTX_data_main(C); + ViewLayer *view_layer = CTX_data_view_layer(C); + if (eval_mode == DAG_EVAL_RENDER) { + depsgraph_ = DEG_graph_new(bmain, scene, view_layer, DAG_EVAL_RENDER); + needs_free_ = true; + DEG_graph_build_for_all_objects(depsgraph_); + BKE_scene_graph_evaluated_ensure(depsgraph_, bmain); + } + else { + depsgraph_ = CTX_data_ensure_evaluated_depsgraph(C); + needs_free_ = false; + } +} + +OBJDepsgraph::~OBJDepsgraph() +{ + if (needs_free_) { + DEG_graph_free(depsgraph_); + } +} + +Depsgraph *OBJDepsgraph::get() +{ + return depsgraph_; +} + +void OBJDepsgraph::update_for_newframe() +{ + BKE_scene_graph_update_for_newframe(depsgraph_); +} + +static void print_exception_error(const std::system_error &ex) +{ + std::cerr << ex.code().category().name() << ": " << ex.what() << ": " << ex.code().message() + << std::endl; +} + +/** + * Filter supported objects from the Scene. + * + * \note Curves are also stored with Meshes if export settings specify so. + */ +std::pair<Vector<std::unique_ptr<OBJMesh>>, Vector<std::unique_ptr<OBJCurve>>> +filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params) +{ + Vector<std::unique_ptr<OBJMesh>> r_exportable_meshes; + Vector<std::unique_ptr<OBJCurve>> r_exportable_nurbs; + const ViewLayer *view_layer = DEG_get_input_view_layer(depsgraph); + LISTBASE_FOREACH (const Base *, base, &view_layer->object_bases) { + Object *object_in_layer = base->object; + if (export_params.export_selected_objects && !(object_in_layer->base_flag & BASE_SELECTED)) { + continue; + } + switch (object_in_layer->type) { + case OB_SURF: + /* Export in mesh form: vertices and polygons. */ + ATTR_FALLTHROUGH; + case OB_MESH: { + r_exportable_meshes.append( + std::make_unique<OBJMesh>(depsgraph, export_params, object_in_layer)); + break; + } + case OB_CURVE: { + Curve *curve = static_cast<Curve *>(object_in_layer->data); + Nurb *nurb{static_cast<Nurb *>(curve->nurb.first)}; + if (!nurb) { + /* An empty curve. Not yet supported to export these as meshes. */ + if (export_params.export_curves_as_nurbs) { + r_exportable_nurbs.append( + std::make_unique<OBJCurve>(depsgraph, export_params, object_in_layer)); + } + break; + } + switch (nurb->type) { + case CU_NURBS: { + if (export_params.export_curves_as_nurbs) { + /* Export in parameter form: control points. */ + r_exportable_nurbs.append( + std::make_unique<OBJCurve>(depsgraph, export_params, object_in_layer)); + } + else { + /* Export in mesh form: edges and vertices. */ + r_exportable_meshes.append( + std::make_unique<OBJMesh>(depsgraph, export_params, object_in_layer)); + } + break; + } + case CU_BEZIER: { + /* Always export in mesh form: edges and vertices. */ + r_exportable_meshes.append( + std::make_unique<OBJMesh>(depsgraph, export_params, object_in_layer)); + break; + } + default: { + /* Other curve types are not supported. */ + break; + } + } + break; + } + default: { + /* Other object types are not supported. */ + break; + } + } + } + return {std::move(r_exportable_meshes), std::move(r_exportable_nurbs)}; +} + +static void write_mesh_objects(Vector<std::unique_ptr<OBJMesh>> exportable_as_mesh, + OBJWriter &obj_writer, + MTLWriter *mtl_writer, + const OBJExportParams &export_params) +{ + if (mtl_writer) { + obj_writer.write_mtllib_name(mtl_writer->mtl_file_path()); + } + + /* Smooth groups and UV vertex indices may make huge memory allocations, so they should be freed + * right after they're written, instead of waiting for #blender::Vector to clean them up after + * all the objects are exported. */ + for (StealUniquePtr<OBJMesh> obj_mesh : exportable_as_mesh) { + obj_writer.write_object_name(*obj_mesh); + obj_writer.write_vertex_coords(*obj_mesh); + Vector<int> obj_mtlindices; + + if (obj_mesh->tot_polygons() > 0) { + if (export_params.export_smooth_groups) { + obj_mesh->calc_smooth_groups(export_params.smooth_groups_bitflags); + } + if (export_params.export_normals) { + obj_writer.write_poly_normals(*obj_mesh); + } + if (export_params.export_uv) { + obj_writer.write_uv_coords(*obj_mesh); + } + if (mtl_writer) { + obj_mtlindices = mtl_writer->add_materials(*obj_mesh); + } + /* This function takes a 0-indexed slot index for the obj_mesh object and + * returns the material name that we are using in the .obj file for it. */ + std::function<const char *(int)> matname_fn = [&](int s) -> const char * { + if (!mtl_writer || s < 0 || s >= obj_mtlindices.size()) { + return nullptr; + } + return mtl_writer->mtlmaterial_name(obj_mtlindices[s]); + }; + obj_writer.write_poly_elements(*obj_mesh, matname_fn); + } + obj_writer.write_edges_indices(*obj_mesh); + + obj_writer.update_index_offsets(*obj_mesh); + } +} + +/** + * Export NURBS Curves in parameter form, not as vertices and edges. + */ +static void write_nurbs_curve_objects(const Vector<std::unique_ptr<OBJCurve>> &exportable_as_nurbs, + const OBJWriter &obj_writer) +{ + /* #OBJCurve doesn't have any dynamically allocated memory, so it's fine + * to wait for #blender::Vector to clean the objects up. */ + for (const std::unique_ptr<OBJCurve> &obj_curve : exportable_as_nurbs) { + obj_writer.write_nurbs_curve(*obj_curve); + } +} + +/** + * Export a single frame to a .OBJ file. + * + * Conditionally write a .MTL file also. + */ +void export_frame(Depsgraph *depsgraph, const OBJExportParams &export_params, const char *filepath) +{ + std::unique_ptr<OBJWriter> frame_writer = nullptr; + try { + frame_writer = std::make_unique<OBJWriter>(filepath, export_params); + } + catch (const std::system_error &ex) { + print_exception_error(ex); + return; + } + if (!frame_writer) { + BLI_assert(!"File should be writable by now."); + return; + } + std::unique_ptr<MTLWriter> mtl_writer = nullptr; + if (export_params.export_materials) { + try { + mtl_writer = std::make_unique<MTLWriter>(export_params.filepath); + } + catch (const std::system_error &ex) { + print_exception_error(ex); + } + } + + frame_writer->write_header(); + + auto [exportable_as_mesh, exportable_as_nurbs] = filter_supported_objects(depsgraph, + export_params); + + write_mesh_objects( + std::move(exportable_as_mesh), *frame_writer, mtl_writer.get(), export_params); + if (mtl_writer) { + mtl_writer->write_header(export_params.blen_filepath); + mtl_writer->write_materials(); + } + write_nurbs_curve_objects(std::move(exportable_as_nurbs), *frame_writer); +} + +/** + * Append the current frame number in the .OBJ file name. + * + * \return Whether the filepath is in #FILE_MAX limits. + */ +bool append_frame_to_filename(const char *filepath, const int frame, char *r_filepath_with_frames) +{ + BLI_strncpy(r_filepath_with_frames, filepath, FILE_MAX); + BLI_path_extension_replace(r_filepath_with_frames, FILE_MAX, ""); + const int digits = frame == 0 ? 1 : integer_digits_i(abs(frame)); + BLI_path_frame(r_filepath_with_frames, frame, digits); + return BLI_path_extension_replace(r_filepath_with_frames, FILE_MAX, ".obj"); +} + +/** + * Central internal function to call Scene update & writer functions. + */ +void exporter_main(bContext *C, const OBJExportParams &export_params) +{ + ED_object_mode_set(C, OB_MODE_OBJECT); + OBJDepsgraph obj_depsgraph(C, export_params.export_eval_mode); + Scene *scene = DEG_get_input_scene(obj_depsgraph.get()); + const char *filepath = export_params.filepath; + + /* Single frame export, i.e. no animation. */ + if (!export_params.export_animation) { + fprintf(stderr, "Writing to %s\n", filepath); + export_frame(obj_depsgraph.get(), export_params, filepath); + return; + } + + char filepath_with_frames[FILE_MAX]; + /* Used to reset the Scene to its original state. */ + const int original_frame = CFRA; + + for (int frame = export_params.start_frame; frame <= export_params.end_frame; frame++) { + const bool filepath_ok = append_frame_to_filename(filepath, frame, filepath_with_frames); + if (!filepath_ok) { + fprintf(stderr, "Error: File Path too long.\n%s\n", filepath_with_frames); + return; + } + + CFRA = frame; + obj_depsgraph.update_for_newframe(); + fprintf(stderr, "Writing to %s\n", filepath_with_frames); + export_frame(obj_depsgraph.get(), export_params, filepath_with_frames); + } + CFRA = original_frame; +} +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_exporter.hh b/source/blender/io/wavefront_obj/exporter/obj_exporter.hh new file mode 100644 index 00000000000..dff9eb2681c --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_exporter.hh @@ -0,0 +1,82 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include "BLI_utility_mixins.hh" + +#include "BLI_vector.hh" + +#include "IO_wavefront_obj.h" + +namespace blender::io::obj { + +/** + * Steal elements' ownership in a range-based for-loop. + */ +template<typename T> struct StealUniquePtr { + std::unique_ptr<T> owning; + StealUniquePtr(std::unique_ptr<T> &owning) : owning(std::move(owning)) + { + } + T *operator->() + { + return owning.operator->(); + } + T &operator*() + { + return owning.operator*(); + } +}; + +/** + * Behaves like `std::unique_ptr<Depsgraph, custom_deleter>`. + * Needed to free a new Depsgraph created for #DAG_EVAL_RENDER. + */ +class OBJDepsgraph : NonMovable, NonCopyable { + private: + Depsgraph *depsgraph_ = nullptr; + bool needs_free_ = false; + + public: + OBJDepsgraph(const bContext *C, const eEvaluationMode eval_mode); + ~OBJDepsgraph(); + + Depsgraph *get(); + void update_for_newframe(); +}; + +void exporter_main(bContext *C, const OBJExportParams &export_params); + +class OBJMesh; +class OBJCurve; + +void export_frame(Depsgraph *depsgraph, + const OBJExportParams &export_params, + const char *filepath); + +std::pair<Vector<std::unique_ptr<OBJMesh>>, Vector<std::unique_ptr<OBJCurve>>> +filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params); + +bool append_frame_to_filename(const char *filepath, const int frame, char *r_filepath_with_frames); +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/importer_mesh_utils.cc b/source/blender/io/wavefront_obj/importer/importer_mesh_utils.cc new file mode 100644 index 00000000000..eb86da836f7 --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/importer_mesh_utils.cc @@ -0,0 +1,368 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include <array> + +#include "BKE_displist.h" +#include "BKE_mesh.h" + +#include "BLI_set.hh" + +#include "DNA_object_types.h" + +#include "IO_wavefront_obj.h" + +#include "importer_mesh_utils.hh" + +namespace blender::io::obj { + +static float manhatten_len(const float3 coord) +{ + return std::abs(coord[0]) + std::abs(coord[1]) + std::abs(coord[2]); +} + +/** + * Keeps original index of the vertex as well as manhatten length for future use. + */ +struct vert_index_mlen { + const float3 v; + const int i; + const float mlen; + + vert_index_mlen(float3 v, int i) : v(v), i(i), mlen(manhatten_len(v)) + { + } + friend bool operator==(const vert_index_mlen &one, const vert_index_mlen &other) + { + return other.v == one.v; + } + friend bool operator!=(const vert_index_mlen &one, const vert_index_mlen &other) + { + return !(one == other); + } +}; + +/** + * Reorder vertices `v1` and `v2` so that edges like (v1,v2) and (v2,v2) are processed as the same. + */ +static std::pair<float3, float3> ed_key_mlen(const vert_index_mlen &v1, const vert_index_mlen &v2) +{ + if (v2.mlen < v1.mlen) { + return {v2.v, v1.v}; + } + return {v1.v, v2.v}; +} + +/** + * Join segments which have same starting or ending points. + * Caller should ensure non-empty segments. + */ +static bool join_segments(Vector<vert_index_mlen> *r_seg1, Vector<vert_index_mlen> *r_seg2) +{ + if ((*r_seg1)[0].v == r_seg2->last().v) { + Vector<vert_index_mlen> *temp = r_seg1; + r_seg1 = r_seg2; + r_seg2 = temp; + } + else if (r_seg1->last().v == (*r_seg2)[0].v) { + } + else { + return false; + } + r_seg1->remove_last(); + r_seg1->extend(*r_seg2); + if (r_seg1->last().v == (*r_seg1)[0].v) { + r_seg1->remove_last(); + } + r_seg2->clear(); + return true; +} + +/** + * A simplified version of `M_Geometry_tessellate_polygon`. + * + * \param polyLineSeq List of polylines. + * \param r_new_line_seq Empty vector that fill be filled with indices of corners of triangles. + */ + +static void tessellate_polygon(const Vector<Vector<float3>> &polyLineSeq, + Vector<Vector<int>> &r_new_line_seq) +{ + int64_t totpoints = 0; + /* Display #ListBase. */ + ListBase dispbase = {nullptr, nullptr}; + const int64_t len_polylines{polyLineSeq.size()}; + + for (int i = 0; i < len_polylines; i++) { + Span<float3> polyLine = polyLineSeq[i]; + + const int64_t len_polypoints{polyLine.size()}; + totpoints += len_polypoints; + if (len_polypoints <= 0) { /* don't bother adding edges as polylines */ + continue; + } + DispList *dl = static_cast<DispList *>(MEM_callocN(sizeof(DispList), __func__)); + BLI_addtail(&dispbase, dl); + dl->type = DL_INDEX3; + dl->nr = len_polypoints; + dl->type = DL_POLY; + dl->parts = 1; /* no faces, 1 edge loop */ + dl->col = 0; /* no material */ + dl->verts = static_cast<float *>(MEM_mallocN(sizeof(float[3]) * len_polypoints, "dl verts")); + dl->index = static_cast<int *>(MEM_callocN(sizeof(int[3]) * len_polypoints, "dl index")); + float *fp_verts = dl->verts; + for (int j = 0; j < len_polypoints; j++, fp_verts += 3) { + copy_v3_v3(fp_verts, polyLine[j]); + } + } + + if (totpoints) { + /* now make the list to fill */ + BKE_displist_fill(&dispbase, &dispbase, nullptr, false); + + /* The faces are stored in a new DisplayList + * that's added to the head of the #ListBase. */ + const DispList *dl = static_cast<DispList *>(dispbase.first); + + for (int index = 0, *dl_face = dl->index; index < dl->parts; index++, dl_face += 3) { + r_new_line_seq.append({dl_face[0], dl_face[1], dl_face[2]}); + } + BKE_displist_free(&dispbase); + } +} + +/** + * Tessellate an ngon with holes to triangles. + * + * \param face_vertex_indices A polygon's indices that index into the given vertex coordinate list. + * \return List of polygons with each element containing indices of one polygon. + */ +Vector<Vector<int>> ngon_tessellate(Span<float3> vertex_coords, Span<int> face_vertex_indices) +{ + if (face_vertex_indices.is_empty()) { + return {}; + } + Vector<vert_index_mlen> verts; + verts.reserve(face_vertex_indices.size()); + + for (int i = 0; i < face_vertex_indices.size(); i++) { + verts.append({vertex_coords[face_vertex_indices[i]], i}); + } + + Vector<std::array<int, 2>> edges; + for (int i = 0; i < face_vertex_indices.size(); i++) { + edges.append({i, i - 1}); + } + edges[0] = {0, static_cast<int>(face_vertex_indices.size() - 1)}; + + Set<std::pair<float3, float3>> edges_double; + { + Set<std::pair<float3, float3>> edges_used; + for (Span<int> edge : edges) { + std::pair<float3, float3> edge_key{ed_key_mlen(verts[edge[0]], verts[edge[1]])}; + if (edges_used.contains(edge_key)) { + edges_double.add(edge_key); + } + else { + edges_used.add(edge_key); + } + } + } + + Vector<Vector<vert_index_mlen>> loop_segments; + { + const vert_index_mlen *vert_prev = &verts[0]; + Vector<vert_index_mlen> context_loop{1, *vert_prev}; + loop_segments.append(context_loop); + for (const vert_index_mlen &vertex : verts) { + if (vertex == *vert_prev) { + continue; + } + if (edges_double.contains(ed_key_mlen(vertex, *vert_prev))) { + context_loop = {vertex}; + loop_segments.append(context_loop); + } + else { + if (!context_loop.is_empty() && context_loop.last() == vertex) { + } + else { + loop_segments.last().append(vertex); + context_loop.append(vertex); + } + } + vert_prev = &vertex; + } + } + + bool joining_segements = true; + while (joining_segements) { + joining_segements = false; + for (int j = loop_segments.size() - 1; j >= 0; j--) { + Vector<vert_index_mlen> &seg_j = loop_segments[j]; + if (seg_j.is_empty()) { + continue; + } + for (int k = j - 1; k >= 0; k--) { + if (seg_j.is_empty()) { + break; + } + Vector<vert_index_mlen> &seg_k = loop_segments[k]; + if (!seg_k.is_empty() && join_segments(&seg_j, &seg_k)) { + joining_segements = true; + } + } + } + } + + for (Vector<vert_index_mlen> &loop : loop_segments) { + while (!loop.is_empty() && loop[0].v == loop.last().v) { + loop.remove_last(); + } + } + + Vector<Vector<vert_index_mlen>> loop_list; + for (Vector<vert_index_mlen> &loop : loop_segments) { + if (loop.size() > 2) { + loop_list.append(loop); + } + } + // Done with loop fixing. + + Vector<int> vert_map(face_vertex_indices.size(), 0); + int ii = 0; + for (Span<vert_index_mlen> verts : loop_list) { + if (verts.size() <= 2) { + continue; + } + for (int i = 0; i < verts.size(); i++) { + vert_map[i + ii] = verts[i].i; + } + ii += verts.size(); + } + + Vector<Vector<int>> fill; + { + Vector<Vector<float3>> coord_list; + for (Span<vert_index_mlen> loop : loop_list) { + Vector<float3> coord; + for (const vert_index_mlen &vert : loop) { + coord.append(vert.v); + } + coord_list.append(coord); + } + tessellate_polygon(coord_list, fill); + } + + Vector<Vector<int>> fill_indices; + Vector<Vector<int>> fill_indices_reversed; + for (Span<int> f : fill) { + Vector<int> tri; + for (const int i : f) { + tri.append(vert_map[i]); + } + fill_indices.append(tri); + } + + if (fill_indices.is_empty()) { + std::cerr << "Warning: could not scanfill, fallback on triangle fan" << std::endl; + for (int i = 2; i < face_vertex_indices.size(); i++) { + fill_indices.append({0, i - 1, i}); + } + } + else { + int flip = -1; + for (Span<int> fi : fill_indices) { + if (flip != -1) { + break; + } + for (int i = 0; i < fi.size(); i++) { + if (fi[i] == 0 && fi[i - 1] == 1) { + flip = 0; + break; + } + if (fi[i] == 1 && fi[i - 1] == 0) { + flip = 1; + break; + } + } + } + if (flip == 1) { + for (Span<int> fill_index : fill_indices) { + Vector<int> rev_face(fill_index.size()); + for (int j = 0; j < rev_face.size(); j++) { + rev_face[j] = fill_index[rev_face.size() - 1 - j]; + } + fill_indices_reversed.append(rev_face); + } + } + } + + if (!fill_indices_reversed.is_empty()) { + return fill_indices_reversed; + } + return fill_indices; +} + +/** + * Apply axes transform to the Object, and clamp object dimensions to the specified value. + * + * Ideally, this should be a member of a base class which `MeshFromGeometry` and + * `CurveFromGeometry` derive from. + */ +void transform_object(Object *object, const OBJImportParams &import_params) +{ + float axes_transform[3][3]; + unit_m3(axes_transform); + unit_m4(object->obmat); + /* Location shift should be 0. */ + copy_v3_fl(object->obmat[3], 0.0f); + /* +Y-forward and +Z-up are the default Blender axis settings. */ + mat3_from_axis_conversion(OBJ_AXIS_Y_FORWARD, + OBJ_AXIS_Z_UP, + import_params.forward_axis, + import_params.up_axis, + axes_transform); + /* mat3_from_axis_conversion returns a transposed matrix! */ + transpose_m3(axes_transform); + mul_m4_m3m4(object->obmat, axes_transform, object->obmat); + + if (import_params.clamp_size != 0.0f) { + float3 max_coord(-INT_MAX); + float3 min_coord(INT_MAX); + BoundBox *bb = BKE_mesh_boundbox_get(object); + for (const float(&vertex)[3] : bb->vec) { + for (int axis = 0; axis < 3; axis++) { + max_coord[axis] = max_ff(max_coord[axis], vertex[axis]); + min_coord[axis] = min_ff(min_coord[axis], vertex[axis]); + } + } + const float max_diff = max_fff( + max_coord[0] - min_coord[0], max_coord[1] - min_coord[1], max_coord[2] - min_coord[2]); + float scale = 1.0f; + while (import_params.clamp_size < max_diff * scale) { + scale = scale / 10; + } + copy_v3_fl(object->scale, scale); + } +} +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/importer_mesh_utils.hh b/source/blender/io/wavefront_obj/importer/importer_mesh_utils.hh new file mode 100644 index 00000000000..faf4b4b552d --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/importer_mesh_utils.hh @@ -0,0 +1,37 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include "BLI_float3.hh" +#include "BLI_span.hh" +#include "BLI_vector.hh" + +struct Object; +struct OBJImportParams; + +namespace blender::io::obj { +Vector<Vector<int>> ngon_tessellate(Span<float3> vertex_coords, Span<int> face_vertex_indices); + +void transform_object(Object *object, const OBJImportParams &import_params); +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/obj_import_file_reader.cc b/source/blender/io/wavefront_obj/importer/obj_import_file_reader.cc new file mode 100644 index 00000000000..8e59d0cf119 --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_import_file_reader.cc @@ -0,0 +1,603 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include <fstream> +#include <iostream> + +#include "BLI_map.hh" +#include "BLI_string_ref.hh" +#include "BLI_vector.hh" + +#include "parser_string_utils.hh" + +#include "obj_import_file_reader.hh" + +namespace blender::io::obj { + +using std::string; + +/** + * Based on the properties of the given Geometry instance, create a new Geometry instance + * or return the previous one. + * + * Also update index offsets which should always happen if a new Geometry instance is created. + */ +static Geometry *create_geometry(Geometry *const prev_geometry, + const eGeometryType new_type, + StringRef name, + const GlobalVertices &global_vertices, + Vector<std::unique_ptr<Geometry>> &r_all_geometries, + VertexIndexOffset &r_offset) +{ + auto new_geometry = [&]() { + if (name.is_empty()) { + r_all_geometries.append(std::make_unique<Geometry>(new_type, "New object")); + } + else { + r_all_geometries.append(std::make_unique<Geometry>(new_type, name)); + } + r_offset.set_index_offset(global_vertices.vertices.size()); + return r_all_geometries.last().get(); + }; + + if (prev_geometry && prev_geometry->get_geom_type() == GEOM_MESH) { + /* After the creation of a Geometry instance, at least one element has been found in the OBJ + * file that indicates that it is a mesh. */ + if (prev_geometry->total_verts() || prev_geometry->total_face_elems() || + prev_geometry->total_normals() || prev_geometry->total_edges()) { + return new_geometry(); + } + if (new_type == GEOM_MESH) { + /* A Geometry created initially with a default name now found its name. */ + prev_geometry->set_geometry_name(name); + return prev_geometry; + } + if (new_type == GEOM_CURVE) { + /* The object originally created is not a mesh now that curve data + * follows the vertex coordinates list. */ + prev_geometry->set_geom_type(GEOM_CURVE); + return prev_geometry; + } + } + + if (prev_geometry && prev_geometry->get_geom_type() == GEOM_CURVE) { + return new_geometry(); + } + + return new_geometry(); +} + +void OBJStorer::add_vertex(const StringRef rest_line, GlobalVertices &r_global_vertices) +{ + float3 curr_vert; + Vector<StringRef> str_vert_split; + split_by_char(rest_line, ' ', str_vert_split); + copy_string_to_float(str_vert_split, FLT_MAX, {curr_vert, 3}); + r_global_vertices.vertices.append(curr_vert); + r_geom_.vertex_indices_.append(r_global_vertices.vertices.size() - 1); +} + +void OBJStorer::add_vertex_normal(const StringRef rest_line, GlobalVertices &r_global_vertices) +{ + float3 curr_vert_normal; + Vector<StringRef> str_vert_normal_split; + split_by_char(rest_line, ' ', str_vert_normal_split); + copy_string_to_float(str_vert_normal_split, FLT_MAX, {curr_vert_normal, 2}); + r_global_vertices.vertex_normals.append(curr_vert_normal); + r_geom_.vertex_normal_indices_.append(r_global_vertices.vertex_normals.size() - 1); +} + +void OBJStorer::add_uv_vertex(const StringRef rest_line, GlobalVertices &r_global_vertices) +{ + float2 curr_uv_vert; + Vector<StringRef> str_uv_vert_split; + split_by_char(rest_line, ' ', str_uv_vert_split); + copy_string_to_float(str_uv_vert_split, FLT_MAX, {curr_uv_vert, 2}); + r_global_vertices.uv_vertices.append(curr_uv_vert); +} + +void OBJStorer::add_edge(const StringRef rest_line, + const VertexIndexOffset &offsets, + GlobalVertices &r_global_vertices) +{ + int edge_v1 = -1, edge_v2 = -1; + Vector<StringRef> str_edge_split; + split_by_char(rest_line, ' ', str_edge_split); + copy_string_to_int(str_edge_split[0], -1, edge_v1); + copy_string_to_int(str_edge_split[1], -1, edge_v2); + /* Always keep stored indices non-negative and zero-based. */ + edge_v1 += edge_v1 < 0 ? r_global_vertices.vertices.size() : -offsets.get_index_offset() - 1; + edge_v2 += edge_v2 < 0 ? r_global_vertices.vertices.size() : -offsets.get_index_offset() - 1; + BLI_assert(edge_v1 >= 0 && edge_v2 >= 0); + r_geom_.edges_.append({static_cast<uint>(edge_v1), static_cast<uint>(edge_v2)}); +} + +void OBJStorer::add_polygon(const StringRef rest_line, + const GlobalVertices &global_vertices, + const VertexIndexOffset &offsets, + const StringRef state_material_name, + const StringRef state_object_group, + const bool state_shaded_smooth) +{ + PolyElem curr_face; + curr_face.shaded_smooth = state_shaded_smooth; + if (!state_material_name.is_empty()) { + curr_face.material_name = state_material_name; + } + if (!state_object_group.is_empty()) { + curr_face.vertex_group = state_object_group; + /* Yes it repeats several times, but another if-check will not reduce steps either. */ + r_geom_.use_vertex_groups_ = true; + } + + Vector<StringRef> str_corners_split; + split_by_char(rest_line, ' ', str_corners_split); + for (StringRef str_corner : str_corners_split) { + PolyCorner corner; + const size_t n_slash = std::count(str_corner.begin(), str_corner.end(), '/'); + if (n_slash == 0) { + /* Case: "f v1 v2 v3". */ + copy_string_to_int(str_corner, INT32_MAX, corner.vert_index); + } + else if (n_slash == 1) { + /* Case: "f v1/vt1 v2/vt2 v3/vt3". */ + Vector<StringRef> vert_uv_split; + split_by_char(str_corner, '/', vert_uv_split); + copy_string_to_int(vert_uv_split[0], INT32_MAX, corner.vert_index); + if (vert_uv_split.size() == 2) { + copy_string_to_int(vert_uv_split[1], INT32_MAX, corner.uv_vert_index); + } + } + else if (n_slash == 2) { + /* Case: "f v1//vn1 v2//vn2 v3//vn3". */ + /* Case: "f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3". */ + Vector<StringRef> vert_uv_normal_split; + split_by_char(str_corner, '/', vert_uv_normal_split); + copy_string_to_int(vert_uv_normal_split[0], INT32_MAX, corner.vert_index); + copy_string_to_int(vert_uv_normal_split[1], INT32_MAX, corner.uv_vert_index); + if (vert_uv_normal_split.size() == 3) { + copy_string_to_int(vert_uv_normal_split[2], INT32_MAX, corner.vertex_normal_index); + } + } + /* Always keep stored indices non-negative and zero-based. */ + corner.vert_index += corner.vert_index < 0 ? global_vertices.vertices.size() : + -offsets.get_index_offset() - 1; + corner.uv_vert_index += corner.uv_vert_index < 0 ? global_vertices.uv_vertices.size() : -1; + corner.vertex_normal_index += corner.vertex_normal_index < 0 ? + global_vertices.vertex_normals.size() : + -1; + curr_face.face_corners.append(corner); + } + + r_geom_.face_elements_.append(curr_face); + r_geom_.total_loops_ += curr_face.face_corners.size(); +} + +void OBJStorer::set_curve_type(const StringRef rest_line, + const GlobalVertices &global_vertices, + const StringRef state_object_group, + VertexIndexOffset &r_offsets, + Vector<std::unique_ptr<Geometry>> &r_all_geometries) +{ + if (rest_line.find("bspline") != StringRef::not_found) { + r_geom_ = *create_geometry( + &r_geom_, GEOM_CURVE, state_object_group, global_vertices, r_all_geometries, r_offsets); + r_geom_.nurbs_element_.group_ = state_object_group; + } + else { + std::cerr << "Curve type not supported:'" << rest_line << "'" << std::endl; + } +} + +void OBJStorer::set_curve_degree(const StringRef rest_line) +{ + copy_string_to_int(rest_line, 3, r_geom_.nurbs_element_.degree); +} + +void OBJStorer::add_curve_vertex_indices(const StringRef rest_line, + const GlobalVertices &global_vertices) +{ + Vector<StringRef> str_curv_split; + split_by_char(rest_line, ' ', str_curv_split); + /* Remove "0.0" and "1.0" from the strings. They are hardcoded. */ + str_curv_split.remove(0); + str_curv_split.remove(0); + r_geom_.nurbs_element_.curv_indices.resize(str_curv_split.size()); + copy_string_to_int(str_curv_split, INT32_MAX, r_geom_.nurbs_element_.curv_indices); + for (int &curv_index : r_geom_.nurbs_element_.curv_indices) { + /* Always keep stored indices non-negative and zero-based. */ + curv_index += curv_index < 0 ? global_vertices.vertices.size() : -1; + } +} + +void OBJStorer::add_curve_parameters(const StringRef rest_line) +{ + Vector<StringRef> str_parm_split; + split_by_char(rest_line, ' ', str_parm_split); + if (str_parm_split[0] == "u" || str_parm_split[0] == "v") { + str_parm_split.remove(0); + r_geom_.nurbs_element_.parm.resize(str_parm_split.size()); + copy_string_to_float(str_parm_split, FLT_MAX, r_geom_.nurbs_element_.parm); + } + else { + std::cerr << "Surfaces are not supported:'" << str_parm_split[0] << "'" << std::endl; + } +} + +void OBJStorer::update_object_group(const StringRef rest_line, + std::string &r_state_object_group) const +{ + + if (rest_line.find("off") != string::npos || rest_line.find("null") != string::npos || + rest_line.find("default") != string::npos) { + /* Set group for future elements like faces or curves to empty. */ + r_state_object_group = ""; + return; + } + r_state_object_group = rest_line; +} + +void OBJStorer::update_polygon_material(const StringRef rest_line, + std::string &r_state_material_name) const +{ + /* Materials may repeat if faces are written without sorting. */ + r_geom_.material_names_.add(string(rest_line)); + r_state_material_name = rest_line; +} + +void OBJStorer::update_smooth_group(const StringRef rest_line, bool &r_state_shaded_smooth) const +{ + /* Some implementations use "0" and "null" too, in addition to "off". */ + if (rest_line != "0" && rest_line.find("off") == StringRef::not_found && + rest_line.find("null") == StringRef::not_found) { + int smooth = 0; + copy_string_to_int(rest_line, 0, smooth); + r_state_shaded_smooth = smooth != 0; + } + else { + /* The OBJ file explicitly set shading to off. */ + r_state_shaded_smooth = false; + } +} + +/** + * Open OBJ file at the path given in import parameters. + */ +OBJParser::OBJParser(const OBJImportParams &import_params) : import_params_(import_params) +{ + obj_file_.open(import_params_.filepath); + if (!obj_file_.good()) { + fprintf(stderr, "Cannot read from OBJ file:'%s'.\n", import_params_.filepath); + return; + } + fprintf(stderr, "Reading OBJ file from '%s'\n", import_params.filepath); +} + +/** + * Read the OBJ file line by line and create OBJ Geometry instances. Also store all the vertex + * and UV vertex coordinates in a struct accessible by all objects. + */ +void OBJParser::parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries, + GlobalVertices &r_global_vertices) +{ + if (!obj_file_.good()) { + return; + } + + string line; + /* Store vertex coordinates that belong to other Geometry instances. */ + VertexIndexOffset offsets; + /* Non owning raw pointer to a Geometry. To be updated while creating a new Geometry. */ + Geometry *current_geometry = create_geometry( + nullptr, GEOM_MESH, "", r_global_vertices, r_all_geometries, offsets); + + /* State-setting variables: if set, they remain the same for the remaining + * elements in the object. */ + bool state_shaded_smooth = false; + string state_object_group; + string state_material_name; + + while (std::getline(obj_file_, line)) { + /* Keep reading new lines if the last character is `\`. */ + /* Another way is to make a getline wrapper and use it in the while condition. */ + read_next_line(obj_file_, line); + + StringRef line_key, rest_line; + split_line_key_rest(line, line_key, rest_line); + if (line.empty() || rest_line.is_empty()) { + continue; + } + OBJStorer storer(*current_geometry); + switch (line_key_str_to_enum(line_key)) { + case eOBJLineKey::V: { + storer.add_vertex(rest_line, r_global_vertices); + break; + } + case eOBJLineKey::VN: { + storer.add_vertex_normal(rest_line, r_global_vertices); + break; + } + case eOBJLineKey::VT: { + storer.add_uv_vertex(rest_line, r_global_vertices); + break; + } + case eOBJLineKey::F: { + storer.add_polygon(rest_line, + r_global_vertices, + offsets, + state_material_name, + state_material_name, + state_shaded_smooth); + break; + } + case eOBJLineKey::L: { + storer.add_edge(rest_line, offsets, r_global_vertices); + break; + } + case eOBJLineKey::CSTYPE: { + storer.set_curve_type( + rest_line, r_global_vertices, state_object_group, offsets, r_all_geometries); + break; + } + case eOBJLineKey::DEG: { + storer.set_curve_degree(rest_line); + break; + } + case eOBJLineKey::CURV: { + storer.add_curve_vertex_indices(rest_line, r_global_vertices); + break; + } + case eOBJLineKey::PARM: { + storer.add_curve_parameters(rest_line); + break; + } + case eOBJLineKey::O: { + state_shaded_smooth = false; + state_object_group = ""; + state_material_name = ""; + current_geometry = create_geometry( + current_geometry, GEOM_MESH, rest_line, r_global_vertices, r_all_geometries, offsets); + break; + } + case eOBJLineKey::G: { + storer.update_object_group(rest_line, state_object_group); + break; + } + case eOBJLineKey::S: { + storer.update_smooth_group(rest_line, state_shaded_smooth); + break; + } + case eOBJLineKey::USEMTL: { + storer.update_polygon_material(rest_line, state_material_name); + break; + } + case eOBJLineKey::MTLLIB: { + mtl_libraries_.append(string(rest_line)); + break; + } + case eOBJLineKey::COMMENT: + break; + default: + std::cout << "Element not recognised: '" << line_key << "'" << std::endl; + break; + } + } +} + +/** + * Skip all texture map options and get the filepath from a "map_" line. + */ +static StringRef skip_unsupported_options(StringRef line) +{ + TextureMapOptions map_options; + StringRef last_option; + int64_t last_option_pos = 0; + + /* Find the last texture map option. */ + for (StringRef option : map_options.all_options()) { + const int64_t pos{line.find(option)}; + /* Equality (>=) takes care of finding an option in the beginning of the line. Avoid messing + * with signed-unsigned int comparison. */ + if (pos != StringRef::not_found && pos >= last_option_pos) { + last_option = option; + last_option_pos = pos; + } + } + + if (last_option.is_empty()) { + /* No option found, line is the filepath */ + return line; + } + + /* Remove upto start of the last option + size of the last option + space after it. */ + line = line.drop_prefix(last_option_pos + last_option.size() + 1); + for (int i = 0; i < map_options.number_of_args(last_option); i++) { + const int64_t pos_space{line.find_first_of(' ')}; + if (pos_space != StringRef::not_found) { + BLI_assert(pos_space + 1 < line.size()); + line = line.drop_prefix(pos_space + 1); + } + } + + return line; +} + +/** + * Fix incoming texture map line keys for variations due to other exporters. + */ +static string fix_bad_map_keys(StringRef map_key) +{ + string new_map_key(map_key); + if (map_key == "refl") { + new_map_key = "map_refl"; + } + if (map_key.find("bump") != StringRef::not_found) { + /* Handles both "bump" and "map_Bump" */ + new_map_key = "map_Bump"; + } + return new_map_key; +} + +/** + * Return a list of all material library filepaths referenced by the OBJ file. + */ +Span<std::string> OBJParser::mtl_libraries() const +{ + return mtl_libraries_; +} + +/** + * Open material library file. + */ +MTLParser::MTLParser(StringRef mtl_library, StringRefNull obj_filepath) +{ + char obj_file_dir[FILE_MAXDIR]; + BLI_split_dir_part(obj_filepath.data(), obj_file_dir, FILE_MAXDIR); + BLI_path_join(mtl_file_path_, FILE_MAX, obj_file_dir, mtl_library.data(), NULL); + BLI_split_dir_part(mtl_file_path_, mtl_dir_path_, FILE_MAXDIR); + mtl_file_.open(mtl_file_path_); + if (!mtl_file_.good()) { + fprintf(stderr, "Cannot read from MTL file:'%s'\n", mtl_file_path_); + return; + } + fprintf(stderr, "Reading MTL file from:'%s'\n", mtl_file_path_); +} + +/** + * Read MTL file(s) and add MTLMaterial instances to the given Map reference. + */ +void MTLParser::parse_and_store(Map<string, std::unique_ptr<MTLMaterial>> &r_mtl_materials) +{ + if (!mtl_file_.good()) { + return; + } + + string line; + MTLMaterial *current_mtlmaterial = nullptr; + + while (std::getline(mtl_file_, line)) { + StringRef line_key, rest_line; + split_line_key_rest(line, line_key, rest_line); + if (line.empty() || rest_line.is_empty()) { + continue; + } + + /* Fix lower case/ incomplete texture map identifiers. */ + const string fixed_key = fix_bad_map_keys(line_key); + line_key = fixed_key; + + if (line_key == "newmtl") { + if (r_mtl_materials.remove_as(rest_line)) { + std::cerr << "Duplicate material found:'" << rest_line + << "', using the last encountered Material definition." << std::endl; + } + current_mtlmaterial = + r_mtl_materials.lookup_or_add(string(rest_line), std::make_unique<MTLMaterial>()).get(); + } + else if (line_key == "Ns") { + copy_string_to_float(rest_line, 324.0f, current_mtlmaterial->Ns); + } + else if (line_key == "Ka") { + Vector<StringRef> str_ka_split; + split_by_char(rest_line, ' ', str_ka_split); + copy_string_to_float(str_ka_split, 0.0f, {current_mtlmaterial->Ka, 3}); + } + else if (line_key == "Kd") { + Vector<StringRef> str_kd_split; + split_by_char(rest_line, ' ', str_kd_split); + copy_string_to_float(str_kd_split, 0.8f, {current_mtlmaterial->Kd, 3}); + } + else if (line_key == "Ks") { + Vector<StringRef> str_ks_split; + split_by_char(rest_line, ' ', str_ks_split); + copy_string_to_float(str_ks_split, 0.5f, {current_mtlmaterial->Ks, 3}); + } + else if (line_key == "Ke") { + Vector<StringRef> str_ke_split; + split_by_char(rest_line, ' ', str_ke_split); + copy_string_to_float(str_ke_split, 0.0f, {current_mtlmaterial->Ke, 3}); + } + else if (line_key == "Ni") { + copy_string_to_float(rest_line, 1.45f, current_mtlmaterial->Ni); + } + else if (line_key == "d") { + copy_string_to_float(rest_line, 1.0f, current_mtlmaterial->d); + } + else if (line_key == "illum") { + copy_string_to_int(rest_line, 2, current_mtlmaterial->illum); + } + + /* Parse image textures. */ + else if (line_key.find("map_") != StringRef::not_found) { + /* TODO howardt: fix this */ + eMTLSyntaxElement line_key_enum = mtl_line_key_str_to_enum(line_key); + if (line_key_enum == eMTLSyntaxElement::string || + !current_mtlmaterial->texture_maps.contains_as(line_key_enum)) { + /* No supported texture map found. */ + std::cerr << "Texture map type not supported:'" << line_key << "'" << std::endl; + continue; + } + tex_map_XX &tex_map = current_mtlmaterial->texture_maps.lookup(line_key_enum); + Vector<StringRef> str_map_xx_split; + split_by_char(rest_line, ' ', str_map_xx_split); + + /* TODO ankitm: use `skip_unsupported_options` for parsing these options too? */ + const int64_t pos_o{str_map_xx_split.first_index_of_try("-o")}; + if (pos_o != -1 && pos_o + 3 < str_map_xx_split.size()) { + copy_string_to_float({str_map_xx_split[pos_o + 1], + str_map_xx_split[pos_o + 2], + str_map_xx_split[pos_o + 3]}, + 0.0f, + {tex_map.translation, 3}); + } + const int64_t pos_s{str_map_xx_split.first_index_of_try("-s")}; + if (pos_s != -1 && pos_s + 3 < str_map_xx_split.size()) { + copy_string_to_float({str_map_xx_split[pos_s + 1], + str_map_xx_split[pos_s + 2], + str_map_xx_split[pos_s + 3]}, + 1.0f, + {tex_map.scale, 3}); + } + /* Only specific to Normal Map node. */ + const int64_t pos_bm{str_map_xx_split.first_index_of_try("-bm")}; + if (pos_bm != -1 && pos_bm + 1 < str_map_xx_split.size()) { + copy_string_to_float( + str_map_xx_split[pos_bm + 1], 0.0f, current_mtlmaterial->map_Bump_strength); + } + const int64_t pos_projection{str_map_xx_split.first_index_of_try("-type")}; + if (pos_projection != -1 && pos_projection + 1 < str_map_xx_split.size()) { + /* Only Sphere is supported, so whatever the type is, set it to Sphere. */ + tex_map.projection_type = SHD_PROJ_SPHERE; + if (str_map_xx_split[pos_projection + 1] != "sphere") { + std::cerr << "Using projection type 'sphere', not:'" + << str_map_xx_split[pos_projection + 1] << "'." << std::endl; + } + } + + /* Skip all unsupported options and arguments. */ + tex_map.image_path = string(skip_unsupported_options(rest_line)); + tex_map.mtl_dir_path = mtl_dir_path_; + } + } +} +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/obj_import_file_reader.hh b/source/blender/io/wavefront_obj/importer/obj_import_file_reader.hh new file mode 100644 index 00000000000..49e46d3494a --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_import_file_reader.hh @@ -0,0 +1,203 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include <fstream> + +#include "IO_wavefront_obj.h" +#include "obj_import_mtl.hh" +#include "obj_import_objects.hh" + +namespace blender::io::obj { + +class OBJParser { + private: + const OBJImportParams &import_params_; + std::ifstream obj_file_; + Vector<std::string> mtl_libraries_; + + public: + OBJParser(const OBJImportParams &import_params); + + void parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries, + GlobalVertices &r_global_vertices); + Span<std::string> mtl_libraries() const; +}; + +class OBJStorer { + private: + Geometry &r_geom_; + + public: + OBJStorer(Geometry &r_geom) : r_geom_(r_geom) + { + } + void add_vertex(const StringRef rest_line, GlobalVertices &r_global_vertices); + void add_vertex_normal(const StringRef rest_line, GlobalVertices &r_global_vertices); + void add_uv_vertex(const StringRef rest_line, GlobalVertices &r_global_vertices); + void add_edge(const StringRef rest_line, + const VertexIndexOffset &offsets, + GlobalVertices &r_global_vertices); + void add_polygon(const StringRef rest_line, + const GlobalVertices &global_vertices, + const VertexIndexOffset &offsets, + const StringRef state_material_name, + const StringRef state_object_group, + const bool state_shaded_smooth); + + void set_curve_type(const StringRef rest_line, + const GlobalVertices &global_vertices, + const StringRef state_object_group, + VertexIndexOffset &r_offsets, + Vector<std::unique_ptr<Geometry>> &r_all_geometries); + void set_curve_degree(const StringRef rest_line); + void add_curve_vertex_indices(const StringRef rest_line, const GlobalVertices &global_vertices); + void add_curve_parameters(const StringRef rest_line); + + void update_object_group(const StringRef rest_line, std::string &r_state_object_group) const; + void update_polygon_material(const StringRef rest_line, + std::string &r_state_material_name) const; + void update_smooth_group(const StringRef rest_line, bool &r_state_shaded_smooth) const; +}; + +enum class eOBJLineKey { + V, + VN, + VT, + F, + L, + CSTYPE, + DEG, + CURV, + PARM, + O, + G, + S, + USEMTL, + MTLLIB, + COMMENT +}; + +constexpr eOBJLineKey line_key_str_to_enum(const std::string_view key_str) +{ + if (key_str == "v" || key_str == "V") { + return eOBJLineKey::V; + } + if (key_str == "vn" || key_str == "VN") { + return eOBJLineKey::VN; + } + if (key_str == "vt" || key_str == "VT") { + return eOBJLineKey::VT; + } + if (key_str == "f" || key_str == "F") { + return eOBJLineKey::F; + } + if (key_str == "l" || key_str == "L") { + return eOBJLineKey::L; + } + if (key_str == "cstype" || key_str == "CSTYPE") { + return eOBJLineKey::CSTYPE; + } + if (key_str == "deg" || key_str == "DEG") { + return eOBJLineKey::DEG; + } + if (key_str == "curv" || key_str == "CURV") { + return eOBJLineKey::CURV; + } + if (key_str == "parm" || key_str == "PARM") { + return eOBJLineKey::PARM; + } + if (key_str == "o" || key_str == "O") { + return eOBJLineKey::O; + } + if (key_str == "g" || key_str == "G") { + return eOBJLineKey::G; + } + if (key_str == "s" || key_str == "S") { + return eOBJLineKey::S; + } + if (key_str == "usemtl" || key_str == "USEMTL") { + return eOBJLineKey::USEMTL; + } + if (key_str == "mtllib" || key_str == "MTLLIB") { + return eOBJLineKey::MTLLIB; + } + if (key_str == "#") { + return eOBJLineKey::COMMENT; + } + return eOBJLineKey::COMMENT; +} + +/** + * All texture map options with number of arguments they accept. + */ +class TextureMapOptions { + private: + Map<const std::string, int> tex_map_options; + + public: + TextureMapOptions() + { + tex_map_options.add_new("-blendu", 1); + tex_map_options.add_new("-blendv", 1); + tex_map_options.add_new("-boost", 1); + tex_map_options.add_new("-mm", 2); + tex_map_options.add_new("-o", 3); + tex_map_options.add_new("-s", 3); + tex_map_options.add_new("-t", 3); + tex_map_options.add_new("-texres", 1); + tex_map_options.add_new("-clamp", 1); + tex_map_options.add_new("-bm", 1); + tex_map_options.add_new("-imfchan", 1); + } + + /** + * All valid option strings. + */ + Map<const std::string, int>::KeyIterator all_options() const + { + return tex_map_options.keys(); + } + + int number_of_args(StringRef option) const + { + return tex_map_options.lookup_as(std::string(option)); + } +}; + +class MTLParser { + private: + char mtl_file_path_[FILE_MAX]; + /** + * Directory in which the MTL file is found. + */ + char mtl_dir_path_[FILE_MAX]; + std::ifstream mtl_file_; + + public: + MTLParser(StringRef mtl_library_, StringRefNull obj_filepath); + + void parse_and_store(Map<std::string, std::unique_ptr<MTLMaterial>> &r_mtl_materials); +}; +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/obj_import_mesh.cc b/source/blender/io/wavefront_obj/importer/obj_import_mesh.cc new file mode 100644 index 00000000000..c143595da89 --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_import_mesh.cc @@ -0,0 +1,413 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include <array> + +#include "DNA_scene_types.h" /* For eVGroupSelect. */ + +#include "BKE_customdata.h" +#include "BKE_material.h" +#include "BKE_mesh.h" +#include "BKE_node.h" +#include "BKE_object.h" +#include "BKE_object_deform.h" + +#include "BLI_map.hh" +#include "BLI_set.hh" +#include "BLI_vector_set.hh" + +#include "bmesh.h" +#include "bmesh_operator_api.h" +#include "bmesh_tools.h" + +#include "DNA_customdata_types.h" +#include "DNA_material_types.h" +#include "DNA_mesh_types.h" +#include "DNA_meshdata_types.h" +#include "DNA_modifier_types.h" + +#include "importer_mesh_utils.hh" +#include "obj_import_mesh.hh" + +namespace blender::io::obj { + +MeshFromGeometry::~MeshFromGeometry() +{ + if (mesh_object_ || blender_mesh_) { + /* Move the object to own it. */ + mesh_object_.reset(); + blender_mesh_.reset(); + BLI_assert(0); + } +} + +void MeshFromGeometry::create_mesh(Main *bmain, + const Map<std::string, std::unique_ptr<MTLMaterial>> &materials, + const OBJImportParams &import_params) +{ + std::string ob_name{mesh_geometry_.get_geometry_name()}; + if (ob_name.empty()) { + ob_name = "Untitled"; + } + Vector<PolyElem> new_faces; + Set<std::pair<int, int>> fgon_edges; + const auto [removed_faces, removed_loops]{tessellate_polygons(new_faces, fgon_edges)}; + + const int64_t tot_verts_object{mesh_geometry_.total_verts()}; + /* Total explicitly imported edges, not the ones belonging the polygons to be created. */ + const int64_t tot_edges{mesh_geometry_.total_edges()}; + const int64_t tot_face_elems{mesh_geometry_.total_face_elems() - removed_faces + + new_faces.size()}; + const int64_t tot_loops{mesh_geometry_.total_loops() - removed_loops + 3 * new_faces.size()}; + + blender_mesh_.reset( + BKE_mesh_new_nomain(tot_verts_object, tot_edges, 0, tot_loops, tot_face_elems)); + mesh_object_.reset(BKE_object_add_only_object(bmain, OB_MESH, ob_name.c_str())); + mesh_object_->data = BKE_object_obdata_add_from_type(bmain, OB_MESH, ob_name.c_str()); + + create_vertices(); + new_faces.extend(mesh_geometry_.face_elements()); + create_polys_loops(new_faces); + create_edges(); + create_uv_verts(); + create_materials(bmain, materials); + + bool verbose_validate = false; +#ifdef DEBUG + verbose_validate = true; +#endif + BKE_mesh_validate(blender_mesh_.get(), verbose_validate, false); +#if 0 + /* TODO ankitm Check if it should be executed or not. */ + add_custom_normals(); +#endif + /* Un-tessellate unnecesarily triangulated n-gons. */ + dissolve_edges(fgon_edges); + transform_object(mesh_object_.get(), import_params); + + BKE_mesh_nomain_to_mesh(blender_mesh_.release(), + static_cast<Mesh *>(mesh_object_->data), + mesh_object_.get(), + &CD_MASK_EVERYTHING, + true); +} + +/** + * Tessellate potentially invalid polygons and fill the + */ +std::pair<int64_t, int64_t> MeshFromGeometry::tessellate_polygons( + Vector<PolyElem> &r_new_faces, Set<std::pair<int, int>> &fgon_edges) +{ + int64_t removed_faces = 0; + int64_t removed_loops = 0; + for (const PolyElem &curr_face : mesh_geometry_.face_elements()) { + if (curr_face.shaded_smooth || true) { // should be valid/invalid + return {removed_faces, removed_loops}; + } + Vector<int> face_vert_indices; + Vector<int> face_uv_indices; + Vector<int> face_normal_indices; + face_vert_indices.reserve(curr_face.face_corners.size()); + face_uv_indices.reserve(curr_face.face_corners.size()); + face_normal_indices.reserve(curr_face.face_corners.size()); + for (const PolyCorner &corner : curr_face.face_corners) { + face_vert_indices.append(corner.vert_index); + face_normal_indices.append(corner.vertex_normal_index); + face_uv_indices.append(corner.uv_vert_index); + removed_loops++; + } + + Vector<Vector<int>> new_polygon_indices = ngon_tessellate(global_vertices_.vertices, + face_vert_indices); + for (Span<int> triangle : new_polygon_indices) { + r_new_faces.append({curr_face.vertex_group, + curr_face.material_name, + curr_face.shaded_smooth, + {{face_vert_indices[triangle[0]], + face_uv_indices[triangle[0]], + face_normal_indices[triangle[0]]}, + {face_vert_indices[triangle[1]], + face_uv_indices[triangle[1]], + face_normal_indices[triangle[1]]}, + {face_vert_indices[triangle[2]], + face_uv_indices[triangle[2]], + face_normal_indices[triangle[2]]}}, + false}); + } + + if (new_polygon_indices.size() > 1) { + Set<std::pair<int, int>> edge_users; + for (Span<int> triangle : new_polygon_indices) { + int prev_vidx = face_vert_indices[triangle.last()]; + for (const int ngidx : triangle) { + int vidx = face_vert_indices[ngidx]; + if (vidx == prev_vidx) { + continue; + } + std::pair<int, int> edge_key = {min_ii(prev_vidx, vidx), max_ii(prev_vidx, vidx)}; + prev_vidx = vidx; + if (edge_users.contains(edge_key)) { + fgon_edges.add(edge_key); + } + else { + edge_users.add(edge_key); + } + } + } + } + removed_faces++; + } + + return {removed_faces, removed_loops}; +} + +void MeshFromGeometry::dissolve_edges(const Set<std::pair<int, int>> &fgon_edges) +{ + if (fgon_edges.is_empty()) { + return; + } + const struct BMeshCreateParams bm_create_params = {1u}; + /* If calc_face_normal is false, it triggers BLI_assert(BM_face_is_normal_valid(f)). */ + const struct BMeshFromMeshParams bm_convert_params = {1u, 0, 0, 0}; + + BMesh *bmesh = BKE_mesh_to_bmesh_ex(blender_mesh_.get(), &bm_create_params, &bm_convert_params); + + Vector<std::array<BMVert *, 2>> edges; + edges.reserve(fgon_edges.size()); + BM_mesh_elem_table_ensure(bmesh, BM_VERT); + for (const std::pair<int, int> &edge : fgon_edges) { + edges.append({BM_vert_at_index(bmesh, edge.first), BM_vert_at_index(bmesh, edge.second)}); + } + + BMO_op_callf(bmesh, + BMO_FLAG_DEFAULTS, + "dissolve_edges edges=%eb use_verts=%b use_face_split=%b", + edges.data(), + 0, + 0); + unique_mesh_ptr to_free = std::move(blender_mesh_); + blender_mesh_.reset(BKE_mesh_from_bmesh_for_eval_nomain(bmesh, nullptr, to_free.get())); + to_free.reset(); + BM_mesh_free(bmesh); +} + +void MeshFromGeometry::create_vertices() +{ + const int64_t tot_verts_object{mesh_geometry_.total_verts()}; + for (int i = 0; i < tot_verts_object; ++i) { + if (mesh_geometry_.vertex_index(i) < global_vertices_.vertices.size()) { + copy_v3_v3(blender_mesh_->mvert[i].co, + global_vertices_.vertices[mesh_geometry_.vertex_index(i)]); + if (i >= mesh_geometry_.total_normals()) { + /* Silence debug warning in mesh validate. */ + const float3 normals = {1.0f, 1.0f, 1.0f}; + normal_float_to_short_v3(blender_mesh_->mvert[i].no, normals); + } + } + else { + std::cerr << "Vertex index:" << mesh_geometry_.vertex_index(i) + << " larger than total vertices:" << global_vertices_.vertices.size() << " ." + << std::endl; + } + } +} + +/** + * Create polygons for the Mesh, set smooth shading flag, deform group name, assigned material + * also. + * + * It must receive all polygons to be added to the mesh. Remove holes from polygons before + * calling this. + */ +void MeshFromGeometry::create_polys_loops(Span<PolyElem> all_faces) +{ + /* Will not be used if vertex groups are not imported. */ + blender_mesh_->dvert = nullptr; + float weight = 0.0f; + if (mesh_geometry_.total_verts() && mesh_geometry_.use_vertex_groups()) { + blender_mesh_->dvert = static_cast<MDeformVert *>(CustomData_add_layer( + &blender_mesh_->vdata, CD_MDEFORMVERT, CD_CALLOC, nullptr, mesh_geometry_.total_verts())); + weight = 1.0f / mesh_geometry_.total_verts(); + } + else { + UNUSED_VARS(weight); + } + + /* Do not remove elements from the VectorSet since order of insertion is required. + * StringRef is fine since per-face deform group name outlives the VectorSet. */ + VectorSet<StringRef> group_names; + const int64_t tot_face_elems{blender_mesh_->totpoly}; + int tot_loop_idx = 0; + + for (int poly_idx = 0; poly_idx < tot_face_elems; ++poly_idx) { + const PolyElem &curr_face = all_faces[poly_idx]; + if (curr_face.face_corners.size() < 3) { + /* Don't add single vertex face, or edges. */ + std::cerr << "Face with less than 3 vertices found, skipping." << std::endl; + continue; + } + + MPoly &mpoly = blender_mesh_->mpoly[poly_idx]; + mpoly.totloop = curr_face.face_corners.size(); + mpoly.loopstart = tot_loop_idx; + if (curr_face.shaded_smooth) { + mpoly.flag |= ME_SMOOTH; + } + mpoly.mat_nr = mesh_geometry_.material_names().index_of_try(curr_face.material_name); + + for (const PolyCorner &curr_corner : curr_face.face_corners) { + MLoop &mloop = blender_mesh_->mloop[tot_loop_idx]; + tot_loop_idx++; + mloop.v = curr_corner.vert_index; + /* Set normals to silence mesh validate zero normals warnings. */ + if (curr_corner.vertex_normal_index >= 0 && + curr_corner.vertex_normal_index < global_vertices_.vertex_normals.size()) { + normal_float_to_short_v3(blender_mesh_->mvert[mloop.v].no, + global_vertices_.vertex_normals[curr_corner.vertex_normal_index]); + } + + if (blender_mesh_->dvert) { + /* Iterating over mloop results in finding the same vertex multiple times. + * Another way is to allocate memory for dvert while creating vertices and fill them here. + */ + MDeformVert &def_vert = blender_mesh_->dvert[mloop.v]; + if (!def_vert.dw) { + def_vert.dw = static_cast<MDeformWeight *>( + MEM_callocN(sizeof(MDeformWeight), "OBJ Import Deform Weight")); + } + /* Every vertex in a face is assigned the same deform group. */ + int64_t pos_name{group_names.index_of_try(curr_face.vertex_group)}; + if (pos_name == -1) { + group_names.add_new(curr_face.vertex_group); + pos_name = group_names.size() - 1; + } + BLI_assert(pos_name >= 0); + /* Deform group number (def_nr) must behave like an index into the names' list. */ + *(def_vert.dw) = {static_cast<unsigned int>(pos_name), weight}; + } + } + } + + if (!blender_mesh_->dvert) { + return; + } + /* Add deform group(s) to the object's defbase. */ + for (StringRef name : group_names) { + /* Adding groups in this order assumes that def_nr is an index into the names' list. */ + BKE_object_defgroup_add_name(mesh_object_.get(), name.data()); + } +} + +/** + * Add explicitly imported OBJ edges to the mesh. + */ +void MeshFromGeometry::create_edges() +{ + const int64_t tot_edges{mesh_geometry_.total_edges()}; + for (int i = 0; i < tot_edges; ++i) { + const MEdge &src_edge = mesh_geometry_.edges()[i]; + MEdge &dst_edge = blender_mesh_->medge[i]; + BLI_assert(src_edge.v1 < mesh_geometry_.total_verts() && + src_edge.v2 < mesh_geometry_.total_verts()); + dst_edge.v1 = src_edge.v1; + dst_edge.v2 = src_edge.v2; + dst_edge.flag = ME_LOOSEEDGE; + } + + /* Set argument `update` to true so that existing, explicitly imported edges can be merged + * with the new ones created from polygons. */ + BKE_mesh_calc_edges(blender_mesh_.get(), true, false); + BKE_mesh_calc_edges_loose(blender_mesh_.get()); +} + +/** + * Add UV layer and vertices to the Mesh. + */ +void MeshFromGeometry::create_uv_verts() +{ + if (global_vertices_.uv_vertices.size() <= 0) { + return; + } + MLoopUV *mluv_dst = static_cast<MLoopUV *>(CustomData_add_layer( + &blender_mesh_->ldata, CD_MLOOPUV, CD_DEFAULT, nullptr, mesh_geometry_.total_loops())); + int tot_loop_idx = 0; + + for (const PolyElem &curr_face : mesh_geometry_.face_elements()) { + for (const PolyCorner &curr_corner : curr_face.face_corners) { + if (curr_corner.uv_vert_index >= 0 && + curr_corner.uv_vert_index < global_vertices_.uv_vertices.size()) { + const float2 &mluv_src = global_vertices_.uv_vertices[curr_corner.uv_vert_index]; + copy_v2_v2(mluv_dst[tot_loop_idx].uv, mluv_src); + tot_loop_idx++; + } + } + } +} + +/** + * Add materials and the nodetree to the Mesh Object. + */ +void MeshFromGeometry::create_materials( + Main *bmain, const Map<std::string, std::unique_ptr<MTLMaterial>> &materials) +{ + for (StringRef material_name : mesh_geometry_.material_names()) { + if (!materials.contains_as(material_name)) { + std::cerr << "Material named '" << material_name << "' not found in material library." + << std::endl; + continue; + } + BKE_object_material_slot_add(bmain, mesh_object_.get()); + Material *mat = BKE_material_add(bmain, material_name.data()); + BKE_object_material_assign( + bmain, mesh_object_.get(), mat, mesh_object_->totcol, BKE_MAT_ASSIGN_USERPREF); + + const MTLMaterial &curr_mat = *materials.lookup_as(material_name); + ShaderNodetreeWrap mat_wrap{bmain, curr_mat}; + mat->use_nodes = true; + mat->nodetree = mat_wrap.get_nodetree(); + ntreeUpdateTree(bmain, mat->nodetree); + } +} + +/** + * Needs more clarity about what is expected in the viewport if the function works. + */ +void MeshFromGeometry::add_custom_normals() +{ + const int64_t tot_loop_normals{mesh_geometry_.total_normals()}; + float(*loop_normals)[3] = static_cast<float(*)[3]>( + MEM_malloc_arrayN(tot_loop_normals, sizeof(float[3]), __func__)); + + for (int index = 0; index < tot_loop_normals; index++) { + copy_v3_v3(loop_normals[index], + global_vertices_.vertex_normals[mesh_geometry_.vertex_normal_index(index)]); + } + + blender_mesh_->flag |= ME_AUTOSMOOTH; + BKE_mesh_set_custom_normals(blender_mesh_.get(), loop_normals); + for (int i = 0; i < tot_loop_normals; i++) { + print_v3("", loop_normals[i]); + } + MEM_freeN(loop_normals); +} +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/obj_import_mesh.hh b/source/blender/io/wavefront_obj/importer/obj_import_mesh.hh new file mode 100644 index 00000000000..db31ae754b6 --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_import_mesh.hh @@ -0,0 +1,94 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include "BKE_lib_id.h" + +#include "BLI_utility_mixins.hh" + +#include "obj_import_mtl.hh" +#include "obj_import_objects.hh" + +namespace blender::io::obj { +/** + * An custom unique_ptr deleter for a Mesh object. + */ +struct UniqueMeshDeleter { + void operator()(Mesh *mesh) + { + BKE_id_free(nullptr, mesh); + } +}; + +/** + * An unique_ptr to a Mesh with a custom deleter. + */ +using unique_mesh_ptr = std::unique_ptr<Mesh, UniqueMeshDeleter>; + +/** + * Make a Blender Mesh Object from a Geometry of GEOM_MESH type. + * Use the mover function to own the mesh after creation. + */ +class MeshFromGeometry : NonMovable, NonCopyable { + private: + /** + * Mesh datablock made from OBJ data. + */ + unique_mesh_ptr blender_mesh_{nullptr}; + /** + * An Object of type OB_MESH. Use the mover function to own it. + */ + unique_object_ptr mesh_object_{nullptr}; + const Geometry &mesh_geometry_; + const GlobalVertices &global_vertices_; + + public: + MeshFromGeometry(const Geometry &mesh_geometry, const GlobalVertices &global_vertices) + : mesh_geometry_(mesh_geometry), global_vertices_(global_vertices) + { + } + + ~MeshFromGeometry(); + void create_mesh(Main *bmain, + const Map<std::string, std::unique_ptr<MTLMaterial>> &materials, + const OBJImportParams &import_params); + unique_object_ptr mover() + { + return std::move(mesh_object_); + } + + private: + std::pair<int64_t, int64_t> tessellate_polygons(Vector<PolyElem> &new_faces, + Set<std::pair<int, int>> &fgon_edges); + void create_vertices(); + void create_polys_loops(Span<PolyElem> all_faces); + void create_edges(); + void create_uv_verts(); + void create_materials(Main *bmain, + const Map<std::string, std::unique_ptr<MTLMaterial>> &materials); + void add_custom_normals(); + void dissolve_edges(const Set<std::pair<int, int>> &fgon_edges); +}; + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/obj_import_mtl.cc b/source/blender/io/wavefront_obj/importer/obj_import_mtl.cc new file mode 100644 index 00000000000..eaa65b3f000 --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_import_mtl.cc @@ -0,0 +1,377 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include "BKE_image.h" +#include "BKE_node.h" + +#include "BLI_map.hh" + +#include "DNA_node_types.h" + +#include "NOD_shader.h" + +/* TODO: move eMTLSyntaxElement out of following file into a more neutral place */ +#include "obj_export_io.hh" +#include "obj_import_mtl.hh" +#include "parser_string_utils.hh" + +namespace blender::io::obj { + +/** + * Set the socket's (of given ID) value to the given number(s). + * Only float value(s) can be set using this method. + */ +static void set_property_of_socket(eNodeSocketDatatype property_type, + StringRef socket_id, + Span<float> value, + bNode *r_node) +{ + BLI_assert(r_node); + bNodeSocket *socket{nodeFindSocket(r_node, SOCK_IN, socket_id.data())}; + BLI_assert(socket && socket->type == property_type); + switch (property_type) { + case SOCK_FLOAT: { + BLI_assert(value.size() == 1); + static_cast<bNodeSocketValueFloat *>(socket->default_value)->value = value[0]; + break; + } + case SOCK_RGBA: { + /* Alpha will be added manually. It is not read from the MTL file either. */ + BLI_assert(value.size() == 3); + copy_v3_v3(static_cast<bNodeSocketValueRGBA *>(socket->default_value)->value, value.data()); + static_cast<bNodeSocketValueRGBA *>(socket->default_value)->value[3] = 1.0f; + break; + } + case SOCK_VECTOR: { + BLI_assert(value.size() == 3); + copy_v4_v4(static_cast<bNodeSocketValueVector *>(socket->default_value)->value, + value.data()); + break; + } + default: { + BLI_assert(0); + break; + } + } +} + +/** + * Load image for Image Texture node and set the node properties. + * Return success if Image can be loaded successfully. + */ +static bool load_texture_image(Main *bmain, const tex_map_XX &tex_map, bNode *r_node) +{ + BLI_assert(r_node && r_node->type == SH_NODE_TEX_IMAGE); + + std::string tex_file_path{tex_map.mtl_dir_path + tex_map.image_path}; + Image *tex_image = BKE_image_load(bmain, tex_file_path.c_str()); + if (!tex_image) { + /* Could be absolute, so load the image directly. */ + fprintf(stderr, "Cannot load image file:'%s'\n", tex_file_path.c_str()); + tex_image = BKE_image_load(bmain, tex_map.image_path.c_str()); + } + if (!tex_image) { + fprintf(stderr, "Cannot load image file:'%s'\n", tex_map.image_path.c_str()); + /* Remove quotes from the filepath. */ + std::string no_quote_path{tex_map.mtl_dir_path + + replace_all_occurences(tex_map.image_path, "\"", "")}; + tex_image = BKE_image_load(nullptr, no_quote_path.c_str()); + if (!tex_image) { + fprintf(stderr, "Cannot load image file:'%s'\n", no_quote_path.data()); + std::string no_underscore_path{replace_all_occurences(no_quote_path, "_", " ")}; + tex_image = BKE_image_load(nullptr, no_underscore_path.c_str()); + if (!tex_image) { + fprintf(stderr, "Cannot load image file:'%s'\n", no_underscore_path.data()); + } + } + } + BLI_assert(tex_image); + if (tex_image) { + fprintf(stderr, "Loaded image from:'%s'\n", tex_image->filepath); + r_node->id = reinterpret_cast<ID *>(tex_image); + NodeTexImage *image = static_cast<NodeTexImage *>(r_node->storage); + image->projection = tex_map.projection_type; + return true; + } + return false; +} + +/** + * Initializes a nodetree with a p-BSDF node's BSDF socket connected to shader output node's + * surface socket. + */ +ShaderNodetreeWrap::ShaderNodetreeWrap(Main *bmain, const MTLMaterial &mtl_mat) : mtl_mat_(mtl_mat) +{ + nodetree_.reset(ntreeAddTree(nullptr, "Shader Nodetree", ntreeType_Shader->idname)); + bsdf_.reset(add_node_to_tree(SH_NODE_BSDF_PRINCIPLED)); + shader_output_.reset(add_node_to_tree(SH_NODE_OUTPUT_MATERIAL)); + + set_bsdf_socket_values(); + add_image_textures(bmain); + link_sockets(std::move(bsdf_), "BSDF", shader_output_.get(), "Surface", 4); + + nodeSetActive(nodetree_.get(), shader_output_.get()); +} + +/** + * Assert if caller hasn't acquired nodetree. + */ +ShaderNodetreeWrap::~ShaderNodetreeWrap() +{ + if (nodetree_) { + /* nodetree's ownership must be acquired by the caller. */ + nodetree_.reset(); + BLI_assert(0); + } +} + +/** + * Release nodetree for materials to own it. nodetree has its unique deleter + * if destructor is not reached for some reason. + */ +bNodeTree *ShaderNodetreeWrap::get_nodetree() +{ + /* If this function has been reached, we know that nodes and the nodetree + * can be added to the scene safely. */ + static_cast<void>(shader_output_.release()); + return nodetree_.release(); +} + +/** + * Add a new static node to the tree. + * No two nodes are linked here. + */ +bNode *ShaderNodetreeWrap::add_node_to_tree(const int node_type) +{ + return nodeAddStaticNode(nullptr, nodetree_.get(), node_type); +} + +/** + * Return x-y coordinates for a node where y is determined by other nodes present in + * the same vertical column. + */ +std::pair<float, float> ShaderNodetreeWrap::set_node_locations(const int pos_x) +{ + int pos_y = 0; + bool found = false; + while (true) { + for (Span<int> location : node_locations) { + if (location[0] == pos_x && location[1] == pos_y) { + pos_y += 1; + found = true; + } + else { + found = false; + } + } + if (!found) { + node_locations.append({pos_x, pos_y}); + return {pos_x * node_size_, pos_y * node_size_ * 2.0 / 3.0}; + } + } +} + +/** + * Link two nodes by the sockets of given IDs. + * Also releases the ownership of the "from" node for nodetree to free it. + * \param from_node_pos_x 0 to 4 value as per nodetree arrangement. + */ +void ShaderNodetreeWrap::link_sockets(unique_node_ptr from_node, + StringRef from_node_id, + bNode *to_node, + StringRef to_node_id, + const int from_node_pos_x) +{ + std::tie(from_node->locx, from_node->locy) = set_node_locations(from_node_pos_x); + std::tie(to_node->locx, to_node->locy) = set_node_locations(from_node_pos_x + 1); + bNodeSocket *from_sock{nodeFindSocket(from_node.get(), SOCK_OUT, from_node_id.data())}; + bNodeSocket *to_sock{nodeFindSocket(to_node, SOCK_IN, to_node_id.data())}; + BLI_assert(from_sock && to_sock); + nodeAddLink(nodetree_.get(), from_node.get(), from_sock, to_node, to_sock); + static_cast<void>(from_node.release()); +} + +/** + * Set values of sockets in p-BSDF node of the nodetree. + */ +void ShaderNodetreeWrap::set_bsdf_socket_values() +{ + const int illum = mtl_mat_.illum; + bool do_highlight = false; + bool do_tranparency = false; + bool do_reflection = false; + bool do_glass = false; + switch (illum) { + case 1: { + /* Base color on, ambient on. */ + break; + } + case 2: { + /* Highlight on. */ + do_highlight = true; + break; + } + case 3: { + /* Reflection on and Ray trace on. */ + do_reflection = true; + break; + } + case 4: { + /* Transparency: Glass on, Reflection: Ray trace on. */ + do_glass = true; + do_reflection = true; + do_tranparency = true; + break; + } + case 5: { + /* Reflection: Fresnel on and Ray trace on. */ + do_reflection = true; + break; + } + case 6: { + /* Transparency: Refraction on, Reflection: Fresnel off and Ray trace on. */ + do_reflection = true; + do_tranparency = true; + break; + } + case 7: { + /* Transparency: Refraction on, Reflection: Fresnel on and Ray trace on. */ + do_reflection = true; + do_tranparency = true; + break; + } + case 8: { + /* Reflection on and Ray trace off. */ + do_reflection = true; + break; + } + case 9: { + /* Transparency: Glass on, Reflection: Ray trace off. */ + do_glass = true; + do_reflection = false; + do_tranparency = true; + break; + } + default: { + std::cerr << "Warning! illum value = " << illum + << "is not supported by the Principled-BSDF shader." << std::endl; + break; + } + } + float specular = (mtl_mat_.Ks[0] + mtl_mat_.Ks[1] + mtl_mat_.Ks[2]) / 3; + float roughness = 1.0f - 1.0f / 30 * sqrt(std::max(0.0f, std::min(900.0f, mtl_mat_.Ns))); + float metallic = (mtl_mat_.Ka[0] + mtl_mat_.Ka[1] + mtl_mat_.Ka[2]) / 3; + float ior = mtl_mat_.Ni; + float alpha = mtl_mat_.d; + + if (specular < 0.0f) { + specular = static_cast<float>(do_highlight); + } + if (mtl_mat_.Ns < 0.0f) { + roughness = static_cast<float>(!do_highlight); + } + if (metallic < 0.0f) { + if (do_reflection) { + metallic = 1.0f; + } + } + else { + metallic = 0.0f; + } + if (ior < 0) { + if (do_tranparency) { + ior = 1.0f; + } + if (do_glass) { + ior = 1.5f; + } + } + if (alpha < 0) { + if (do_tranparency) { + alpha = 1.0f; + } + } + float3 base_color = {std::max(0.0f, mtl_mat_.Kd[0]), + std::max(0.0f, mtl_mat_.Kd[1]), + std::max(0.0f, mtl_mat_.Kd[2])}; + float3 emission_color = {std::max(0.0f, mtl_mat_.Ke[0]), + std::max(0.0f, mtl_mat_.Ke[1]), + std::max(0.0f, mtl_mat_.Ke[2])}; + + set_property_of_socket(SOCK_RGBA, "Base Color", {base_color, 3}, bsdf_.get()); + set_property_of_socket(SOCK_RGBA, "Emission", {emission_color, 3}, bsdf_.get()); + if (mtl_mat_.texture_maps.contains_as(eMTLSyntaxElement::map_Ke)) { + set_property_of_socket(SOCK_FLOAT, "Emission Strength", {1.0f}, bsdf_.get()); + } + set_property_of_socket(SOCK_FLOAT, "Specular", {specular}, bsdf_.get()); + set_property_of_socket(SOCK_FLOAT, "Roughness", {roughness}, bsdf_.get()); + set_property_of_socket(SOCK_FLOAT, "Metallic", {metallic}, bsdf_.get()); + set_property_of_socket(SOCK_FLOAT, "IOR", {ior}, bsdf_.get()); + set_property_of_socket(SOCK_FLOAT, "Alpha", {alpha}, bsdf_.get()); +} + +/** + * Create image texture, vector and normal mapping nodes from MTL materials and link the + * nodes to p-BSDF node. + */ +void ShaderNodetreeWrap::add_image_textures(Main *bmain) +{ + for (const Map<const eMTLSyntaxElement, tex_map_XX>::Item texture_map : + mtl_mat_.texture_maps.items()) { + if (texture_map.value.image_path.empty()) { + /* No Image texture node of this map type can be added to this material. */ + continue; + } + + unique_node_ptr image_texture{add_node_to_tree(SH_NODE_TEX_IMAGE)}; + unique_node_ptr mapping{add_node_to_tree(SH_NODE_MAPPING)}; + unique_node_ptr texture_coordinate(add_node_to_tree(SH_NODE_TEX_COORD)); + unique_node_ptr normal_map = nullptr; + + if (texture_map.key == eMTLSyntaxElement::map_Bump) { + normal_map.reset(add_node_to_tree(SH_NODE_NORMAL_MAP)); + const float bump = std::max(0.0f, mtl_mat_.map_Bump_strength); + set_property_of_socket(SOCK_FLOAT, "Strength", {bump}, normal_map.get()); + } + + if (!load_texture_image(bmain, texture_map.value, image_texture.get())) { + /* Image could not be added, so don't link image texture, vector, normal map nodes. */ + continue; + } + set_property_of_socket( + SOCK_VECTOR, "Location", {texture_map.value.translation, 3}, mapping.get()); + set_property_of_socket(SOCK_VECTOR, "Scale", {texture_map.value.scale, 3}, mapping.get()); + + link_sockets(std::move(texture_coordinate), "UV", mapping.get(), "Vector", 0); + link_sockets(std::move(mapping), "Vector", image_texture.get(), "Vector", 1); + if (normal_map) { + link_sockets(std::move(image_texture), "Color", normal_map.get(), "Color", 2); + link_sockets(std::move(normal_map), "Normal", bsdf_.get(), "Normal", 3); + } + else { + link_sockets( + std::move(image_texture), "Color", bsdf_.get(), texture_map.value.dest_socket_id, 2); + } + } +} +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/obj_import_mtl.hh b/source/blender/io/wavefront_obj/importer/obj_import_mtl.hh new file mode 100644 index 00000000000..c59b9c8d245 --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_import_mtl.hh @@ -0,0 +1,115 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include <array> + +#include "BLI_float3.hh" +#include "BLI_map.hh" +#include "BLI_string_ref.hh" +#include "BLI_vector.hh" + +#include "DNA_node_types.h" + +#include "MEM_guardedalloc.h" + +#include "obj_export_mtl.hh" + +namespace blender::io::obj { + +struct UniqueNodeDeleter { + void operator()(bNode *node) + { + MEM_freeN(node); + } +}; + +struct UniqueNodetreeDeleter { + void operator()(bNodeTree *node) + { + MEM_freeN(node); + } +}; + +using unique_node_ptr = std::unique_ptr<bNode, UniqueNodeDeleter>; +using unique_nodetree_ptr = std::unique_ptr<bNodeTree, UniqueNodetreeDeleter>; + +class ShaderNodetreeWrap { + private: + /* Node arrangement: + * Texture Coordinates -> Mapping -> Image Texture -> (optional) Normal Map -> p-BSDF -> Material + * Output. */ + unique_nodetree_ptr nodetree_; + unique_node_ptr bsdf_; + unique_node_ptr shader_output_; + const MTLMaterial &mtl_mat_; + + /* List of all locations occupied by nodes. */ + Vector<std::array<int, 2>> node_locations; + const float node_size_{300.f}; + + public: + ShaderNodetreeWrap(Main *bmain, const MTLMaterial &mtl_mat); + ~ShaderNodetreeWrap(); + + bNodeTree *get_nodetree(); + + private: + bNode *add_node_to_tree(const int node_type); + std::pair<float, float> set_node_locations(const int pos_x); + void link_sockets(unique_node_ptr from_node, + StringRef from_node_id, + bNode *to_node, + StringRef to_node_id, + const int from_node_pos_x); + void set_bsdf_socket_values(); + void add_image_textures(Main *bmain); +}; + +constexpr eMTLSyntaxElement mtl_line_key_str_to_enum(const std::string_view key_str) +{ + if (key_str == "map_Kd") { + return eMTLSyntaxElement::map_Kd; + } + if (key_str == "map_Ks") { + return eMTLSyntaxElement::map_Ks; + } + if (key_str == "map_Ns") { + return eMTLSyntaxElement::map_Ns; + } + if (key_str == "map_d") { + return eMTLSyntaxElement::map_d; + } + if (key_str == "refl" || key_str == "map_refl") { + return eMTLSyntaxElement::map_refl; + } + if (key_str == "map_Ke") { + return eMTLSyntaxElement::map_Ke; + } + if (key_str == "map_Bump" || key_str == "bump") { + return eMTLSyntaxElement::map_Bump; + } + return eMTLSyntaxElement::string; +} +} // namespace blender::io::obj
\ No newline at end of file diff --git a/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc new file mode 100644 index 00000000000..ed5dda2be34 --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc @@ -0,0 +1,123 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include "BKE_object.h" + +#include "DNA_curve_types.h" + +#include "importer_mesh_utils.hh" +#include "obj_import_nurbs.hh" +#include "obj_import_objects.hh" + +namespace blender::io::obj { + +CurveFromGeometry::~CurveFromGeometry() +{ + if (curve_object_ || blender_curve_) { + /* Move the object to own it. */ + curve_object_.reset(); + blender_curve_.reset(); + BLI_assert(0); + } +} + +void CurveFromGeometry::create_curve(Main *bmain, const OBJImportParams &import_params) +{ + std::string ob_name{curve_geometry_.get_geometry_name()}; + if (ob_name.empty() && !curve_geometry_.group().empty()) { + ob_name = curve_geometry_.group(); + } + else { + ob_name = "Untitled"; + } + + blender_curve_.reset(BKE_curve_add(bmain, ob_name.c_str(), OB_CURVE)); + curve_object_.reset(BKE_object_add_only_object(bmain, OB_CURVE, ob_name.c_str())); + + blender_curve_->flag = CU_3D; + blender_curve_->resolu = blender_curve_->resolv = 12; + /* Only one NURBS spline will be created in the curve object. */ + blender_curve_->actnu = 0; + + Nurb *nurb = static_cast<Nurb *>(MEM_callocN(sizeof(Nurb), "OBJ import NURBS curve")); + BLI_addtail(BKE_curve_nurbs_get(blender_curve_.get()), nurb); + create_nurbs(import_params); + + curve_object_->data = blender_curve_.release(); + transform_object(curve_object_.get(), import_params); +} + +/** + * Create a NURBS spline for the Curve converted from Geometry. + */ +void CurveFromGeometry::create_nurbs(const OBJImportParams & /*import_params */) +{ + const NurbsElement &nurbs_geometry = curve_geometry_.nurbs_elem(); + Nurb *nurb = static_cast<Nurb *>(blender_curve_->nurb.first); + + nurb->type = CU_NURBS; + nurb->flag = CU_3D; + nurb->next = nurb->prev = nullptr; + /* BKE_nurb_points_add later on will update pntsu. If this were set to total curv points, + * we get double the total points in viewport. */ + nurb->pntsu = 0; + /* Total points = pntsu * pntsv. */ + nurb->pntsv = 1; + nurb->orderu = nurb->orderv = (nurbs_geometry.degree + 1 > SHRT_MAX) ? 4 : + nurbs_geometry.degree + 1; + nurb->resolu = nurb->resolv = blender_curve_->resolu; + + const int64_t tot_vert{curve_geometry_.nurbs_elem().curv_indices.size()}; + + BKE_nurb_points_add(nurb, tot_vert); + for (int i = 0; i < tot_vert; i++) { + BPoint &bpoint = nurb->bp[i]; + copy_v3_v3(bpoint.vec, global_vertices_.vertices[nurbs_geometry.curv_indices[i]]); + bpoint.vec[3] = 1.0f; + bpoint.weight = 1.0f; + } + + BKE_nurb_knot_calc_u(nurb); + bool do_endpoints = true; + if (nurbs_geometry.curv_indices.size() && + nurbs_geometry.parm.size() > nurbs_geometry.degree + 1) { + for (int i = 0; i < nurbs_geometry.degree + 1; i++) { + if (abs(nurbs_geometry.parm[i] - nurbs_geometry.curv_indices[0]) > 0.0001) { + do_endpoints = false; + break; + } + if (abs(nurbs_geometry.parm[-(i + 1)] - nurbs_geometry.curv_indices[1]) > 0.0001) { + do_endpoints = false; + break; + } + } + } + else { + do_endpoints = false; + } + if (do_endpoints) { + nurb->flagu = CU_NURB_ENDPOINT; + } +} + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/obj_import_nurbs.hh b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.hh new file mode 100644 index 00000000000..3d9eeae09ee --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.hh @@ -0,0 +1,86 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include <memory> + +#include "BKE_curve.h" + +#include "BLI_utility_mixins.hh" + +#include "DNA_curve_types.h" + +#include "obj_import_objects.hh" + +namespace blender::io::obj { + +/** Free a curve's memory using Blender's memory management. */ +struct UniqueCurveDeleter { + void operator()(Curve *curve) + { + if (curve) { + BKE_nurbList_free(&curve->nurb); + } + } +}; + +/** An unique_ptr to a Curve with a custom deleter. Don't let unique_ptr free a curve with a + * different deallocator. + */ +using unique_curve_ptr = std::unique_ptr<Curve, UniqueCurveDeleter>; + +/** + * Make a Blender NURBS Curve block from a Geometry of GEOM_CURVE type. + * Use the mover function to own the curve. + */ +class CurveFromGeometry : NonMovable, NonCopyable { + private: + /** + * Curve datablock of type CU_NURBS made from OBJ data.. + */ + unique_curve_ptr blender_curve_; + /** + * Object of type OB_CURVE. Use the mover function to own it. + */ + unique_object_ptr curve_object_; + const Geometry &curve_geometry_; + const GlobalVertices &global_vertices_; + + public: + CurveFromGeometry(const Geometry &geometry, const GlobalVertices &global_vertices) + : curve_geometry_(geometry), global_vertices_(global_vertices) + { + } + ~CurveFromGeometry(); + + void create_curve(Main *bmain, const OBJImportParams &import_params); + unique_object_ptr mover() + { + return std::move(curve_object_); + } + + private: + void create_nurbs(const OBJImportParams &import_params); +}; +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/obj_import_objects.cc b/source/blender/io/wavefront_obj/importer/obj_import_objects.cc new file mode 100644 index 00000000000..3ca4cf9f7c2 --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_import_objects.cc @@ -0,0 +1,155 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include "BKE_collection.h" + +#include "DEG_depsgraph.h" +#include "DEG_depsgraph_build.h" + +#include "DNA_collection_types.h" +#include "DNA_object_types.h" +#include "DNA_scene_types.h" + +#include "obj_import_objects.hh" + +namespace blender::io::obj { + +eGeometryType Geometry::get_geom_type() const +{ + return geom_type_; +} + +/** + * Use very rarely. Only when it is guaranteed that the + * type originally set is wrong. + */ +void Geometry::set_geom_type(const eGeometryType new_type) +{ + geom_type_ = new_type; +} + +StringRef Geometry::get_geometry_name() const +{ + return geometry_name_; +} + +void Geometry::set_geometry_name(StringRef new_name) +{ + geometry_name_ = std::string(new_name); +} + +/** + * Returns an index that ranges from zero to total coordinates in the + * global list of vertices. + */ +int64_t Geometry::vertex_index(const int64_t index) const +{ + return vertex_indices_[index]; +} + +int64_t Geometry::total_verts() const +{ + return vertex_indices_.size(); +} + +Span<PolyElem> Geometry::face_elements() const +{ + return face_elements_; +} + +const PolyElem &Geometry::ith_face_element(const int64_t index) const +{ + return face_elements_[index]; +} + +int64_t Geometry::total_face_elems() const +{ + return face_elements_.size(); +} + +bool Geometry::use_vertex_groups() const +{ + return use_vertex_groups_; +} + +Span<MEdge> Geometry::edges() const +{ + return edges_; +} + +int64_t Geometry::total_edges() const +{ + return edges_.size(); +} + +int Geometry::total_loops() const +{ + return total_loops_; +} + +int64_t Geometry::vertex_normal_index(const int64_t vertex_index) const +{ + return vertex_normal_indices_[vertex_index]; +} + +int64_t Geometry::total_normals() const +{ + return vertex_normal_indices_.size(); +} + +const VectorSet<std::string> &Geometry::material_names() const +{ + return material_names_; +} + +const NurbsElement &Geometry::nurbs_elem() const +{ + return nurbs_element_; +} + +const std::string &Geometry::group() const +{ + return nurbs_element_.group_; +} + +/** + * Create a collection to store all imported objects. + */ +OBJImportCollection::OBJImportCollection(Main *bmain, Scene *scene) : bmain_(bmain), scene_(scene) +{ + obj_import_collection_ = BKE_collection_add( + bmain_, scene_->master_collection, "OBJ import collection"); +} + +/** + * Add the given Mesh/Curve object to the OBJ import collection. + */ +void OBJImportCollection::add_object_to_collection(unique_object_ptr b_object) +{ + BKE_collection_object_add(bmain_, obj_import_collection_, b_object.release()); + id_fake_user_set(&obj_import_collection_->id); + DEG_id_tag_update(&obj_import_collection_->id, ID_RECALC_COPY_ON_WRITE); + DEG_relations_tag_update(bmain_); +} + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/obj_import_objects.hh b/source/blender/io/wavefront_obj/importer/obj_import_objects.hh new file mode 100644 index 00000000000..699dec9b62e --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_import_objects.hh @@ -0,0 +1,183 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include "BKE_lib_id.h" + +#include "BLI_float2.hh" +#include "BLI_float3.hh" +#include "BLI_vector.hh" +#include "BLI_vector_set.hh" + +#include "DNA_meshdata_types.h" +#include "DNA_object_types.h" + +namespace blender::io::obj { + +/** + * List of all vertex and UV vertex coordinates in an OBJ file accessible to any + * Geometry instance at any time. + */ +struct GlobalVertices { + Vector<float3> vertices; + Vector<float2> uv_vertices; + Vector<float3> vertex_normals; +}; + +/** + * Keeps track of the vertices that belong to other Geometries. + * Needed only for MLoop.v and MEdge.v1 which needs vertex indices ranging from (0 to total + * vertices in the mesh) as opposed to the other OBJ indices ranging from (0 to total vertices + * in the global list). + */ +struct VertexIndexOffset { + private: + int offset_ = 0; + + public: + void set_index_offset(const int64_t total_vertices) + { + offset_ = total_vertices; + } + int64_t get_index_offset() const + { + return offset_; + } +}; + +/** + * A face's corner in an OBJ file. In Blender, it translates to a mloop vertex. + */ +struct PolyCorner { + /* These indices range from zero to total vertices in the OBJ file. */ + int vert_index; + /* -1 is to indicate absence of UV vertices. Only < 0 condition should be checked since + * it can be less than -1 too. */ + int uv_vert_index = -1; + int vertex_normal_index; +}; + +struct PolyElem { + std::string vertex_group; + std::string material_name; + bool shaded_smooth = false; + Vector<PolyCorner> face_corners; + /* Not read from the OBJ file. Set to true for potentially invalid polygons. */ + bool invalid = false; +}; + +/** + * Contains data for one single NURBS curve in the OBJ file. + */ +struct NurbsElement { + /** + * For curves, groups may be used to specify multiple splines in the same curve object. + * It may also serve as the name of the curve if not specified explicitly. + */ + std::string group_; + int degree = 0; + /** + * Indices into the global list of vertex coordinates. Must be non-negative. + */ + Vector<int> curv_indices; + /* Values in the parm u/v line in a curve definition. */ + Vector<float> parm; +}; + +enum eGeometryType { + GEOM_MESH = OB_MESH, + GEOM_CURVE = OB_CURVE, +}; + +class Geometry { + private: + eGeometryType geom_type_ = GEOM_MESH; + std::string geometry_name_; + VectorSet<std::string> material_names_; + /** + * Indices in the vector range from zero to total vertices in a geometry. + * Values range from zero to total coordinates in the global list. + */ + Vector<int> vertex_indices_; + Vector<int> vertex_normal_indices_; + /** Edges written in the file in addition to (or even without polygon) elements. */ + Vector<MEdge> edges_; + Vector<PolyElem> face_elements_; + bool use_vertex_groups_ = false; + NurbsElement nurbs_element_; + int total_loops_ = 0; + + public: + Geometry(eGeometryType type, StringRef ob_name) + : geom_type_(type), geometry_name_(std::string(ob_name)){}; + + eGeometryType get_geom_type() const; + void set_geom_type(const eGeometryType new_type); + StringRef get_geometry_name() const; + void set_geometry_name(StringRef new_name); + + int64_t vertex_index(const int64_t index) const; + int64_t total_verts() const; + Span<PolyElem> face_elements() const; + const PolyElem &ith_face_element(const int64_t index) const; + int64_t total_face_elems() const; + bool use_vertex_groups() const; + Span<MEdge> edges() const; + int64_t total_edges() const; + int total_loops() const; + int64_t vertex_normal_index(const int64_t vertex_index) const; + int64_t total_normals() const; + + const VectorSet<std::string> &material_names() const; + + const NurbsElement &nurbs_elem() const; + const std::string &group() const; + + friend class OBJStorer; +}; + +struct UniqueObjectDeleter { + void operator()(Object *object) + { + BKE_id_free(nullptr, object); + } +}; + +using unique_object_ptr = std::unique_ptr<Object, UniqueObjectDeleter>; + +class OBJImportCollection { + private: + Main *bmain_; + Scene *scene_; + /** + * The collection that holds all the imported objects. + */ + Collection *obj_import_collection_; + + public: + OBJImportCollection(Main *bmain, Scene *scene); + + void add_object_to_collection(unique_object_ptr b_object); +}; +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/obj_importer.cc b/source/blender/io/wavefront_obj/importer/obj_importer.cc new file mode 100644 index 00000000000..9caf3e0d2af --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_importer.cc @@ -0,0 +1,91 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#include <string> + +#include "BLI_float2.hh" +#include "BLI_float3.hh" +#include "BLI_map.hh" +#include "BLI_set.hh" +#include "BLI_string_ref.hh" + +#include "BKE_scene.h" + +#include "obj_import_file_reader.hh" +#include "obj_import_mesh.hh" +#include "obj_import_nurbs.hh" +#include "obj_import_objects.hh" +#include "obj_importer.hh" + +namespace blender::io::obj { + +/** + * Make Blender Mesh, Curve etc from Geometry and add them to the import collection. + */ +static void geometry_to_blender_objects( + Main *bmain, + Scene *scene, + const OBJImportParams &import_params, + Vector<std::unique_ptr<Geometry>> &all_geometries, + const GlobalVertices &global_vertices, + const Map<std::string, std::unique_ptr<MTLMaterial>> &materials) +{ + OBJImportCollection import_collection{bmain, scene}; + for (const std::unique_ptr<Geometry> &geometry : all_geometries) { + if (geometry->get_geom_type() == GEOM_MESH) { + MeshFromGeometry mesh_ob_from_geometry{*geometry, global_vertices}; + mesh_ob_from_geometry.create_mesh(bmain, materials, import_params); + import_collection.add_object_to_collection(mesh_ob_from_geometry.mover()); + } + else if (geometry->get_geom_type() == GEOM_CURVE) { + CurveFromGeometry curve_ob_from_geometry(*geometry, global_vertices); + curve_ob_from_geometry.create_curve(bmain, import_params); + import_collection.add_object_to_collection(curve_ob_from_geometry.mover()); + } + } +} + +void importer_main(bContext *C, const OBJImportParams &import_params) +{ + Main *bmain = CTX_data_main(C); + Scene *scene = CTX_data_scene(C); + /* List of Geometry instances to be parsed from OBJ file. */ + Vector<std::unique_ptr<Geometry>> all_geometries; + /* Container for vertex and UV vertex coordinates. */ + GlobalVertices global_vertices; + /* List of MTLMaterial instances to be parsed from MTL file. */ + Map<std::string, std::unique_ptr<MTLMaterial>> materials; + + OBJParser obj_parser{import_params}; + obj_parser.parse(all_geometries, global_vertices); + + for (StringRef mtl_library : obj_parser.mtl_libraries()) { + MTLParser mtl_parser{mtl_library, import_params.filepath}; + mtl_parser.parse_and_store(materials); + } + + geometry_to_blender_objects( + bmain, scene, import_params, all_geometries, global_vertices, materials); + static_cast<void>(CTX_data_ensure_evaluated_depsgraph(C)); +} +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/obj_importer.hh b/source/blender/io/wavefront_obj/importer/obj_importer.hh new file mode 100644 index 00000000000..cde2e3a6d0d --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_importer.hh @@ -0,0 +1,31 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include "IO_wavefront_obj.h" + +namespace blender::io::obj { + +void importer_main(bContext *C, const OBJImportParams &import_params); +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/parser_string_utils.cc b/source/blender/io/wavefront_obj/importer/parser_string_utils.cc new file mode 100644 index 00000000000..3d48d6310b7 --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/parser_string_utils.cc @@ -0,0 +1,217 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +#include <fstream> +#include <iostream> +#include <sstream> + +#include "BLI_float3.hh" +#include "BLI_span.hh" +#include "BLI_string_ref.hh" +#include "BLI_vector.hh" + +#include "parser_string_utils.hh" + +namespace blender::io::obj { +using std::string; + +/** + * Store multiple lines separated by an escaped newline character: `\\n`. + * Use this before doing any parse operations on the read string. + */ +void read_next_line(std::ifstream &file, string &r_line) +{ + std::string new_line; + while (file.good() && !r_line.empty() && r_line.back() == '\\') { + new_line.clear(); + const bool ok = static_cast<bool>(std::getline(file, new_line)); + /* Remove the last backslash character. */ + r_line.pop_back(); + r_line.append(new_line); + if (!ok || new_line.empty()) { + return; + } + } +} + +/** + * Split a line string into the first word (key) and the rest of the line. + * Also remove leading & trailing spaces as well as `\r` carriage return + * character if present. + */ +void split_line_key_rest(const StringRef line, StringRef &r_line_key, StringRef &r_rest_line) +{ + if (line.is_empty()) { + return; + } + + const int64_t pos_split{line.find_first_of(' ')}; + if (pos_split == StringRef::not_found) { + /* Use the first character if no space is found in the line. It's usually a comment like: + * #This is a comment. */ + r_line_key = line.substr(0, 1); + } + else { + r_line_key = line.substr(0, pos_split); + } + + /* Eat the delimiter also using "+ 1". */ + r_rest_line = line.drop_prefix(r_line_key.size() + 1); + if (r_rest_line.is_empty()) { + return; + } + + /* Remove any leading spaces, trailing spaces & \r character, if any. */ + const int64_t leading_space{r_rest_line.find_first_not_of(' ')}; + if (leading_space != StringRef::not_found) { + r_rest_line = r_rest_line.drop_prefix(leading_space); + } + + /* Another way is to do a test run before the actual parsing to find the newline + * character and use it in the getline. */ + const int64_t carriage_return{r_rest_line.find_first_of('\r')}; + if (carriage_return != StringRef::not_found) { + r_rest_line = r_rest_line.substr(0, carriage_return + 1); + } + + const int64_t trailing_space{r_rest_line.find_last_not_of(' ')}; + if (trailing_space != StringRef::not_found) { + /* The position is of a character that is not ' ', so count of characters is position + 1. */ + r_rest_line = r_rest_line.substr(0, trailing_space + 1); + } +} + +/** + * Split the given string by the delimiter and fill the given vector. + * If an intermediate string is empty, or space or null character, it is not appended to the + * vector. + */ +void split_by_char(StringRef in_string, const char delimiter, Vector<StringRef> &r_out_list) +{ + r_out_list.clear(); + + while (!in_string.is_empty()) { + const int64_t pos_delim{in_string.find_first_of(delimiter)}; + const int64_t word_len = pos_delim == StringRef::not_found ? in_string.size() : pos_delim; + + StringRef word{in_string.data(), word_len}; + if (!word.is_empty() && !(word == " " && !(word[0] == '\0'))) { + r_out_list.append(word); + } + if (pos_delim == StringRef::not_found) { + return; + } + /* Skip the word already stored. */ + in_string = in_string.drop_prefix(word_len); + /* Skip all delimiters. */ + in_string = in_string.drop_prefix( + std::min(in_string.find_first_not_of(delimiter), in_string.size())); + } +} + +/** + * Convert the given string to float and assign it to the destination value. + * + * Catches exception if the string cannot be converted to a float. The destination value + * is set to the given fallback value in that case. + */ + +void copy_string_to_float(StringRef src, const float fallback_value, float &r_dst) +{ + try { + r_dst = std::stof(string(src)); + } + catch (const std::invalid_argument &inv_arg) { + std::cerr << "Bad conversion to float:'" << inv_arg.what() << "':'" << src << "'" << std::endl; + r_dst = fallback_value; + } + catch (const std::out_of_range &out_of_range) { + std::cerr << "Out of range for float:'" << out_of_range.what() << ":'" << src << "'" + << std::endl; + r_dst = fallback_value; + } +} + +/** + * Convert all members of the Span of strings to floats and assign them to the float + * array members. Usually used for values like coordinates. + * + * Catches exception if any string cannot be converted to a float. The destination + * float is set to the given fallback value in that case. + */ +void copy_string_to_float(Span<StringRef> src, + const float fallback_value, + MutableSpan<float> r_dst) +{ + BLI_assert(src.size() >= r_dst.size()); + for (int i = 0; i < r_dst.size(); ++i) { + copy_string_to_float(src[i], fallback_value, r_dst[i]); + } +} + +/** + * Convert the given string to int and assign it to the destination value. + * + * Catches exception if the string cannot be converted to an integer. The destination + * int is set to the given fallback value in that case. + */ +void copy_string_to_int(StringRef src, const int fallback_value, int &r_dst) +{ + try { + r_dst = std::stoi(string(src)); + } + catch (const std::invalid_argument &inv_arg) { + std::cerr << "Bad conversion to int:'" << inv_arg.what() << "':'" << src << "'" << std::endl; + r_dst = fallback_value; + } + catch (const std::out_of_range &out_of_range) { + std::cerr << "Out of range for int:'" << out_of_range.what() << ":'" << src << "'" + << std::endl; + r_dst = fallback_value; + } +} + +/** + * Convert the given strings to ints and fill the destination int buffer. + * + * Catches exception if any string cannot be converted to an integer. The destination + * int is set to the given fallback value in that case. + */ +void copy_string_to_int(Span<StringRef> src, const int fallback_value, MutableSpan<int> r_dst) +{ + BLI_assert(src.size() >= r_dst.size()); + for (int i = 0; i < r_dst.size(); ++i) { + copy_string_to_int(src[i], fallback_value, r_dst[i]); + } +} + +std::string replace_all_occurences(StringRef original, StringRef to_remove, StringRef to_add) +{ + std::string clean{original}; + while (true) { + const std::string::size_type pos = clean.find(to_remove); + if (pos == std::string::npos) { + break; + } + clean.replace(pos, to_add.size(), to_add); + } + return clean; +} + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/parser_string_utils.hh b/source/blender/io/wavefront_obj/importer/parser_string_utils.hh new file mode 100644 index 00000000000..e0b9feb8e82 --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/parser_string_utils.hh @@ -0,0 +1,33 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2020 Blender Foundation. + * All rights reserved. + */ + +namespace blender::io::obj { + +void read_next_line(std::ifstream &file, std::string &r_line); +void split_line_key_rest(StringRef line, StringRef &r_line_key, StringRef &r_rest_line); +void split_by_char(StringRef in_string, const char delimiter, Vector<StringRef> &r_out_list); +void copy_string_to_float(StringRef src, const float fallback_value, float &r_dst); +void copy_string_to_float(Span<StringRef> src, + const float fallback_value, + MutableSpan<float> r_dst); +void copy_string_to_int(StringRef src, const int fallback_value, int &r_dst); +void copy_string_to_int(Span<StringRef> src, const int fallback_value, MutableSpan<int> r_dst); +std::string replace_all_occurences(StringRef original, StringRef to_remove, StringRef to_add); + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc new file mode 100644 index 00000000000..cec8fc6006f --- /dev/null +++ b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc @@ -0,0 +1,416 @@ +/* Apache License, Version 2.0 */ + +#include <fstream> +#include <gtest/gtest.h> +#include <ios> +#include <memory> +#include <sstream> +#include <string> +#include <system_error> + +#include "testing/testing.h" +#include "tests/blendfile_loading_base_test.h" + +#include "BKE_appdir.h" +#include "BKE_blender_version.h" + +#include "BLI_fileops.h" +#include "BLI_index_range.hh" +#include "BLI_string_utf8.h" +#include "BLI_vector.hh" + +#include "DEG_depsgraph.h" + +#include "obj_export_file_writer.hh" +#include "obj_export_mesh.hh" +#include "obj_export_nurbs.hh" +#include "obj_exporter.hh" + +#include "obj_exporter_tests.hh" + +namespace blender::io::obj { + +/* This is also the test name. */ +class obj_exporter_test : public BlendfileLoadingBaseTest { + public: + /** + * \param filepath: relative to "tests" directory. + */ + 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; + } +}; + +const std::string all_objects_file = "io_tests/blend_scene/all_objects.blend"; +const std::string all_curve_objects_file = "io_tests/blend_scene/all_curves.blend"; + +TEST_F(obj_exporter_test, filter_objects_curves_as_mesh) +{ + OBJExportParamsDefault _export; + if (!load_file_and_depsgraph(all_objects_file)) { + ADD_FAILURE(); + return; + } + auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)}; + EXPECT_EQ(objmeshes.size(), 17); + EXPECT_EQ(objcurves.size(), 0); +} + +TEST_F(obj_exporter_test, filter_objects_curves_as_nurbs) +{ + OBJExportParamsDefault _export; + if (!load_file_and_depsgraph(all_objects_file)) { + ADD_FAILURE(); + return; + } + _export.params.export_curves_as_nurbs = true; + auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)}; + EXPECT_EQ(objmeshes.size(), 16); + EXPECT_EQ(objcurves.size(), 2); +} + +TEST_F(obj_exporter_test, filter_objects_selected) +{ + OBJExportParamsDefault _export; + if (!load_file_and_depsgraph(all_objects_file)) { + ADD_FAILURE(); + return; + } + _export.params.export_selected_objects = true; + _export.params.export_curves_as_nurbs = true; + auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)}; + EXPECT_EQ(objmeshes.size(), 1); + EXPECT_EQ(objcurves.size(), 0); +} + +TEST(obj_exporter_utils, append_negative_frame_to_filename) +{ + const char path_original[FILE_MAX] = "/my_file.obj"; + const char path_truth[FILE_MAX] = "/my_file-123.obj"; + const int frame = -123; + char path_with_frame[FILE_MAX] = {0}; + const bool ok = append_frame_to_filename(path_original, frame, path_with_frame); + EXPECT_TRUE(ok); + EXPECT_EQ_ARRAY(path_with_frame, path_truth, BLI_strlen_utf8(path_truth)); +} + +TEST(obj_exporter_utils, append_positive_frame_to_filename) +{ + const char path_original[FILE_MAX] = "/my_file.obj"; + const char path_truth[FILE_MAX] = "/my_file123.obj"; + const int frame = 123; + char path_with_frame[FILE_MAX] = {0}; + const bool ok = append_frame_to_filename(path_original, frame, path_with_frame); + EXPECT_TRUE(ok); + EXPECT_EQ_ARRAY(path_with_frame, path_truth, BLI_strlen_utf8(path_truth)); +} + +TEST_F(obj_exporter_test, curve_nurbs_points) +{ + if (!load_file_and_depsgraph(all_curve_objects_file)) { + ADD_FAILURE(); + return; + } + + OBJExportParamsDefault _export; + _export.params.export_curves_as_nurbs = true; + auto [objmeshes_unused, objcurves]{filter_supported_objects(depsgraph, _export.params)}; + + for (StealUniquePtr<OBJCurve> objcurve : objcurves) { + if (all_nurbs_truth.count(objcurve->get_curve_name()) != 1) { + ADD_FAILURE(); + return; + } + const NurbsObject *const nurbs_truth = all_nurbs_truth.at(objcurve->get_curve_name()).get(); + EXPECT_EQ(objcurve->total_splines(), nurbs_truth->total_splines()); + for (int spline_index : IndexRange(objcurve->total_splines())) { + EXPECT_EQ(objcurve->total_spline_vertices(spline_index), + nurbs_truth->total_spline_vertices(spline_index)); + EXPECT_EQ(objcurve->get_nurbs_degree(spline_index), + nurbs_truth->get_nurbs_degree(spline_index)); + EXPECT_EQ(objcurve->total_spline_control_points(spline_index), + nurbs_truth->total_spline_control_points(spline_index)); + } + } +} + +TEST_F(obj_exporter_test, curve_coordinates) +{ + if (!load_file_and_depsgraph(all_curve_objects_file)) { + ADD_FAILURE(); + return; + } + + OBJExportParamsDefault _export; + _export.params.export_curves_as_nurbs = true; + auto [objmeshes_unused, objcurves]{filter_supported_objects(depsgraph, _export.params)}; + + for (StealUniquePtr<OBJCurve> objcurve : objcurves) { + if (all_nurbs_truth.count(objcurve->get_curve_name()) != 1) { + ADD_FAILURE(); + return; + } + const NurbsObject *const nurbs_truth = all_nurbs_truth.at(objcurve->get_curve_name()).get(); + EXPECT_EQ(objcurve->total_splines(), nurbs_truth->total_splines()); + for (int spline_index : IndexRange(objcurve->total_splines())) { + for (int vertex_index : IndexRange(objcurve->total_spline_vertices(spline_index))) { + EXPECT_V3_NEAR(objcurve->vertex_coordinates( + spline_index, vertex_index, _export.params.scaling_factor), + nurbs_truth->vertex_coordinates(spline_index, vertex_index), + 0.000001f); + } + } + } +} + +static std::unique_ptr<OBJWriter> init_writer(const OBJExportParams ¶ms, + const std::string out_filepath) +{ + try { + auto writer = std::make_unique<OBJWriter>(out_filepath.c_str(), params); + return writer; + } + catch (const std::system_error &ex) { + std::cerr << ex.code().category().name() << ": " << ex.what() << ": " << ex.code().message() + << std::endl; + return nullptr; + } +} + +/* The following is relative to BKE_tempdir_base. */ +const char *const temp_file_path = "output.OBJ"; + +static std::string read_temp_file_in_string(const std::string &file_path) +{ + std::ifstream temp_stream(file_path); + std::ostringstream input_ss; + input_ss << temp_stream.rdbuf(); + return input_ss.str(); +} + +TEST(obj_exporter_writer, header) +{ + /* Because testing doesn't fully initialize Blender, we need the following. */ + BKE_tempdir_init(NULL); + std::string out_file_path = blender::tests::flags_test_release_dir() + "/" + temp_file_path; + { + OBJExportParamsDefault _export; + std::unique_ptr<OBJWriter> writer = init_writer(_export.params, out_file_path); + if (!writer) { + ADD_FAILURE(); + return; + } + writer->write_header(); + } + const std::string result = read_temp_file_in_string(out_file_path); + using namespace std::string_literals; + ASSERT_EQ(result, "# Blender "s + BKE_blender_version_string() + "\n" + "# www.blender.org\n"); + BLI_delete(out_file_path.c_str(), false, false); +} + +TEST(obj_exporter_writer, mtllib) +{ + std::string out_file_path = blender::tests::flags_test_release_dir() + "/" + temp_file_path; + { + OBJExportParamsDefault _export; + std::unique_ptr<OBJWriter> writer = init_writer(_export.params, out_file_path); + if (!writer) { + ADD_FAILURE(); + return; + } + writer->write_mtllib_name("/Users/blah.mtl"); + writer->write_mtllib_name("\\C:\\blah.mtl"); + } + const std::string result = read_temp_file_in_string(out_file_path); + ASSERT_EQ(result, "mtllib blah.mtl\nmtllib blah.mtl\n"); +} + +/* Return true if string #a and string #b are equal after their first newline. */ +static bool strings_equal_after_first_lines(const std::string &a, const std::string &b) +{ + bool dbg_level = 0; + size_t a_len = a.size(); + size_t b_len = b.size(); + size_t a_next = a.find_first_of('\n'); + size_t b_next = b.find_first_of('\n'); + if (a_next == std::string::npos || b_next == std::string::npos) { + if (dbg_level > 0) { + std::cout << "Couldn't find newline in one of args\n"; + } + return false; + } + if (dbg_level > 0) { + if (a.compare(a_next, a_len - a_next, b, b_next, b_len - b_next) != 0) { + for (int i = 0; i < a_len - a_next && i < b_len - b_next; ++i) { + if (a[a_next + i] != b[b_next + i]) { + std::cout << "Difference found at pos " << a_next + i << " of a\n"; + std::cout << "a: " << a.substr(a_next + i, 100) << " ...\n"; + std::cout << "b: " << b.substr(b_next + i, 100) << " ... \n"; + return false; + } + } + } + else { + return true; + } + } + return a.compare(a_next, a_len - a_next, b, b_next, b_len - b_next) == 0; +} + +/* From here on, tests are whole file tests, testing for golden output. */ +class obj_exporter_regression_test : public obj_exporter_test { + public: + /** + * Export the given blend file with the given parameters and + * test to see if it matches a golden file (ignoring any difference in Blender version number). + * \param blendfile: input, relative to "tests" directory. + * \param golden_obj: expected output, relative to "tests" directory. + * \param params: the parameters to be used for export. + */ + void compare_obj_export_to_golden(const std::string &blendfile, + const std::string &golden_obj, + const std::string &golden_mtl, + OBJExportParams ¶ms) + { + if (!load_file_and_depsgraph(blendfile)) { + return; + } + /* Because testing doesn't fully initialize Blender, we need the following. */ + BKE_tempdir_init(NULL); + std::string tempdir = std::string(BKE_tempdir_base()); + std::string out_file_path = tempdir + BLI_path_basename(golden_obj.c_str()); + strncpy(params.filepath, out_file_path.c_str(), FILE_MAX); + params.blen_filepath = blendfile.c_str(); + export_frame(depsgraph, params, out_file_path.c_str()); + std::string output_str = read_temp_file_in_string(out_file_path); + + std::string golden_file_path = blender::tests::flags_test_asset_dir() + "/" + golden_obj; + std::string golden_str = read_temp_file_in_string(golden_file_path); + ASSERT_TRUE(strings_equal_after_first_lines(output_str, golden_str)); + BLI_delete(out_file_path.c_str(), false, false); + if (!golden_mtl.empty()) { + std::string out_mtl_file_path = tempdir + BLI_path_basename(golden_mtl.c_str()); + std::string output_mtl_str = read_temp_file_in_string(out_mtl_file_path); + std::string golden_mtl_file_path = blender::tests::flags_test_asset_dir() + "/" + golden_mtl; + std::string golden_mtl_str = read_temp_file_in_string(golden_mtl_file_path); + ASSERT_TRUE(strings_equal_after_first_lines(output_mtl_str, golden_mtl_str)); + BLI_delete(out_mtl_file_path.c_str(), false, false); + } + } +}; + +TEST_F(obj_exporter_regression_test, all_tris) +{ + OBJExportParamsDefault _export; + compare_obj_export_to_golden("io_tests/blend_geometry/all_tris.blend", + "io_tests/obj/all_tris.obj", + "io_tests/obj/all_tris.mtl", + _export.params); +} + +TEST_F(obj_exporter_regression_test, all_quads) +{ + OBJExportParamsDefault _export; + _export.params.scaling_factor = 2.0f; + _export.params.export_materials = false; + compare_obj_export_to_golden( + "io_tests/blend_geometry/all_quads.blend", "io_tests/obj/all_quads.obj", "", _export.params); +} + +TEST_F(obj_exporter_regression_test, fgons) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + compare_obj_export_to_golden( + "io_tests/blend_geometry/fgons.blend", "io_tests/obj/fgons.obj", "", _export.params); +} + +TEST_F(obj_exporter_regression_test, edges) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + compare_obj_export_to_golden( + "io_tests/blend_geometry/edges.blend", "io_tests/obj/edges.obj", "", _export.params); +} + +TEST_F(obj_exporter_regression_test, vertices) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + compare_obj_export_to_golden( + "io_tests/blend_geometry/vertices.blend", "io_tests/obj/vertices.obj", "", _export.params); +} + +TEST_F(obj_exporter_regression_test, nurbs_as_nurbs) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + _export.params.export_curves_as_nurbs = true; + compare_obj_export_to_golden( + "io_tests/blend_geometry/nurbs.blend", "io_tests/obj/nurbs.obj", "", _export.params); +} + +TEST_F(obj_exporter_regression_test, nurbs_as_mesh) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + _export.params.export_curves_as_nurbs = false; + compare_obj_export_to_golden( + "io_tests/blend_geometry/nurbs.blend", "io_tests/obj/nurbs_mesh.obj", "", _export.params); +} + +TEST_F(obj_exporter_regression_test, cube_all_data_triangulated) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + _export.params.export_triangulated_mesh = true; + compare_obj_export_to_golden("io_tests/blend_geometry/cube_all_data.blend", + "io_tests/obj/cube_all_data_triangulated.obj", + "", + _export.params); +} + +TEST_F(obj_exporter_regression_test, suzanne_all_data) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + _export.params.export_smooth_groups = true; + compare_obj_export_to_golden("io_tests/blend_geometry/suzanne_all_data.blend", + "io_tests/obj/suzanne_all_data.obj", + "", + _export.params); +} + +TEST_F(obj_exporter_regression_test, all_objects) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_smooth_groups = true; + compare_obj_export_to_golden("io_tests/blend_scene/all_objects.blend", + "io_tests/obj/all_objects.obj", + "io_tests/obj/all_objects.mtl", + _export.params); +} + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.hh b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.hh new file mode 100644 index 00000000000..def70eff0ee --- /dev/null +++ b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.hh @@ -0,0 +1,149 @@ +/* Apache License, Version 2.0 */ + +/** + * This file contains default values for several items like + * vertex coordinates, export parameters, MTL values etc. + */ + +#pragma once + +#include <array> +#include <gtest/gtest.h> +#include <string> +#include <vector> + +#include "IO_wavefront_obj.h" + +namespace blender::io::obj { + +using array_float_3 = std::array<float, 3>; + +/** + * This matches #OBJCurve's member functions, except that all the numbers and names are known + * constants. Used to store expected values of NURBS Curve sobjects. + */ +class NurbsObject { + private: + std::string nurbs_name_; + /* The indices in these vectors are spline indices. */ + std::vector<std::vector<array_float_3>> coordinates_; + std::vector<int> degrees_; + std::vector<int> control_points_; + + public: + NurbsObject(const std::string nurbs_name, + const std::vector<std::vector<array_float_3>> coordinates, + const std::vector<int> degrees, + const std::vector<int> control_points) + : nurbs_name_(nurbs_name), + coordinates_(coordinates), + degrees_(degrees), + control_points_(control_points) + { + } + + int total_splines() const + { + return coordinates_.size(); + } + + int total_spline_vertices(const int spline_index) const + { + if (spline_index >= coordinates_.size()) { + ADD_FAILURE(); + return 0; + } + return coordinates_[spline_index].size(); + } + + const float *vertex_coordinates(const int spline_index, const int vertex_index) const + { + return coordinates_[spline_index][vertex_index].data(); + } + + int get_nurbs_degree(const int spline_index) const + { + return degrees_[spline_index]; + } + + int total_spline_control_points(const int spline_index) const + { + return control_points_[spline_index]; + } +}; + +struct OBJExportParamsDefault { + OBJExportParams params; + OBJExportParamsDefault() + { + params.filepath[0] = '\0'; + params.blen_filepath = ""; + params.export_animation = false; + params.start_frame = 0; + params.end_frame = 1; + + params.forward_axis = OBJ_AXIS_NEGATIVE_Z_FORWARD; + params.up_axis = OBJ_AXIS_Y_UP; + params.scaling_factor = 1.f; + + params.export_eval_mode = DAG_EVAL_VIEWPORT; + params.export_selected_objects = false; + params.export_uv = true; + params.export_normals = true; + params.export_materials = true; + params.export_triangulated_mesh = false; + params.export_curves_as_nurbs = false; + + params.export_object_groups = false; + params.export_material_groups = false; + params.export_vertex_groups = false; + params.export_smooth_groups = true; + params.smooth_groups_bitflags = false; + } +}; + +const std::vector<std::vector<array_float_3>> coordinates_NurbsCurve{ + {{6.94742, 0.000000, 0.000000}, + {7.44742, 0.000000, -1.000000}, + {9.44742, 0.000000, -1.000000}, + {9.94742, 0.000000, 0.000000}}}; +const std::vector<std::vector<array_float_3>> coordinates_NurbsCircle{ + {{11.463165, 0.000000, 1.000000}, + {10.463165, 0.000000, 1.000000}, + {10.463165, 0.000000, 0.000000}, + {10.463165, 0.000000, -1.000000}, + {11.463165, 0.000000, -1.000000}, + {12.463165, 0.000000, -1.000000}, + {12.463165, 0.000000, 0.000000}, + {12.463165, 0.000000, 1.000000}}}; +const std::vector<std::vector<array_float_3>> coordinates_NurbsPathCurve{ + {{13.690557, 0.000000, 0.000000}, + {14.690557, 0.000000, 0.000000}, + {15.690557, 0.000000, 0.000000}, + {16.690557, 0.000000, 0.000000}, + {17.690557, 0.000000, 0.000000}}, + {{14.192808, 0.000000, 0.000000}, + {14.692808, 0.000000, -1.000000}, + {16.692808, 0.000000, -1.000000}, + {17.192808, 0.000000, 0.000000}}}; + +const std::map<std::string, std::unique_ptr<NurbsObject>> all_nurbs_truth = []() { + std::map<std::string, std::unique_ptr<NurbsObject>> all_nurbs; + all_nurbs.emplace( + "NurbsCurve", + /* Name, coordinates, degrees of splines, control points of splines. */ + std::make_unique<NurbsObject>( + "NurbsCurve", coordinates_NurbsCurve, std::vector<int>{3}, std::vector<int>{4})); + all_nurbs.emplace( + "NurbsCircle", + std::make_unique<NurbsObject>( + "NurbsCircle", coordinates_NurbsCircle, std::vector<int>{3}, std::vector<int>{11})); + /* This is actually an Object containing a NurbsPath and a NurbsCurve spline. */ + all_nurbs.emplace("NurbsPathCurve", + std::make_unique<NurbsObject>("NurbsPathCurve", + coordinates_NurbsPathCurve, + std::vector<int>{3, 3}, + std::vector<int>{5, 4})); + return all_nurbs; +}(); +} // namespace blender::io::obj diff --git a/source/blender/windowmanager/intern/wm_operator_props.c b/source/blender/windowmanager/intern/wm_operator_props.c index 898671706d1..ae0d3bf96e0 100644 --- a/source/blender/windowmanager/intern/wm_operator_props.c +++ b/source/blender/windowmanager/intern/wm_operator_props.c @@ -185,6 +185,9 @@ void WM_operator_properties_filesel(wmOperatorType *ot, prop = RNA_def_boolean( ot->srna, "filter_usd", (filter & FILE_TYPE_USD) != 0, "Filter USD files", ""); RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE); + prop = RNA_def_boolean( + ot->srna, "filter_obj", (filter & FILE_TYPE_OBJECT_IO) != 0, "Filter OBJ files", ""); + RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE); prop = RNA_def_boolean(ot->srna, "filter_volume", (filter & FILE_TYPE_VOLUME) != 0, |