Welcome to mirror list, hosted at ThFree Co, Russian Federation.

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