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:
authorAnkit Meel <ankitjmeel@gmail.com>2022-04-04 13:36:10 +0300
committerAras Pranckevicius <aras@nesnausk.org>2022-04-04 13:36:10 +0300
commite6a9b223844346a34ce195652449fec3229a2ec1 (patch)
tree38b9621299a83515670af0189b8cddc51813f838 /source/blender/io
parentee3f71d747e3ffd5091335437d52b3ec518d7b67 (diff)
OBJ: New C++ based wavefront OBJ importer
This takes state of soc-2020-io-performance branch as it was at e9bbfd0c8c7 (2021 Oct 31), merges latest master (2022 Apr 4), adds a bunch of tests, and fixes a bunch of stuff found by said tests. The fixes are detailed in the differential. Timings on my machine (Windows, VS2022 release build, AMD Ryzen 5950X 32 threads): - Rungholt minecraft level (269MB file, 1 mesh): 54.2s -> 14.2s (memory usage: 7.0GB -> 1.9GB). - Blender 3.0 splash scene: "I waited for 90 minutes and gave up" -> 109s. Now, this time is not great, but at least 20% of the time is spent assigning unique names for the imported objects (the scene has 24 thousand objects). This is not specific to obj importer, but rather a general issue across blender overall. Test suite file updates done in Subversion tests repository. Reviewed By: @howardt, @sybren Differential Revision: https://developer.blender.org/D13958
Diffstat (limited to 'source/blender/io')
-rw-r--r--source/blender/io/wavefront_obj/CMakeLists.txt17
-rw-r--r--source/blender/io/wavefront_obj/IO_wavefront_obj.cc10
-rw-r--r--source/blender/io/wavefront_obj/IO_wavefront_obj.h11
-rw-r--r--source/blender/io/wavefront_obj/importer/importer_mesh_utils.cc133
-rw-r--r--source/blender/io/wavefront_obj/importer/importer_mesh_utils.hh35
-rw-r--r--source/blender/io/wavefront_obj/importer/obj_import_file_reader.cc639
-rw-r--r--source/blender/io/wavefront_obj/importer/obj_import_file_reader.hh151
-rw-r--r--source/blender/io/wavefront_obj/importer/obj_import_mesh.cc380
-rw-r--r--source/blender/io/wavefront_obj/importer/obj_import_mesh.hh52
-rw-r--r--source/blender/io/wavefront_obj/importer/obj_import_mtl.cc386
-rw-r--r--source/blender/io/wavefront_obj/importer/obj_import_mtl.hh90
-rw-r--r--source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc99
-rw-r--r--source/blender/io/wavefront_obj/importer/obj_import_nurbs.hh38
-rw-r--r--source/blender/io/wavefront_obj/importer/obj_import_objects.hh111
-rw-r--r--source/blender/io/wavefront_obj/importer/obj_importer.cc111
-rw-r--r--source/blender/io/wavefront_obj/importer/obj_importer.hh22
-rw-r--r--source/blender/io/wavefront_obj/importer/parser_string_utils.cc209
-rw-r--r--source/blender/io/wavefront_obj/importer/parser_string_utils.hh19
-rw-r--r--source/blender/io/wavefront_obj/tests/obj_importer_tests.cc491
19 files changed, 3004 insertions, 0 deletions
diff --git a/source/blender/io/wavefront_obj/CMakeLists.txt b/source/blender/io/wavefront_obj/CMakeLists.txt
index cc375577b52..9cdd96ee7be 100644
--- a/source/blender/io/wavefront_obj/CMakeLists.txt
+++ b/source/blender/io/wavefront_obj/CMakeLists.txt
@@ -3,6 +3,7 @@
set(INC
.
./exporter
+ ./importer
../../blenkernel
../../blenlib
../../bmesh
@@ -28,6 +29,13 @@ set(SRC
exporter/obj_export_mtl.cc
exporter/obj_export_nurbs.cc
exporter/obj_exporter.cc
+ importer/importer_mesh_utils.cc
+ importer/obj_import_file_reader.cc
+ importer/obj_import_mesh.cc
+ importer/obj_import_mtl.cc
+ importer/obj_import_nurbs.cc
+ importer/obj_importer.cc
+ importer/parser_string_utils.cc
IO_wavefront_obj.h
exporter/obj_export_file_writer.hh
@@ -36,6 +44,14 @@ set(SRC
exporter/obj_export_mtl.hh
exporter/obj_export_nurbs.hh
exporter/obj_exporter.hh
+ importer/importer_mesh_utils.hh
+ importer/obj_import_file_reader.hh
+ importer/obj_import_mesh.hh
+ importer/obj_import_mtl.hh
+ importer/obj_import_nurbs.hh
+ importer/obj_import_objects.hh
+ importer/obj_importer.hh
+ importer/parser_string_utils.hh
)
set(LIB
@@ -53,6 +69,7 @@ blender_add_lib(bf_wavefront_obj "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
if(WITH_GTESTS)
set(TEST_SRC
tests/obj_exporter_tests.cc
+ tests/obj_importer_tests.cc
tests/obj_exporter_tests.hh
)
diff --git a/source/blender/io/wavefront_obj/IO_wavefront_obj.cc b/source/blender/io/wavefront_obj/IO_wavefront_obj.cc
index cebdf33413e..c80d10d9efd 100644
--- a/source/blender/io/wavefront_obj/IO_wavefront_obj.cc
+++ b/source/blender/io/wavefront_obj/IO_wavefront_obj.cc
@@ -9,6 +9,7 @@
#include "IO_wavefront_obj.h"
#include "obj_exporter.hh"
+#include "obj_importer.hh"
/**
* C-interface for the exporter.
@@ -18,3 +19,12 @@ void OBJ_export(bContext *C, const OBJExportParams *export_params)
SCOPED_TIMER("OBJ export");
blender::io::obj::exporter_main(C, *export_params);
}
+
+/**
+ * Time the full import process.
+ */
+void OBJ_import(bContext *C, const OBJImportParams *import_params)
+{
+ SCOPED_TIMER(__func__);
+ blender::io::obj::importer_main(C, *import_params);
+}
diff --git a/source/blender/io/wavefront_obj/IO_wavefront_obj.h b/source/blender/io/wavefront_obj/IO_wavefront_obj.h
index 289e37b3885..b80c1e073b8 100644
--- a/source/blender/io/wavefront_obj/IO_wavefront_obj.h
+++ b/source/blender/io/wavefront_obj/IO_wavefront_obj.h
@@ -77,6 +77,17 @@ struct OBJExportParams {
bool smooth_groups_bitflags;
};
+struct OBJImportParams {
+ /** Full path to the source OBJ file to import. */
+ char filepath[FILE_MAX];
+ /** Value 0 disables clamping. */
+ float clamp_size;
+ eTransformAxisForward forward_axis;
+ eTransformAxisUp up_axis;
+};
+
+void OBJ_import(bContext *C, const struct OBJImportParams *import_params);
+
void OBJ_export(bContext *C, const struct OBJExportParams *export_params);
#ifdef __cplusplus
diff --git a/source/blender/io/wavefront_obj/importer/importer_mesh_utils.cc b/source/blender/io/wavefront_obj/importer/importer_mesh_utils.cc
new file mode 100644
index 00000000000..7019e67419e
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/importer_mesh_utils.cc
@@ -0,0 +1,133 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#include "BKE_mesh.h"
+#include "BKE_object.h"
+
+#include "BLI_delaunay_2d.h"
+#include "BLI_math_vector.h"
+#include "BLI_set.hh"
+
+#include "DNA_object_types.h"
+
+#include "IO_wavefront_obj.h"
+
+#include "importer_mesh_utils.hh"
+
+namespace blender::io::obj {
+
+Vector<Vector<int>> fixup_invalid_polygon(Span<float3> vertex_coords,
+ Span<int> face_vertex_indices)
+{
+ using namespace blender::meshintersect;
+ if (face_vertex_indices.size() < 3) {
+ return {};
+ }
+
+ /* Calculate face normal, to project verts to 2D. */
+ float normal[3] = {0, 0, 0};
+ float3 co_prev = vertex_coords[face_vertex_indices.last()];
+ for (int idx : face_vertex_indices) {
+ BLI_assert(idx >= 0 && idx < vertex_coords.size());
+ float3 co_curr = vertex_coords[idx];
+ add_newell_cross_v3_v3v3(normal, co_prev, co_curr);
+ co_prev = co_curr;
+ }
+ if (UNLIKELY(normalize_v3(normal) == 0.0f)) {
+ normal[2] = 1.0f;
+ }
+ float axis_mat[3][3];
+ axis_dominant_v3_to_m3(axis_mat, normal);
+
+ /* Prepare data for CDT. */
+ CDT_input<double> input;
+ input.vert.reinitialize(face_vertex_indices.size());
+ input.face.reinitialize(1);
+ input.face[0].resize(face_vertex_indices.size());
+ for (int64_t i = 0; i < face_vertex_indices.size(); ++i) {
+ input.face[0][i] = i;
+ }
+ input.epsilon = 1.0e-6f;
+ input.need_ids = true;
+ /* Project vertices to 2D. */
+ for (size_t i = 0; i < face_vertex_indices.size(); ++i) {
+ int idx = face_vertex_indices[i];
+ BLI_assert(idx >= 0 && idx < vertex_coords.size());
+ float3 coord = vertex_coords[idx];
+ float2 coord2d;
+ mul_v2_m3v3(coord2d, axis_mat, coord);
+ input.vert[i] = double2(coord2d.x, coord2d.y);
+ }
+
+ CDT_result<double> res = delaunay_2d_calc(input, CDT_CONSTRAINTS_VALID_BMESH_WITH_HOLES);
+
+ /* Emit new face information from CDT result. */
+ Vector<Vector<int>> faces;
+ faces.reserve(res.face.size());
+ for (const auto &f : res.face) {
+ Vector<int> face_verts;
+ face_verts.reserve(f.size());
+ for (int64_t i = 0; i < f.size(); ++i) {
+ int idx = f[i];
+ BLI_assert(idx >= 0 && idx < res.vert_orig.size());
+ if (res.vert_orig[idx].is_empty()) {
+ /* If we have a whole new vertex in the tessellated result,
+ * we won't quite know what to do with it (how to create normal/UV
+ * for it, for example). Such vertices are often due to
+ * self-intersecting polygons. Just skip them from the output
+ * polygon. */
+ }
+ else {
+ /* Vertex corresponds to one or more of the input vertices, use it. */
+ idx = res.vert_orig[idx][0];
+ BLI_assert(idx >= 0 && idx < face_vertex_indices.size());
+ face_verts.append(idx);
+ }
+ }
+ faces.append(face_verts);
+ }
+ return faces;
+}
+
+void transform_object(Object *object, const OBJImportParams &import_params)
+{
+ float axes_transform[3][3];
+ unit_m3(axes_transform);
+ float obmat[4][4];
+ unit_m4(obmat);
+ /* +Y-forward and +Z-up are the default Blender axis settings. */
+ mat3_from_axis_conversion(import_params.forward_axis,
+ import_params.up_axis,
+ OBJ_AXIS_Y_FORWARD,
+ OBJ_AXIS_Z_UP,
+ axes_transform);
+ /* mat3_from_axis_conversion returns a transposed matrix! */
+ transpose_m3(axes_transform);
+ copy_m4_m3(obmat, axes_transform);
+
+ BKE_object_apply_mat4(object, obmat, true, false);
+
+ if (import_params.clamp_size != 0.0f) {
+ float3 max_coord(-INT_MAX);
+ float3 min_coord(INT_MAX);
+ BoundBox *bb = BKE_mesh_boundbox_get(object);
+ for (const float(&vertex)[3] : bb->vec) {
+ for (int axis = 0; axis < 3; axis++) {
+ max_coord[axis] = max_ff(max_coord[axis], vertex[axis]);
+ min_coord[axis] = min_ff(min_coord[axis], vertex[axis]);
+ }
+ }
+ const float max_diff = max_fff(
+ max_coord[0] - min_coord[0], max_coord[1] - min_coord[1], max_coord[2] - min_coord[2]);
+ float scale = 1.0f;
+ while (import_params.clamp_size < max_diff * scale) {
+ scale = scale / 10;
+ }
+ copy_v3_fl(object->scale, scale);
+ }
+}
+
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/importer_mesh_utils.hh b/source/blender/io/wavefront_obj/importer/importer_mesh_utils.hh
new file mode 100644
index 00000000000..6db1fcdb2b7
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/importer_mesh_utils.hh
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include "BLI_math_vec_types.hh"
+#include "BLI_span.hh"
+#include "BLI_vector.hh"
+
+struct Object;
+struct OBJImportParams;
+
+namespace blender::io::obj {
+
+/**
+ * Given an invalid polygon (with holes or duplicated vertex indices),
+ * turn it into possibly multiple polygons that are valid.
+ *
+ * \param vertex_coords Polygon's vertex coordinate list.
+ * \param face_vertex_indices A polygon's indices that index into the given vertex coordinate list.
+ * \return List of polygons with each element containing indices of one polygon.
+ * The indices are into face_vertex_indices array.
+ */
+Vector<Vector<int>> fixup_invalid_polygon(Span<float3> vertex_coords,
+ Span<int> face_vertex_indices);
+
+/**
+ * Apply axes transform to the Object, and clamp object dimensions to the specified value.
+ */
+void transform_object(Object *object, const OBJImportParams &import_params);
+
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/obj_import_file_reader.cc b/source/blender/io/wavefront_obj/importer/obj_import_file_reader.cc
new file mode 100644
index 00000000000..9111ff05e8a
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/obj_import_file_reader.cc
@@ -0,0 +1,639 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#include "BLI_map.hh"
+#include "BLI_string_ref.hh"
+#include "BLI_vector.hh"
+
+#include "parser_string_utils.hh"
+
+#include "obj_import_file_reader.hh"
+
+namespace blender::io::obj {
+
+using std::string;
+
+/**
+ * Based on the properties of the given Geometry instance, create a new Geometry instance
+ * or return the previous one.
+ *
+ * Also update index offsets which should always happen if a new Geometry instance is created.
+ */
+static Geometry *create_geometry(Geometry *const prev_geometry,
+ const eGeometryType new_type,
+ StringRef name,
+ const GlobalVertices &global_vertices,
+ Vector<std::unique_ptr<Geometry>> &r_all_geometries,
+ VertexIndexOffset &r_offset)
+{
+ auto new_geometry = [&]() {
+ r_all_geometries.append(std::make_unique<Geometry>());
+ Geometry *g = r_all_geometries.last().get();
+ g->geom_type_ = new_type;
+ g->geometry_name_ = name.is_empty() ? "New object" : name;
+ r_offset.set_index_offset(global_vertices.vertices.size());
+ return g;
+ };
+
+ if (prev_geometry && prev_geometry->geom_type_ == GEOM_MESH) {
+ /* After the creation of a Geometry instance, at least one element has been found in the OBJ
+ * file that indicates that it is a mesh (basically anything but the vertex positions). */
+ if (!prev_geometry->face_elements_.is_empty() || prev_geometry->has_vertex_normals_ ||
+ !prev_geometry->edges_.is_empty()) {
+ return new_geometry();
+ }
+ if (new_type == GEOM_MESH) {
+ /* A Geometry created initially with a default name now found its name. */
+ prev_geometry->geometry_name_ = name;
+ return prev_geometry;
+ }
+ if (new_type == GEOM_CURVE) {
+ /* The object originally created is not a mesh now that curve data
+ * follows the vertex coordinates list. */
+ prev_geometry->geom_type_ = GEOM_CURVE;
+ return prev_geometry;
+ }
+ }
+
+ if (prev_geometry && prev_geometry->geom_type_ == GEOM_CURVE) {
+ return new_geometry();
+ }
+
+ return new_geometry();
+}
+
+static void geom_add_vertex(Geometry *geom,
+ const StringRef rest_line,
+ GlobalVertices &r_global_vertices)
+{
+ float3 curr_vert;
+ Vector<StringRef> str_vert_split;
+ split_by_char(rest_line, ' ', str_vert_split);
+ copy_string_to_float(str_vert_split, FLT_MAX, {curr_vert, 3});
+ r_global_vertices.vertices.append(curr_vert);
+ geom->vertex_indices_.append(r_global_vertices.vertices.size() - 1);
+}
+
+static void geom_add_vertex_normal(Geometry *geom,
+ const StringRef rest_line,
+ GlobalVertices &r_global_vertices)
+{
+ float3 curr_vert_normal;
+ Vector<StringRef> str_vert_normal_split;
+ split_by_char(rest_line, ' ', str_vert_normal_split);
+ copy_string_to_float(str_vert_normal_split, FLT_MAX, {curr_vert_normal, 3});
+ r_global_vertices.vertex_normals.append(curr_vert_normal);
+ geom->has_vertex_normals_ = true;
+}
+
+static void geom_add_uv_vertex(const StringRef rest_line, GlobalVertices &r_global_vertices)
+{
+ float2 curr_uv_vert;
+ Vector<StringRef> str_uv_vert_split;
+ split_by_char(rest_line, ' ', str_uv_vert_split);
+ copy_string_to_float(str_uv_vert_split, FLT_MAX, {curr_uv_vert, 2});
+ r_global_vertices.uv_vertices.append(curr_uv_vert);
+}
+
+static void geom_add_edge(Geometry *geom,
+ const StringRef rest_line,
+ const VertexIndexOffset &offsets,
+ GlobalVertices &r_global_vertices)
+{
+ int edge_v1 = -1, edge_v2 = -1;
+ Vector<StringRef> str_edge_split;
+ split_by_char(rest_line, ' ', str_edge_split);
+ copy_string_to_int(str_edge_split[0], -1, edge_v1);
+ copy_string_to_int(str_edge_split[1], -1, edge_v2);
+ /* Always keep stored indices non-negative and zero-based. */
+ edge_v1 += edge_v1 < 0 ? r_global_vertices.vertices.size() : -offsets.get_index_offset() - 1;
+ edge_v2 += edge_v2 < 0 ? r_global_vertices.vertices.size() : -offsets.get_index_offset() - 1;
+ BLI_assert(edge_v1 >= 0 && edge_v2 >= 0);
+ geom->edges_.append({static_cast<uint>(edge_v1), static_cast<uint>(edge_v2)});
+}
+
+static void geom_add_polygon(Geometry *geom,
+ const StringRef rest_line,
+ const GlobalVertices &global_vertices,
+ const VertexIndexOffset &offsets,
+ const StringRef state_material_name,
+ const StringRef state_object_group,
+ const bool state_shaded_smooth)
+{
+ PolyElem curr_face;
+ curr_face.shaded_smooth = state_shaded_smooth;
+ if (!state_material_name.is_empty()) {
+ curr_face.material_name = state_material_name;
+ }
+ if (!state_object_group.is_empty()) {
+ curr_face.vertex_group = state_object_group;
+ /* Yes it repeats several times, but another if-check will not reduce steps either. */
+ geom->use_vertex_groups_ = true;
+ }
+
+ bool face_valid = true;
+ Vector<StringRef> str_corners_split;
+ split_by_char(rest_line, ' ', str_corners_split);
+ for (StringRef str_corner : str_corners_split) {
+ PolyCorner corner;
+ const size_t n_slash = std::count(str_corner.begin(), str_corner.end(), '/');
+ bool got_uv = false, got_normal = false;
+ if (n_slash == 0) {
+ /* Case: "f v1 v2 v3". */
+ copy_string_to_int(str_corner, INT32_MAX, corner.vert_index);
+ }
+ else if (n_slash == 1) {
+ /* Case: "f v1/vt1 v2/vt2 v3/vt3". */
+ Vector<StringRef> vert_uv_split;
+ split_by_char(str_corner, '/', vert_uv_split);
+ if (vert_uv_split.size() != 1 && vert_uv_split.size() != 2) {
+ fprintf(stderr, "Invalid face syntax '%s', ignoring\n", std::string(str_corner).c_str());
+ face_valid = false;
+ }
+ else {
+ copy_string_to_int(vert_uv_split[0], INT32_MAX, corner.vert_index);
+ if (vert_uv_split.size() == 2) {
+ copy_string_to_int(vert_uv_split[1], INT32_MAX, corner.uv_vert_index);
+ got_uv = corner.uv_vert_index != INT32_MAX;
+ }
+ }
+ }
+ else if (n_slash == 2) {
+ /* Case: "f v1//vn1 v2//vn2 v3//vn3". */
+ /* Case: "f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3". */
+ Vector<StringRef> vert_uv_normal_split;
+ split_by_char(str_corner, '/', vert_uv_normal_split);
+ if (vert_uv_normal_split.size() != 2 && vert_uv_normal_split.size() != 3) {
+ fprintf(stderr, "Invalid face syntax '%s', ignoring\n", std::string(str_corner).c_str());
+ face_valid = false;
+ }
+ else {
+ copy_string_to_int(vert_uv_normal_split[0], INT32_MAX, corner.vert_index);
+ if (vert_uv_normal_split.size() == 3) {
+ copy_string_to_int(vert_uv_normal_split[1], INT32_MAX, corner.uv_vert_index);
+ got_uv = corner.uv_vert_index != INT32_MAX;
+ copy_string_to_int(vert_uv_normal_split[2], INT32_MAX, corner.vertex_normal_index);
+ got_normal = corner.vertex_normal_index != INT32_MAX;
+ }
+ else {
+ copy_string_to_int(vert_uv_normal_split[1], INT32_MAX, corner.vertex_normal_index);
+ got_normal = corner.vertex_normal_index != INT32_MAX;
+ }
+ }
+ }
+ /* Always keep stored indices non-negative and zero-based. */
+ corner.vert_index += corner.vert_index < 0 ? global_vertices.vertices.size() :
+ -offsets.get_index_offset() - 1;
+ if (corner.vert_index < 0 || corner.vert_index >= global_vertices.vertices.size()) {
+ fprintf(stderr,
+ "Invalid vertex index %i (valid range [0, %zi)), ignoring face\n",
+ corner.vert_index,
+ global_vertices.vertices.size());
+ face_valid = false;
+ }
+ if (got_uv) {
+ corner.uv_vert_index += corner.uv_vert_index < 0 ? global_vertices.uv_vertices.size() : -1;
+ if (corner.uv_vert_index < 0 || corner.uv_vert_index >= global_vertices.uv_vertices.size()) {
+ fprintf(stderr,
+ "Invalid UV index %i (valid range [0, %zi)), ignoring face\n",
+ corner.uv_vert_index,
+ global_vertices.uv_vertices.size());
+ face_valid = false;
+ }
+ }
+ if (got_normal) {
+ corner.vertex_normal_index += corner.vertex_normal_index < 0 ?
+ global_vertices.vertex_normals.size() :
+ -1;
+ if (corner.vertex_normal_index < 0 ||
+ corner.vertex_normal_index >= global_vertices.vertex_normals.size()) {
+ fprintf(stderr,
+ "Invalid normal index %i (valid range [0, %zi)), ignoring face\n",
+ corner.vertex_normal_index,
+ global_vertices.vertex_normals.size());
+ face_valid = false;
+ }
+ }
+ curr_face.face_corners.append(corner);
+ }
+
+ if (face_valid) {
+ geom->face_elements_.append(curr_face);
+ geom->total_loops_ += curr_face.face_corners.size();
+ }
+}
+
+static Geometry *geom_set_curve_type(Geometry *geom,
+ const StringRef rest_line,
+ const GlobalVertices &global_vertices,
+ const StringRef state_object_group,
+ VertexIndexOffset &r_offsets,
+ Vector<std::unique_ptr<Geometry>> &r_all_geometries)
+{
+ if (rest_line.find("bspline") == StringRef::not_found) {
+ std::cerr << "Curve type not supported:'" << rest_line << "'" << std::endl;
+ return geom;
+ }
+ geom = create_geometry(
+ geom, GEOM_CURVE, state_object_group, global_vertices, r_all_geometries, r_offsets);
+ geom->nurbs_element_.group_ = state_object_group;
+ return geom;
+}
+
+static void geom_set_curve_degree(Geometry *geom, const StringRef rest_line)
+{
+ copy_string_to_int(rest_line, 3, geom->nurbs_element_.degree);
+}
+
+static void geom_add_curve_vertex_indices(Geometry *geom,
+ const StringRef rest_line,
+ const GlobalVertices &global_vertices)
+{
+ Vector<StringRef> str_curv_split;
+ split_by_char(rest_line, ' ', str_curv_split);
+ /* Remove "0.0" and "1.0" from the strings. They are hardcoded. */
+ str_curv_split.remove(0);
+ str_curv_split.remove(0);
+ geom->nurbs_element_.curv_indices.resize(str_curv_split.size());
+ copy_string_to_int(str_curv_split, INT32_MAX, geom->nurbs_element_.curv_indices);
+ for (int &curv_index : geom->nurbs_element_.curv_indices) {
+ /* Always keep stored indices non-negative and zero-based. */
+ curv_index += curv_index < 0 ? global_vertices.vertices.size() : -1;
+ }
+}
+
+static void geom_add_curve_parameters(Geometry *geom, const StringRef rest_line)
+{
+ Vector<StringRef> str_parm_split;
+ split_by_char(rest_line, ' ', str_parm_split);
+ if (str_parm_split[0] != "u" && str_parm_split[0] != "v") {
+ std::cerr << "Surfaces are not supported:'" << str_parm_split[0] << "'" << std::endl;
+ return;
+ }
+ str_parm_split.remove(0);
+ geom->nurbs_element_.parm.resize(str_parm_split.size());
+ copy_string_to_float(str_parm_split, FLT_MAX, geom->nurbs_element_.parm);
+}
+
+static void geom_update_object_group(const StringRef rest_line, std::string &r_state_object_group)
+{
+
+ if (rest_line.find("off") != string::npos || rest_line.find("null") != string::npos ||
+ rest_line.find("default") != string::npos) {
+ /* Set group for future elements like faces or curves to empty. */
+ r_state_object_group = "";
+ return;
+ }
+ r_state_object_group = rest_line;
+}
+
+static void geom_update_polygon_material(Geometry *geom,
+ const StringRef rest_line,
+ std::string &r_state_material_name)
+{
+ /* Materials may repeat if faces are written without sorting. */
+ geom->material_names_.add(string(rest_line));
+ r_state_material_name = rest_line;
+}
+
+static void geom_update_smooth_group(const StringRef rest_line, bool &r_state_shaded_smooth)
+{
+ /* Some implementations use "0" and "null" too, in addition to "off". */
+ if (rest_line != "0" && rest_line.find("off") == StringRef::not_found &&
+ rest_line.find("null") == StringRef::not_found) {
+ int smooth = 0;
+ copy_string_to_int(rest_line, 0, smooth);
+ r_state_shaded_smooth = smooth != 0;
+ }
+ else {
+ /* The OBJ file explicitly set shading to off. */
+ r_state_shaded_smooth = false;
+ }
+}
+
+/**
+ * Open OBJ file at the path given in import parameters.
+ */
+OBJParser::OBJParser(const OBJImportParams &import_params) : import_params_(import_params)
+{
+ obj_file_.open(import_params_.filepath);
+ if (!obj_file_.good()) {
+ fprintf(stderr, "Cannot read from OBJ file:'%s'.\n", import_params_.filepath);
+ return;
+ }
+}
+
+/**
+ * Read the OBJ file line by line and create OBJ Geometry instances. Also store all the vertex
+ * and UV vertex coordinates in a struct accessible by all objects.
+ */
+void OBJParser::parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries,
+ GlobalVertices &r_global_vertices)
+{
+ if (!obj_file_.good()) {
+ return;
+ }
+
+ string line;
+ /* Store vertex coordinates that belong to other Geometry instances. */
+ VertexIndexOffset offsets;
+ /* Non owning raw pointer to a Geometry. To be updated while creating a new Geometry. */
+ Geometry *curr_geom = create_geometry(
+ nullptr, GEOM_MESH, "", r_global_vertices, r_all_geometries, offsets);
+
+ /* State-setting variables: if set, they remain the same for the remaining
+ * elements in the object. */
+ bool state_shaded_smooth = false;
+ string state_object_group;
+ string state_material_name;
+
+ while (std::getline(obj_file_, line)) {
+ /* Keep reading new lines if the last character is `\`. */
+ /* Another way is to make a getline wrapper and use it in the while condition. */
+ read_next_line(obj_file_, line);
+
+ StringRef line_key, rest_line;
+ split_line_key_rest(line, line_key, rest_line);
+ if (line.empty() || rest_line.is_empty()) {
+ continue;
+ }
+ switch (line_key_str_to_enum(line_key)) {
+ case eOBJLineKey::V: {
+ geom_add_vertex(curr_geom, rest_line, r_global_vertices);
+ break;
+ }
+ case eOBJLineKey::VN: {
+ geom_add_vertex_normal(curr_geom, rest_line, r_global_vertices);
+ break;
+ }
+ case eOBJLineKey::VT: {
+ geom_add_uv_vertex(rest_line, r_global_vertices);
+ break;
+ }
+ case eOBJLineKey::F: {
+ geom_add_polygon(curr_geom,
+ rest_line,
+ r_global_vertices,
+ offsets,
+ state_material_name,
+ state_material_name,
+ state_shaded_smooth);
+ break;
+ }
+ case eOBJLineKey::L: {
+ geom_add_edge(curr_geom, rest_line, offsets, r_global_vertices);
+ break;
+ }
+ case eOBJLineKey::CSTYPE: {
+ curr_geom = geom_set_curve_type(curr_geom,
+ rest_line,
+ r_global_vertices,
+ state_object_group,
+ offsets,
+ r_all_geometries);
+ break;
+ }
+ case eOBJLineKey::DEG: {
+ geom_set_curve_degree(curr_geom, rest_line);
+ break;
+ }
+ case eOBJLineKey::CURV: {
+ geom_add_curve_vertex_indices(curr_geom, rest_line, r_global_vertices);
+ break;
+ }
+ case eOBJLineKey::PARM: {
+ geom_add_curve_parameters(curr_geom, rest_line);
+ break;
+ }
+ case eOBJLineKey::O: {
+ state_shaded_smooth = false;
+ state_object_group = "";
+ state_material_name = "";
+ curr_geom = create_geometry(
+ curr_geom, GEOM_MESH, rest_line, r_global_vertices, r_all_geometries, offsets);
+ break;
+ }
+ case eOBJLineKey::G: {
+ geom_update_object_group(rest_line, state_object_group);
+ break;
+ }
+ case eOBJLineKey::S: {
+ geom_update_smooth_group(rest_line, state_shaded_smooth);
+ break;
+ }
+ case eOBJLineKey::USEMTL: {
+ geom_update_polygon_material(curr_geom, rest_line, state_material_name);
+ break;
+ }
+ case eOBJLineKey::MTLLIB: {
+ mtl_libraries_.append(string(rest_line));
+ break;
+ }
+ case eOBJLineKey::COMMENT:
+ break;
+ default:
+ std::cout << "Element not recognised: '" << line_key << "'" << std::endl;
+ break;
+ }
+ }
+}
+
+/**
+ * Skip all texture map options and get the filepath from a "map_" line.
+ */
+static StringRef skip_unsupported_options(StringRef line)
+{
+ TextureMapOptions map_options;
+ StringRef last_option;
+ int64_t last_option_pos = 0;
+
+ /* Find the last texture map option. */
+ for (StringRef option : map_options.all_options()) {
+ const int64_t pos{line.find(option)};
+ /* Equality (>=) takes care of finding an option in the beginning of the line. Avoid messing
+ * with signed-unsigned int comparison. */
+ if (pos != StringRef::not_found && pos >= last_option_pos) {
+ last_option = option;
+ last_option_pos = pos;
+ }
+ }
+
+ if (last_option.is_empty()) {
+ /* No option found, line is the filepath */
+ return line;
+ }
+
+ /* Remove upto start of the last option + size of the last option + space after it. */
+ line = line.drop_prefix(last_option_pos + last_option.size() + 1);
+ for (int i = 0; i < map_options.number_of_args(last_option); i++) {
+ const int64_t pos_space{line.find_first_of(' ')};
+ if (pos_space != StringRef::not_found) {
+ BLI_assert(pos_space + 1 < line.size());
+ line = line.drop_prefix(pos_space + 1);
+ }
+ }
+
+ return line;
+}
+
+/**
+ * Fix incoming texture map line keys for variations due to other exporters.
+ */
+static string fix_bad_map_keys(StringRef map_key)
+{
+ string new_map_key(map_key);
+ if (map_key == "refl") {
+ new_map_key = "map_refl";
+ }
+ if (map_key.find("bump") != StringRef::not_found) {
+ /* Handles both "bump" and "map_Bump" */
+ new_map_key = "map_Bump";
+ }
+ return new_map_key;
+}
+
+/**
+ * Return a list of all material library filepaths referenced by the OBJ file.
+ */
+Span<std::string> OBJParser::mtl_libraries() const
+{
+ return mtl_libraries_;
+}
+
+/**
+ * Open material library file.
+ */
+MTLParser::MTLParser(StringRef mtl_library, StringRefNull obj_filepath)
+{
+ char obj_file_dir[FILE_MAXDIR];
+ BLI_split_dir_part(obj_filepath.data(), obj_file_dir, FILE_MAXDIR);
+ BLI_path_join(mtl_file_path_, FILE_MAX, obj_file_dir, mtl_library.data(), NULL);
+ BLI_split_dir_part(mtl_file_path_, mtl_dir_path_, FILE_MAXDIR);
+ mtl_file_.open(mtl_file_path_);
+ if (!mtl_file_.good()) {
+ fprintf(stderr, "Cannot read from MTL file:'%s'\n", mtl_file_path_);
+ return;
+ }
+}
+
+/**
+ * Read MTL file(s) and add MTLMaterial instances to the given Map reference.
+ */
+void MTLParser::parse_and_store(Map<string, std::unique_ptr<MTLMaterial>> &r_mtl_materials)
+{
+ if (!mtl_file_.good()) {
+ return;
+ }
+
+ string line;
+ MTLMaterial *current_mtlmaterial = nullptr;
+
+ while (std::getline(mtl_file_, line)) {
+ StringRef line_key, rest_line;
+ split_line_key_rest(line, line_key, rest_line);
+ if (line.empty() || rest_line.is_empty()) {
+ continue;
+ }
+
+ /* Fix lower case/ incomplete texture map identifiers. */
+ const string fixed_key = fix_bad_map_keys(line_key);
+ line_key = fixed_key;
+
+ if (line_key == "newmtl") {
+ if (r_mtl_materials.remove_as(rest_line)) {
+ std::cerr << "Duplicate material found:'" << rest_line
+ << "', using the last encountered Material definition." << std::endl;
+ }
+ current_mtlmaterial =
+ r_mtl_materials.lookup_or_add(string(rest_line), std::make_unique<MTLMaterial>()).get();
+ }
+ else if (line_key == "Ns") {
+ copy_string_to_float(rest_line, 324.0f, current_mtlmaterial->Ns);
+ }
+ else if (line_key == "Ka") {
+ Vector<StringRef> str_ka_split;
+ split_by_char(rest_line, ' ', str_ka_split);
+ copy_string_to_float(str_ka_split, 0.0f, {current_mtlmaterial->Ka, 3});
+ }
+ else if (line_key == "Kd") {
+ Vector<StringRef> str_kd_split;
+ split_by_char(rest_line, ' ', str_kd_split);
+ copy_string_to_float(str_kd_split, 0.8f, {current_mtlmaterial->Kd, 3});
+ }
+ else if (line_key == "Ks") {
+ Vector<StringRef> str_ks_split;
+ split_by_char(rest_line, ' ', str_ks_split);
+ copy_string_to_float(str_ks_split, 0.5f, {current_mtlmaterial->Ks, 3});
+ }
+ else if (line_key == "Ke") {
+ Vector<StringRef> str_ke_split;
+ split_by_char(rest_line, ' ', str_ke_split);
+ copy_string_to_float(str_ke_split, 0.0f, {current_mtlmaterial->Ke, 3});
+ }
+ else if (line_key == "Ni") {
+ copy_string_to_float(rest_line, 1.45f, current_mtlmaterial->Ni);
+ }
+ else if (line_key == "d") {
+ copy_string_to_float(rest_line, 1.0f, current_mtlmaterial->d);
+ }
+ else if (line_key == "illum") {
+ copy_string_to_int(rest_line, 2, current_mtlmaterial->illum);
+ }
+
+ /* Parse image textures. */
+ else if (line_key.find("map_") != StringRef::not_found) {
+ /* TODO howardt: fix this */
+ eMTLSyntaxElement line_key_enum = mtl_line_key_str_to_enum(line_key);
+ if (line_key_enum == eMTLSyntaxElement::string ||
+ !current_mtlmaterial->texture_maps.contains_as(line_key_enum)) {
+ /* No supported texture map found. */
+ std::cerr << "Texture map type not supported:'" << line_key << "'" << std::endl;
+ continue;
+ }
+ tex_map_XX &tex_map = current_mtlmaterial->texture_maps.lookup(line_key_enum);
+ Vector<StringRef> str_map_xx_split;
+ split_by_char(rest_line, ' ', str_map_xx_split);
+
+ /* TODO ankitm: use `skip_unsupported_options` for parsing these options too? */
+ const int64_t pos_o{str_map_xx_split.first_index_of_try("-o")};
+ if (pos_o != -1 && pos_o + 3 < str_map_xx_split.size()) {
+ copy_string_to_float({str_map_xx_split[pos_o + 1],
+ str_map_xx_split[pos_o + 2],
+ str_map_xx_split[pos_o + 3]},
+ 0.0f,
+ {tex_map.translation, 3});
+ }
+ const int64_t pos_s{str_map_xx_split.first_index_of_try("-s")};
+ if (pos_s != -1 && pos_s + 3 < str_map_xx_split.size()) {
+ copy_string_to_float({str_map_xx_split[pos_s + 1],
+ str_map_xx_split[pos_s + 2],
+ str_map_xx_split[pos_s + 3]},
+ 1.0f,
+ {tex_map.scale, 3});
+ }
+ /* Only specific to Normal Map node. */
+ const int64_t pos_bm{str_map_xx_split.first_index_of_try("-bm")};
+ if (pos_bm != -1 && pos_bm + 1 < str_map_xx_split.size()) {
+ copy_string_to_float(
+ str_map_xx_split[pos_bm + 1], 0.0f, current_mtlmaterial->map_Bump_strength);
+ }
+ const int64_t pos_projection{str_map_xx_split.first_index_of_try("-type")};
+ if (pos_projection != -1 && pos_projection + 1 < str_map_xx_split.size()) {
+ /* Only Sphere is supported, so whatever the type is, set it to Sphere. */
+ tex_map.projection_type = SHD_PROJ_SPHERE;
+ if (str_map_xx_split[pos_projection + 1] != "sphere") {
+ std::cerr << "Using projection type 'sphere', not:'"
+ << str_map_xx_split[pos_projection + 1] << "'." << std::endl;
+ }
+ }
+
+ /* Skip all unsupported options and arguments. */
+ tex_map.image_path = string(skip_unsupported_options(rest_line));
+ tex_map.mtl_dir_path = mtl_dir_path_;
+ }
+ }
+}
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/obj_import_file_reader.hh b/source/blender/io/wavefront_obj/importer/obj_import_file_reader.hh
new file mode 100644
index 00000000000..c8d8b78fc0e
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/obj_import_file_reader.hh
@@ -0,0 +1,151 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include "BLI_fileops.hh"
+#include "IO_wavefront_obj.h"
+#include "obj_import_mtl.hh"
+#include "obj_import_objects.hh"
+
+namespace blender::io::obj {
+
+/* Note: the OBJ parser implementation is planned to get fairly large changes "soon",
+ * so don't read too much into current implementation... */
+class OBJParser {
+ private:
+ const OBJImportParams &import_params_;
+ blender::fstream obj_file_;
+ Vector<std::string> mtl_libraries_;
+
+ public:
+ OBJParser(const OBJImportParams &import_params);
+
+ void parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries,
+ GlobalVertices &r_global_vertices);
+ Span<std::string> mtl_libraries() const;
+};
+
+enum class eOBJLineKey {
+ V,
+ VN,
+ VT,
+ F,
+ L,
+ CSTYPE,
+ DEG,
+ CURV,
+ PARM,
+ O,
+ G,
+ S,
+ USEMTL,
+ MTLLIB,
+ COMMENT
+};
+
+constexpr eOBJLineKey line_key_str_to_enum(const std::string_view key_str)
+{
+ if (key_str == "v" || key_str == "V") {
+ return eOBJLineKey::V;
+ }
+ if (key_str == "vn" || key_str == "VN") {
+ return eOBJLineKey::VN;
+ }
+ if (key_str == "vt" || key_str == "VT") {
+ return eOBJLineKey::VT;
+ }
+ if (key_str == "f" || key_str == "F") {
+ return eOBJLineKey::F;
+ }
+ if (key_str == "l" || key_str == "L") {
+ return eOBJLineKey::L;
+ }
+ if (key_str == "cstype" || key_str == "CSTYPE") {
+ return eOBJLineKey::CSTYPE;
+ }
+ if (key_str == "deg" || key_str == "DEG") {
+ return eOBJLineKey::DEG;
+ }
+ if (key_str == "curv" || key_str == "CURV") {
+ return eOBJLineKey::CURV;
+ }
+ if (key_str == "parm" || key_str == "PARM") {
+ return eOBJLineKey::PARM;
+ }
+ if (key_str == "o" || key_str == "O") {
+ return eOBJLineKey::O;
+ }
+ if (key_str == "g" || key_str == "G") {
+ return eOBJLineKey::G;
+ }
+ if (key_str == "s" || key_str == "S") {
+ return eOBJLineKey::S;
+ }
+ if (key_str == "usemtl" || key_str == "USEMTL") {
+ return eOBJLineKey::USEMTL;
+ }
+ if (key_str == "mtllib" || key_str == "MTLLIB") {
+ return eOBJLineKey::MTLLIB;
+ }
+ if (key_str == "#") {
+ return eOBJLineKey::COMMENT;
+ }
+ return eOBJLineKey::COMMENT;
+}
+
+/**
+ * All texture map options with number of arguments they accept.
+ */
+class TextureMapOptions {
+ private:
+ Map<const std::string, int> tex_map_options;
+
+ public:
+ TextureMapOptions()
+ {
+ tex_map_options.add_new("-blendu", 1);
+ tex_map_options.add_new("-blendv", 1);
+ tex_map_options.add_new("-boost", 1);
+ tex_map_options.add_new("-mm", 2);
+ tex_map_options.add_new("-o", 3);
+ tex_map_options.add_new("-s", 3);
+ tex_map_options.add_new("-t", 3);
+ tex_map_options.add_new("-texres", 1);
+ tex_map_options.add_new("-clamp", 1);
+ tex_map_options.add_new("-bm", 1);
+ tex_map_options.add_new("-imfchan", 1);
+ }
+
+ /**
+ * All valid option strings.
+ */
+ Map<const std::string, int>::KeyIterator all_options() const
+ {
+ return tex_map_options.keys();
+ }
+
+ int number_of_args(StringRef option) const
+ {
+ return tex_map_options.lookup_as(std::string(option));
+ }
+};
+
+class MTLParser {
+ private:
+ char mtl_file_path_[FILE_MAX];
+ /**
+ * Directory in which the MTL file is found.
+ */
+ char mtl_dir_path_[FILE_MAX];
+ blender::fstream mtl_file_;
+
+ public:
+ MTLParser(StringRef mtl_library_, StringRefNull obj_filepath);
+
+ void parse_and_store(Map<std::string, std::unique_ptr<MTLMaterial>> &r_mtl_materials);
+};
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/obj_import_mesh.cc b/source/blender/io/wavefront_obj/importer/obj_import_mesh.cc
new file mode 100644
index 00000000000..e9dfcf6d091
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/obj_import_mesh.cc
@@ -0,0 +1,380 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#include "DNA_material_types.h"
+#include "DNA_mesh_types.h"
+#include "DNA_scene_types.h"
+
+#include "BKE_customdata.h"
+#include "BKE_material.h"
+#include "BKE_mesh.h"
+#include "BKE_node_tree_update.h"
+#include "BKE_object.h"
+#include "BKE_object_deform.h"
+
+#include "BLI_math_vector.h"
+#include "BLI_set.hh"
+
+#include "importer_mesh_utils.hh"
+#include "obj_import_mesh.hh"
+
+namespace blender::io::obj {
+
+Object *MeshFromGeometry::create_mesh(
+ Main *bmain,
+ const Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
+ Map<std::string, Material *> &created_materials,
+ const OBJImportParams &import_params)
+{
+ std::string ob_name{mesh_geometry_.geometry_name_};
+ if (ob_name.empty()) {
+ ob_name = "Untitled";
+ }
+ fixup_invalid_faces();
+
+ const int64_t tot_verts_object{mesh_geometry_.vertex_indices_.size()};
+ /* Total explicitly imported edges, not the ones belonging the polygons to be created. */
+ const int64_t tot_edges{mesh_geometry_.edges_.size()};
+ const int64_t tot_face_elems{mesh_geometry_.face_elements_.size()};
+ const int64_t tot_loops{mesh_geometry_.total_loops_};
+
+ Mesh *mesh = BKE_mesh_new_nomain(tot_verts_object, tot_edges, 0, tot_loops, tot_face_elems);
+ Object *obj = BKE_object_add_only_object(bmain, OB_MESH, ob_name.c_str());
+ obj->data = BKE_object_obdata_add_from_type(bmain, OB_MESH, ob_name.c_str());
+
+ create_vertices(mesh);
+ create_polys_loops(obj, mesh);
+ create_edges(mesh);
+ create_uv_verts(mesh);
+ create_normals(mesh);
+ create_materials(bmain, materials, created_materials, obj);
+
+ bool verbose_validate = false;
+#ifdef DEBUG
+ verbose_validate = true;
+#endif
+ BKE_mesh_validate(mesh, verbose_validate, false);
+ transform_object(obj, import_params);
+
+ /* FIXME: after 2.80; `mesh->flag` isn't copied by #BKE_mesh_nomain_to_mesh() */
+ const short autosmooth = (mesh->flag & ME_AUTOSMOOTH);
+ Mesh *dst = static_cast<Mesh *>(obj->data);
+ BKE_mesh_nomain_to_mesh(mesh, dst, obj, &CD_MASK_EVERYTHING, true);
+ dst->flag |= autosmooth;
+
+ return obj;
+}
+
+/**
+ * OBJ files coming from the wild might have faces that are invalid in Blender
+ * (mostly with duplicate vertex indices, used by some software to indicate
+ * polygons with holes). This method tries to fix them up.
+ */
+void MeshFromGeometry::fixup_invalid_faces()
+{
+ for (int64_t face_idx = 0; face_idx < mesh_geometry_.face_elements_.size(); ++face_idx) {
+ const PolyElem &curr_face = mesh_geometry_.face_elements_[face_idx];
+
+ if (curr_face.face_corners.size() < 3) {
+ /* Skip and remove faces that have fewer than 3 corners. */
+ mesh_geometry_.total_loops_ -= curr_face.face_corners.size();
+ mesh_geometry_.face_elements_.remove_and_reorder(face_idx);
+ continue;
+ }
+
+ /* Check if face is invalid for Blender conventions:
+ * basically whether it has duplicate vertex indices. */
+ bool valid = true;
+ Set<int, 8> used_verts;
+ for (const PolyCorner &corner : curr_face.face_corners) {
+ if (used_verts.contains(corner.vert_index)) {
+ valid = false;
+ break;
+ }
+ used_verts.add(corner.vert_index);
+ }
+ if (valid) {
+ continue;
+ }
+
+ /* We have an invalid face, have to turn it into possibly
+ * multiple valid faces. */
+ Vector<int, 8> face_verts;
+ Vector<int, 8> face_uvs;
+ Vector<int, 8> face_normals;
+ face_verts.reserve(curr_face.face_corners.size());
+ face_uvs.reserve(curr_face.face_corners.size());
+ face_normals.reserve(curr_face.face_corners.size());
+ for (const PolyCorner &corner : curr_face.face_corners) {
+ face_verts.append(corner.vert_index);
+ face_normals.append(corner.vertex_normal_index);
+ face_uvs.append(corner.uv_vert_index);
+ }
+ std::string face_vertex_group = curr_face.vertex_group;
+ std::string face_material_name = curr_face.material_name;
+ bool face_shaded_smooth = curr_face.shaded_smooth;
+
+ /* Remove the invalid face. */
+ mesh_geometry_.total_loops_ -= curr_face.face_corners.size();
+ mesh_geometry_.face_elements_.remove_and_reorder(face_idx);
+
+ Vector<Vector<int>> new_faces = fixup_invalid_polygon(global_vertices_.vertices, face_verts);
+
+ /* Create the newly formed faces. */
+ for (Span<int> face : new_faces) {
+ if (face.size() < 3) {
+ continue;
+ }
+ PolyElem new_face{};
+ new_face.vertex_group = face_vertex_group;
+ new_face.material_name = face_material_name;
+ new_face.shaded_smooth = face_shaded_smooth;
+ new_face.face_corners.reserve(face.size());
+ for (int idx : face) {
+ BLI_assert(idx >= 0 && idx < face_verts.size());
+ new_face.face_corners.append({face_verts[idx], face_uvs[idx], face_normals[idx]});
+ }
+ mesh_geometry_.face_elements_.append(new_face);
+ mesh_geometry_.total_loops_ += face.size();
+ }
+ }
+}
+
+void MeshFromGeometry::create_vertices(Mesh *mesh)
+{
+ const int64_t tot_verts_object{mesh_geometry_.vertex_indices_.size()};
+ for (int i = 0; i < tot_verts_object; ++i) {
+ if (mesh_geometry_.vertex_indices_[i] < global_vertices_.vertices.size()) {
+ copy_v3_v3(mesh->mvert[i].co, global_vertices_.vertices[mesh_geometry_.vertex_indices_[i]]);
+ }
+ else {
+ std::cerr << "Vertex index:" << mesh_geometry_.vertex_indices_[i]
+ << " larger than total vertices:" << global_vertices_.vertices.size() << " ."
+ << std::endl;
+ }
+ }
+}
+
+/**
+ * Create polygons for the Mesh, set smooth shading flag, deform group name, assigned material
+ * also.
+ *
+ * It must receive all polygons to be added to the mesh. Remove holes from polygons before
+ * calling this.
+ */
+void MeshFromGeometry::create_polys_loops(Object *obj, Mesh *mesh)
+{
+ /* Will not be used if vertex groups are not imported. */
+ mesh->dvert = nullptr;
+ float weight = 0.0f;
+ const int64_t total_verts = mesh_geometry_.vertex_indices_.size();
+ if (total_verts && mesh_geometry_.use_vertex_groups_) {
+ mesh->dvert = static_cast<MDeformVert *>(
+ CustomData_add_layer(&mesh->vdata, CD_MDEFORMVERT, CD_CALLOC, nullptr, total_verts));
+ weight = 1.0f / total_verts;
+ }
+ else {
+ UNUSED_VARS(weight);
+ }
+
+ /* Do not remove elements from the VectorSet since order of insertion is required.
+ * StringRef is fine since per-face deform group name outlives the VectorSet. */
+ VectorSet<StringRef> group_names;
+ const int64_t tot_face_elems{mesh->totpoly};
+ int tot_loop_idx = 0;
+
+ for (int poly_idx = 0; poly_idx < tot_face_elems; ++poly_idx) {
+ const PolyElem &curr_face = mesh_geometry_.face_elements_[poly_idx];
+ if (curr_face.face_corners.size() < 3) {
+ /* Don't add single vertex face, or edges. */
+ std::cerr << "Face with less than 3 vertices found, skipping." << std::endl;
+ continue;
+ }
+
+ MPoly &mpoly = mesh->mpoly[poly_idx];
+ mpoly.totloop = curr_face.face_corners.size();
+ mpoly.loopstart = tot_loop_idx;
+ if (curr_face.shaded_smooth) {
+ mpoly.flag |= ME_SMOOTH;
+ }
+ mpoly.mat_nr = mesh_geometry_.material_names_.index_of_try(curr_face.material_name);
+ /* Importing obj files without any materials would result in negative indices, which is not
+ * supported. */
+ if (mpoly.mat_nr < 0) {
+ mpoly.mat_nr = 0;
+ }
+
+ for (const PolyCorner &curr_corner : curr_face.face_corners) {
+ MLoop &mloop = mesh->mloop[tot_loop_idx];
+ tot_loop_idx++;
+ mloop.v = curr_corner.vert_index;
+
+ if (!mesh->dvert) {
+ continue;
+ }
+ /* Iterating over mloop results in finding the same vertex multiple times.
+ * Another way is to allocate memory for dvert while creating vertices and fill them here.
+ */
+ MDeformVert &def_vert = mesh->dvert[mloop.v];
+ if (!def_vert.dw) {
+ def_vert.dw = static_cast<MDeformWeight *>(
+ MEM_callocN(sizeof(MDeformWeight), "OBJ Import Deform Weight"));
+ }
+ /* Every vertex in a face is assigned the same deform group. */
+ int64_t pos_name{group_names.index_of_try(curr_face.vertex_group)};
+ if (pos_name == -1) {
+ group_names.add_new(curr_face.vertex_group);
+ pos_name = group_names.size() - 1;
+ }
+ BLI_assert(pos_name >= 0);
+ /* Deform group number (def_nr) must behave like an index into the names' list. */
+ *(def_vert.dw) = {static_cast<unsigned int>(pos_name), weight};
+ }
+ }
+
+ if (!mesh->dvert) {
+ return;
+ }
+ /* Add deform group(s) to the object's defbase. */
+ for (StringRef name : group_names) {
+ /* Adding groups in this order assumes that def_nr is an index into the names' list. */
+ BKE_object_defgroup_add_name(obj, name.data());
+ }
+}
+
+/**
+ * Add explicitly imported OBJ edges to the mesh.
+ */
+void MeshFromGeometry::create_edges(Mesh *mesh)
+{
+ const int64_t tot_edges{mesh_geometry_.edges_.size()};
+ const int64_t total_verts{mesh_geometry_.vertex_indices_.size()};
+ UNUSED_VARS_NDEBUG(total_verts);
+ for (int i = 0; i < tot_edges; ++i) {
+ const MEdge &src_edge = mesh_geometry_.edges_[i];
+ MEdge &dst_edge = mesh->medge[i];
+ BLI_assert(src_edge.v1 < total_verts && src_edge.v2 < total_verts);
+ dst_edge.v1 = src_edge.v1;
+ dst_edge.v2 = src_edge.v2;
+ dst_edge.flag = ME_LOOSEEDGE;
+ }
+
+ /* Set argument `update` to true so that existing, explicitly imported edges can be merged
+ * with the new ones created from polygons. */
+ BKE_mesh_calc_edges(mesh, true, false);
+ BKE_mesh_calc_edges_loose(mesh);
+}
+
+/**
+ * Add UV layer and vertices to the Mesh.
+ */
+void MeshFromGeometry::create_uv_verts(Mesh *mesh)
+{
+ if (global_vertices_.uv_vertices.size() <= 0) {
+ return;
+ }
+ MLoopUV *mluv_dst = static_cast<MLoopUV *>(CustomData_add_layer(
+ &mesh->ldata, CD_MLOOPUV, CD_DEFAULT, nullptr, mesh_geometry_.total_loops_));
+ int tot_loop_idx = 0;
+
+ for (const PolyElem &curr_face : mesh_geometry_.face_elements_) {
+ for (const PolyCorner &curr_corner : curr_face.face_corners) {
+ if (curr_corner.uv_vert_index >= 0 &&
+ curr_corner.uv_vert_index < global_vertices_.uv_vertices.size()) {
+ const float2 &mluv_src = global_vertices_.uv_vertices[curr_corner.uv_vert_index];
+ copy_v2_v2(mluv_dst[tot_loop_idx].uv, mluv_src);
+ tot_loop_idx++;
+ }
+ }
+ }
+}
+
+static Material *get_or_create_material(
+ Main *bmain,
+ const std::string &name,
+ const Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
+ Map<std::string, Material *> &created_materials)
+{
+ /* Have we created this material already? */
+ Material **found_mat = created_materials.lookup_ptr(name);
+ if (found_mat != nullptr) {
+ return *found_mat;
+ }
+
+ /* We have not, will have to create it. */
+ if (!materials.contains(name)) {
+ std::cerr << "Material named '" << name << "' not found in material library." << std::endl;
+ return nullptr;
+ }
+
+ Material *mat = BKE_material_add(bmain, name.c_str());
+ const MTLMaterial &mtl = *materials.lookup(name);
+ ShaderNodetreeWrap mat_wrap{bmain, mtl, mat};
+
+ /* Viewport shading uses legacy r,g,b material values. */
+ if (mtl.Kd[0] >= 0 && mtl.Kd[1] >= 0 && mtl.Kd[2] >= 0) {
+ mat->r = mtl.Kd[0];
+ mat->g = mtl.Kd[1];
+ mat->b = mtl.Kd[2];
+ }
+
+ mat->use_nodes = true;
+ mat->nodetree = mat_wrap.get_nodetree();
+ BKE_ntree_update_main_tree(bmain, mat->nodetree, nullptr);
+
+ created_materials.add_new(name, mat);
+ return mat;
+}
+
+/**
+ * Add materials and the nodetree to the Mesh Object.
+ */
+void MeshFromGeometry::create_materials(
+ Main *bmain,
+ const Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
+ Map<std::string, Material *> &created_materials,
+ Object *obj)
+{
+ for (const std::string &name : mesh_geometry_.material_names_) {
+ Material *mat = get_or_create_material(bmain, name, materials, created_materials);
+ if (mat == nullptr) {
+ continue;
+ }
+ BKE_object_material_slot_add(bmain, obj);
+ BKE_object_material_assign(bmain, obj, mat, obj->totcol, BKE_MAT_ASSIGN_USERPREF);
+ }
+}
+
+/**
+ * Needs more clarity about what is expected in the viewport if the function works.
+ */
+void MeshFromGeometry::create_normals(Mesh *mesh)
+{
+ /* No normal data: nothing to do. */
+ if (global_vertices_.vertex_normals.is_empty() || !mesh_geometry_.has_vertex_normals_) {
+ return;
+ }
+
+ float(*loop_normals)[3] = static_cast<float(*)[3]>(
+ MEM_malloc_arrayN(mesh_geometry_.total_loops_, sizeof(float[3]), __func__));
+ int tot_loop_idx = 0;
+ for (const PolyElem &curr_face : mesh_geometry_.face_elements_) {
+ for (const PolyCorner &curr_corner : curr_face.face_corners) {
+ int n_index = curr_corner.vertex_normal_index;
+ float3 normal(0, 0, 0);
+ if (n_index >= 0) {
+ normal = global_vertices_.vertex_normals[n_index];
+ }
+ copy_v3_v3(loop_normals[tot_loop_idx], normal);
+ tot_loop_idx++;
+ }
+ }
+ mesh->flag |= ME_AUTOSMOOTH;
+ BKE_mesh_set_custom_normals(mesh, loop_normals);
+ MEM_freeN(loop_normals);
+}
+
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/obj_import_mesh.hh b/source/blender/io/wavefront_obj/importer/obj_import_mesh.hh
new file mode 100644
index 00000000000..86132b94a31
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/obj_import_mesh.hh
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include "BKE_lib_id.h"
+
+#include "BLI_utility_mixins.hh"
+
+#include "obj_import_mtl.hh"
+#include "obj_import_objects.hh"
+
+struct Material;
+
+namespace blender::io::obj {
+
+/**
+ * Make a Blender Mesh Object from a Geometry of GEOM_MESH type.
+ */
+class MeshFromGeometry : NonMovable, NonCopyable {
+ private:
+ Geometry &mesh_geometry_;
+ const GlobalVertices &global_vertices_;
+
+ public:
+ MeshFromGeometry(Geometry &mesh_geometry, const GlobalVertices &global_vertices)
+ : mesh_geometry_(mesh_geometry), global_vertices_(global_vertices)
+ {
+ }
+
+ Object *create_mesh(Main *bmain,
+ const Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
+ Map<std::string, Material *> &created_materials,
+ const OBJImportParams &import_params);
+
+ private:
+ void fixup_invalid_faces();
+ void create_vertices(Mesh *mesh);
+ void create_polys_loops(Object *obj, Mesh *mesh);
+ void create_edges(Mesh *mesh);
+ void create_uv_verts(Mesh *mesh);
+ void create_materials(Main *bmain,
+ const Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
+ Map<std::string, Material *> &created_materials,
+ Object *obj);
+ void create_normals(Mesh *mesh);
+};
+
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/obj_import_mtl.cc b/source/blender/io/wavefront_obj/importer/obj_import_mtl.cc
new file mode 100644
index 00000000000..ef97f58eb0e
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/obj_import_mtl.cc
@@ -0,0 +1,386 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#include "BKE_image.h"
+#include "BKE_node.h"
+
+#include "BLI_map.hh"
+#include "BLI_math_vector.h"
+
+#include "DNA_material_types.h"
+#include "DNA_node_types.h"
+
+#include "NOD_shader.h"
+
+/* TODO: move eMTLSyntaxElement out of following file into a more neutral place */
+#include "obj_export_io.hh"
+#include "obj_import_mtl.hh"
+#include "parser_string_utils.hh"
+
+namespace blender::io::obj {
+
+/**
+ * Set the socket's (of given ID) value to the given number(s).
+ * Only float value(s) can be set using this method.
+ */
+static void set_property_of_socket(eNodeSocketDatatype property_type,
+ StringRef socket_id,
+ Span<float> value,
+ bNode *r_node)
+{
+ BLI_assert(r_node);
+ bNodeSocket *socket{nodeFindSocket(r_node, SOCK_IN, socket_id.data())};
+ BLI_assert(socket && socket->type == property_type);
+ switch (property_type) {
+ case SOCK_FLOAT: {
+ BLI_assert(value.size() == 1);
+ static_cast<bNodeSocketValueFloat *>(socket->default_value)->value = value[0];
+ break;
+ }
+ case SOCK_RGBA: {
+ /* Alpha will be added manually. It is not read from the MTL file either. */
+ BLI_assert(value.size() == 3);
+ copy_v3_v3(static_cast<bNodeSocketValueRGBA *>(socket->default_value)->value, value.data());
+ static_cast<bNodeSocketValueRGBA *>(socket->default_value)->value[3] = 1.0f;
+ break;
+ }
+ case SOCK_VECTOR: {
+ BLI_assert(value.size() == 3);
+ copy_v4_v4(static_cast<bNodeSocketValueVector *>(socket->default_value)->value,
+ value.data());
+ break;
+ }
+ default: {
+ BLI_assert(0);
+ break;
+ }
+ }
+}
+
+static bool load_texture_image_at_path(Main *bmain,
+ const tex_map_XX &tex_map,
+ bNode *r_node,
+ const std::string &path)
+{
+ Image *tex_image = BKE_image_load(bmain, path.c_str());
+ if (!tex_image) {
+ fprintf(stderr, "Cannot load image file: '%s'\n", path.c_str());
+ return false;
+ }
+ fprintf(stderr, "Loaded image from: '%s'\n", path.c_str());
+ r_node->id = reinterpret_cast<ID *>(tex_image);
+ NodeTexImage *image = static_cast<NodeTexImage *>(r_node->storage);
+ image->projection = tex_map.projection_type;
+ return true;
+}
+
+/**
+ * Load image for Image Texture node and set the node properties.
+ * Return success if Image can be loaded successfully.
+ */
+static bool load_texture_image(Main *bmain, const tex_map_XX &tex_map, bNode *r_node)
+{
+ BLI_assert(r_node && r_node->type == SH_NODE_TEX_IMAGE);
+
+ /* First try treating texture path as relative. */
+ std::string tex_path{tex_map.mtl_dir_path + tex_map.image_path};
+ if (load_texture_image_at_path(bmain, tex_map, r_node, tex_path)) {
+ return true;
+ }
+ /* Then try using it directly as absolute path. */
+ std::string raw_path{tex_map.image_path};
+ if (load_texture_image_at_path(bmain, tex_map, r_node, raw_path)) {
+ return true;
+ }
+ /* Try removing quotes. */
+ std::string no_quote_path{replace_all_occurences(tex_path, "\"", "")};
+ if (no_quote_path != tex_path &&
+ load_texture_image_at_path(bmain, tex_map, r_node, no_quote_path)) {
+ return true;
+ }
+ /* Try replacing underscores with spaces. */
+ std::string no_underscore_path{replace_all_occurences(no_quote_path, "_", " ")};
+ if (no_underscore_path != no_quote_path && no_underscore_path != tex_path &&
+ load_texture_image_at_path(bmain, tex_map, r_node, no_underscore_path)) {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Initializes a nodetree with a p-BSDF node's BSDF socket connected to shader output node's
+ * surface socket.
+ */
+ShaderNodetreeWrap::ShaderNodetreeWrap(Main *bmain, const MTLMaterial &mtl_mat, Material *mat)
+ : mtl_mat_(mtl_mat)
+{
+ nodetree_.reset(ntreeAddTree(nullptr, "Shader Nodetree", ntreeType_Shader->idname));
+ bsdf_ = add_node_to_tree(SH_NODE_BSDF_PRINCIPLED);
+ shader_output_ = add_node_to_tree(SH_NODE_OUTPUT_MATERIAL);
+
+ set_bsdf_socket_values();
+ add_image_textures(bmain, mat);
+ link_sockets(bsdf_, "BSDF", shader_output_, "Surface", 4);
+
+ nodeSetActive(nodetree_.get(), shader_output_);
+}
+
+/**
+ * Assert if caller hasn't acquired nodetree.
+ */
+ShaderNodetreeWrap::~ShaderNodetreeWrap()
+{
+ if (nodetree_) {
+ /* nodetree's ownership must be acquired by the caller. */
+ nodetree_.reset();
+ BLI_assert(0);
+ }
+}
+
+/**
+ * Release nodetree for materials to own it. nodetree has its unique deleter
+ * if destructor is not reached for some reason.
+ */
+bNodeTree *ShaderNodetreeWrap::get_nodetree()
+{
+ /* If this function has been reached, we know that nodes and the nodetree
+ * can be added to the scene safely. */
+ return nodetree_.release();
+}
+
+/**
+ * Add a new static node to the tree.
+ * No two nodes are linked here.
+ */
+bNode *ShaderNodetreeWrap::add_node_to_tree(const int node_type)
+{
+ return nodeAddStaticNode(nullptr, nodetree_.get(), node_type);
+}
+
+/**
+ * Return x-y coordinates for a node where y is determined by other nodes present in
+ * the same vertical column.
+ */
+std::pair<float, float> ShaderNodetreeWrap::set_node_locations(const int pos_x)
+{
+ int pos_y = 0;
+ bool found = false;
+ while (true) {
+ for (Span<int> location : node_locations) {
+ if (location[0] == pos_x && location[1] == pos_y) {
+ pos_y += 1;
+ found = true;
+ }
+ else {
+ found = false;
+ }
+ }
+ if (!found) {
+ node_locations.append({pos_x, pos_y});
+ return {pos_x * node_size_, pos_y * node_size_ * 2.0 / 3.0};
+ }
+ }
+}
+
+/**
+ * Link two nodes by the sockets of given IDs.
+ * Also releases the ownership of the "from" node for nodetree to free it.
+ * \param from_node_pos_x 0 to 4 value as per nodetree arrangement.
+ */
+void ShaderNodetreeWrap::link_sockets(bNode *from_node,
+ StringRef from_node_id,
+ bNode *to_node,
+ StringRef to_node_id,
+ const int from_node_pos_x)
+{
+ std::tie(from_node->locx, from_node->locy) = set_node_locations(from_node_pos_x);
+ std::tie(to_node->locx, to_node->locy) = set_node_locations(from_node_pos_x + 1);
+ bNodeSocket *from_sock{nodeFindSocket(from_node, SOCK_OUT, from_node_id.data())};
+ bNodeSocket *to_sock{nodeFindSocket(to_node, SOCK_IN, to_node_id.data())};
+ BLI_assert(from_sock && to_sock);
+ nodeAddLink(nodetree_.get(), from_node, from_sock, to_node, to_sock);
+}
+
+/**
+ * Set values of sockets in p-BSDF node of the nodetree.
+ */
+void ShaderNodetreeWrap::set_bsdf_socket_values()
+{
+ const int illum = mtl_mat_.illum;
+ bool do_highlight = false;
+ bool do_tranparency = false;
+ bool do_reflection = false;
+ bool do_glass = false;
+ /* See https://wikipedia.org/wiki/Wavefront_.obj_file for possible values of illum. */
+ switch (illum) {
+ case 1: {
+ /* Base color on, ambient on. */
+ break;
+ }
+ case 2: {
+ /* Highlight on. */
+ do_highlight = true;
+ break;
+ }
+ case 3: {
+ /* Reflection on and Ray trace on. */
+ do_reflection = true;
+ break;
+ }
+ case 4: {
+ /* Transparency: Glass on, Reflection: Ray trace on. */
+ do_glass = true;
+ do_reflection = true;
+ do_tranparency = true;
+ break;
+ }
+ case 5: {
+ /* Reflection: Fresnel on and Ray trace on. */
+ do_reflection = true;
+ break;
+ }
+ case 6: {
+ /* Transparency: Refraction on, Reflection: Fresnel off and Ray trace on. */
+ do_reflection = true;
+ do_tranparency = true;
+ break;
+ }
+ case 7: {
+ /* Transparency: Refraction on, Reflection: Fresnel on and Ray trace on. */
+ do_reflection = true;
+ do_tranparency = true;
+ break;
+ }
+ case 8: {
+ /* Reflection on and Ray trace off. */
+ do_reflection = true;
+ break;
+ }
+ case 9: {
+ /* Transparency: Glass on, Reflection: Ray trace off. */
+ do_glass = true;
+ do_reflection = false;
+ do_tranparency = true;
+ break;
+ }
+ default: {
+ std::cerr << "Warning! illum value = " << illum
+ << "is not supported by the Principled-BSDF shader." << std::endl;
+ break;
+ }
+ }
+ /* Approximations for trying to map obj/mtl material model into
+ * Principled BSDF: */
+ /* Specular: average of Ks components. */
+ float specular = (mtl_mat_.Ks[0] + mtl_mat_.Ks[1] + mtl_mat_.Ks[2]) / 3;
+ /* Roughness: map 0..1000 range to 1..0 and apply non-linearity. */
+ float clamped_ns = std::max(0.0f, std::min(1000.0f, mtl_mat_.Ns));
+ float roughness = 1.0f - sqrt(clamped_ns / 1000.0f);
+ /* Metallic: average of Ka components. */
+ float metallic = (mtl_mat_.Ka[0] + mtl_mat_.Ka[1] + mtl_mat_.Ka[2]) / 3;
+ float ior = mtl_mat_.Ni;
+ float alpha = mtl_mat_.d;
+
+ if (specular < 0.0f) {
+ specular = static_cast<float>(do_highlight);
+ }
+ if (mtl_mat_.Ns < 0.0f) {
+ roughness = static_cast<float>(!do_highlight);
+ }
+ if (metallic < 0.0f) {
+ if (do_reflection) {
+ metallic = 1.0f;
+ }
+ }
+ else {
+ metallic = 0.0f;
+ }
+ if (ior < 0) {
+ if (do_tranparency) {
+ ior = 1.0f;
+ }
+ if (do_glass) {
+ ior = 1.5f;
+ }
+ }
+ if (alpha < 0) {
+ if (do_tranparency) {
+ alpha = 1.0f;
+ }
+ }
+ float3 base_color = {std::max(0.0f, mtl_mat_.Kd[0]),
+ std::max(0.0f, mtl_mat_.Kd[1]),
+ std::max(0.0f, mtl_mat_.Kd[2])};
+ float3 emission_color = {std::max(0.0f, mtl_mat_.Ke[0]),
+ std::max(0.0f, mtl_mat_.Ke[1]),
+ std::max(0.0f, mtl_mat_.Ke[2])};
+
+ set_property_of_socket(SOCK_RGBA, "Base Color", {base_color, 3}, bsdf_);
+ set_property_of_socket(SOCK_RGBA, "Emission", {emission_color, 3}, bsdf_);
+ if (mtl_mat_.texture_maps.contains_as(eMTLSyntaxElement::map_Ke)) {
+ set_property_of_socket(SOCK_FLOAT, "Emission Strength", {1.0f}, bsdf_);
+ }
+ set_property_of_socket(SOCK_FLOAT, "Specular", {specular}, bsdf_);
+ set_property_of_socket(SOCK_FLOAT, "Roughness", {roughness}, bsdf_);
+ set_property_of_socket(SOCK_FLOAT, "Metallic", {metallic}, bsdf_);
+ set_property_of_socket(SOCK_FLOAT, "IOR", {ior}, bsdf_);
+ set_property_of_socket(SOCK_FLOAT, "Alpha", {alpha}, bsdf_);
+}
+
+/**
+ * Create image texture, vector and normal mapping nodes from MTL materials and link the
+ * nodes to p-BSDF node.
+ */
+void ShaderNodetreeWrap::add_image_textures(Main *bmain, Material *mat)
+{
+ for (const Map<const eMTLSyntaxElement, tex_map_XX>::Item texture_map :
+ mtl_mat_.texture_maps.items()) {
+ if (texture_map.value.image_path.empty()) {
+ /* No Image texture node of this map type can be added to this material. */
+ continue;
+ }
+
+ bNode *image_texture = add_node_to_tree(SH_NODE_TEX_IMAGE);
+ if (!load_texture_image(bmain, texture_map.value, image_texture)) {
+ /* Image could not be added, so don't add or link further nodes. */
+ continue;
+ }
+
+ /* Add normal map node if needed. */
+ bNode *normal_map = nullptr;
+ if (texture_map.key == eMTLSyntaxElement::map_Bump) {
+ normal_map = add_node_to_tree(SH_NODE_NORMAL_MAP);
+ const float bump = std::max(0.0f, mtl_mat_.map_Bump_strength);
+ set_property_of_socket(SOCK_FLOAT, "Strength", {bump}, normal_map);
+ }
+
+ /* Add UV mapping & coordinate nodes only if needed. */
+ if (texture_map.value.translation != float3(0, 0, 0) ||
+ texture_map.value.scale != float3(1, 1, 1)) {
+ bNode *mapping = add_node_to_tree(SH_NODE_MAPPING);
+ bNode *texture_coordinate = add_node_to_tree(SH_NODE_TEX_COORD);
+ set_property_of_socket(SOCK_VECTOR, "Location", {texture_map.value.translation, 3}, mapping);
+ set_property_of_socket(SOCK_VECTOR, "Scale", {texture_map.value.scale, 3}, mapping);
+
+ link_sockets(texture_coordinate, "UV", mapping, "Vector", 0);
+ link_sockets(mapping, "Vector", image_texture, "Vector", 1);
+ }
+
+ if (normal_map) {
+ link_sockets(image_texture, "Color", normal_map, "Color", 2);
+ link_sockets(normal_map, "Normal", bsdf_, "Normal", 3);
+ }
+ else if (texture_map.key == eMTLSyntaxElement::map_d) {
+ link_sockets(image_texture, "Alpha", bsdf_, texture_map.value.dest_socket_id, 2);
+ mat->blend_method = MA_BM_BLEND;
+ }
+ else {
+ link_sockets(image_texture, "Color", bsdf_, texture_map.value.dest_socket_id, 2);
+ }
+ }
+}
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/obj_import_mtl.hh b/source/blender/io/wavefront_obj/importer/obj_import_mtl.hh
new file mode 100644
index 00000000000..e48cf6e56da
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/obj_import_mtl.hh
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include <array>
+
+#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 "MEM_guardedalloc.h"
+
+#include "obj_export_mtl.hh"
+
+namespace blender::io::obj {
+
+struct UniqueNodetreeDeleter {
+ void operator()(bNodeTree *node)
+ {
+ MEM_freeN(node);
+ }
+};
+
+using unique_nodetree_ptr = std::unique_ptr<bNodeTree, UniqueNodetreeDeleter>;
+
+class ShaderNodetreeWrap {
+ private:
+ /* Node arrangement:
+ * Texture Coordinates -> Mapping -> Image Texture -> (optional) Normal Map -> p-BSDF -> Material
+ * Output. */
+ unique_nodetree_ptr nodetree_;
+ bNode *bsdf_;
+ bNode *shader_output_;
+ const MTLMaterial &mtl_mat_;
+
+ /* List of all locations occupied by nodes. */
+ Vector<std::array<int, 2>> node_locations;
+ const float node_size_{300.f};
+
+ public:
+ ShaderNodetreeWrap(Main *bmain, const MTLMaterial &mtl_mat, Material *mat);
+ ~ShaderNodetreeWrap();
+
+ bNodeTree *get_nodetree();
+
+ private:
+ bNode *add_node_to_tree(const int node_type);
+ std::pair<float, float> set_node_locations(const int pos_x);
+ void link_sockets(bNode *from_node,
+ StringRef from_node_id,
+ bNode *to_node,
+ StringRef to_node_id,
+ const int from_node_pos_x);
+ void set_bsdf_socket_values();
+ void add_image_textures(Main *bmain, Material *mat);
+};
+
+constexpr eMTLSyntaxElement mtl_line_key_str_to_enum(const std::string_view key_str)
+{
+ if (key_str == "map_Kd") {
+ return eMTLSyntaxElement::map_Kd;
+ }
+ if (key_str == "map_Ks") {
+ return eMTLSyntaxElement::map_Ks;
+ }
+ if (key_str == "map_Ns") {
+ return eMTLSyntaxElement::map_Ns;
+ }
+ if (key_str == "map_d") {
+ return eMTLSyntaxElement::map_d;
+ }
+ if (key_str == "refl" || key_str == "map_refl") {
+ return eMTLSyntaxElement::map_refl;
+ }
+ if (key_str == "map_Ke") {
+ return eMTLSyntaxElement::map_Ke;
+ }
+ if (key_str == "map_Bump" || key_str == "bump") {
+ return eMTLSyntaxElement::map_Bump;
+ }
+ return eMTLSyntaxElement::string;
+}
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc
new file mode 100644
index 00000000000..80293c9ebfe
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc
@@ -0,0 +1,99 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#include "BKE_object.h"
+
+#include "BLI_math_vector.h"
+
+#include "DNA_curve_types.h"
+
+#include "importer_mesh_utils.hh"
+#include "obj_import_nurbs.hh"
+#include "obj_import_objects.hh"
+
+namespace blender::io::obj {
+
+Object *CurveFromGeometry::create_curve(Main *bmain, const OBJImportParams &import_params)
+{
+ std::string ob_name{curve_geometry_.geometry_name_};
+ if (ob_name.empty() && !curve_geometry_.nurbs_element_.group_.empty()) {
+ ob_name = curve_geometry_.nurbs_element_.group_;
+ }
+ if (ob_name.empty()) {
+ ob_name = "Untitled";
+ }
+ BLI_assert(!curve_geometry_.nurbs_element_.curv_indices.is_empty());
+
+ Curve *curve = BKE_curve_add(bmain, ob_name.c_str(), OB_CURVES_LEGACY);
+ Object *obj = BKE_object_add_only_object(bmain, OB_CURVES_LEGACY, ob_name.c_str());
+
+ curve->flag = CU_3D;
+ curve->resolu = curve->resolv = 12;
+ /* Only one NURBS spline will be created in the curve object. */
+ curve->actnu = 0;
+
+ Nurb *nurb = static_cast<Nurb *>(MEM_callocN(sizeof(Nurb), "OBJ import NURBS curve"));
+ BLI_addtail(BKE_curve_nurbs_get(curve), nurb);
+ create_nurbs(curve);
+
+ obj->data = curve;
+ transform_object(obj, import_params);
+
+ return obj;
+}
+
+/**
+ * Create a NURBS spline for the Curve converted from Geometry.
+ */
+void CurveFromGeometry::create_nurbs(Curve *curve)
+{
+ const NurbsElement &nurbs_geometry = curve_geometry_.nurbs_element_;
+ Nurb *nurb = static_cast<Nurb *>(curve->nurb.first);
+
+ nurb->type = CU_NURBS;
+ nurb->flag = CU_3D;
+ nurb->next = nurb->prev = nullptr;
+ /* BKE_nurb_points_add later on will update pntsu. If this were set to total curv points,
+ * we get double the total points in viewport. */
+ nurb->pntsu = 0;
+ /* Total points = pntsu * pntsv. */
+ nurb->pntsv = 1;
+ nurb->orderu = nurb->orderv = (nurbs_geometry.degree + 1 > SHRT_MAX) ? 4 :
+ nurbs_geometry.degree + 1;
+ nurb->resolu = nurb->resolv = curve->resolu;
+
+ const int64_t tot_vert{nurbs_geometry.curv_indices.size()};
+
+ BKE_nurb_points_add(nurb, tot_vert);
+ for (int i = 0; i < tot_vert; i++) {
+ BPoint &bpoint = nurb->bp[i];
+ copy_v3_v3(bpoint.vec, global_vertices_.vertices[nurbs_geometry.curv_indices[i]]);
+ bpoint.vec[3] = 1.0f;
+ bpoint.weight = 1.0f;
+ }
+
+ BKE_nurb_knot_calc_u(nurb);
+ bool do_endpoints = false;
+ int deg1 = nurbs_geometry.degree + 1;
+ if (nurbs_geometry.parm.size() >= deg1 * 2) {
+ do_endpoints = true;
+ for (int i = 0; i < deg1; ++i) {
+ if (abs(nurbs_geometry.parm[i]) > 0.0001f) {
+ do_endpoints = false;
+ break;
+ }
+ if (abs(nurbs_geometry.parm[nurbs_geometry.parm.size() - 1 - i] - 1.0f) > 0.0001f) {
+ do_endpoints = false;
+ break;
+ }
+ }
+ }
+ if (do_endpoints) {
+ nurb->flagu = CU_NURB_ENDPOINT;
+ }
+}
+
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/obj_import_nurbs.hh b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.hh
new file mode 100644
index 00000000000..56ac299283d
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.hh
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include "BKE_curve.h"
+
+#include "BLI_utility_mixins.hh"
+
+#include "DNA_curve_types.h"
+
+#include "obj_import_objects.hh"
+
+namespace blender::io::obj {
+
+/**
+ * Make a Blender NURBS Curve block from a Geometry of GEOM_CURVE type.
+ */
+class CurveFromGeometry : NonMovable, NonCopyable {
+ private:
+ const Geometry &curve_geometry_;
+ const GlobalVertices &global_vertices_;
+
+ public:
+ CurveFromGeometry(const Geometry &geometry, const GlobalVertices &global_vertices)
+ : curve_geometry_(geometry), global_vertices_(global_vertices)
+ {
+ }
+
+ Object *create_curve(Main *bmain, const OBJImportParams &import_params);
+
+ private:
+ void create_nurbs(Curve *curve);
+};
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/obj_import_objects.hh b/source/blender/io/wavefront_obj/importer/obj_import_objects.hh
new file mode 100644
index 00000000000..c6ce7d3c434
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/obj_import_objects.hh
@@ -0,0 +1,111 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include "BKE_lib_id.h"
+
+#include "BLI_math_vec_types.hh"
+#include "BLI_vector.hh"
+#include "BLI_vector_set.hh"
+
+#include "DNA_meshdata_types.h"
+#include "DNA_object_types.h"
+
+namespace blender::io::obj {
+
+/**
+ * List of all vertex and UV vertex coordinates in an OBJ file accessible to any
+ * Geometry instance at any time.
+ */
+struct GlobalVertices {
+ Vector<float3> vertices;
+ Vector<float2> uv_vertices;
+ Vector<float3> vertex_normals;
+};
+
+/**
+ * Keeps track of the vertices that belong to other Geometries.
+ * Needed only for MLoop.v and MEdge.v1 which needs vertex indices ranging from (0 to total
+ * vertices in the mesh) as opposed to the other OBJ indices ranging from (0 to total vertices
+ * in the global list).
+ */
+struct VertexIndexOffset {
+ private:
+ int offset_ = 0;
+
+ public:
+ void set_index_offset(const int64_t total_vertices)
+ {
+ offset_ = total_vertices;
+ }
+ int64_t get_index_offset() const
+ {
+ return offset_;
+ }
+};
+
+/**
+ * A face's corner in an OBJ file. In Blender, it translates to a mloop vertex.
+ */
+struct PolyCorner {
+ /* These indices range from zero to total vertices in the OBJ file. */
+ int vert_index;
+ /* -1 is to indicate absence of UV vertices. Only < 0 condition should be checked since
+ * it can be less than -1 too. */
+ int uv_vert_index = -1;
+ int vertex_normal_index = -1;
+};
+
+struct PolyElem {
+ std::string vertex_group;
+ std::string material_name;
+ bool shaded_smooth = false;
+ Vector<PolyCorner> face_corners;
+};
+
+/**
+ * Contains data for one single NURBS curve in the OBJ file.
+ */
+struct NurbsElement {
+ /**
+ * For curves, groups may be used to specify multiple splines in the same curve object.
+ * It may also serve as the name of the curve if not specified explicitly.
+ */
+ std::string group_;
+ int degree = 0;
+ /**
+ * Indices into the global list of vertex coordinates. Must be non-negative.
+ */
+ Vector<int> curv_indices;
+ /* Values in the parm u/v line in a curve definition. */
+ Vector<float> parm;
+};
+
+enum eGeometryType {
+ GEOM_MESH = OB_MESH,
+ GEOM_CURVE = OB_CURVES_LEGACY,
+};
+
+struct Geometry {
+ eGeometryType geom_type_ = GEOM_MESH;
+ std::string geometry_name_;
+ VectorSet<std::string> material_names_;
+ /**
+ * Indices in the vector range from zero to total vertices in a geometry.
+ * Values range from zero to total coordinates in the global list.
+ */
+ Vector<int> vertex_indices_;
+ /** Edges written in the file in addition to (or even without polygon) elements. */
+ Vector<MEdge> edges_;
+ Vector<PolyElem> face_elements_;
+ bool has_vertex_normals_ = false;
+ bool use_vertex_groups_ = false;
+ NurbsElement nurbs_element_;
+ int total_loops_ = 0;
+};
+
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/obj_importer.cc b/source/blender/io/wavefront_obj/importer/obj_importer.cc
new file mode 100644
index 00000000000..631ddcc5cf4
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/obj_importer.cc
@@ -0,0 +1,111 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#include <string>
+
+#include "BLI_map.hh"
+#include "BLI_math_vec_types.hh"
+#include "BLI_set.hh"
+#include "BLI_string_ref.hh"
+
+#include "BKE_layer.h"
+#include "BKE_scene.h"
+
+#include "DEG_depsgraph_build.h"
+
+#include "DNA_collection_types.h"
+
+#include "obj_import_file_reader.hh"
+#include "obj_import_mesh.hh"
+#include "obj_import_nurbs.hh"
+#include "obj_import_objects.hh"
+#include "obj_importer.hh"
+
+namespace blender::io::obj {
+
+/**
+ * Make Blender Mesh, Curve etc from Geometry and add them to the import collection.
+ */
+static void geometry_to_blender_objects(
+ Main *bmain,
+ Scene *scene,
+ ViewLayer *view_layer,
+ const OBJImportParams &import_params,
+ Vector<std::unique_ptr<Geometry>> &all_geometries,
+ const GlobalVertices &global_vertices,
+ const Map<std::string, std::unique_ptr<MTLMaterial>> &materials,
+ Map<std::string, Material *> &created_materials)
+{
+ BKE_view_layer_base_deselect_all(view_layer);
+ LayerCollection *lc = BKE_layer_collection_get_active(view_layer);
+
+ for (const std::unique_ptr<Geometry> &geometry : all_geometries) {
+ Object *obj = nullptr;
+ if (geometry->geom_type_ == GEOM_MESH) {
+ MeshFromGeometry mesh_ob_from_geometry{*geometry, global_vertices};
+ obj = mesh_ob_from_geometry.create_mesh(bmain, materials, created_materials, import_params);
+ }
+ else if (geometry->geom_type_ == GEOM_CURVE) {
+ CurveFromGeometry curve_ob_from_geometry(*geometry, global_vertices);
+ obj = curve_ob_from_geometry.create_curve(bmain, import_params);
+ }
+ if (obj != nullptr) {
+ BKE_collection_object_add(bmain, lc->collection, obj);
+ Base *base = BKE_view_layer_base_find(view_layer, obj);
+ /* TODO: is setting active needed? */
+ BKE_view_layer_base_select_and_set_active(view_layer, base);
+
+ DEG_id_tag_update(&lc->collection->id, ID_RECALC_COPY_ON_WRITE);
+ DEG_id_tag_update_ex(bmain,
+ &obj->id,
+ ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION |
+ ID_RECALC_BASE_FLAGS);
+ }
+ }
+ DEG_id_tag_update(&scene->id, ID_RECALC_BASE_FLAGS);
+ DEG_relations_tag_update(bmain);
+}
+
+void importer_main(bContext *C, const OBJImportParams &import_params)
+{
+ Main *bmain = CTX_data_main(C);
+ Scene *scene = CTX_data_scene(C);
+ ViewLayer *view_layer = CTX_data_view_layer(C);
+ importer_main(bmain, scene, view_layer, import_params);
+ static_cast<void>(CTX_data_ensure_evaluated_depsgraph(C));
+}
+
+void importer_main(Main *bmain,
+ Scene *scene,
+ ViewLayer *view_layer,
+ const OBJImportParams &import_params)
+{
+ /* List of Geometry instances to be parsed from OBJ file. */
+ Vector<std::unique_ptr<Geometry>> all_geometries;
+ /* Container for vertex and UV vertex coordinates. */
+ GlobalVertices global_vertices;
+ /* List of MTLMaterial instances to be parsed from MTL file. */
+ Map<std::string, std::unique_ptr<MTLMaterial>> materials;
+ Map<std::string, Material *> created_materials;
+
+ OBJParser obj_parser{import_params};
+ obj_parser.parse(all_geometries, global_vertices);
+
+ for (StringRef mtl_library : obj_parser.mtl_libraries()) {
+ MTLParser mtl_parser{mtl_library, import_params.filepath};
+ mtl_parser.parse_and_store(materials);
+ }
+
+ geometry_to_blender_objects(bmain,
+ scene,
+ view_layer,
+ import_params,
+ all_geometries,
+ global_vertices,
+ materials,
+ created_materials);
+}
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/obj_importer.hh b/source/blender/io/wavefront_obj/importer/obj_importer.hh
new file mode 100644
index 00000000000..fd83117ebc6
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/obj_importer.hh
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/** \file
+ * \ingroup obj
+ */
+
+#pragma once
+
+#include "IO_wavefront_obj.h"
+
+namespace blender::io::obj {
+
+/* Main import function used from within Blender. */
+void importer_main(bContext *C, const OBJImportParams &import_params);
+
+/* Used from tests, where full bContext does not exist. */
+void importer_main(Main *bmain,
+ Scene *scene,
+ ViewLayer *view_layer,
+ const OBJImportParams &import_params);
+
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/parser_string_utils.cc b/source/blender/io/wavefront_obj/importer/parser_string_utils.cc
new file mode 100644
index 00000000000..3e45529c698
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/parser_string_utils.cc
@@ -0,0 +1,209 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <fstream>
+#include <iostream>
+#include <sstream>
+
+#include "BLI_math_vec_types.hh"
+#include "BLI_span.hh"
+#include "BLI_string_ref.hh"
+#include "BLI_vector.hh"
+
+#include "parser_string_utils.hh"
+
+/* Note: these OBJ parser helper functions are planned to get fairly large
+ * changes "soon", so don't read too much into current implementation... */
+
+namespace blender::io::obj {
+using std::string;
+
+/**
+ * Store multiple lines separated by an escaped newline character: `\\n`.
+ * Use this before doing any parse operations on the read string.
+ */
+void read_next_line(std::fstream &file, string &r_line)
+{
+ std::string new_line;
+ while (file.good() && !r_line.empty() && r_line.back() == '\\') {
+ new_line.clear();
+ const bool ok = static_cast<bool>(std::getline(file, new_line));
+ /* Remove the last backslash character. */
+ r_line.pop_back();
+ r_line.append(new_line);
+ if (!ok || new_line.empty()) {
+ return;
+ }
+ }
+}
+
+/**
+ * Split a line string into the first word (key) and the rest of the line.
+ * Also remove leading & trailing spaces as well as `\r` carriage return
+ * character if present.
+ */
+void split_line_key_rest(const StringRef line, StringRef &r_line_key, StringRef &r_rest_line)
+{
+ if (line.is_empty()) {
+ return;
+ }
+
+ const int64_t pos_split{line.find_first_of(' ')};
+ if (pos_split == StringRef::not_found) {
+ /* Use the first character if no space is found in the line. It's usually a comment like:
+ * #This is a comment. */
+ r_line_key = line.substr(0, 1);
+ }
+ else {
+ r_line_key = line.substr(0, pos_split);
+ }
+
+ /* Eat the delimiter also using "+ 1". */
+ r_rest_line = line.drop_prefix(r_line_key.size() + 1);
+ if (r_rest_line.is_empty()) {
+ return;
+ }
+
+ /* Remove any leading spaces, trailing spaces & \r character, if any. */
+ const int64_t leading_space{r_rest_line.find_first_not_of(' ')};
+ if (leading_space != StringRef::not_found) {
+ r_rest_line = r_rest_line.drop_prefix(leading_space);
+ }
+
+ /* Another way is to do a test run before the actual parsing to find the newline
+ * character and use it in the getline. */
+ const int64_t carriage_return{r_rest_line.find_first_of('\r')};
+ if (carriage_return != StringRef::not_found) {
+ r_rest_line = r_rest_line.substr(0, carriage_return + 1);
+ }
+
+ const int64_t trailing_space{r_rest_line.find_last_not_of(' ')};
+ if (trailing_space != StringRef::not_found) {
+ /* The position is of a character that is not ' ', so count of characters is position + 1. */
+ r_rest_line = r_rest_line.substr(0, trailing_space + 1);
+ }
+}
+
+/**
+ * Split the given string by the delimiter and fill the given vector.
+ * If an intermediate string is empty, or space or null character, it is not appended to the
+ * vector.
+ */
+void split_by_char(StringRef in_string, const char delimiter, Vector<StringRef> &r_out_list)
+{
+ r_out_list.clear();
+
+ while (!in_string.is_empty()) {
+ const int64_t pos_delim{in_string.find_first_of(delimiter)};
+ const int64_t word_len = pos_delim == StringRef::not_found ? in_string.size() : pos_delim;
+
+ StringRef word{in_string.data(), word_len};
+ if (!word.is_empty() && !(word == " " && !(word[0] == '\0'))) {
+ r_out_list.append(word);
+ }
+ if (pos_delim == StringRef::not_found) {
+ return;
+ }
+ /* Skip the word already stored. */
+ in_string = in_string.drop_prefix(word_len);
+ /* Skip all delimiters. */
+ const int64_t pos_non_delim = in_string.find_first_not_of(delimiter);
+ if (pos_non_delim == StringRef::not_found) {
+ return;
+ }
+ in_string = in_string.drop_prefix(std::min(pos_non_delim, in_string.size()));
+ }
+}
+
+/**
+ * Convert the given string to float and assign it to the destination value.
+ *
+ * If the string cannot be converted to a float, the fallback value is used.
+ */
+void copy_string_to_float(StringRef src, const float fallback_value, float &r_dst)
+{
+ try {
+ r_dst = std::stof(string(src));
+ }
+ catch (const std::invalid_argument &inv_arg) {
+ std::cerr << "Bad conversion to float:'" << inv_arg.what() << "':'" << src << "'" << std::endl;
+ r_dst = fallback_value;
+ }
+ catch (const std::out_of_range &out_of_range) {
+ std::cerr << "Out of range for float:'" << out_of_range.what() << ":'" << src << "'"
+ << std::endl;
+ r_dst = fallback_value;
+ }
+}
+
+/**
+ * Convert all members of the Span of strings to floats and assign them to the float
+ * array members. Usually used for values like coordinates.
+ *
+ * If a string cannot be converted to a float, the fallback value is used.
+ */
+void copy_string_to_float(Span<StringRef> src,
+ const float fallback_value,
+ MutableSpan<float> r_dst)
+{
+ for (int i = 0; i < r_dst.size(); ++i) {
+ if (i < src.size()) {
+ copy_string_to_float(src[i], fallback_value, r_dst[i]);
+ }
+ else {
+ r_dst[i] = fallback_value;
+ }
+ }
+}
+
+/**
+ * Convert the given string to int and assign it to the destination value.
+ *
+ * If the string cannot be converted to an integer, the fallback value is used.
+ */
+void copy_string_to_int(StringRef src, const int fallback_value, int &r_dst)
+{
+ try {
+ r_dst = std::stoi(string(src));
+ }
+ catch (const std::invalid_argument &inv_arg) {
+ std::cerr << "Bad conversion to int:'" << inv_arg.what() << "':'" << src << "'" << std::endl;
+ r_dst = fallback_value;
+ }
+ catch (const std::out_of_range &out_of_range) {
+ std::cerr << "Out of range for int:'" << out_of_range.what() << ":'" << src << "'"
+ << std::endl;
+ r_dst = fallback_value;
+ }
+}
+
+/**
+ * Convert the given strings to ints and fill the destination int buffer.
+ *
+ * If a string cannot be converted to an integer, the fallback value is used.
+ */
+void copy_string_to_int(Span<StringRef> src, const int fallback_value, MutableSpan<int> r_dst)
+{
+ for (int i = 0; i < r_dst.size(); ++i) {
+ if (i < src.size()) {
+ copy_string_to_int(src[i], fallback_value, r_dst[i]);
+ }
+ else {
+ r_dst[i] = fallback_value;
+ }
+ }
+}
+
+std::string replace_all_occurences(StringRef original, StringRef to_remove, StringRef to_add)
+{
+ std::string clean{original};
+ while (true) {
+ const std::string::size_type pos = clean.find(to_remove);
+ if (pos == std::string::npos) {
+ break;
+ }
+ clean.replace(pos, to_add.size(), to_add);
+ }
+ return clean;
+}
+
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/importer/parser_string_utils.hh b/source/blender/io/wavefront_obj/importer/parser_string_utils.hh
new file mode 100644
index 00000000000..09540721604
--- /dev/null
+++ b/source/blender/io/wavefront_obj/importer/parser_string_utils.hh
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+namespace blender::io::obj {
+
+/* Note: these OBJ parser helper functions are planned to get fairly large
+ * changes "soon", so don't read too much into current implementation... */
+
+void read_next_line(std::fstream &file, std::string &r_line);
+void split_line_key_rest(StringRef line, StringRef &r_line_key, StringRef &r_rest_line);
+void split_by_char(StringRef in_string, const char delimiter, Vector<StringRef> &r_out_list);
+void copy_string_to_float(StringRef src, const float fallback_value, float &r_dst);
+void copy_string_to_float(Span<StringRef> src,
+ const float fallback_value,
+ MutableSpan<float> r_dst);
+void copy_string_to_int(StringRef src, const int fallback_value, int &r_dst);
+void copy_string_to_int(Span<StringRef> src, const int fallback_value, MutableSpan<int> r_dst);
+std::string replace_all_occurences(StringRef original, StringRef to_remove, StringRef to_add);
+
+} // namespace blender::io::obj
diff --git a/source/blender/io/wavefront_obj/tests/obj_importer_tests.cc b/source/blender/io/wavefront_obj/tests/obj_importer_tests.cc
new file mode 100644
index 00000000000..ddcfd6176f0
--- /dev/null
+++ b/source/blender/io/wavefront_obj/tests/obj_importer_tests.cc
@@ -0,0 +1,491 @@
+/* SPDX-License-Identifier: Apache-2.0 */
+
+#include <gtest/gtest.h>
+
+#include "testing/testing.h"
+#include "tests/blendfile_loading_base_test.h"
+
+#include "BKE_curve.h"
+#include "BKE_customdata.h"
+#include "BKE_main.h"
+#include "BKE_object.h"
+#include "BKE_scene.h"
+
+#include "BLI_listbase.h"
+#include "BLI_math_base.h"
+#include "BLI_math_vec_types.hh"
+
+#include "BLO_readfile.h"
+
+#include "DEG_depsgraph.h"
+#include "DEG_depsgraph_query.h"
+
+#include "DNA_curve_types.h"
+#include "DNA_mesh_types.h"
+#include "DNA_meshdata_types.h"
+#include "DNA_scene_types.h"
+
+#include "MEM_guardedalloc.h"
+
+#include "obj_importer.hh"
+
+namespace blender::io::obj {
+
+struct Expectation {
+ std::string name;
+ short type; /* OB_MESH, ... */
+ int totvert, mesh_totedge_or_curve_endp, mesh_totpoly_or_curve_order,
+ mesh_totloop_or_curve_cyclic;
+ float3 vert_first, vert_last;
+ float3 normal_first;
+ float2 uv_first;
+};
+
+class obj_importer_test : public BlendfileLoadingBaseTest {
+ public:
+ void import_and_check(const char *path,
+ const Expectation *expect,
+ size_t expect_count,
+ int expect_mat_count)
+ {
+ if (!blendfile_load("io_tests/blend_geometry/all_quads.blend")) {
+ ADD_FAILURE();
+ return;
+ }
+
+ OBJImportParams params;
+ params.clamp_size = 0;
+ params.forward_axis = OBJ_AXIS_NEGATIVE_Z_FORWARD;
+ params.up_axis = OBJ_AXIS_Y_UP;
+
+ std::string obj_path = blender::tests::flags_test_asset_dir() + "/io_tests/obj/" + path;
+ strncpy(params.filepath, obj_path.c_str(), FILE_MAX - 1);
+ importer_main(bfile->main, bfile->curscene, bfile->cur_view_layer, params);
+
+ depsgraph_create(DAG_EVAL_VIEWPORT);
+
+ 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;
+ size_t object_index = 0;
+ DEG_OBJECT_ITER_BEGIN (depsgraph, object, deg_objects_visibility_flags) {
+ if (object_index >= expect_count) {
+ ADD_FAILURE();
+ break;
+ }
+ const Expectation &exp = expect[object_index];
+ ASSERT_STREQ(object->id.name, exp.name.c_str());
+ EXPECT_EQ(object->type, exp.type);
+ EXPECT_V3_NEAR(object->loc, float3(0, 0, 0), 0.0001f);
+ if (strcmp(object->id.name, "OBCube") != 0) {
+ EXPECT_V3_NEAR(object->rot, float3(M_PI_2, 0, 0), 0.0001f);
+ }
+ EXPECT_V3_NEAR(object->scale, float3(1, 1, 1), 0.0001f);
+ if (object->type == OB_MESH || object->type == OB_SURF) {
+ Mesh *mesh = BKE_object_get_evaluated_mesh(object);
+ EXPECT_EQ(mesh->totvert, exp.totvert);
+ EXPECT_EQ(mesh->totedge, exp.mesh_totedge_or_curve_endp);
+ EXPECT_EQ(mesh->totpoly, exp.mesh_totpoly_or_curve_order);
+ EXPECT_EQ(mesh->totloop, exp.mesh_totloop_or_curve_cyclic);
+ EXPECT_V3_NEAR(mesh->mvert[0].co, exp.vert_first, 0.0001f);
+ EXPECT_V3_NEAR(mesh->mvert[mesh->totvert - 1].co, exp.vert_last, 0.0001f);
+ const float3 *lnors = (const float3 *)(CustomData_get_layer(&mesh->ldata, CD_NORMAL));
+ float3 normal_first = lnors != nullptr ? lnors[0] : float3(0, 0, 0);
+ EXPECT_V3_NEAR(normal_first, exp.normal_first, 0.0001f);
+ const MLoopUV *mloopuv = static_cast<const MLoopUV *>(
+ CustomData_get_layer(&mesh->ldata, CD_MLOOPUV));
+ float2 uv_first = mloopuv ? float2(mloopuv->uv) : float2(0, 0);
+ EXPECT_V2_NEAR(uv_first, exp.uv_first, 0.0001f);
+ }
+ if (object->type == OB_CURVES_LEGACY) {
+ Curve *curve = static_cast<Curve *>(DEG_get_evaluated_object(depsgraph, object)->data);
+ int numVerts;
+ float(*vertexCos)[3] = BKE_curve_nurbs_vert_coords_alloc(&curve->nurb, &numVerts);
+ EXPECT_EQ(numVerts, exp.totvert);
+ EXPECT_V3_NEAR(vertexCos[0], exp.vert_first, 0.0001f);
+ EXPECT_V3_NEAR(vertexCos[numVerts - 1], exp.vert_last, 0.0001f);
+ MEM_freeN(vertexCos);
+ const Nurb *nurb = static_cast<const Nurb *>(BLI_findlink(&curve->nurb, 0));
+ int endpoint = (nurb->flagu & CU_NURB_ENDPOINT) ? 1 : 0;
+ EXPECT_EQ(nurb->orderu, exp.mesh_totpoly_or_curve_order);
+ EXPECT_EQ(endpoint, exp.mesh_totedge_or_curve_endp);
+ // Cyclic flag is not set by the importer yet
+ // int cyclic = (nurb->flagu & CU_NURB_CYCLIC) ? 1 : 0;
+ // EXPECT_EQ(cyclic, exp.mesh_totloop_or_curve_cyclic);
+ }
+ ++object_index;
+ }
+ DEG_OBJECT_ITER_END;
+ EXPECT_EQ(object_index, expect_count);
+
+ /* Count number of materials. */
+ int mat_count = 0;
+ LISTBASE_FOREACH (ID *, id, &bfile->main->materials) {
+ ++mat_count;
+ }
+ EXPECT_EQ(mat_count, expect_mat_count);
+ }
+};
+
+TEST_F(obj_importer_test, import_cube)
+{
+ Expectation expect[] = {
+ {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
+ {"OBNew object",
+ OB_MESH,
+ 8,
+ 12,
+ 6,
+ 24,
+ float3(-1, -1, 1),
+ float3(1, -1, -1),
+ float3(-0.57735f, 0.57735f, -0.57735f)},
+ };
+ import_and_check("cube.obj", expect, std::size(expect), 1);
+}
+
+TEST_F(obj_importer_test, import_suzanne_all_data)
+{
+ Expectation expect[] = {
+ {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
+ {"OBMonkey",
+ OB_MESH,
+ 505,
+ 1005,
+ 500,
+ 1968,
+ float3(-0.4375f, 0.164062f, 0.765625f),
+ float3(0.4375f, 0.164062f, 0.765625f),
+ float3(-0.6040f, -0.5102f, 0.6122f),
+ float2(0.692094f, 0.40191f)},
+ };
+ import_and_check("suzanne_all_data.obj", expect, std::size(expect), 0);
+}
+
+TEST_F(obj_importer_test, import_nurbs)
+{
+ Expectation expect[] = {
+ {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
+ {"OBNew object",
+ OB_CURVES_LEGACY,
+ 12,
+ 0,
+ 4,
+ 1,
+ float3(0.260472f, -1.477212f, -0.866025f),
+ float3(-1.5f, 2.598076f, 0)},
+ };
+ import_and_check("nurbs.obj", expect, std::size(expect), 0);
+}
+
+TEST_F(obj_importer_test, import_nurbs_curves)
+{
+ Expectation expect[] = {
+ {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
+ {"OBNew object", OB_CURVES_LEGACY, 4, 0, 4, 0, float3(2, -2, 0), float3(-2, -2, 0)},
+ {"OBNurbsCurveDiffWeights",
+ OB_CURVES_LEGACY,
+ 4,
+ 0,
+ 4,
+ 0,
+ float3(6, -2, 0),
+ float3(2, -2, 0)},
+ {"OBNurbsCurveCyclic", OB_CURVES_LEGACY, 7, 0, 4, 1, float3(-2, -2, 0), float3(-6, 2, 0)},
+ {"OBNurbsCurveEndpoint",
+ OB_CURVES_LEGACY,
+ 4,
+ 1,
+ 4,
+ 0,
+ float3(-6, -2, 0),
+ float3(-10, -2, 0)},
+ {"OBCurveDeg3", OB_CURVES_LEGACY, 4, 0, 3, 0, float3(10, -2, 0), float3(6, -2, 0)},
+ };
+ import_and_check("nurbs_curves.obj", expect, std::size(expect), 0);
+}
+
+TEST_F(obj_importer_test, import_nurbs_cyclic)
+{
+ Expectation expect[] = {
+ {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
+ {"OBNew object",
+ OB_CURVES_LEGACY,
+ 31,
+ 0,
+ 4,
+ 1,
+ float3(2.591002f, 0, -0.794829f),
+ float3(3.280729f, 0, 3.043217f)},
+ };
+ import_and_check("nurbs_cyclic.obj", expect, std::size(expect), 0);
+}
+
+TEST_F(obj_importer_test, import_nurbs_manual)
+{
+ Expectation expect[] = {
+ {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
+ {"OBCurve_Uniform_Parm", OB_CURVES_LEGACY, 5, 0, 4, 0, float3(-2, 0, 2), float3(-2, 0, 2)},
+ {"OBCurve_NonUniform_Parm",
+ OB_CURVES_LEGACY,
+ 5,
+ 0,
+ 4,
+ 0,
+ float3(-2, 0, 2),
+ float3(-2, 0, 2)},
+ {"OBCurve_Endpoints", OB_CURVES_LEGACY, 5, 1, 4, 0, float3(-2, 0, 2), float3(-2, 0, 2)},
+ {"OBCurve_Cyclic", OB_CURVES_LEGACY, 7, 0, 4, 1, float3(-2, 0, 2), float3(2, 0, -2)},
+ };
+ import_and_check("nurbs_manual.obj", expect, std::size(expect), 0);
+}
+
+TEST_F(obj_importer_test, import_nurbs_mesh)
+{
+ Expectation expect[] = {
+ {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
+ {"OBTorus Knot",
+ OB_MESH,
+ 108,
+ 108,
+ 0,
+ 0,
+ float3(0.438725f, 1.070313f, 0.433013f),
+ float3(0.625557f, 1.040691f, 0.460328f)},
+ };
+ import_and_check("nurbs_mesh.obj", expect, std::size(expect), 0);
+}
+
+TEST_F(obj_importer_test, import_materials)
+{
+ Expectation expect[] = {
+ {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
+ {"OBNew object", OB_MESH, 8, 12, 6, 24, float3(-1, -1, 1), float3(1, -1, -1)},
+ };
+ import_and_check("materials.obj", expect, std::size(expect), 4);
+}
+
+TEST_F(obj_importer_test, import_faces_invalid_or_with_holes)
+{
+ Expectation expect[] = {
+ {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
+ {"OBFaceWithHole_BecomesTwoFacesFormingAHole",
+ OB_MESH,
+ 8,
+ 10,
+ 2,
+ 12,
+ float3(-2, 0, -2),
+ float3(1, 0, -1)},
+ {"OBFaceQuadDupSomeVerts_BecomesOneQuadUsing4Verts",
+ OB_MESH,
+ 8,
+ 4,
+ 1,
+ 4,
+ float3(3, 0, -2),
+ float3(6, 0, -1)},
+ {"OBFaceTriDupVert_Becomes1Tri", OB_MESH, 8, 3, 1, 3, float3(-2, 0, 3), float3(1, 0, 4)},
+ {"OBFaceAllVertsDup_BecomesOneOverlappingFaceUsingAllVerts",
+ OB_MESH,
+ 8,
+ 8,
+ 1,
+ 8,
+ float3(3, 0, 3),
+ float3(6, 0, 4)},
+ {"OBFaceAllVerts_BecomesOneOverlappingFaceUsingAllVerts",
+ OB_MESH,
+ 8,
+ 8,
+ 1,
+ 8,
+ float3(8, 0, -2),
+ float3(11, 0, -1)},
+ {"OBFaceJustTwoVerts_IsSkipped", OB_MESH, 8, 0, 0, 0, float3(8, 0, 3), float3(11, 0, 4)},
+ };
+ import_and_check("faces_invalid_or_with_holes.obj", expect, std::size(expect), 0);
+}
+
+TEST_F(obj_importer_test, import_invalid_indices)
+{
+ Expectation expect[] = {
+ {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
+ {"OBQuad",
+ OB_MESH,
+ 4,
+ 3,
+ 1,
+ 3,
+ float3(-2, 0, -2),
+ float3(2, 0, -2),
+ float3(0, 1, 0),
+ float2(0.5f, 0.25f)},
+ };
+ import_and_check("invalid_indices.obj", expect, std::size(expect), 0);
+}
+
+TEST_F(obj_importer_test, import_invalid_syntax)
+{
+ Expectation expect[] = {
+ {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
+ {"OBObjectWithAReallyLongNameToCheckHowImportHandlesNamesThatAreLon",
+ OB_MESH,
+ 10, /* Note: right now parses some invalid obj syntax as valid vertices. */
+ 3,
+ 1,
+ 3,
+ float3(1, 2, 3),
+ float3(10, 11, 12),
+ float3(0.4082f, -0.8165f, 0.4082f),
+ float2(0, 0)},
+ };
+ import_and_check("invalid_syntax.obj", expect, std::size(expect), 0);
+}
+
+TEST_F(obj_importer_test, import_all_objects)
+{
+ Expectation expect[] = {
+ {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)},
+ /* .obj file has empty EmptyText and EmptyMesh objects; these are ignored and skipped */
+ {"OBSurfPatch",
+ OB_MESH,
+ 256,
+ 480,
+ 225,
+ 900,
+ float3(12.5f, -2.5f, 0.694444f),
+ float3(13.5f, -1.5f, 0.694444f),
+ float3(-0.3246f, -0.3531f, 0.8775f),
+ float2(0, 0.066667f)},
+ {"OBSurfSphere",
+ OB_MESH,
+ 640,
+ 1248,
+ 608,
+ 2432,
+ float3(11, -2, -1),
+ float3(11, -2, 1),
+ float3(-0.0541f, -0.0541f, -0.9971f),
+ float2(0, 1)},
+ {"OBSmoothCube",
+ OB_MESH,
+ 8,
+ 13,
+ 7,
+ 26,
+ float3(4, 1, -1),
+ float3(2, 1, 1),
+ float3(0.5774f, 0.5773f, 0.5774f)},
+ {"OBMaterialCube",
+ OB_MESH,
+ 8,
+ 13,
+ 7,
+ 26,
+ float3(28, 1, -1),
+ float3(26, 1, 1),
+ float3(-1, 0, 0)},
+ {"OBSubSurfCube",
+ OB_MESH,
+ 106,
+ 208,
+ 104,
+ 416,
+ float3(24.444445f, 0.444444f, -0.666667f),
+ float3(23.790743f, 0.490725f, -0.816819f),
+ float3(0.1697f, 0.1697f, 0.9708f)},
+ {"OBParticleCube",
+ OB_MESH,
+ 8,
+ 13,
+ 7,
+ 26,
+ float3(22, 1, -1),
+ float3(20, 1, 1),
+ float3(0, 0, 1)},
+ {"OBShapeKeyCube",
+ OB_MESH,
+ 8,
+ 13,
+ 7,
+ 26,
+ float3(19, 1, -1),
+ float3(17, 1, 1),
+ float3(-0.4082f, -0.4082f, 0.8165f)},
+ {"OBUVImageCube",
+ OB_MESH,
+ 8,
+ 13,
+ 7,
+ 26,
+ float3(10, 1, -1),
+ float3(8, 1, 1),
+ float3(0, 0, 1),
+ float2(0.654526f, 0.579873f)},
+ {"OBVGroupCube",
+ OB_MESH,
+ 8,
+ 13,
+ 7,
+ 26,
+ float3(16, 1, -1),
+ float3(14, 1, 1),
+ float3(0, 0, 1)},
+ {"OBVColCube", OB_MESH, 8, 13, 7, 26, float3(13, 1, -1), float3(11, 1, 1), float3(0, 0, 1)},
+ {"OBUVCube",
+ OB_MESH,
+ 8,
+ 13,
+ 7,
+ 26,
+ float3(7, 1, -1),
+ float3(5, 1, 1),
+ float3(0, 0, 1),
+ float2(0.654526f, 0.579873f)},
+ {"OBSurface",
+ OB_MESH,
+ 256,
+ 480,
+ 224,
+ 896,
+ float3(7.292893f, -2.707107f, -1),
+ float3(7.525872f, -2.883338f, 1),
+ float3(-0.7071f, -0.7071f, 0),
+ float2(0, 0.142857f)},
+ {"OBText",
+ OB_MESH,
+ 177,
+ 345,
+ 171,
+ 513,
+ float3(1.75f, -9.458f, 0),
+ float3(0.587f, -9.406f, 0),
+ float3(0, 0, 1),
+ float2(0.017544f, 0)},
+ {"OBSurfTorus.001",
+ OB_MESH,
+ 1024,
+ 2048,
+ 1024,
+ 4096,
+ float3(5.34467f, -2.65533f, -0.176777f),
+ float3(5.232792f, -2.411795f, -0.220835f),
+ float3(-0.5042f, -0.5042f, -0.7011f),
+ float2(0, 1)},
+ {"OBNurbsCircle",
+ OB_MESH,
+ 96,
+ 96,
+ 0,
+ 0,
+ float3(3.292893f, -2.707107f, 0),
+ float3(3.369084f, -2.77607f, 0)},
+ {"OBBezierCurve", OB_MESH, 13, 12, 0, 0, float3(-1, -2, 0), float3(1, -2, 0)},
+ {"OBBlankCube", OB_MESH, 8, 13, 7, 26, float3(1, 1, -1), float3(-1, 1, 1), float3(0, 0, 1)},
+ };
+ import_and_check("all_objects.obj", expect, std::size(expect), 7);
+}
+
+} // namespace blender::io::obj