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:
authorClément Foucault <foucault.clem@gmail.com>2022-01-26 23:57:44 +0300
committerClément Foucault <foucault.clem@gmail.com>2022-01-27 00:03:58 +0300
commit4226c484bdbe7336f1221094916fcdfb12850034 (patch)
tree33428e72be40105c222ca77935ee1554b702facc /source/blender/io/wavefront_obj
parent55a6a8900aec81e94f4d82401d6051e3b5507c0e (diff)
parentaf87b6d8cb75d9d625378dee25d726a0d55f75c6 (diff)
Merge branch 'draw-viewport-data' into eevee-rewrite
# Conflicts: # release/scripts/startup/bl_ui/properties_data_camera.py # source/blender/blenkernel/BKE_camera.h # source/blender/blenkernel/BKE_node.h # source/blender/blenkernel/intern/camera.c # source/blender/blenlib/BLI_float2.hh # source/blender/blenlib/BLI_float3.hh # source/blender/blenlib/BLI_float4.hh # source/blender/blenlib/BLI_math_geom.h # source/blender/blenlib/intern/math_geom.c # source/blender/draw/CMakeLists.txt # source/blender/draw/engines/basic/basic_engine.c # source/blender/draw/engines/eevee/eevee_cryptomatte.c # source/blender/draw/engines/eevee/eevee_effects.c # source/blender/draw/engines/eevee/eevee_engine.c # source/blender/draw/engines/eevee/eevee_lightcache.c # source/blender/draw/engines/eevee/eevee_lightcache.h # source/blender/draw/engines/eevee/eevee_lightprobes.c # source/blender/draw/engines/eevee/eevee_lights.c # source/blender/draw/engines/eevee/eevee_materials.c # source/blender/draw/engines/eevee/eevee_motion_blur.c # source/blender/draw/engines/eevee/eevee_occlusion.c # source/blender/draw/engines/eevee/eevee_private.h # source/blender/draw/engines/eevee/eevee_render.c # source/blender/draw/engines/eevee/eevee_renderpasses.c # source/blender/draw/engines/eevee/eevee_sampling.c # source/blender/draw/engines/eevee/eevee_screen_raytrace.c # source/blender/draw/engines/eevee/eevee_shaders.c # source/blender/draw/engines/eevee/eevee_shadows.c # source/blender/draw/engines/eevee/eevee_shadows_cube.c # source/blender/draw/engines/eevee/eevee_temporal_sampling.c # source/blender/draw/engines/eevee/shaders/ambient_occlusion_lib.glsl # source/blender/draw/engines/eevee/shaders/closure_eval_lib.glsl # source/blender/draw/engines/eevee/shaders/common_utiltex_lib.glsl # source/blender/draw/engines/eevee/shaders/effect_dof_bokeh_frag.glsl # source/blender/draw/engines/eevee/shaders/effect_dof_gather_frag.glsl # source/blender/draw/engines/eevee/shaders/effect_dof_reduce_frag.glsl # source/blender/draw/engines/eevee/shaders/effect_reflection_resolve_frag.glsl # source/blender/draw/engines/eevee/shaders/effect_temporal_aa.glsl # source/blender/draw/engines/eevee/shaders/random_lib.glsl # source/blender/draw/engines/eevee/shaders/shadow_vert.glsl # source/blender/draw/engines/eevee/shaders/surface_lib.glsl # source/blender/draw/engines/eevee/shaders/surface_vert.glsl # source/blender/draw/engines/eevee/shaders/volumetric_lib.glsl # source/blender/draw/engines/external/external_engine.c # source/blender/draw/engines/gpencil/gpencil_engine.c # source/blender/draw/engines/image/image_engine.c # source/blender/draw/engines/overlay/overlay_engine.c # source/blender/draw/engines/select/select_engine.c # source/blender/draw/engines/workbench/shaders/workbench_volume_frag.glsl # source/blender/draw/engines/workbench/shaders/workbench_volume_vert.glsl # source/blender/draw/engines/workbench/workbench_engine.c # source/blender/draw/engines/workbench/workbench_shader.c # source/blender/draw/intern/DRW_render.h # source/blender/draw/intern/draw_debug.h # source/blender/draw/intern/draw_manager_data.c # source/blender/draw/intern/draw_manager_exec.c # source/blender/draw/intern/draw_view_data.h # source/blender/gpu/CMakeLists.txt # source/blender/gpu/GPU_material.h # source/blender/gpu/GPU_shader.h # source/blender/gpu/GPU_state.h # source/blender/gpu/GPU_vertex_buffer.h # source/blender/gpu/intern/gpu_codegen.c # source/blender/gpu/intern/gpu_material.c # source/blender/gpu/intern/gpu_material_library.h # source/blender/gpu/intern/gpu_node_graph.c # source/blender/gpu/intern/gpu_texture_private.hh # source/blender/gpu/intern/gpu_vertex_buffer.cc # source/blender/gpu/opengl/gl_shader.cc # source/blender/gpu/shaders/gpu_shader_common_obinfos_lib.glsl # source/blender/gpu/shaders/material/gpu_shader_material_shader_to_rgba.glsl # source/blender/nodes/shader/node_shader_tree.cc # source/blender/nodes/shader/nodes/node_shader_background.cc # source/blender/nodes/shader/nodes/node_shader_bsdf_anisotropic.cc # source/blender/nodes/shader/nodes/node_shader_bsdf_diffuse.cc # source/blender/nodes/shader/nodes/node_shader_bsdf_glass.cc # source/blender/nodes/shader/nodes/node_shader_bsdf_glossy.cc # source/blender/nodes/shader/nodes/node_shader_bsdf_hair.cc # source/blender/nodes/shader/nodes/node_shader_bsdf_hair_principled.c # source/blender/nodes/shader/nodes/node_shader_bsdf_principled.c # source/blender/nodes/shader/nodes/node_shader_bsdf_refraction.cc # source/blender/nodes/shader/nodes/node_shader_bsdf_toon.cc # source/blender/nodes/shader/nodes/node_shader_bsdf_translucent.cc # source/blender/nodes/shader/nodes/node_shader_bsdf_transparent.cc # source/blender/nodes/shader/nodes/node_shader_bsdf_velvet.cc # source/blender/nodes/shader/nodes/node_shader_eevee_specular.cc # source/blender/nodes/shader/nodes/node_shader_emission.cc # source/blender/nodes/shader/nodes/node_shader_holdout.cc # source/blender/nodes/shader/nodes/node_shader_output_material.cc # source/blender/nodes/shader/nodes/node_shader_subsurface_scattering.c # source/blender/nodes/shader/nodes/node_shader_tex_coord.cc # source/blender/nodes/shader/nodes/node_shader_vector_transform.cc # source/blender/nodes/shader/nodes/node_shader_volume_absorption.cc # source/blender/nodes/shader/nodes/node_shader_volume_principled.cc # source/blender/nodes/shader/nodes/node_shader_volume_scatter.cc # source/blender/render/RE_pipeline.h # source/blender/render/intern/initrender.c
Diffstat (limited to 'source/blender/io/wavefront_obj')
-rw-r--r--source/blender/io/wavefront_obj/CMakeLists.txt84
-rw-r--r--source/blender/io/wavefront_obj/IO_wavefront_obj.cc34
-rw-r--r--source/blender/io/wavefront_obj/IO_wavefront_obj.h97
-rw-r--r--source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc521
-rw-r--r--source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh217
-rw-r--r--source/blender/io/wavefront_obj/exporter/obj_export_io.hh353
-rw-r--r--source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc468
-rw-r--r--source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh226
-rw-r--r--source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc360
-rw-r--r--source/blender/io/wavefront_obj/exporter/obj_export_mtl.hh104
-rw-r--r--source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc105
-rw-r--r--source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh72
-rw-r--r--source/blender/io/wavefront_obj/exporter/obj_exporter.cc291
-rw-r--r--source/blender/io/wavefront_obj/exporter/obj_exporter.hh101
-rw-r--r--source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc430
-rw-r--r--source/blender/io/wavefront_obj/tests/obj_exporter_tests.hh149
16 files changed, 3612 insertions, 0 deletions
diff --git a/source/blender/io/wavefront_obj/CMakeLists.txt b/source/blender/io/wavefront_obj/CMakeLists.txt
new file mode 100644
index 00000000000..296dd70b5a2
--- /dev/null
+++ b/source/blender/io/wavefront_obj/CMakeLists.txt
@@ -0,0 +1,84 @@
+# ***** BEGIN GPL LICENSE BLOCK *****
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# ***** END GPL LICENSE BLOCK *****
+
+set(INC
+ .
+ ./exporter
+ ../../blenkernel
+ ../../blenlib
+ ../../bmesh
+ ../../bmesh/intern
+ ../../depsgraph
+ ../../editors/include
+ ../../makesdna
+ ../../makesrna
+ ../../nodes
+ ../../windowmanager
+ ../../../../intern/guardedalloc
+)
+
+set(INC_SYS
+
+)
+
+set(SRC
+ IO_wavefront_obj.cc
+ exporter/obj_export_file_writer.cc
+ exporter/obj_export_mesh.cc
+ exporter/obj_export_mtl.cc
+ exporter/obj_export_nurbs.cc
+ exporter/obj_exporter.cc
+
+ IO_wavefront_obj.h
+ 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
+ exporter/obj_exporter.hh
+)
+
+set(LIB
+ bf_blenkernel
+)
+
+blender_add_lib(bf_wavefront_obj "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
+
+if(WITH_GTESTS)
+ set(TEST_SRC
+ tests/obj_exporter_tests.cc
+ tests/obj_exporter_tests.hh
+ )
+
+ set(TEST_INC
+ ${INC}
+
+ ../../blenloader
+ ../../../../tests/gtests
+ )
+
+ set(TEST_LIB
+ ${LIB}
+
+ bf_blenloader_tests
+ bf_wavefront_obj
+ )
+
+ include(GTestTesting)
+ blender_add_test_lib(bf_wavefront_obj_tests "${TEST_SRC}" "${TEST_INC}" "${INC_SYS}" "${TEST_LIB}")
+ add_dependencies(bf_wavefront_obj_tests bf_wavefront_obj)
+endif()
diff --git a/source/blender/io/wavefront_obj/IO_wavefront_obj.cc b/source/blender/io/wavefront_obj/IO_wavefront_obj.cc
new file mode 100644
index 00000000000..1c93eafe91a
--- /dev/null
+++ b/source/blender/io/wavefront_obj/IO_wavefront_obj.cc
@@ -0,0 +1,34 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup obj
+ */
+
+#include "BLI_timeit.hh"
+
+#include "IO_wavefront_obj.h"
+
+#include "obj_exporter.hh"
+
+/**
+ * C-interface for the exporter.
+ */
+void OBJ_export(bContext *C, const OBJExportParams *export_params)
+{
+ SCOPED_TIMER("OBJ export");
+ blender::io::obj::exporter_main(C, *export_params);
+}
diff --git a/source/blender/io/wavefront_obj/IO_wavefront_obj.h b/source/blender/io/wavefront_obj/IO_wavefront_obj.h
new file mode 100644
index 00000000000..684eb3eda41
--- /dev/null
+++ b/source/blender/io/wavefront_obj/IO_wavefront_obj.h
@@ -0,0 +1,97 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include "BKE_context.h"
+#include "BLI_path_util.h"
+#include "DEG_depsgraph.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum {
+ OBJ_AXIS_X_UP = 0,
+ OBJ_AXIS_Y_UP = 1,
+ OBJ_AXIS_Z_UP = 2,
+ OBJ_AXIS_NEGATIVE_X_UP = 3,
+ OBJ_AXIS_NEGATIVE_Y_UP = 4,
+ OBJ_AXIS_NEGATIVE_Z_UP = 5,
+} eTransformAxisUp;
+
+typedef enum {
+ OBJ_AXIS_X_FORWARD = 0,
+ OBJ_AXIS_Y_FORWARD = 1,
+ OBJ_AXIS_Z_FORWARD = 2,
+ OBJ_AXIS_NEGATIVE_X_FORWARD = 3,
+ OBJ_AXIS_NEGATIVE_Y_FORWARD = 4,
+ OBJ_AXIS_NEGATIVE_Z_FORWARD = 5,
+} eTransformAxisForward;
+
+static const int TOTAL_AXES = 3;
+
+struct OBJExportParams {
+ /** Full path to the destination .OBJ file. */
+ char filepath[FILE_MAX];
+
+ /** Full path to current blender file (used for comments in output). */
+ const char *blen_filepath;
+
+ /** Whether multiple frames should be exported. */
+ bool export_animation;
+ /** The first frame to be exported. */
+ int start_frame;
+ /** The last frame to be exported. */
+ int end_frame;
+
+ /* Geometry Transform options. */
+ eTransformAxisForward forward_axis;
+ eTransformAxisUp up_axis;
+ float scaling_factor;
+
+ /* File Write Options. */
+ bool export_selected_objects;
+ eEvaluationMode export_eval_mode;
+ bool export_uv;
+ bool export_normals;
+ bool export_materials;
+ bool export_triangulated_mesh;
+ bool export_curves_as_nurbs;
+
+ /* Grouping options. */
+ bool export_object_groups;
+ bool export_material_groups;
+ bool export_vertex_groups;
+ /**
+ * Calculate smooth groups from sharp edges.
+ */
+ bool export_smooth_groups;
+ /**
+ * Create bitflags instead of the default "0"/"1" group IDs.
+ */
+ bool smooth_groups_bitflags;
+};
+
+void OBJ_export(bContext *C, const struct OBJExportParams *export_params);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc
new file mode 100644
index 00000000000..d31353c4a76
--- /dev/null
+++ b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc
@@ -0,0 +1,521 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup obj
+ */
+
+#include <algorithm>
+#include <cstdio>
+
+#include "BKE_blender_version.h"
+
+#include "BLI_path_util.h"
+
+#include "obj_export_mesh.hh"
+#include "obj_export_mtl.hh"
+#include "obj_export_nurbs.hh"
+
+#include "obj_export_file_writer.hh"
+
+namespace blender::io::obj {
+/**
+ * Per reference http://www.martinreddy.net/gfx/3d/OBJ.spec:
+ * To turn off smoothing groups, use a value of 0 or off.
+ * Polygonal elements use group numbers to put elements in different smoothing groups.
+ * For free-form surfaces, smoothing groups are either turned on or off;
+ * there is no difference between values greater than 0.
+ */
+const int SMOOTH_GROUP_DISABLED = 0;
+const int SMOOTH_GROUP_DEFAULT = 1;
+
+const char *DEFORM_GROUP_DISABLED = "off";
+/* There is no deform group default name. Use what the user set in the UI. */
+
+/**
+ * Per reference http://www.martinreddy.net/gfx/3d/OBJ.spec:
+ * Once a material is assigned, it cannot be turned off; it can only be changed.
+ * If a material name is not specified, a white material is used.
+ * So an empty material name is written. */
+const char *MATERIAL_GROUP_DISABLED = "";
+
+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>();
+}
+
+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>();
+}
+
+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>();
+}
+
+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");
+}
+
+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);
+}
+
+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);
+}
+
+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);
+}
+
+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]);
+ }
+}
+
+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]);
+ }
+}
+
+void OBJWriter::write_poly_normals(OBJMesh &obj_mesh_data)
+{
+ obj_mesh_data.ensure_mesh_normals();
+ Vector<float3> normals;
+ obj_mesh_data.store_normal_coords_and_indices(normals);
+ for (const float3 &normal : normals) {
+ file_handler_->write<eOBJSyntaxElement::normal>(normal[0], normal[1], normal[2]);
+ }
+}
+
+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;
+}
+
+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;
+}
+
+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;
+}
+
+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;
+}
+
+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());
+
+ 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);
+ Vector<int> poly_normal_indices = obj_mesh_data.calc_poly_normal_indices(i);
+
+ 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);
+ }
+}
+
+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);
+ }
+}
+
+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>();
+ }
+}
+
+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();
+ index_offsets_.normal_offset += obj_mesh_data.tot_normal_indices();
+}
+
+/* -------------------------------------------------------------------- */
+/** \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();
+};
+
+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<FormattedFileHandler<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_;
+}
+
+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);
+}
+
+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.");
+}
+
+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);
+ }
+ }
+ }
+}
+
+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..7385d9fabe2
--- /dev/null
+++ b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh
@@ -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.
+ */
+
+/** \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<FormattedFileHandler<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<FormattedFileHandler<eFileType::OBJ>>(filepath);
+ }
+
+ void write_header() const;
+
+ /**
+ * Write object's name or group.
+ */
+ void write_object_name(const OBJMesh &obj_mesh_data) const;
+ /**
+ * Write an object's group with mesh and/or material name appended conditionally.
+ */
+ void write_object_group(const OBJMesh &obj_mesh_data) const;
+ /**
+ * Write file name of Material Library in .OBJ file.
+ */
+ void write_mtllib_name(const StringRefNull mtl_filepath) const;
+ /**
+ * Write vertex coordinates for all vertices as "v x y z".
+ */
+ void write_vertex_coords(const OBJMesh &obj_mesh_data) const;
+ /**
+ * Write UV vertex coordinates for all vertices as `vt u v`.
+ * \note UV indices are stored here, but written with polygons later.
+ */
+ void write_uv_coords(OBJMesh &obj_mesh_data) const;
+ /**
+ * Write loop normals for smooth-shaded polygons, and polygon normals otherwise, as "vn x y z".
+ * \note Normal indices ares stored here, but written with polygons later.
+ */
+ void write_poly_normals(OBJMesh &obj_mesh_data);
+ /**
+ * Write smooth group if polygon at the given index is shaded smooth else "s 0"
+ */
+ int write_smooth_group(const OBJMesh &obj_mesh_data,
+ int poly_index,
+ int last_poly_smooth_group) const;
+ /**
+ * 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 write_poly_material(const OBJMesh &obj_mesh_data,
+ int poly_index,
+ int16_t last_poly_mat_nr,
+ std::function<const char *(int)> matname_fn) const;
+ /**
+ * Write the name of the deform group of a polygon.
+ */
+ int16_t write_vertex_group(const OBJMesh &obj_mesh_data,
+ int poly_index,
+ int16_t last_poly_vertex_group) const;
+ /**
+ * 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 write_poly_elements(const OBJMesh &obj_mesh_data,
+ std::function<const char *(int)> matname_fn);
+ /**
+ * Write loose edges of a mesh as "l v1 v2".
+ */
+ void write_edges_indices(const OBJMesh &obj_mesh_data) const;
+ /**
+ * Write a NURBS curve to the .OBJ file in parameter form.
+ */
+ void write_nurbs_curve(const OBJCurve &obj_nurbs_data) const;
+
+ /**
+ * When there are multiple objects in a frame, the indices of previous objects' coordinates or
+ * normals add up.
+ */
+ 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;
+ /**
+ * \return Writer function with appropriate polygon-element syntax.
+ */
+ func_vert_uv_normal_indices get_poly_element_writer(int total_uv_vertices) const;
+
+ /**
+ * Write one line of polygon indices as "f v1/vt1/vn1 v2/vt2/vn2 ...".
+ */
+ void write_vert_uv_normal_indices(Span<int> vert_indices,
+ Span<int> uv_indices,
+ Span<int> normal_indices) const;
+ /**
+ * Write one line of polygon indices as "f v1//vn1 v2//vn2 ...".
+ */
+ void write_vert_normal_indices(Span<int> vert_indices,
+ Span<int> /*uv_indices*/,
+ Span<int> normal_indices) const;
+ /**
+ * Write one line of polygon indices as "f v1/vt1 v2/vt2 ...".
+ */
+ void write_vert_uv_indices(Span<int> vert_indices,
+ Span<int> uv_indices,
+ Span<int> /*normal_indices*/) const;
+ /**
+ * Write one line of polygon indices as "f v1 v2 ...".
+ */
+ 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<FormattedFileHandler<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:
+ /*
+ * Create the .MTL file.
+ */
+ MTLWriter(const char *obj_filepath) noexcept(false);
+
+ void write_header(const char *blen_filepath) const;
+ /**
+ * 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 write_materials();
+ StringRefNull mtl_file_path() const;
+ /**
+ * Add the materials of the given object to #MTLWriter, de-duplicating
+ * 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> add_materials(const OBJMesh &mesh_to_export);
+ const char *mtlmaterial_name(int index);
+
+ private:
+ /**
+ * Write properties sourced from p-BSDF node or #Object.Material.
+ */
+ void write_bsdf_properties(const MTLMaterial &mtl_material);
+ /**
+ * Write a texture map in the form "map_XX -s 1. 1. 1. -o 0. 0. 0. [-bm 1.] path/to/image".
+ */
+ 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..1bbefaee75f
--- /dev/null
+++ b/source/blender/io/wavefront_obj/exporter/obj_export_io.hh
@@ -0,0 +1,353 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include <cstdio>
+#include <string>
+#include <system_error>
+#include <type_traits>
+
+#include "BLI_compiler_attrs.h"
+#include "BLI_fileops.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;
+
+/* Used to prevent mixing of say OBJ file format with MTL syntax elements. */
+template<> struct FileTypeTraits<eFileType::OBJ> {
+ using SyntaxType = eOBJSyntaxElement;
+};
+
+template<> struct FileTypeTraits<eFileType::MTL> {
+ using SyntaxType = eMTLSyntaxElement;
+};
+
+struct FormattingSyntax {
+ /* Formatting syntax with the file format key like `newmtl %s\n`. */
+ const char *fmt = nullptr;
+ /* Number of arguments needed by the syntax. */
+ const int total_args = 0;
+ /* Whether types of the given arguments are accepted by the syntax above. Fail to compile by
+ * default.
+ */
+ const bool are_types_valid = false;
+};
+
+/**
+ * Type dependent but always false. Use to add a `constexpr` 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<typename... T>
+constexpr FormattingSyntax 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 %.4f %.4f %.4f\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<typename... T>
+constexpr FormattingSyntax 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...>};
+ }
+ }
+}
+
+/**
+ * File format and syntax agnostic file writer.
+ */
+template<eFileType filetype> class FormattedFileHandler : NonCopyable, NonMovable {
+ private:
+ std::FILE *outfile_ = nullptr;
+ std::string outfile_path_;
+
+ public:
+ FormattedFileHandler(std::string outfile_path) noexcept(false)
+ : outfile_path_(std::move(outfile_path))
+ {
+ outfile_ = BLI_fopen(outfile_path_.c_str(), "w");
+ if (!outfile_) {
+ throw std::system_error(errno, std::system_category(), "Cannot open file " + outfile_path_);
+ }
+ }
+
+ ~FormattedFileHandler()
+ {
+ if (outfile_ && std::fclose(outfile_)) {
+ std::cerr << "Error: could not close the file '" << outfile_path_
+ << "' properly, it may be corrupted." << std::endl;
+ }
+ }
+
+ /**
+ * Example invocation: `writer->write<eMTLSyntaxElement::newmtl>("foo")`.
+ *
+ * \param key Must match what the instance's filetype expects; i.e., `eMTLSyntaxElement` for
+ * `eFileType::MTL`.
+ */
+ template<typename FileTypeTraits<filetype>::SyntaxType key, typename... T>
+ constexpr void write(T &&...args) const
+ {
+ /* Get format syntax, number of arguments expected and whether types of given arguments are
+ * valid.
+ */
+ constexpr FormattingSyntax fmt_nargs_valid = syntax_elem_to_formatting<T...>(key);
+ BLI_STATIC_ASSERT(fmt_nargs_valid.are_types_valid &&
+ (sizeof...(T) == fmt_nargs_valid.total_args),
+ "Types of all arguments and the number of arguments should match what the "
+ "formatting specifies.");
+ write_impl(fmt_nargs_valid.fmt, std::forward<T>(args)...);
+ }
+
+ 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. int float etc. are not affected.
+ * \return: `const char *` or the original argument if the argument is
+ * not related to #std::string.
+ */
+ template<typename T> constexpr auto convert_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 {
+ /* For int, float etc. */
+ return std::forward<T>(arg);
+ }
+ }
+
+ template<typename... T> constexpr void write_impl(const char *fmt, T &&...args) const
+ {
+ if constexpr (sizeof...(T) == 0) {
+ std::fputs(fmt, outfile_);
+ }
+ else {
+ std::fprintf(outfile_, fmt, convert_to_primitive(std::forward<T>(args))...);
+ }
+ }
+};
+
+} // 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..c1b12ddd217
--- /dev/null
+++ b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc
@@ -0,0 +1,468 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup obj
+ */
+/* Silence warnings from copying deprecated fields. Needed for an Object copy constructor use. */
+#define DNA_DEPRECATED_ALLOW
+
+#include "BKE_customdata.h"
+#include "BKE_deform.h"
+#include "BKE_lib_id.h"
+#include "BKE_material.h"
+#include "BKE_mesh.h"
+#include "BKE_mesh_mapping.h"
+#include "BKE_object.h"
+
+#include "BLI_listbase.h"
+#include "BLI_map.hh"
+#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 {
+OBJMesh::OBJMesh(Depsgraph *depsgraph, const OBJExportParams &export_params, Object *mesh_object)
+{
+ /* We need to copy the object because it may be in temporary space. */
+ Object *obj_eval = DEG_get_evaluated_object(depsgraph, mesh_object);
+ export_object_eval_ = *obj_eval;
+ 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_);
+ }
+}
+
+void OBJMesh::free_mesh_if_needed()
+{
+ if (mesh_eval_needs_free_ && export_mesh_eval_) {
+ BKE_id_free(nullptr, export_mesh_eval_);
+ }
+}
+
+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};
+}
+
+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;
+}
+
+int16_t OBJMesh::tot_materials() const
+{
+ return export_mesh_eval_->totcol;
+}
+
+int OBJMesh::tot_normal_indices() const
+{
+ return tot_normal_indices_;
+}
+
+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_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_);
+}
+
+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);
+}
+
+const Material *OBJMesh::get_object_material(const int16_t mat_nr) const
+{
+ /**
+ * The const_cast is safe here because BKE_object_material_get won't change the object
+ * but it is a big can of worms to fix the declaration of that function right now.
+ *
+ * The call uses "+ 1" as material getter needs one-based indices.
+ */
+ Object *obj = const_cast<Object *>(&export_object_eval_);
+ const Material *r_mat = BKE_object_material_get(obj, 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;
+}
+
+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;
+}
+
+const char *OBJMesh::get_object_name() const
+{
+ return export_object_eval_.id.name + 2;
+}
+
+const char *OBJMesh::get_object_mesh_name() const
+{
+ return export_mesh_eval_->id.name + 2;
+}
+
+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;
+}
+
+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;
+}
+
+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;
+}
+
+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];
+}
+
+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;
+}
+
+/** Round \a f to \a round_digits decimal digits. */
+static float round_float_to_n_digits(const float f, int round_digits)
+{
+ float scale = powf(10.0, round_digits);
+ return ceilf((scale * f - 0.49999999f)) / scale;
+}
+
+static float3 round_float3_to_n_digits(const float3 &v, int round_digits)
+{
+ float3 ans;
+ ans.x = round_float_to_n_digits(v.x, round_digits);
+ ans.y = round_float_to_n_digits(v.y, round_digits);
+ ans.z = round_float_to_n_digits(v.z, round_digits);
+ return ans;
+}
+
+void OBJMesh::store_normal_coords_and_indices(Vector<float3> &r_normal_coords)
+{
+ /* We'll round normal components to 4 digits.
+ * This will cover up some minor differences
+ * between floating point calculations on different platforms.
+ * Since normals are normalized, there will be no perceptible loss
+ * of precision when rounding to 4 digits. */
+ constexpr int round_digits = 4;
+ int cur_normal_index = 0;
+ Map<float3, int> normal_to_index;
+ /* We don't know how many unique normals there will be, but this is a guess. */
+ normal_to_index.reserve(export_mesh_eval_->totpoly);
+ loop_to_normal_index_.resize(export_mesh_eval_->totloop);
+ loop_to_normal_index_.fill(-1);
+ const float(
+ *lnors)[3] = (const float(*)[3])(CustomData_get_layer(&export_mesh_eval_->ldata, CD_NORMAL));
+ for (int poly_index = 0; poly_index < export_mesh_eval_->totpoly; ++poly_index) {
+ const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index];
+ bool need_per_loop_normals = is_ith_poly_smooth(poly_index);
+ if (need_per_loop_normals) {
+ for (int loop_of_poly = 0; loop_of_poly < mpoly.totloop; ++loop_of_poly) {
+ float3 loop_normal;
+ int loop_index = mpoly.loopstart + loop_of_poly;
+ BLI_assert(loop_index < export_mesh_eval_->totloop);
+ copy_v3_v3(loop_normal, lnors[loop_index]);
+ mul_mat3_m4_v3(world_and_axes_transform_, loop_normal);
+ float3 rounded_loop_normal = round_float3_to_n_digits(loop_normal, round_digits);
+ int loop_norm_index = normal_to_index.lookup_default(rounded_loop_normal, -1);
+ if (loop_norm_index == -1) {
+ loop_norm_index = cur_normal_index++;
+ normal_to_index.add(rounded_loop_normal, loop_norm_index);
+ r_normal_coords.append(rounded_loop_normal);
+ }
+ loop_to_normal_index_[loop_index] = loop_norm_index;
+ }
+ }
+ else {
+ float3 poly_normal = calc_poly_normal(poly_index);
+ float3 rounded_poly_normal = round_float3_to_n_digits(poly_normal, round_digits);
+ int poly_norm_index = normal_to_index.lookup_default(rounded_poly_normal, -1);
+ if (poly_norm_index == -1) {
+ poly_norm_index = cur_normal_index++;
+ normal_to_index.add(rounded_poly_normal, poly_norm_index);
+ r_normal_coords.append(rounded_poly_normal);
+ }
+ for (int i = 0; i < mpoly.totloop; ++i) {
+ int loop_index = mpoly.loopstart + i;
+ BLI_assert(loop_index < export_mesh_eval_->totloop);
+ loop_to_normal_index_[loop_index] = poly_norm_index;
+ }
+ }
+ }
+ tot_normal_indices_ = cur_normal_index;
+}
+
+Vector<int> OBJMesh::calc_poly_normal_indices(const int poly_index) const
+{
+ const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index];
+ const int totloop = mpoly.totloop;
+ Vector<int> r_poly_normal_indices(totloop);
+ for (int poly_loop_index = 0; poly_loop_index < totloop; poly_loop_index++) {
+ int loop_index = mpoly.loopstart + poly_loop_index;
+ r_poly_normal_indices[poly_loop_index] = loop_to_normal_index_[loop_index];
+ }
+ return r_poly_normal_indices;
+}
+
+int16_t OBJMesh::get_poly_deform_group_index(const int poly_index) const
+{
+ BLI_assert(poly_index < export_mesh_eval_->totpoly);
+ const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index];
+ const MLoop *mloop = &export_mesh_eval_->mloop[mpoly.loopstart];
+ const Object *obj = &export_object_eval_;
+ const int tot_deform_groups = BKE_object_defgroup_count(obj);
+ /* Indices of the vector index into deform groups of an object; values are the]
+ * number of vertex members in one deform group. */
+ Vector<int16_t> deform_group_members(tot_deform_groups, 0);
+ /* Whether at least one vertex in the polygon belongs to any group. */
+ bool found_group = false;
+
+ const MDeformVert *dvert_orig = static_cast<MDeformVert *>(
+ CustomData_get_layer(&export_mesh_eval_->vdata, CD_MDEFORMVERT));
+ if (!dvert_orig) {
+ return NOT_FOUND;
+ }
+
+ const MDeformWeight *curr_weight = nullptr;
+ const MDeformVert *dvert = nullptr;
+ for (int loop_index = 0; loop_index < mpoly.totloop; loop_index++) {
+ dvert = &dvert_orig[(mloop + loop_index)->v];
+ curr_weight = dvert->dw;
+ if (curr_weight) {
+ bDeformGroup *vertex_group = static_cast<bDeformGroup *>(
+ BLI_findlink(BKE_object_defgroup_list(obj), curr_weight->def_nr));
+ if (vertex_group) {
+ deform_group_members[curr_weight->def_nr] += 1;
+ found_group = true;
+ }
+ }
+ }
+
+ if (!found_group) {
+ return NOT_FOUND;
+ }
+ /* Index of the group with maximum vertices. */
+ int16_t max_idx = std::max_element(deform_group_members.begin(), deform_group_members.end()) -
+ deform_group_members.begin();
+ return max_idx;
+}
+
+const char *OBJMesh::get_poly_deform_group_name(const int16_t def_group_index) const
+{
+ const bDeformGroup &vertex_group = *(static_cast<bDeformGroup *>(
+ BLI_findlink(BKE_object_defgroup_list(&export_object_eval_), def_group_index)));
+ return vertex_group.name;
+}
+
+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..f3ace140006
--- /dev/null
+++ b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh
@@ -0,0 +1,226 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include <optional>
+
+#include "BLI_math_vec_types.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 "DNA_object_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 initialize usually non-negative numbers. */
+const int NEGATIVE_INIT = -10;
+
+/**
+ * #std::unique_ptr than handles freeing #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:
+ /**
+ * We need to copy the entire Object structure here because the dependency graph iterator
+ * sometimes builds an Object in a temporary space that doesn't persist.
+ */
+ 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_;
+ /**
+ * Per-loop normal index.
+ */
+ Vector<int> loop_to_normal_index_;
+ /*
+ * Total number of normal indices (maximum entry, plus 1, in
+ * the loop_to_norm_index_ vector).
+ */
+ int tot_normal_indices_ = 0;
+ /**
+ * 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:
+ /**
+ * Store evaluated Object and Mesh pointers. Conditionally triangulate a mesh, or
+ * create a new Mesh from a Curve.
+ */
+ 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_normal_indices() const;
+ int tot_edges() const;
+
+ /**
+ * \return Total materials in the object.
+ */
+ int16_t tot_materials() const;
+ /**
+ * Return mat_nr-th material of the object. The given index should be zero-based.
+ */
+ const Material *get_object_material(int16_t mat_nr) const;
+ /**
+ * Returns a zero-based index of a polygon's material indexing into
+ * the Object's material slots.
+ */
+ int16_t ith_poly_matnr(int poly_index) const;
+
+ void ensure_mesh_normals() const;
+ void ensure_mesh_edges() const;
+
+ /**
+ * Calculate smooth groups of a smooth-shaded object.
+ * \return A polygon aligned array of smooth group numbers.
+ */
+ void calc_smooth_groups(bool use_bitflags);
+ /**
+ * \return Smooth group of the polygon at the given index.
+ */
+ int ith_smooth_group(int poly_index) const;
+ bool is_ith_poly_smooth(int poly_index) const;
+
+ /**
+ * Get object name as it appears in the outliner.
+ */
+ const char *get_object_name() const;
+ /**
+ * Get Object's Mesh's name.
+ */
+ const char *get_object_mesh_name() const;
+ /**
+ * Get object's material (at the given index) name. The given index should be zero-based.
+ */
+ const char *get_object_material_name(int16_t mat_nr) const;
+
+ /**
+ * Calculate coordinates of the vertex at the given index.
+ */
+ float3 calc_vertex_coords(int vert_index, float scaling_factor) const;
+ /**
+ * Calculate vertex indices of all vertices of the polygon at the given index.
+ */
+ Vector<int> calc_poly_vertex_indices(int poly_index) const;
+ /**
+ * Calculate UV vertex coordinates of an Object.
+ *
+ * \note Also store the UV vertex indices in the member variable.
+ */
+ void store_uv_coords_and_indices(Vector<std::array<float, 2>> &r_uv_coords);
+ Span<int> calc_poly_uv_indices(int poly_index) const;
+ /**
+ * Calculate polygon normal of a polygon at given index.
+ *
+ * Should be used for flat-shaded polygons.
+ */
+ float3 calc_poly_normal(int poly_index) const;
+ /**
+ * Find the unique normals of the mesh and return them in \a r_normal_coords.
+ * Store the indices into that vector with for each loop in this #OBJMesh.
+ */
+ void store_normal_coords_and_indices(Vector<float3> &r_normal_coords);
+ /**
+ * Calculate a polygon's polygon/loop normal indices.
+ * \param poly_index Index of the polygon to calculate indices for.
+ * \return Vector of normal indices, aligned with vertices of polygon.
+ */
+ Vector<int> calc_poly_normal_indices(int poly_index) const;
+ /**
+ * 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 get_poly_deform_group_index(int poly_index) const;
+ /**
+ * Find the name of the vertex deform group at the given index.
+ * The index indices into the #Object.defbase.
+ */
+ const char *get_poly_deform_group_name(int16_t def_group_index) const;
+
+ /**
+ * Calculate vertex indices of an edge's corners if it is a loose edge.
+ */
+ std::optional<std::array<int, 2>> calc_loose_edge_vert_indices(int edge_index) const;
+
+ private:
+ /**
+ * Free the mesh if _the exporter_ created it.
+ */
+ void free_mesh_if_needed();
+ /**
+ * 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> triangulate_mesh_eval();
+ /**
+ * Set the final transform after applying axes settings and an Object's world transform.
+ */
+ void set_world_axes_transform(eTransformAxisForward forward, 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..48136dad5f7
--- /dev/null
+++ b/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc
@@ -0,0 +1,360 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup obj
+ */
+
+#include "BKE_image.h"
+#include "BKE_node.h"
+
+#include "BLI_map.hh"
+#include "BLI_math_vec_types.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});
+ }
+ /* Empirical 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 file-paths 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);
+ 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..a84dcb80a48
--- /dev/null
+++ b/source/blender/io/wavefront_obj/exporter/obj_export_mtl.hh
@@ -0,0 +1,104 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include "BLI_map.hh"
+#include "BLI_math_vec_types.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..ec690115115
--- /dev/null
+++ b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc
@@ -0,0 +1,105 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup obj
+ */
+
+#include "BLI_listbase.h"
+#include "BLI_math.h"
+#include "BLI_math_vec_types.hh"
+
+#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);
+}
+
+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);
+}
+
+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;
+}
+
+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;
+}
+
+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;
+}
+
+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..0c71c3cc09d
--- /dev/null
+++ b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh
@@ -0,0 +1,72 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include "BLI_utility_mixins.hh"
+
+#include "DNA_curve_types.h"
+
+namespace blender::io::obj {
+
+/**
+ * Provides access to the a Curve Object's properties.
+ * Only #CU_NURBS type is supported.
+ *
+ * \note Used for Curves to be exported in parameter form, and not converted to meshes.
+ */
+class OBJCurve : NonCopyable {
+ private:
+ const Object *export_object_eval_;
+ const Curve *export_curve_;
+ float world_axes_transform_[4][4];
+
+ public:
+ OBJCurve(const Depsgraph *depsgraph, const OBJExportParams &export_params, Object *curve_object);
+
+ const char *get_curve_name() const;
+ int total_splines() const;
+ /**
+ * \param spline_index: Zero-based index of spline of interest.
+ * \return: Total vertices in a spline.
+ */
+ int total_spline_vertices(int spline_index) const;
+ /**
+ * Get coordinates of the vertex at the given index on the given spline.
+ */
+ float3 vertex_coordinates(int spline_index, int vertex_index, float scaling_factor) const;
+ /**
+ * Get total control points of the NURBS spline at the given index. This is different than total
+ * vertices of a spline.
+ */
+ int total_spline_control_points(int spline_index) const;
+ /**
+ * Get the degree of the NURBS spline at the given index.
+ */
+ int get_nurbs_degree(int spline_index) const;
+
+ private:
+ /**
+ * Set the final transform after applying axes settings and an Object's world transform.
+ */
+ void set_world_axes_transform(eTransformAxisForward forward, 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..0c753ccdcac
--- /dev/null
+++ b/source/blender/io/wavefront_obj/exporter/obj_exporter.cc
@@ -0,0 +1,291 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup obj
+ */
+
+#include <cstdio>
+#include <exception>
+#include <memory>
+
+#include "BKE_scene.h"
+
+#include "BLI_path_util.h"
+#include "BLI_vector.hh"
+
+#include "DEG_depsgraph_query.h"
+
+#include "DNA_scene_types.h"
+
+#include "ED_object.h"
+
+#include "obj_export_mesh.hh"
+#include "obj_export_nurbs.hh"
+#include "obj_exporter.hh"
+
+#include "obj_export_file_writer.hh"
+
+namespace blender::io::obj {
+
+OBJDepsgraph::OBJDepsgraph(const bContext *C, const eEvaluationMode eval_mode)
+{
+ Scene *scene = CTX_data_scene(C);
+ Main *bmain = CTX_data_main(C);
+ ViewLayer *view_layer = CTX_data_view_layer(C);
+ if (eval_mode == DAG_EVAL_RENDER) {
+ depsgraph_ = DEG_graph_new(bmain, scene, view_layer, DAG_EVAL_RENDER);
+ needs_free_ = true;
+ DEG_graph_build_for_all_objects(depsgraph_);
+ BKE_scene_graph_evaluated_ensure(depsgraph_, bmain);
+ }
+ else {
+ depsgraph_ = CTX_data_ensure_evaluated_depsgraph(C);
+ needs_free_ = false;
+ }
+}
+
+OBJDepsgraph::~OBJDepsgraph()
+{
+ if (needs_free_) {
+ DEG_graph_free(depsgraph_);
+ }
+}
+
+Depsgraph *OBJDepsgraph::get()
+{
+ return depsgraph_;
+}
+
+void OBJDepsgraph::update_for_newframe()
+{
+ BKE_scene_graph_update_for_newframe(depsgraph_);
+}
+
+static void print_exception_error(const std::system_error &ex)
+{
+ std::cerr << ex.code().category().name() << ": " << ex.what() << ": " << ex.code().message()
+ << std::endl;
+}
+
+/**
+ * Filter supported objects from the Scene.
+ *
+ * \note Curves are also stored with Meshes if export settings specify so.
+ */
+std::pair<Vector<std::unique_ptr<OBJMesh>>, Vector<std::unique_ptr<OBJCurve>>>
+filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params)
+{
+ Vector<std::unique_ptr<OBJMesh>> r_exportable_meshes;
+ Vector<std::unique_ptr<OBJCurve>> r_exportable_nurbs;
+ const int deg_objects_visibility_flags = DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY |
+ DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET |
+ DEG_ITER_OBJECT_FLAG_VISIBLE |
+ DEG_ITER_OBJECT_FLAG_DUPLI;
+ DEG_OBJECT_ITER_BEGIN (depsgraph, object, deg_objects_visibility_flags) {
+ if (export_params.export_selected_objects && !(object->base_flag & BASE_SELECTED)) {
+ continue;
+ }
+ switch (object->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));
+ break;
+ case OB_CURVE: {
+ Curve *curve = static_cast<Curve *>(object->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));
+ }
+ 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));
+ }
+ else {
+ /* Export in mesh form: edges and vertices. */
+ r_exportable_meshes.append(
+ std::make_unique<OBJMesh>(depsgraph, export_params, object));
+ }
+ break;
+ case CU_BEZIER:
+ /* Always export in mesh form: edges and vertices. */
+ r_exportable_meshes.append(
+ std::make_unique<OBJMesh>(depsgraph, export_params, object));
+ break;
+ default:
+ /* Other curve types are not supported. */
+ break;
+ }
+ break;
+ }
+ default:
+ /* Other object types are not supported. */
+ break;
+ }
+ }
+ DEG_OBJECT_ITER_END;
+ return {std::move(r_exportable_meshes), std::move(r_exportable_nurbs)};
+}
+
+static void write_mesh_objects(Vector<std::unique_ptr<OBJMesh>> exportable_as_mesh,
+ OBJWriter &obj_writer,
+ MTLWriter *mtl_writer,
+ const OBJExportParams &export_params)
+{
+ if (mtl_writer) {
+ obj_writer.write_mtllib_name(mtl_writer->mtl_file_path());
+ }
+
+ /* Smooth groups and UV vertex indices may make huge memory allocations, so they should be freed
+ * right after they're written, instead of waiting for #blender::Vector to clean them up after
+ * all the objects are exported. */
+ for (auto &obj_mesh : exportable_as_mesh) {
+ obj_writer.write_object_name(*obj_mesh);
+ obj_writer.write_vertex_coords(*obj_mesh);
+ Vector<int> obj_mtlindices;
+
+ if (obj_mesh->tot_polygons() > 0) {
+ if (export_params.export_smooth_groups) {
+ obj_mesh->calc_smooth_groups(export_params.smooth_groups_bitflags);
+ }
+ if (export_params.export_normals) {
+ obj_writer.write_poly_normals(*obj_mesh);
+ }
+ if (export_params.export_uv) {
+ obj_writer.write_uv_coords(*obj_mesh);
+ }
+ if (mtl_writer) {
+ obj_mtlindices = mtl_writer->add_materials(*obj_mesh);
+ }
+ /* This function takes a 0-indexed slot index for the obj_mesh object and
+ * returns the material name that we are using in the .obj file for it. */
+ std::function<const char *(int)> matname_fn = [&](int s) -> const char * {
+ if (!mtl_writer || s < 0 || s >= obj_mtlindices.size()) {
+ return nullptr;
+ }
+ return mtl_writer->mtlmaterial_name(obj_mtlindices[s]);
+ };
+ obj_writer.write_poly_elements(*obj_mesh, matname_fn);
+ }
+ obj_writer.write_edges_indices(*obj_mesh);
+
+ obj_writer.update_index_offsets(*obj_mesh);
+ }
+}
+
+/**
+ * Export NURBS Curves in parameter form, not as vertices and edges.
+ */
+static void write_nurbs_curve_objects(const Vector<std::unique_ptr<OBJCurve>> &exportable_as_nurbs,
+ const OBJWriter &obj_writer)
+{
+ /* #OBJCurve doesn't have any dynamically allocated memory, so it's fine
+ * to wait for #blender::Vector to clean the objects up. */
+ for (const std::unique_ptr<OBJCurve> &obj_curve : exportable_as_nurbs) {
+ obj_writer.write_nurbs_curve(*obj_curve);
+ }
+}
+
+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);
+}
+
+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");
+}
+
+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..a06898a21cf
--- /dev/null
+++ b/source/blender/io/wavefront_obj/exporter/obj_exporter.hh
@@ -0,0 +1,101 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include "BLI_utility_mixins.hh"
+
+#include "BLI_vector.hh"
+
+#include "IO_wavefront_obj.h"
+
+namespace blender::io::obj {
+
+/**
+ * Behaves like `std::unique_ptr<Depsgraph, custom_deleter>`.
+ * Needed to free a new Depsgraph created for #DAG_EVAL_RENDER.
+ */
+class OBJDepsgraph : NonMovable, NonCopyable {
+ private:
+ Depsgraph *depsgraph_ = nullptr;
+ bool needs_free_ = false;
+
+ public:
+ OBJDepsgraph(const bContext *C, eEvaluationMode eval_mode);
+ ~OBJDepsgraph();
+
+ Depsgraph *get();
+ void update_for_newframe();
+};
+
+/**
+ * The main function for exporting a .obj file according to the given `export_parameters`.
+ * It uses the context `C` to get the dependency graph, and from that, the `Scene`.
+ * Depending on whether or not `export_params.export_animation` is set, it writes
+ * either one file per animation frame, or just one file.
+ */
+/**
+ * Central internal function to call Scene update & writer functions.
+ */
+void exporter_main(bContext *C, const OBJExportParams &export_params);
+
+class OBJMesh;
+class OBJCurve;
+
+/**
+ * Export a single frame of a .obj file, according to the given `export_parameters`.
+ * The frame state is given in `depsgraph`.
+ * The output file name is given by `filepath`.
+ * This function is normally called from `exporter_main`, but is exposed here for testing purposes.
+ */
+/**
+ * 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);
+
+/**
+ * Find the objects to be exported in the `view_layer` of the dependency graph`depsgraph`,
+ * and return them in vectors `unique_ptr`s of `OBJMesh` and `OBJCurve`.
+ * If `export_params.export_selected_objects` is set, then only selected objects are to be
+ * exported, else all objects are to be exported. But only objects of type `OB_MESH`, `OB_CURVE`,
+ * and `OB_SURF` are supported; the rest will be ignored. If `export_params.export_curves_as_nurbs`
+ * is set, then curves of type `CU_NURBS` are exported in curve form in the .obj file, otherwise
+ * they are converted to mesh and returned in the `OBJMesh` vector. All other exportable types are
+ * always converted to mesh and returned in the `OBJMesh` vector.
+ */
+std::pair<Vector<std::unique_ptr<OBJMesh>>, Vector<std::unique_ptr<OBJCurve>>>
+filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params);
+
+/**
+ * Makes `r_filepath_with_frames` (which should point at a character array of size `FILE_MAX`)
+ * be `filepath` with its "#" characters replaced by the number representing `frame`, and with
+ * a .obj extension.
+ */
+/**
+ * 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, int frame, char *r_filepath_with_frames);
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc
new file mode 100644
index 00000000000..89e1de49511
--- /dev/null
+++ b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc
@@ -0,0 +1,430 @@
+/* Apache License, Version 2.0 */
+
+#include <gtest/gtest.h>
+#include <ios>
+#include <memory>
+#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 {
+/* Set this true to keep comparison-failing test output in temp file directory. */
+constexpr bool save_failing_test_output = false;
+
+/* 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(), 19);
+ 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(), 18);
+ EXPECT_EQ(objcurves.size(), 2);
+}
+
+TEST_F(obj_exporter_test, filter_objects_selected)
+{
+ OBJExportParamsDefault _export;
+ if (!load_file_and_depsgraph(all_objects_file)) {
+ ADD_FAILURE();
+ return;
+ }
+ _export.params.export_selected_objects = true;
+ _export.params.export_curves_as_nurbs = true;
+ auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)};
+ EXPECT_EQ(objmeshes.size(), 1);
+ EXPECT_EQ(objcurves.size(), 0);
+}
+
+TEST(obj_exporter_utils, append_negative_frame_to_filename)
+{
+ const char path_original[FILE_MAX] = "/my_file.obj";
+ const char path_truth[FILE_MAX] = "/my_file-123.obj";
+ const int frame = -123;
+ char path_with_frame[FILE_MAX] = {0};
+ const bool ok = append_frame_to_filename(path_original, frame, path_with_frame);
+ EXPECT_TRUE(ok);
+ EXPECT_EQ_ARRAY(path_with_frame, path_truth, BLI_strlen_utf8(path_truth));
+}
+
+TEST(obj_exporter_utils, append_positive_frame_to_filename)
+{
+ const char path_original[FILE_MAX] = "/my_file.obj";
+ const char path_truth[FILE_MAX] = "/my_file123.obj";
+ const int frame = 123;
+ char path_with_frame[FILE_MAX] = {0};
+ const bool ok = append_frame_to_filename(path_original, frame, path_with_frame);
+ EXPECT_TRUE(ok);
+ EXPECT_EQ_ARRAY(path_with_frame, path_truth, BLI_strlen_utf8(path_truth));
+}
+
+TEST_F(obj_exporter_test, curve_nurbs_points)
+{
+ if (!load_file_and_depsgraph(all_curve_objects_file)) {
+ ADD_FAILURE();
+ return;
+ }
+
+ OBJExportParamsDefault _export;
+ _export.params.export_curves_as_nurbs = true;
+ auto [objmeshes_unused, objcurves]{filter_supported_objects(depsgraph, _export.params)};
+
+ for (auto &objcurve : objcurves) {
+ if (all_nurbs_truth.count(objcurve->get_curve_name()) != 1) {
+ ADD_FAILURE();
+ return;
+ }
+ const NurbsObject *const nurbs_truth = all_nurbs_truth.at(objcurve->get_curve_name()).get();
+ EXPECT_EQ(objcurve->total_splines(), nurbs_truth->total_splines());
+ for (int spline_index : IndexRange(objcurve->total_splines())) {
+ EXPECT_EQ(objcurve->total_spline_vertices(spline_index),
+ nurbs_truth->total_spline_vertices(spline_index));
+ EXPECT_EQ(objcurve->get_nurbs_degree(spline_index),
+ nurbs_truth->get_nurbs_degree(spline_index));
+ EXPECT_EQ(objcurve->total_spline_control_points(spline_index),
+ nurbs_truth->total_spline_control_points(spline_index));
+ }
+ }
+}
+
+TEST_F(obj_exporter_test, curve_coordinates)
+{
+ if (!load_file_and_depsgraph(all_curve_objects_file)) {
+ ADD_FAILURE();
+ return;
+ }
+
+ OBJExportParamsDefault _export;
+ _export.params.export_curves_as_nurbs = true;
+ auto [objmeshes_unused, objcurves]{filter_supported_objects(depsgraph, _export.params)};
+
+ for (auto &objcurve : objcurves) {
+ if (all_nurbs_truth.count(objcurve->get_curve_name()) != 1) {
+ ADD_FAILURE();
+ return;
+ }
+ const NurbsObject *const nurbs_truth = all_nurbs_truth.at(objcurve->get_curve_name()).get();
+ EXPECT_EQ(objcurve->total_splines(), nurbs_truth->total_splines());
+ for (int spline_index : IndexRange(objcurve->total_splines())) {
+ for (int vertex_index : IndexRange(objcurve->total_spline_vertices(spline_index))) {
+ EXPECT_V3_NEAR(objcurve->vertex_coordinates(
+ spline_index, vertex_index, _export.params.scaling_factor),
+ nurbs_truth->vertex_coordinates(spline_index, vertex_index),
+ 0.000001f);
+ }
+ }
+ }
+}
+
+static std::unique_ptr<OBJWriter> init_writer(const OBJExportParams &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.
+ * Use Latin Capital Letter A with Ogonek, Cyrillic Capital Letter Zhe
+ * at the end, to test I/O on non-English file names. */
+const char *const temp_file_path = "output\xc4\x84\xd0\x96.OBJ";
+
+static std::string read_temp_file_in_string(const std::string &file_path)
+{
+ std::string res;
+ size_t buffer_len;
+ void *buffer = BLI_file_read_text_as_mem(file_path.c_str(), 0, &buffer_len);
+ if (buffer != NULL) {
+ res.assign((const char *)buffer, buffer_len);
+ MEM_freeN(buffer);
+ }
+ return res;
+}
+
+TEST(obj_exporter_writer, header)
+{
+ /* Because testing doesn't fully initialize Blender, we need the following. */
+ BKE_tempdir_init(nullptr);
+ 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");
+ BLI_delete(out_file_path.c_str(), false, false);
+}
+
+/* Return true if string #a and string #b are equal after their first newline. */
+static bool strings_equal_after_first_lines(const std::string &a, const std::string &b)
+{
+ /* If `dbg_level` is true then a failing test will print context around the first mismatch. */
+ const bool dbg_level = false;
+ const size_t a_len = a.size();
+ const size_t b_len = b.size();
+ const size_t a_next = a.find_first_of('\n');
+ const size_t b_next = b.find_first_of('\n');
+ if (a_next == std::string::npos || b_next == std::string::npos) {
+ if (dbg_level) {
+ std::cout << "Couldn't find newline in one of args\n";
+ }
+ return false;
+ }
+ if (dbg_level) {
+ 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(nullptr);
+ 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 - 1);
+ 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);
+ bool are_equal = strings_equal_after_first_lines(output_str, golden_str);
+ if (save_failing_test_output && !are_equal) {
+ printf("failing test output in %s\n", out_file_path.c_str());
+ }
+ ASSERT_TRUE(are_equal);
+ if (!save_failing_test_output || are_equal) {
+ 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..c101081ca54
--- /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 curves objects.
+ */
+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