diff options
Diffstat (limited to 'source/blender/io')
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 |