diff options
26 files changed, 3961 insertions, 0 deletions
diff --git a/release/scripts/startup/bl_ui/space_topbar.py b/release/scripts/startup/bl_ui/space_topbar.py index 3137ac43549..99abc60db6f 100644 --- a/release/scripts/startup/bl_ui/space_topbar.py +++ b/release/scripts/startup/bl_ui/space_topbar.py @@ -481,6 +481,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 05f74bfa834..245514d4977 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 32d288f35e1..7a8afbcb227 100644 --- a/source/blender/blenloader/tests/blendfile_loading_base_test.cc +++ b/source/blender/blenloader/tests/blendfile_loading_base_test.cc @@ -27,9 +27,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" @@ -44,6 +46,8 @@ #include "IMB_imbuf.h" +#include "ED_datafiles.h" + #include "RNA_define.h" #include "WM_api.h" @@ -73,6 +77,7 @@ void BlendfileLoadingBaseTest::SetUpTestCase() RNA_init(); BKE_node_system_init(); BKE_callback_global_init(); + BKE_vfont_builtin_register(datatoc_bfont_pfb, datatoc_bfont_pfb_size); G.background = true; G.factory_startup = true; @@ -111,6 +116,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..1e1e3c2ff42 --- /dev/null +++ b/source/blender/editors/io/io_obj.c @@ -0,0 +1,369 @@ +/* + * 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. + */ + +/** \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)->filepath; + 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", ""); +} diff --git a/source/blender/editors/io/io_obj.h b/source/blender/editors/io/io_obj.h new file mode 100644 index 00000000000..5a0e6971edd --- /dev/null +++ b/source/blender/editors/io/io_obj.h @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** \file + * \ingroup editor/io + */ + +#pragma once + +struct wmOperatorType; + +void WM_OT_obj_export(struct wmOperatorType *ot); diff --git a/source/blender/editors/io/io_ops.c b/source/blender/editors/io/io_ops.c index b2788ee49a2..5dff0b69c2a 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,5 @@ void ED_operatortypes_io(void) WM_operatortype_append(CACHEFILE_OT_open); WM_operatortype_append(CACHEFILE_OT_reload); + 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 03261d6f267..f9783d1b19f 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..190475c5550 --- /dev/null +++ b/source/blender/io/wavefront_obj/CMakeLists.txt @@ -0,0 +1,84 @@ +# ***** 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. +# ***** END GPL LICENSE BLOCK ***** + +set(INC + . + ./exporter + ../../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 + + 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 +) + +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..1c93eafe91a --- /dev/null +++ b/source/blender/io/wavefront_obj/IO_wavefront_obj.cc @@ -0,0 +1,34 @@ +/* + * 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. + */ + +/** \file + * \ingroup obj + */ + +#include "BLI_timeit.hh" + +#include "IO_wavefront_obj.h" + +#include "obj_exporter.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); +} 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..25687fd957c --- /dev/null +++ b/source/blender/io/wavefront_obj/IO_wavefront_obj.h @@ -0,0 +1,97 @@ +/* + * 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. + */ + +/** \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; +}; + +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..d92d1c5ad48 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc @@ -0,0 +1,626 @@ +/* + * 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. + */ + +/** \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 { +/** + * Per reference http://www.martinreddy.net/gfx/3d/OBJ.spec: + * 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. + */ +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. */ + +/** + * Per reference http://www.martinreddy.net/gfx/3d/OBJ.spec: + * 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. + * 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..36d35ae370b --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh @@ -0,0 +1,132 @@ +/* + * 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. + */ + +/** \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..83571d8aa46 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_io.hh @@ -0,0 +1,340 @@ +/* + * 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. + */ + +/** \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..0947d1132b0 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc @@ -0,0 +1,489 @@ +/* + * 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. + */ + +/** \file + * \ingroup obj + */ + +#include "BKE_customdata.h" +#include "BKE_deform.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 Object *obj = export_object_eval_; + const int tot_deform_groups = BKE_object_defgroup_count(obj); + /* 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(BKE_object_defgroup_list(obj), 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(BKE_object_defgroup_list(export_object_eval_), 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..d72dd76d447 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh @@ -0,0 +1,131 @@ +/* + * 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. + */ + +/** \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..b60f8976177 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc @@ -0,0 +1,362 @@ +/* + * 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. + */ + +/** \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..2f62d189bd1 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mtl.hh @@ -0,0 +1,104 @@ +/* + * 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. + */ + +/** \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..4138ad2f697 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc @@ -0,0 +1,122 @@ +/* + * 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. + */ + +/** \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..463e41befb5 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh @@ -0,0 +1,57 @@ +/* + * 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. + */ + +/** \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..d15d053adc9 --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_exporter.cc @@ -0,0 +1,302 @@ +/* + * 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. + */ + +/** \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 (auto &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..e02a240b51a --- /dev/null +++ b/source/blender/io/wavefront_obj/exporter/obj_exporter.hh @@ -0,0 +1,88 @@ +/* + * 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. + */ + +/** \file + * \ingroup obj + */ + +#pragma once + +#include "BLI_utility_mixins.hh" + +#include "BLI_vector.hh" + +#include "IO_wavefront_obj.h" + +namespace blender::io::obj { + +/** + * 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(); +}; + +/** + * The main function for exporting a .obj file according to the given `export_parameters`. + * It uses the context `C` to get the dependency graph, and from that, the `Scene`. + * Depending on whether or not `export_params.export_animation` is set, it writes + * either one file per animation frame, or just one file. + */ +void exporter_main(bContext *C, const OBJExportParams &export_params); + +class OBJMesh; +class OBJCurve; + +/** + * Export a single frame of a .obj file, according to the given `export_parameters`. + * The frame state is given in `depsgraph`. + * The output file name is given by `filepath`. + * This function is normally called from `exporter_main`, but is exposed here for testing purposes. + */ +void export_frame(Depsgraph *depsgraph, + const OBJExportParams &export_params, + const char *filepath); + +/** + * Find the objects to be exported in the `view_layer` of the dependency graph`depsgraph`, + * and return them in vectors `unique_ptr`s of `OBJMesh` and `OBJCurve`. + * If `export_params.export_selected_objects` is set, then only selected objects are to be + * exported, else all objects are to be exported. But only objects of type `OB_MESH`, `OB_CURVE`, + * and `OB_SURF` are supported; the rest will be ignored. If `export_params.export_curves_as_nurbs` + * is set, then curves of type `CU_NURBS` are exported in curve form in the .obj file, otherwise + * they are converted to mesh and returned in the `OBJMesh` vector. All other exportable types are + * always converted to mesh and returned in the `OBJMesh` vector. + */ +std::pair<Vector<std::unique_ptr<OBJMesh>>, Vector<std::unique_ptr<OBJCurve>>> +filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params); + +/** + * Makes `r_filepath_with_frames` (which should point at a character array of size `FILE_MAX`) + * be `filepath` with its "#" characters replaced by the number representing `frame`, and with + * a .obj extension. + */ +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/tests/obj_exporter_tests.cc b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc new file mode 100644 index 00000000000..f12bfd0cea5 --- /dev/null +++ b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc @@ -0,0 +1,417 @@ +/* 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 (auto &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 (auto &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) +{ + /* If `dbg_level > 0` then a failing test will print context around the first mismatch. */ + const bool dbg_level = 0; + const size_t a_len = a.size(); + const size_t b_len = b.size(); + const size_t a_next = a.find_first_of('\n'); + const 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 a5887fc07b3..ebd6719d54d 100644 --- a/source/blender/windowmanager/intern/wm_operator_props.c +++ b/source/blender/windowmanager/intern/wm_operator_props.c @@ -184,6 +184,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, |