diff options
Diffstat (limited to 'source/blender/io/wavefront_obj')
25 files changed, 906 insertions, 288 deletions
diff --git a/source/blender/io/wavefront_obj/CMakeLists.txt b/source/blender/io/wavefront_obj/CMakeLists.txt index e0fe7ed4992..f7958ef4ec6 100644 --- a/source/blender/io/wavefront_obj/CMakeLists.txt +++ b/source/blender/io/wavefront_obj/CMakeLists.txt @@ -15,6 +15,7 @@ set(INC ../../makesrna ../../nodes ../../windowmanager + ../../../../extern/fast_float ../../../../extern/fmtlib/include ../../../../intern/guardedalloc ) @@ -35,6 +36,7 @@ set(SRC importer/obj_import_mesh.cc importer/obj_import_mtl.cc importer/obj_import_nurbs.cc + importer/obj_import_string_utils.cc importer/obj_importer.cc IO_wavefront_obj.h @@ -50,6 +52,7 @@ set(SRC importer/obj_import_mtl.hh importer/obj_import_nurbs.hh importer/obj_import_objects.hh + importer/obj_import_string_utils.hh importer/obj_importer.hh ) @@ -69,6 +72,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_import_string_utils_tests.cc tests/obj_importer_tests.cc tests/obj_mtl_parser_tests.cc diff --git a/source/blender/io/wavefront_obj/IO_wavefront_obj.h b/source/blender/io/wavefront_obj/IO_wavefront_obj.h index 8b71ec750c0..b4a00deb99c 100644 --- a/source/blender/io/wavefront_obj/IO_wavefront_obj.h +++ b/source/blender/io/wavefront_obj/IO_wavefront_obj.h @@ -9,34 +9,20 @@ #include "BKE_context.h" #include "BLI_path_util.h" #include "DEG_depsgraph.h" +#include "IO_orientation.h" +#include "IO_path_util_types.h" #ifdef __cplusplus extern "C" { #endif -typedef enum { - OBJ_AXIS_X_UP = 0, - OBJ_AXIS_Y_UP = 1, - OBJ_AXIS_Z_UP = 2, - OBJ_AXIS_NEGATIVE_X_UP = 3, - OBJ_AXIS_NEGATIVE_Y_UP = 4, - OBJ_AXIS_NEGATIVE_Z_UP = 5, -} eTransformAxisUp; - -typedef enum { - OBJ_AXIS_X_FORWARD = 0, - OBJ_AXIS_Y_FORWARD = 1, - OBJ_AXIS_Z_FORWARD = 2, - OBJ_AXIS_NEGATIVE_X_FORWARD = 3, - OBJ_AXIS_NEGATIVE_Y_FORWARD = 4, - OBJ_AXIS_NEGATIVE_Z_FORWARD = 5, -} eTransformAxisForward; - static const int TOTAL_AXES = 3; struct OBJExportParams { /** Full path to the destination .OBJ file. */ char filepath[FILE_MAX]; + /** Pretend that destination file folder is this, if non-empty. Used only for tests. */ + char file_base_for_tests[FILE_MAX]; /** Full path to current blender file (used for comments in output). */ const char *blen_filepath; @@ -49,8 +35,8 @@ struct OBJExportParams { int end_frame; /* Geometry Transform options. */ - eTransformAxisForward forward_axis; - eTransformAxisUp up_axis; + eIOAxis forward_axis; + eIOAxis up_axis; float scaling_factor; /* File Write Options. */ @@ -59,9 +45,11 @@ struct OBJExportParams { eEvaluationMode export_eval_mode; bool export_uv; bool export_normals; + bool export_colors; bool export_materials; bool export_triangulated_mesh; bool export_curves_as_nurbs; + ePathReferenceMode path_mode; /* Grouping options. */ bool export_object_groups; @@ -82,18 +70,21 @@ struct OBJImportParams { char filepath[FILE_MAX]; /** Value 0 disables clamping. */ float clamp_size; - eTransformAxisForward forward_axis; - eTransformAxisUp up_axis; + eIOAxis forward_axis; + eIOAxis up_axis; + bool import_vertex_groups; bool validate_meshes; }; /** - * Time the full import process. + * Perform the full import process. + * Import also changes the selection & the active object; callers + * need to update the UI bits if needed. */ void OBJ_import(bContext *C, const struct OBJImportParams *import_params); /** - * C-interface for the exporter. + * Perform the full export process. */ void OBJ_export(bContext *C, const struct OBJExportParams *export_params); diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc index 194583e71fe..cb95c561547 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc @@ -8,11 +8,15 @@ #include <cstdio> #include "BKE_blender_version.h" +#include "BKE_geometry_set.hh" +#include "BLI_color.hh" #include "BLI_enumerable_thread_specific.hh" #include "BLI_path_util.h" #include "BLI_task.hh" +#include "IO_path_util.hh" + #include "obj_export_mesh.hh" #include "obj_export_mtl.hh" #include "obj_export_nurbs.hh" @@ -239,13 +243,38 @@ void obj_parallel_chunked_output(FormatHandler<eFileType::OBJ> &fh, } void OBJWriter::write_vertex_coords(FormatHandler<eFileType::OBJ> &fh, - const OBJMesh &obj_mesh_data) const + const OBJMesh &obj_mesh_data, + bool write_colors) const { const int tot_count = obj_mesh_data.tot_vertices(); - obj_parallel_chunked_output(fh, tot_count, [&](FormatHandler<eFileType::OBJ> &buf, int i) { - float3 vertex = obj_mesh_data.calc_vertex_coords(i, export_params_.scaling_factor); - buf.write<eOBJSyntaxElement::vertex_coords>(vertex[0], vertex[1], vertex[2]); - }); + + Mesh *mesh = obj_mesh_data.get_mesh(); + CustomDataLayer *colors_layer = nullptr; + if (write_colors) { + colors_layer = BKE_id_attributes_active_color_get(&mesh->id); + } + if (write_colors && (colors_layer != nullptr)) { + MeshComponent component; + component.replace(mesh, GeometryOwnershipType::ReadOnly); + VArray<ColorGeometry4f> attribute = component.attribute_get_for_read<ColorGeometry4f>( + colors_layer->name, ATTR_DOMAIN_POINT, {0.0f, 0.0f, 0.0f, 0.0f}); + + BLI_assert(tot_count == attribute.size()); + obj_parallel_chunked_output(fh, tot_count, [&](FormatHandler<eFileType::OBJ> &buf, int i) { + float3 vertex = obj_mesh_data.calc_vertex_coords(i, export_params_.scaling_factor); + ColorGeometry4f linear = attribute.get(i); + float srgb[3]; + linearrgb_to_srgb_v3_v3(srgb, linear); + buf.write<eOBJSyntaxElement::vertex_coords_color>( + vertex[0], vertex[1], vertex[2], srgb[0], srgb[1], srgb[2]); + }); + } + else { + obj_parallel_chunked_output(fh, tot_count, [&](FormatHandler<eFileType::OBJ> &buf, int i) { + float3 vertex = obj_mesh_data.calc_vertex_coords(i, export_params_.scaling_factor); + buf.write<eOBJSyntaxElement::vertex_coords>(vertex[0], vertex[1], vertex[2]); + }); + } } void OBJWriter::write_uv_coords(FormatHandler<eFileType::OBJ> &fh, OBJMesh &r_obj_mesh_data) const @@ -383,7 +412,7 @@ void OBJWriter::write_edges_indices(FormatHandler<eFileType::OBJ> &fh, const IndexOffsets &offsets, const OBJMesh &obj_mesh_data) const { - /* Note: ensure_mesh_edges should be called before. */ + /* NOTE: ensure_mesh_edges should be called before. */ const int tot_edges = obj_mesh_data.tot_edges(); for (int edge_index = 0; edge_index < tot_edges; edge_index++) { const std::optional<std::array<int, 2>> vertex_indices = @@ -530,7 +559,11 @@ void MTLWriter::write_bsdf_properties(const MTLMaterial &mtl_material) void MTLWriter::write_texture_map( const MTLMaterial &mtl_material, - const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map) + const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map, + const char *blen_filedir, + const char *dest_dir, + ePathReferenceMode path_mode, + Set<std::pair<std::string, std::string>> ©_set) { std::string options; /* Option strings should have their own leading spaces. */ @@ -546,7 +579,11 @@ void MTLWriter::write_texture_map( #define SYNTAX_DISPATCH(eMTLSyntaxElement) \ if (texture_map.key == eMTLSyntaxElement) { \ - fmt_handler_.write<eMTLSyntaxElement>(options, texture_map.value.image_path); \ + std::string path = path_reference( \ + texture_map.value.image_path.c_str(), blen_filedir, dest_dir, path_mode, ©_set); \ + /* Always emit forward slashes for cross-platform compatibility. */ \ + std::replace(path.begin(), path.end(), '\\', '/'); \ + fmt_handler_.write<eMTLSyntaxElement>(options, path.c_str()); \ return; \ } @@ -561,25 +598,35 @@ void MTLWriter::write_texture_map( BLI_assert(!"This map type was not written to the file."); } -void MTLWriter::write_materials() +void MTLWriter::write_materials(const char *blen_filepath, + ePathReferenceMode path_mode, + const char *dest_dir) { if (mtlmaterials_.size() == 0) { return; } + + char blen_filedir[PATH_MAX]; + BLI_split_dir_part(blen_filepath, blen_filedir, PATH_MAX); + BLI_path_slash_native(blen_filedir); + BLI_path_normalize(nullptr, blen_filedir); + std::sort(mtlmaterials_.begin(), mtlmaterials_.end(), [](const MTLMaterial &a, const MTLMaterial &b) { return a.name < b.name; }); + Set<std::pair<std::string, std::string>> copy_set; for (const MTLMaterial &mtlmat : mtlmaterials_) { fmt_handler_.write<eMTLSyntaxElement::string>("\n"); fmt_handler_.write<eMTLSyntaxElement::newmtl>(mtlmat.name); write_bsdf_properties(mtlmat); - for (const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map : - mtlmat.texture_maps.items()) { - if (!texture_map.value.image_path.empty()) { - write_texture_map(mtlmat, texture_map); + for (const auto &tex : mtlmat.texture_maps.items()) { + if (tex.value.image_path.empty()) { + continue; } + write_texture_map(mtlmat, tex, blen_filedir, dest_dir, path_mode, copy_set); } } + path_reference_copy(copy_set); } Vector<int> MTLWriter::add_materials(const OBJMesh &mesh_to_export) diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh index 96f7d434338..97c23484426 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh @@ -9,6 +9,7 @@ #include "DNA_meshdata_types.h" #include "BLI_map.hh" +#include "BLI_set.hh" #include "BLI_vector.hh" #include "IO_wavefront_obj.h" @@ -71,9 +72,11 @@ class OBJWriter : NonMovable, NonCopyable { */ void write_mtllib_name(const StringRefNull mtl_filepath) const; /** - * Write vertex coordinates for all vertices as "v x y z". + * Write vertex coordinates for all vertices as "v x y z" or "v x y z r g b". */ - void write_vertex_coords(FormatHandler<eFileType::OBJ> &fh, const OBJMesh &obj_mesh_data) const; + void write_vertex_coords(FormatHandler<eFileType::OBJ> &fh, + const OBJMesh &obj_mesh_data, + bool write_colors) const; /** * Write UV vertex coordinates for all vertices as `vt u v`. * \note UV indices are stored here, but written with polygons later. @@ -181,7 +184,9 @@ class MTLWriter : NonMovable, NonCopyable { * For consistency of output from run to run (useful for testing), * the materials are sorted by name before writing. */ - void write_materials(); + void write_materials(const char *blen_filepath, + ePathReferenceMode path_mode, + const char *dest_dir); StringRefNull mtl_file_path() const; /** * Add the materials of the given object to #MTLWriter, de-duplicating @@ -203,6 +208,10 @@ class MTLWriter : NonMovable, NonCopyable { * Write a texture map in the form "map_XX -s 1. 1. 1. -o 0. 0. 0. [-bm 1.] path/to/image". */ void write_texture_map(const MTLMaterial &mtl_material, - const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map); + const Map<const eMTLSyntaxElement, tex_map_XX>::Item &texture_map, + const char *blen_filedir, + const char *dest_dir, + ePathReferenceMode mode, + Set<std::pair<std::string, std::string>> ©_set); }; } // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_io.hh b/source/blender/io/wavefront_obj/exporter/obj_export_io.hh index 0d7c089ec14..5413c9969e3 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_io.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_export_io.hh @@ -30,6 +30,7 @@ enum class eFileType { enum class eOBJSyntaxElement { vertex_coords, + vertex_coords_color, uv_vertex_coords, normal, poly_element_begin, @@ -130,6 +131,9 @@ constexpr FormattingSyntax syntax_elem_to_formatting(const eOBJSyntaxElement key case eOBJSyntaxElement::vertex_coords: { return {"v {:.6f} {:.6f} {:.6f}\n", 3, is_type_float<T...>}; } + case eOBJSyntaxElement::vertex_coords_color: { + return {"v {:.6f} {:.6f} {:.6f} {:.4f} {:.4f} {:.4f}\n", 6, is_type_float<T...>}; + } case eOBJSyntaxElement::uv_vertex_coords: { return {"vt {:.6f} {:.6f}\n", 2, is_type_float<T...>}; } @@ -236,7 +240,7 @@ constexpr FormattingSyntax syntax_elem_to_formatting(const eMTLSyntaxElement key case eMTLSyntaxElement::Ke: { return {"Ke {:.6f} {:.6f} {:.6f}\n", 3, is_type_float<T...>}; } - /* Note: first texture map related argument, if present, will have its own leading space. */ + /* NOTE: first texture map related argument, if present, will have its own leading space. */ case eMTLSyntaxElement::map_Kd: { return {"map_Kd{} {}\n", 2, is_type_string_related<T...>}; } diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc index c2a9e0574eb..e2ecda32717 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc @@ -117,15 +117,12 @@ std::pair<Mesh *, bool> OBJMesh::triangulate_mesh_eval() return {triangulated, true}; } -void OBJMesh::set_world_axes_transform(const eTransformAxisForward forward, - const eTransformAxisUp up) +void OBJMesh::set_world_axes_transform(const eIOAxis forward, const eIOAxis up) { float axes_transform[3][3]; unit_m3(axes_transform); /* +Y-forward and +Z-up are the default Blender axis settings. */ - mat3_from_axis_conversion(OBJ_AXIS_Y_FORWARD, OBJ_AXIS_Z_UP, forward, up, axes_transform); - /* mat3_from_axis_conversion returns a transposed matrix! */ - transpose_m3(axes_transform); + mat3_from_axis_conversion(forward, up, IO_AXIS_Y, IO_AXIS_Z, axes_transform); mul_m4_m3m4(world_and_axes_transform_, axes_transform, export_object_eval_.obmat); /* mul_m4_m3m4 does not transform last row of obmat, i.e. location data. */ mul_v3_m3v3(world_and_axes_transform_[3], axes_transform, export_object_eval_.obmat[3]); @@ -290,7 +287,7 @@ void OBJMesh::store_uv_coords_and_indices() const MLoop *mloop = export_mesh_eval_->mloop; const int totpoly = export_mesh_eval_->totpoly; const int totvert = export_mesh_eval_->totvert; - const MLoopUV *mloopuv = static_cast<MLoopUV *>( + const MLoopUV *mloopuv = static_cast<const MLoopUV *>( CustomData_get_layer(&export_mesh_eval_->ldata, CD_MLOOPUV)); if (!mloopuv) { tot_uv_vertices_ = 0; @@ -382,8 +379,8 @@ void OBJMesh::store_normal_coords_and_indices() normal_to_index.reserve(export_mesh_eval_->totpoly); loop_to_normal_index_.resize(export_mesh_eval_->totloop); loop_to_normal_index_.fill(-1); - const float( - *lnors)[3] = (const float(*)[3])(CustomData_get_layer(&export_mesh_eval_->ldata, CD_NORMAL)); + const float(*lnors)[3] = static_cast<const float(*)[3]>( + CustomData_get_layer(&export_mesh_eval_->ldata, CD_NORMAL)); for (int poly_index = 0; poly_index < export_mesh_eval_->totpoly; ++poly_index) { const MPoly &mpoly = export_mesh_eval_->mpoly[poly_index]; bool need_per_loop_normals = lnors != nullptr || (mpoly.flag & ME_SMOOTH); @@ -453,7 +450,7 @@ int16_t OBJMesh::get_poly_deform_group_index(const int poly_index, BLI_assert(poly_index < export_mesh_eval_->totpoly); BLI_assert(group_weights.size() == BKE_object_defgroup_count(&export_object_eval_)); - const MDeformVert *dvert_layer = static_cast<MDeformVert *>( + const MDeformVert *dvert_layer = static_cast<const MDeformVert *>( CustomData_get_layer(&export_mesh_eval_->vdata, CD_MDEFORMVERT)); if (!dvert_layer) { return NOT_FOUND; diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh index f47ca423dbc..ee2e6227700 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh @@ -241,6 +241,11 @@ class OBJMesh : NonCopyable { return i < 0 || i >= poly_order_.size() ? i : poly_order_[i]; } + Mesh *get_mesh() const + { + return export_mesh_eval_; + } + private: /** * Free the mesh if _the exporter_ created it. @@ -256,6 +261,6 @@ class OBJMesh : NonCopyable { /** * Set the final transform after applying axes settings and an Object's world transform. */ - void set_world_axes_transform(eTransformAxisForward forward, eTransformAxisUp up); + void set_world_axes_transform(eIOAxis forward, eIOAxis up); }; } // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc b/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc index c48d5a5f7f0..4ed148ec64e 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mtl.cc @@ -113,8 +113,7 @@ static const bNode *get_node_of_type(Span<const nodes::OutputSocketRef *> socket /** * From a texture image shader node, get the image's filepath. - * Returned filepath is stripped of initial "//". If packed image is found, - * only the file "name" is returned. + * If packed image is found, only the file "name" is returned. */ static const char *get_image_filepath(const bNode *tex_node) { @@ -134,9 +133,6 @@ static const char *get_image_filepath(const bNode *tex_node) "directory as the .MTL file.\n", path); } - if (path[0] == '/' && path[1] == '/') { - path += 2; - } return path; } diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc index c247048ce13..172a59e5341 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc @@ -25,15 +25,12 @@ OBJCurve::OBJCurve(const Depsgraph *depsgraph, set_world_axes_transform(export_params.forward_axis, export_params.up_axis); } -void OBJCurve::set_world_axes_transform(const eTransformAxisForward forward, - const eTransformAxisUp up) +void OBJCurve::set_world_axes_transform(const eIOAxis forward, const eIOAxis up) { float axes_transform[3][3]; unit_m3(axes_transform); /* +Y-forward and +Z-up are the Blender's default axis settings. */ - mat3_from_axis_conversion(OBJ_AXIS_Y_FORWARD, OBJ_AXIS_Z_UP, forward, up, axes_transform); - /* mat3_from_axis_conversion returns a transposed matrix! */ - transpose_m3(axes_transform); + mat3_from_axis_conversion(forward, up, IO_AXIS_Y, IO_AXIS_Z, axes_transform); mul_m4_m3m4(world_axes_transform_, axes_transform, export_object_eval_->obmat); /* #mul_m4_m3m4 does not transform last row of #Object.obmat, i.e. location data. */ mul_v3_m3v3(world_axes_transform_[3], axes_transform, export_object_eval_->obmat[3]); diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh index fe826725daf..65389d44f59 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh @@ -56,7 +56,7 @@ class OBJCurve : NonCopyable { /** * Set the final transform after applying axes settings and an Object's world transform. */ - void set_world_axes_transform(eTransformAxisForward forward, eTransformAxisUp up); + void set_world_axes_transform(eIOAxis forward, eIOAxis up); }; } // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_exporter.cc b/source/blender/io/wavefront_obj/exporter/obj_exporter.cc index 78b709c884a..b0938084efb 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_exporter.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_exporter.cc @@ -195,7 +195,7 @@ static void write_mesh_objects(Vector<std::unique_ptr<OBJMesh>> exportable_as_me auto &fh = buffers[i]; obj_writer.write_object_name(fh, obj); - obj_writer.write_vertex_coords(fh, obj); + obj_writer.write_vertex_coords(fh, obj, export_params.export_colors); if (obj.tot_polygons() > 0) { if (export_params.export_smooth_groups) { @@ -284,7 +284,16 @@ void export_frame(Depsgraph *depsgraph, const OBJExportParams &export_params, co std::move(exportable_as_mesh), *frame_writer, mtl_writer.get(), export_params); if (mtl_writer) { mtl_writer->write_header(export_params.blen_filepath); - mtl_writer->write_materials(); + char dest_dir[PATH_MAX]; + if (export_params.file_base_for_tests[0] == '\0') { + BLI_split_dir_part(export_params.filepath, dest_dir, PATH_MAX); + } + else { + BLI_strncpy(dest_dir, export_params.file_base_for_tests, PATH_MAX); + } + BLI_path_slash_native(dest_dir); + BLI_path_normalize(nullptr, dest_dir); + mtl_writer->write_materials(export_params.blen_filepath, export_params.path_mode, dest_dir); } write_nurbs_curve_objects(std::move(exportable_as_nurbs), *frame_writer); } diff --git a/source/blender/io/wavefront_obj/importer/importer_mesh_utils.cc b/source/blender/io/wavefront_obj/importer/importer_mesh_utils.cc index 7019e67419e..f33753d720d 100644 --- a/source/blender/io/wavefront_obj/importer/importer_mesh_utils.cc +++ b/source/blender/io/wavefront_obj/importer/importer_mesh_utils.cc @@ -99,13 +99,8 @@ void transform_object(Object *object, const OBJImportParams &import_params) 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); + mat3_from_axis_conversion( + IO_AXIS_Y, IO_AXIS_Z, import_params.forward_axis, import_params.up_axis, axes_transform); copy_m4_m3(obmat, axes_transform); BKE_object_apply_mat4(object, obmat, true, false); 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 index be322f49840..3cc17e7d8e6 100644 --- a/source/blender/io/wavefront_obj/importer/obj_import_file_reader.cc +++ b/source/blender/io/wavefront_obj/importer/obj_import_file_reader.cc @@ -5,12 +5,15 @@ */ #include "BLI_map.hh" +#include "BLI_math_color.h" +#include "BLI_math_vector.h" #include "BLI_string_ref.hh" #include "BLI_vector.hh" -#include "IO_string_utils.hh" - #include "obj_import_file_reader.hh" +#include "obj_import_string_utils.hh" + +#include <charconv> namespace blender::io::obj { @@ -35,6 +38,7 @@ static Geometry *create_geometry(Geometry *const prev_geometry, g->geom_type_ = new_type; g->geometry_name_ = name.is_empty() ? "New object" : name; g->vertex_start_ = global_vertices.vertices.size(); + g->vertex_color_start_ = global_vertices.vertex_colors.size(); r_offset.set_index_offset(g->vertex_start_); return g; }; @@ -67,40 +71,89 @@ static Geometry *create_geometry(Geometry *const prev_geometry, } static void geom_add_vertex(Geometry *geom, - const StringRef line, + const char *p, + const char *end, GlobalVertices &r_global_vertices) { float3 vert; - parse_floats(line, 0.0f, vert, 3); + p = parse_floats(p, end, 0.0f, vert, 3); r_global_vertices.vertices.append(vert); geom->vertex_count_++; + /* OBJ extension: `xyzrgb` vertex colors, when the vertex position + * is followed by 3 more RGB color components. See + * http://paulbourke.net/dataformats/obj/colour.html */ + if (p < end) { + float3 srgb; + p = parse_floats(p, end, -1.0f, srgb, 3); + if (srgb.x >= 0 && srgb.y >= 0 && srgb.z >= 0) { + float3 linear; + srgb_to_linearrgb_v3_v3(linear, srgb); + r_global_vertices.vertex_colors.append(linear); + geom->vertex_color_count_++; + } + } +} + +static void geom_add_mrgb_colors(Geometry *geom, + const char *p, + const char *end, + GlobalVertices &r_global_vertices) +{ + /* MRGB color extension, in the form of + * "#MRGB MMRRGGBBMMRRGGBB ..." + * http://paulbourke.net/dataformats/obj/colour.html */ + p = drop_whitespace(p, end); + const int mrgb_length = 8; + while (p + mrgb_length <= end) { + uint32_t value = 0; + std::from_chars_result res = std::from_chars(p, p + mrgb_length, value, 16); + if (res.ec == std::errc::invalid_argument || res.ec == std::errc::result_out_of_range) { + return; + } + unsigned char srgb[4]; + srgb[0] = (value >> 16) & 0xFF; + srgb[1] = (value >> 8) & 0xFF; + srgb[2] = value & 0xFF; + srgb[3] = 0xFF; + float linear[4]; + srgb_to_linearrgb_uchar4(linear, srgb); + r_global_vertices.vertex_colors.append({linear[0], linear[1], linear[2]}); + geom->vertex_color_count_++; + p += mrgb_length; + } } static void geom_add_vertex_normal(Geometry *geom, - const StringRef line, + const char *p, + const char *end, GlobalVertices &r_global_vertices) { float3 normal; - parse_floats(line, 0.0f, normal, 3); + parse_floats(p, end, 0.0f, normal, 3); + /* Normals can be printed with only several digits in the file, + * making them ever-so-slightly non unit length. Make sure they are + * normalized. */ + normalize_v3(normal); r_global_vertices.vertex_normals.append(normal); geom->has_vertex_normals_ = true; } -static void geom_add_uv_vertex(const StringRef line, GlobalVertices &r_global_vertices) +static void geom_add_uv_vertex(const char *p, const char *end, GlobalVertices &r_global_vertices) { float2 uv; - parse_floats(line, 0.0f, uv, 2); + parse_floats(p, end, 0.0f, uv, 2); r_global_vertices.uv_vertices.append(uv); } static void geom_add_edge(Geometry *geom, - StringRef line, + const char *p, + const char *end, const VertexIndexOffset &offsets, GlobalVertices &r_global_vertices) { int edge_v1, edge_v2; - line = parse_int(line, -1, edge_v1); - line = parse_int(line, -1, edge_v2); + p = parse_int(p, end, -1, edge_v1); + p = parse_int(p, end, -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; @@ -109,7 +162,8 @@ static void geom_add_edge(Geometry *geom, } static void geom_add_polygon(Geometry *geom, - StringRef line, + const char *p, + const char *end, const GlobalVertices &global_vertices, const VertexIndexOffset &offsets, const int material_index, @@ -121,32 +175,32 @@ static void geom_add_polygon(Geometry *geom, curr_face.material_index = material_index; if (group_index >= 0) { curr_face.vertex_group_index = group_index; - geom->use_vertex_groups_ = true; + geom->has_vertex_groups_ = true; } const int orig_corners_size = geom->face_corners_.size(); curr_face.start_index_ = orig_corners_size; bool face_valid = true; - line = drop_whitespace(line); - while (!line.is_empty() && face_valid) { + p = drop_whitespace(p, end); + while (p < end && face_valid) { PolyCorner corner; bool got_uv = false, got_normal = false; /* Parse vertex index. */ - line = parse_int(line, INT32_MAX, corner.vert_index, false); + p = parse_int(p, end, INT32_MAX, corner.vert_index, false); face_valid &= corner.vert_index != INT32_MAX; - if (!line.is_empty() && line[0] == '/') { + if (p < end && *p == '/') { /* Parse UV index. */ - line = line.drop_prefix(1); - if (!line.is_empty() && line[0] != '/') { - line = parse_int(line, INT32_MAX, corner.uv_vert_index, false); + ++p; + if (p < end && *p != '/') { + p = parse_int(p, end, INT32_MAX, corner.uv_vert_index, false); got_uv = corner.uv_vert_index != INT32_MAX; } /* Parse normal index. */ - if (!line.is_empty() && line[0] == '/') { - line = line.drop_prefix(1); - line = parse_int(line, INT32_MAX, corner.vertex_normal_index, false); - got_normal = corner.uv_vert_index != INT32_MAX; + if (p < end && *p == '/') { + ++p; + p = parse_int(p, end, INT32_MAX, corner.vertex_normal_index, false); + got_normal = corner.vertex_normal_index != INT32_MAX; } } /* Always keep stored indices non-negative and zero-based. */ @@ -169,7 +223,10 @@ static void geom_add_polygon(Geometry *geom, face_valid = false; } } - if (got_normal) { + /* Ignore corner normal index, if the geometry does not have any normals. + * Some obj files out there do have face definitions that refer to normal indices, + * without any normals being present (T98782). */ + if (got_normal && geom->has_vertex_normals_) { corner.vertex_normal_index += corner.vertex_normal_index < 0 ? global_vertices.vertex_normals.size() : -1; @@ -186,7 +243,7 @@ static void geom_add_polygon(Geometry *geom, curr_face.corner_count_++; /* Skip whitespace to get to the next face corner. */ - line = drop_whitespace(line); + p = drop_whitespace(p, end); } if (face_valid) { @@ -201,14 +258,16 @@ static void geom_add_polygon(Geometry *geom, } static Geometry *geom_set_curve_type(Geometry *geom, - const StringRef rest_line, + const char *p, + const char *end, const GlobalVertices &global_vertices, const StringRef group_name, 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; + p = drop_whitespace(p, end); + if (!StringRef(p, end).startswith("bspline")) { + std::cerr << "Curve type not supported: '" << std::string(p, end) << "'" << std::endl; return geom; } geom = create_geometry( @@ -217,22 +276,23 @@ static Geometry *geom_set_curve_type(Geometry *geom, return geom; } -static void geom_set_curve_degree(Geometry *geom, const StringRef line) +static void geom_set_curve_degree(Geometry *geom, const char *p, const char *end) { - parse_int(line, 3, geom->nurbs_element_.degree); + parse_int(p, end, 3, geom->nurbs_element_.degree); } static void geom_add_curve_vertex_indices(Geometry *geom, - StringRef line, + const char *p, + const char *end, const GlobalVertices &global_vertices) { /* Curve lines always have "0.0" and "1.0", skip over them. */ float dummy[2]; - line = parse_floats(line, 0, dummy, 2); + p = parse_floats(p, end, 0, dummy, 2); /* Parse indices. */ - while (!line.is_empty()) { + while (p < end) { int index; - line = parse_int(line, INT32_MAX, index); + p = parse_int(p, end, INT32_MAX, index); if (index == INT32_MAX) { return; } @@ -242,22 +302,22 @@ static void geom_add_curve_vertex_indices(Geometry *geom, } } -static void geom_add_curve_parameters(Geometry *geom, StringRef line) +static void geom_add_curve_parameters(Geometry *geom, const char *p, const char *end) { - line = drop_whitespace(line); - if (line.is_empty()) { - std::cerr << "Invalid OBJ curve parm line: '" << line << "'" << std::endl; + p = drop_whitespace(p, end); + if (p == end) { + std::cerr << "Invalid OBJ curve parm line" << std::endl; return; } - if (line[0] != 'u') { - std::cerr << "OBJ curve surfaces are not supported: '" << line[0] << "'" << std::endl; + if (*p != 'u') { + std::cerr << "OBJ curve surfaces are not supported: '" << *p << "'" << std::endl; return; } - line = line.drop_prefix(1); + ++p; - while (!line.is_empty()) { + while (p < end) { float val; - line = parse_float(line, FLT_MAX, val); + p = parse_float(p, end, FLT_MAX, val); if (val != FLT_MAX) { geom->nurbs_element_.parm.append(val); } @@ -270,7 +330,6 @@ static void geom_add_curve_parameters(Geometry *geom, StringRef line) static void geom_update_group(const StringRef rest_line, std::string &r_group_name) { - 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. */ @@ -280,17 +339,18 @@ static void geom_update_group(const StringRef rest_line, std::string &r_group_na r_group_name = rest_line; } -static void geom_update_smooth_group(StringRef line, bool &r_state_shaded_smooth) +static void geom_update_smooth_group(const char *p, const char *end, bool &r_state_shaded_smooth) { - line = drop_whitespace(line); + p = drop_whitespace(p, end); /* Some implementations use "0" and "null" too, in addition to "off". */ + const StringRef line = StringRef(p, end); if (line == "0" || line.startswith("off") || line.startswith("null")) { r_state_shaded_smooth = false; return; } int smooth = 0; - parse_int(line, 0, smooth); + parse_int(p, end, 0, smooth); r_state_shaded_smooth = smooth != 0; } @@ -312,21 +372,21 @@ OBJParser::~OBJParser() } /* If line starts with keyword followed by whitespace, returns true and drops it from the line. */ -static bool parse_keyword(StringRef &line, StringRef keyword) +static bool parse_keyword(const char *&p, const char *end, StringRef keyword) { const size_t keyword_len = keyword.size(); - if (line.size() < keyword_len + 1) { + if (end - p < keyword_len + 1) { return false; } - if (!line.startswith(keyword)) { + if (memcmp(p, keyword.data(), keyword_len) != 0) { return false; } /* Treat any ASCII control character as white-space; * don't use `isspace()` for performance reasons. */ - if (line[keyword_len] > ' ') { + if (p[keyword_len] > ' ') { return false; } - line = line.drop_prefix(keyword_len + 1); + p += keyword_len + 1; return true; } @@ -337,9 +397,14 @@ void OBJParser::parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries, return; } + /* Use the filename as the default name given to the initial object. */ + char ob_name[FILE_MAXFILE]; + BLI_strncpy(ob_name, BLI_path_basename(import_params_.filepath), FILE_MAXFILE); + BLI_path_extension_replace(ob_name, FILE_MAXFILE, ""); + VertexIndexOffset offsets; Geometry *curr_geom = create_geometry( - nullptr, GEOM_MESH, "", r_global_vertices, r_all_geometries, offsets); + nullptr, GEOM_MESH, ob_name, r_global_vertices, r_all_geometries, offsets); /* State variables: once set, they remain the same for the remaining * elements in the object. */ @@ -400,27 +465,29 @@ void OBJParser::parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries, StringRef buffer_str{buffer.data(), (int64_t)last_nl}; while (!buffer_str.is_empty()) { StringRef line = read_next_line(buffer_str); - line = drop_whitespace(line); + const char *p = line.begin(), *end = line.end(); + p = drop_whitespace(p, end); ++line_number; - if (line.is_empty()) { + if (p == end) { continue; } /* Most common things that start with 'v': vertices, normals, UVs. */ - if (line[0] == 'v') { - if (parse_keyword(line, "v")) { - geom_add_vertex(curr_geom, line, r_global_vertices); + if (*p == 'v') { + if (parse_keyword(p, end, "v")) { + geom_add_vertex(curr_geom, p, end, r_global_vertices); } - else if (parse_keyword(line, "vn")) { - geom_add_vertex_normal(curr_geom, line, r_global_vertices); + else if (parse_keyword(p, end, "vn")) { + geom_add_vertex_normal(curr_geom, p, end, r_global_vertices); } - else if (parse_keyword(line, "vt")) { - geom_add_uv_vertex(line, r_global_vertices); + else if (parse_keyword(p, end, "vt")) { + geom_add_uv_vertex(p, end, r_global_vertices); } } /* Faces. */ - else if (parse_keyword(line, "f")) { + else if (parse_keyword(p, end, "f")) { geom_add_polygon(curr_geom, - line, + p, + end, r_global_vertices, offsets, state_material_index, @@ -428,20 +495,24 @@ void OBJParser::parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries, state_shaded_smooth); } /* Faces. */ - else if (parse_keyword(line, "l")) { - geom_add_edge(curr_geom, line, offsets, r_global_vertices); + else if (parse_keyword(p, end, "l")) { + geom_add_edge(curr_geom, p, end, offsets, r_global_vertices); } /* Objects. */ - else if (parse_keyword(line, "o")) { + else if (parse_keyword(p, end, "o")) { state_shaded_smooth = false; state_group_name = ""; state_material_name = ""; - curr_geom = create_geometry( - curr_geom, GEOM_MESH, line.trim(), r_global_vertices, r_all_geometries, offsets); + curr_geom = create_geometry(curr_geom, + GEOM_MESH, + StringRef(p, end).trim(), + r_global_vertices, + r_all_geometries, + offsets); } /* Groups. */ - else if (parse_keyword(line, "g")) { - geom_update_group(line.trim(), state_group_name); + else if (parse_keyword(p, end, "g")) { + geom_update_group(StringRef(p, end).trim(), state_group_name); int new_index = curr_geom->group_indices_.size(); state_group_index = curr_geom->group_indices_.lookup_or_add(state_group_name, new_index); if (new_index == state_group_index) { @@ -449,12 +520,12 @@ void OBJParser::parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries, } } /* Smoothing groups. */ - else if (parse_keyword(line, "s")) { - geom_update_smooth_group(line, state_shaded_smooth); + else if (parse_keyword(p, end, "s")) { + geom_update_smooth_group(p, end, state_shaded_smooth); } /* Materials and their libraries. */ - else if (parse_keyword(line, "usemtl")) { - state_material_name = line.trim(); + else if (parse_keyword(p, end, "usemtl")) { + state_material_name = StringRef(p, end).trim(); int new_mat_index = curr_geom->material_indices_.size(); state_material_index = curr_geom->material_indices_.lookup_or_add(state_material_name, new_mat_index); @@ -462,32 +533,35 @@ void OBJParser::parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries, curr_geom->material_order_.append(state_material_name); } } - else if (parse_keyword(line, "mtllib")) { - add_mtl_library(line.trim()); + else if (parse_keyword(p, end, "mtllib")) { + add_mtl_library(StringRef(p, end).trim()); + } + else if (parse_keyword(p, end, "#MRGB")) { + geom_add_mrgb_colors(curr_geom, p, end, r_global_vertices); } /* Comments. */ - else if (line.startswith("#")) { + else if (*p == '#') { /* Nothing to do. */ } /* Curve related things. */ - else if (parse_keyword(line, "cstype")) { + else if (parse_keyword(p, end, "cstype")) { curr_geom = geom_set_curve_type( - curr_geom, line, r_global_vertices, state_group_name, offsets, r_all_geometries); + curr_geom, p, end, r_global_vertices, state_group_name, offsets, r_all_geometries); } - else if (parse_keyword(line, "deg")) { - geom_set_curve_degree(curr_geom, line); + else if (parse_keyword(p, end, "deg")) { + geom_set_curve_degree(curr_geom, p, end); } - else if (parse_keyword(line, "curv")) { - geom_add_curve_vertex_indices(curr_geom, line, r_global_vertices); + else if (parse_keyword(p, end, "curv")) { + geom_add_curve_vertex_indices(curr_geom, p, end, r_global_vertices); } - else if (parse_keyword(line, "parm")) { - geom_add_curve_parameters(curr_geom, line); + else if (parse_keyword(p, end, "parm")) { + geom_add_curve_parameters(curr_geom, p, end); } - else if (line.startswith("end")) { + else if (StringRef(p, end).startswith("end")) { /* End of curve definition, nothing else to do. */ } else { - std::cout << "OBJ element not recognized: '" << line << "'" << std::endl; + std::cout << "OBJ element not recognized: '" << std::string(p, end) << "'" << std::endl; } } @@ -501,33 +575,33 @@ void OBJParser::parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries, add_default_mtl_library(); } -static eMTLSyntaxElement mtl_line_start_to_enum(StringRef &line) +static eMTLSyntaxElement mtl_line_start_to_enum(const char *&p, const char *end) { - if (parse_keyword(line, "map_Kd")) { + if (parse_keyword(p, end, "map_Kd")) { return eMTLSyntaxElement::map_Kd; } - if (parse_keyword(line, "map_Ks")) { + if (parse_keyword(p, end, "map_Ks")) { return eMTLSyntaxElement::map_Ks; } - if (parse_keyword(line, "map_Ns")) { + if (parse_keyword(p, end, "map_Ns")) { return eMTLSyntaxElement::map_Ns; } - if (parse_keyword(line, "map_d")) { + if (parse_keyword(p, end, "map_d")) { return eMTLSyntaxElement::map_d; } - if (parse_keyword(line, "refl")) { + if (parse_keyword(p, end, "refl")) { return eMTLSyntaxElement::map_refl; } - if (parse_keyword(line, "map_refl")) { + if (parse_keyword(p, end, "map_refl")) { return eMTLSyntaxElement::map_refl; } - if (parse_keyword(line, "map_Ke")) { + if (parse_keyword(p, end, "map_Ke")) { return eMTLSyntaxElement::map_Ke; } - if (parse_keyword(line, "bump")) { + if (parse_keyword(p, end, "bump")) { return eMTLSyntaxElement::map_Bump; } - if (parse_keyword(line, "map_Bump") || parse_keyword(line, "map_bump")) { + if (parse_keyword(p, end, "map_Bump") || parse_keyword(p, end, "map_bump")) { return eMTLSyntaxElement::map_Bump; } return eMTLSyntaxElement::string; @@ -545,39 +619,43 @@ static const std::pair<StringRef, int> unsupported_texture_options[] = { {"-texres", 1}, }; -static bool parse_texture_option(StringRef &line, MTLMaterial *material, tex_map_XX &tex_map) +static bool parse_texture_option(const char *&p, + const char *end, + MTLMaterial *material, + tex_map_XX &tex_map) { - line = drop_whitespace(line); - if (parse_keyword(line, "-o")) { - line = parse_floats(line, 0.0f, tex_map.translation, 3); + p = drop_whitespace(p, end); + if (parse_keyword(p, end, "-o")) { + p = parse_floats(p, end, 0.0f, tex_map.translation, 3); return true; } - if (parse_keyword(line, "-s")) { - line = parse_floats(line, 1.0f, tex_map.scale, 3); + if (parse_keyword(p, end, "-s")) { + p = parse_floats(p, end, 1.0f, tex_map.scale, 3); return true; } - if (parse_keyword(line, "-bm")) { - line = parse_float(line, 1.0f, material->map_Bump_strength); + if (parse_keyword(p, end, "-bm")) { + p = parse_float(p, end, 1.0f, material->map_Bump_strength); return true; } - if (parse_keyword(line, "-type")) { - line = drop_whitespace(line); + if (parse_keyword(p, end, "-type")) { + p = drop_whitespace(p, end); /* Only sphere is supported. */ tex_map.projection_type = SHD_PROJ_SPHERE; + const StringRef line = StringRef(p, end); if (!line.startswith("sphere")) { std::cerr << "OBJ import: only sphere MTL projection type is supported: '" << line << "'" << std::endl; } - line = drop_non_whitespace(line); + p = drop_non_whitespace(p, end); return true; } /* Check for unsupported options and skip them. */ for (const auto &opt : unsupported_texture_options) { - if (parse_keyword(line, opt.first)) { + if (parse_keyword(p, end, opt.first)) { /* Drop the arguments. */ for (int i = 0; i < opt.second; ++i) { - line = drop_whitespace(line); - line = drop_non_whitespace(line); + p = drop_whitespace(p, end); + p = drop_non_whitespace(p, end); } return true; } @@ -586,15 +664,19 @@ static bool parse_texture_option(StringRef &line, MTLMaterial *material, tex_map return false; } -static void parse_texture_map(StringRef line, MTLMaterial *material, const char *mtl_dir_path) +static void parse_texture_map(const char *p, + const char *end, + MTLMaterial *material, + const char *mtl_dir_path) { + const StringRef line = StringRef(p, end); bool is_map = line.startswith("map_"); bool is_refl = line.startswith("refl"); bool is_bump = line.startswith("bump"); if (!is_map && !is_refl && !is_bump) { return; } - eMTLSyntaxElement key = mtl_line_start_to_enum(line); + eMTLSyntaxElement key = mtl_line_start_to_enum(p, end); if (key == eMTLSyntaxElement::string || !material->texture_maps.contains(key)) { /* No supported texture map found. */ std::cerr << "OBJ import: MTL texture map type not supported: '" << line << "'" << std::endl; @@ -604,12 +686,11 @@ static void parse_texture_map(StringRef line, MTLMaterial *material, const char tex_map.mtl_dir_path = mtl_dir_path; /* Parse texture map options. */ - while (parse_texture_option(line, material, tex_map)) { + while (parse_texture_option(p, end, material, tex_map)) { } /* What remains is the image path. */ - line = line.trim(); - tex_map.image_path = line; + tex_map.image_path = StringRef(p, end).trim(); } Span<std::string> OBJParser::mtl_libraries() const @@ -667,51 +748,53 @@ void MTLParser::parse_and_store(Map<string, std::unique_ptr<MTLMaterial>> &r_mat StringRef buffer_str{(const char *)buffer, (int64_t)buffer_len}; while (!buffer_str.is_empty()) { - StringRef line = read_next_line(buffer_str); - line = drop_whitespace(line); - if (line.is_empty()) { + const StringRef line = read_next_line(buffer_str); + const char *p = line.begin(), *end = line.end(); + p = drop_whitespace(p, end); + if (p == end) { continue; } - if (parse_keyword(line, "newmtl")) { - line = line.trim(); - if (r_materials.contains(line)) { + if (parse_keyword(p, end, "newmtl")) { + StringRef mat_name = StringRef(p, end).trim(); + if (r_materials.contains(mat_name)) { material = nullptr; } else { - material = r_materials.lookup_or_add(string(line), std::make_unique<MTLMaterial>()).get(); + material = + r_materials.lookup_or_add(string(mat_name), std::make_unique<MTLMaterial>()).get(); } } else if (material != nullptr) { - if (parse_keyword(line, "Ns")) { - parse_float(line, 324.0f, material->Ns); + if (parse_keyword(p, end, "Ns")) { + parse_float(p, end, 324.0f, material->Ns); } - else if (parse_keyword(line, "Ka")) { - parse_floats(line, 0.0f, material->Ka, 3); + else if (parse_keyword(p, end, "Ka")) { + parse_floats(p, end, 0.0f, material->Ka, 3); } - else if (parse_keyword(line, "Kd")) { - parse_floats(line, 0.8f, material->Kd, 3); + else if (parse_keyword(p, end, "Kd")) { + parse_floats(p, end, 0.8f, material->Kd, 3); } - else if (parse_keyword(line, "Ks")) { - parse_floats(line, 0.5f, material->Ks, 3); + else if (parse_keyword(p, end, "Ks")) { + parse_floats(p, end, 0.5f, material->Ks, 3); } - else if (parse_keyword(line, "Ke")) { - parse_floats(line, 0.0f, material->Ke, 3); + else if (parse_keyword(p, end, "Ke")) { + parse_floats(p, end, 0.0f, material->Ke, 3); } - else if (parse_keyword(line, "Ni")) { - parse_float(line, 1.45f, material->Ni); + else if (parse_keyword(p, end, "Ni")) { + parse_float(p, end, 1.45f, material->Ni); } - else if (parse_keyword(line, "d")) { - parse_float(line, 1.0f, material->d); + else if (parse_keyword(p, end, "d")) { + parse_float(p, end, 1.0f, material->d); } - else if (parse_keyword(line, "illum")) { + else if (parse_keyword(p, end, "illum")) { /* Some files incorrectly use a float (T60135). */ float val; - parse_float(line, 1.0f, val); + parse_float(p, end, 1.0f, val); material->illum = val; } else { - parse_texture_map(line, material, mtl_dir_path_); + parse_texture_map(p, end, material, mtl_dir_path_); } } } 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 index e41a7f8518e..8bfc5fe8bf0 100644 --- a/source/blender/io/wavefront_obj/importer/obj_import_file_reader.hh +++ b/source/blender/io/wavefront_obj/importer/obj_import_file_reader.hh @@ -13,7 +13,7 @@ namespace blender::io::obj { -/* Note: the OBJ parser implementation is planned to get fairly large changes "soon", +/* 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: diff --git a/source/blender/io/wavefront_obj/importer/obj_import_mesh.cc b/source/blender/io/wavefront_obj/importer/obj_import_mesh.cc index fc40333c24d..aa38a4d6715 100644 --- a/source/blender/io/wavefront_obj/importer/obj_import_mesh.cc +++ b/source/blender/io/wavefront_obj/importer/obj_import_mesh.cc @@ -8,7 +8,9 @@ #include "DNA_mesh_types.h" #include "DNA_scene_types.h" +#include "BKE_attribute.h" #include "BKE_customdata.h" +#include "BKE_deform.h" #include "BKE_material.h" #include "BKE_mesh.h" #include "BKE_node_tree_update.h" @@ -46,10 +48,11 @@ Object *MeshFromGeometry::create_mesh(Main *bmain, obj->data = BKE_object_obdata_add_from_type(bmain, OB_MESH, ob_name.c_str()); create_vertices(mesh); - create_polys_loops(obj, mesh); + create_polys_loops(mesh, import_params.import_vertex_groups); create_edges(mesh); create_uv_verts(mesh); create_normals(mesh); + create_colors(mesh); create_materials(bmain, materials, created_materials, obj); if (import_params.validate_meshes || mesh_geometry_.has_invalid_polys_) { @@ -62,11 +65,14 @@ Object *MeshFromGeometry::create_mesh(Main *bmain, 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); + const uint16_t 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; + /* Note: vertex groups have to be created after final mesh is assigned to the object. */ + create_vertex_groups(obj); + return obj; } @@ -161,19 +167,13 @@ void MeshFromGeometry::create_vertices(Mesh *mesh) } } -void MeshFromGeometry::create_polys_loops(Object *obj, Mesh *mesh) +void MeshFromGeometry::create_polys_loops(Mesh *mesh, bool use_vertex_groups) { - /* 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_count_; - if (total_verts && mesh_geometry_.use_vertex_groups_) { + if (use_vertex_groups && total_verts && mesh_geometry_.has_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); } const int64_t tot_face_elems{mesh->totpoly}; @@ -206,28 +206,23 @@ void MeshFromGeometry::create_polys_loops(Object *obj, Mesh *mesh) tot_loop_idx++; mloop.v = curr_corner.vert_index; + /* Setup vertex group data, if needed. */ 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. */ - int group_idx = curr_face.vertex_group_index; - /* Deform group number (def_nr) must behave like an index into the names' list. */ - *(def_vert.dw) = {static_cast<unsigned int>(group_idx), weight}; + const int group_index = curr_face.vertex_group_index; + MDeformWeight *dw = BKE_defvert_ensure_index(mesh->dvert + mloop.v, group_index); + dw->weight = 1.0f; } } +} - if (!mesh->dvert) { +void MeshFromGeometry::create_vertex_groups(Object *obj) +{ + Mesh *mesh = static_cast<Mesh *>(obj->data); + if (mesh->dvert == nullptr) { return; } - /* Add deform group names. */ for (const std::string &name : mesh_geometry_.group_order_) { BKE_object_defgroup_add_name(obj, name.data()); } @@ -289,7 +284,7 @@ static Material *get_or_create_material(Main *bmain, /* We have not, will have to create it. Create a new default * MTLMaterial too, in case the OBJ file tries to use a material * that was not in the MTL file. */ - const MTLMaterial &mtl = *materials.lookup_or_add(name, std::make_unique<MTLMaterial>()).get(); + const MTLMaterial &mtl = *materials.lookup_or_add(name, std::make_unique<MTLMaterial>()); Material *mat = BKE_material_add(bmain, name.c_str()); ShaderNodetreeWrap mat_wrap{bmain, mtl, mat}; @@ -345,4 +340,26 @@ void MeshFromGeometry::create_normals(Mesh *mesh) MEM_freeN(loop_normals); } +void MeshFromGeometry::create_colors(Mesh *mesh) +{ + /* Nothing to do if we don't have vertex colors. */ + if (mesh_geometry_.vertex_color_count_ < 1) { + return; + } + if (mesh_geometry_.vertex_color_count_ != mesh_geometry_.vertex_count_) { + std::cerr << "Mismatching number of vertices (" << mesh_geometry_.vertex_count_ + << ") and colors (" << mesh_geometry_.vertex_color_count_ << ") on object '" + << mesh_geometry_.geometry_name_ << "', ignoring colors." << std::endl; + return; + } + + CustomDataLayer *color_layer = BKE_id_attribute_new( + &mesh->id, "Color", CD_PROP_COLOR, ATTR_DOMAIN_POINT, nullptr); + float4 *colors = (float4 *)color_layer->data; + for (int i = 0; i < mesh_geometry_.vertex_color_count_; ++i) { + float3 c = global_vertices_.vertex_colors[mesh_geometry_.vertex_color_start_ + i]; + colors[i] = float4(c.x, c.y, c.z, 1.0f); + } +} + } // 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 index cf4a2aee394..591a7b81e63 100644 --- a/source/blender/io/wavefront_obj/importer/obj_import_mesh.hh +++ b/source/blender/io/wavefront_obj/importer/obj_import_mesh.hh @@ -45,10 +45,9 @@ class MeshFromGeometry : NonMovable, NonCopyable { void fixup_invalid_faces(); void create_vertices(Mesh *mesh); /** - * Create polygons for the Mesh, set smooth shading flags, deform group names, - * Materials. + * Create polygons for the Mesh, set smooth shading flags, Materials. */ - void create_polys_loops(Object *obj, Mesh *mesh); + void create_polys_loops(Mesh *mesh, bool use_vertex_groups); /** * Add explicitly imported OBJ edges to the mesh. */ @@ -65,6 +64,8 @@ class MeshFromGeometry : NonMovable, NonCopyable { Map<std::string, Material *> &created_materials, Object *obj); void create_normals(Mesh *mesh); + void create_colors(Mesh *mesh); + void create_vertex_groups(Object *obj); }; } // 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 index c2ecd8a37de..f39def0a4af 100644 --- a/source/blender/io/wavefront_obj/importer/obj_import_mtl.cc +++ b/source/blender/io/wavefront_obj/importer/obj_import_mtl.cc @@ -13,13 +13,12 @@ #include "DNA_material_types.h" #include "DNA_node_types.h" -#include "IO_string_utils.hh" - #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 "obj_import_string_utils.hh" 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 index b67ba46af03..3d6733d661e 100644 --- a/source/blender/io/wavefront_obj/importer/obj_import_objects.hh +++ b/source/blender/io/wavefront_obj/importer/obj_import_objects.hh @@ -26,6 +26,7 @@ struct GlobalVertices { Vector<float3> vertices; Vector<float2> uv_vertices; Vector<float3> vertex_normals; + Vector<float3> vertex_colors; }; /** @@ -102,6 +103,8 @@ struct Geometry { int vertex_start_ = 0; int vertex_count_ = 0; + int vertex_color_start_ = 0; + int vertex_color_count_ = 0; /** Edges written in the file in addition to (or even without polygon) elements. */ Vector<MEdge> edges_; @@ -110,7 +113,7 @@ struct Geometry { bool has_invalid_polys_ = false; bool has_vertex_normals_ = false; - bool use_vertex_groups_ = false; + bool has_vertex_groups_ = false; NurbsElement nurbs_element_; int total_loops_ = 0; }; diff --git a/source/blender/io/wavefront_obj/importer/obj_import_string_utils.cc b/source/blender/io/wavefront_obj/importer/obj_import_string_utils.cc new file mode 100644 index 00000000000..c8eaa046e68 --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_import_string_utils.cc @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "obj_import_string_utils.hh" + +/* NOTE: we could use C++17 <charconv> from_chars to parse + * floats, but even if some compilers claim full support, + * their standard libraries are not quite there yet. + * LLVM/libc++ only has a float parser since LLVM 14, + * and gcc/libstdc++ since 11.1. So until at least these are + * the minimum spec, use an external library. */ +#include "fast_float.h" +#include <charconv> + +namespace blender::io::obj { + +StringRef read_next_line(StringRef &buffer) +{ + const char *start = buffer.begin(); + const char *end = buffer.end(); + size_t len = 0; + char prev = 0; + const char *ptr = start; + while (ptr < end) { + char c = *ptr++; + if (c == '\n' && prev != '\\') { + break; + } + prev = c; + ++len; + } + + buffer = StringRef(ptr, end); + return StringRef(start, len); +} + +static bool is_whitespace(char c) +{ + return c <= ' ' || c == '\\'; +} + +const char *drop_whitespace(const char *p, const char *end) +{ + while (p < end && is_whitespace(*p)) { + ++p; + } + return p; +} + +const char *drop_non_whitespace(const char *p, const char *end) +{ + while (p < end && !is_whitespace(*p)) { + ++p; + } + return p; +} + +static const char *drop_plus(const char *p, const char *end) +{ + if (p < end && *p == '+') { + ++p; + } + return p; +} + +const char *parse_float( + const char *p, const char *end, float fallback, float &dst, bool skip_space) +{ + if (skip_space) { + p = drop_whitespace(p, end); + } + p = drop_plus(p, end); + fast_float::from_chars_result res = fast_float::from_chars(p, end, dst); + if (res.ec == std::errc::invalid_argument || res.ec == std::errc::result_out_of_range) { + dst = fallback; + } + return res.ptr; +} + +const char *parse_floats(const char *p, const char *end, float fallback, float *dst, int count) +{ + for (int i = 0; i < count; ++i) { + p = parse_float(p, end, fallback, dst[i]); + } + return p; +} + +const char *parse_int(const char *p, const char *end, int fallback, int &dst, bool skip_space) +{ + if (skip_space) { + p = drop_whitespace(p, end); + } + p = drop_plus(p, end); + std::from_chars_result res = std::from_chars(p, end, dst); + if (res.ec == std::errc::invalid_argument || res.ec == std::errc::result_out_of_range) { + dst = fallback; + } + return res.ptr; +} + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/importer/obj_import_string_utils.hh b/source/blender/io/wavefront_obj/importer/obj_import_string_utils.hh new file mode 100644 index 00000000000..3f428b1ab5c --- /dev/null +++ b/source/blender/io/wavefront_obj/importer/obj_import_string_utils.hh @@ -0,0 +1,82 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include "BLI_string_ref.hh" + +/* + * Various text parsing utilities used by OBJ importer. + * The utilities are not directly usable by other formats, since + * they treat backslash (\) as a whitespace character (OBJ format + * allows backslashes to function as a line-continuation character). + * + * Many of these functions take two pointers (p, end) indicating + * which part of a string to operate on, and return a possibly + * changed new start of the string. They could be taking a StringRef + * as input and returning a new StringRef, but this is a hot path + * in OBJ parsing, and the StringRef approach does lose performance + * (mostly due to return of StringRef being two register-size values + * instead of just one pointer). + */ + +namespace blender::io::obj { + +/** + * Fetches next line from an input string buffer. + * + * The returned line will not have '\n' characters at the end; + * the `buffer` is modified to contain remaining text without + * the input line. + * + * Note that backslash (\) character is treated as a line + * continuation. + */ +StringRef read_next_line(StringRef &buffer); + +/** + * Drop leading white-space from a string part. + * Note that backslash character is considered white-space. + */ +const char *drop_whitespace(const char *p, const char *end); + +/** + * Drop leading non-white-space from a string part. + * Note that backslash character is considered white-space. + */ +const char *drop_non_whitespace(const char *p, const char *end); + +/** + * Parse an integer from an input string. + * The parsed result is stored in `dst`. The function skips + * leading white-space unless `skip_space=false`. If the + * number can't be parsed (invalid syntax, out of range), + * `fallback` value is stored instead. + * + * Returns the start of remainder of the input string after parsing. + */ +const char *parse_int( + const char *p, const char *end, int fallback, int &dst, bool skip_space = true); + +/** + * Parse a float from an input string. + * The parsed result is stored in `dst`. The function skips + * leading white-space unless `skip_space=false`. If the + * number can't be parsed (invalid syntax, out of range), + * `fallback` value is stored instead. + * + * Returns the start of remainder of the input string after parsing. + */ +const char *parse_float( + const char *p, const char *end, float fallback, float &dst, bool skip_space = true); + +/** + * Parse a number of white-space separated floats from an input string. + * The parsed `count` numbers are stored in `dst`. If a + * number can't be parsed (invalid syntax, out of range), + * `fallback` value is stored instead. + * + * Returns the start of remainder of the input string after parsing. + */ +const char *parse_floats(const char *p, const char *end, float fallback, float *dst, int count); + +} // 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 index b18ff2cf454..f2051d195c8 100644 --- a/source/blender/io/wavefront_obj/importer/obj_importer.cc +++ b/source/blender/io/wavefront_obj/importer/obj_importer.cc @@ -88,7 +88,6 @@ void importer_main(bContext *C, const OBJImportParams &import_params) 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, diff --git a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc index f74bfc155dd..6aec848573f 100644 --- a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc +++ b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc @@ -11,12 +11,15 @@ #include "BKE_appdir.h" #include "BKE_blender_version.h" +#include "BKE_main.h" #include "BLI_fileops.h" #include "BLI_index_range.hh" #include "BLI_string_utf8.h" #include "BLI_vector.hh" +#include "BLO_readfile.h" + #include "DEG_depsgraph.h" #include "obj_export_file_writer.hh" @@ -259,11 +262,12 @@ class obj_exporter_regression_test : public obj_exporter_test { std::string tempdir = std::string(BKE_tempdir_base()); std::string out_file_path = tempdir + BLI_path_basename(golden_obj.c_str()); strncpy(params.filepath, out_file_path.c_str(), FILE_MAX - 1); - params.blen_filepath = blendfile.c_str(); + params.blen_filepath = bfile->main->filepath; + std::string golden_file_path = blender::tests::flags_test_asset_dir() + "/" + golden_obj; + BLI_split_dir_part(golden_file_path.c_str(), params.file_base_for_tests, PATH_MAX); export_frame(depsgraph, params, out_file_path.c_str()); std::string output_str = read_temp_file_in_string(out_file_path); - std::string golden_file_path = blender::tests::flags_test_asset_dir() + "/" + golden_obj; std::string golden_str = read_temp_file_in_string(golden_file_path); bool are_equal = strings_equal_after_first_lines(output_str, golden_str); if (save_failing_test_output && !are_equal) { @@ -311,8 +315,8 @@ TEST_F(obj_exporter_regression_test, all_quads) TEST_F(obj_exporter_regression_test, fgons) { OBJExportParamsDefault _export; - _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; - _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.forward_axis = IO_AXIS_Y; + _export.params.up_axis = IO_AXIS_Z; _export.params.export_materials = false; compare_obj_export_to_golden( "io_tests/blend_geometry/fgons.blend", "io_tests/obj/fgons.obj", "", _export.params); @@ -321,8 +325,8 @@ TEST_F(obj_exporter_regression_test, fgons) TEST_F(obj_exporter_regression_test, edges) { OBJExportParamsDefault _export; - _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; - _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.forward_axis = IO_AXIS_Y; + _export.params.up_axis = IO_AXIS_Z; _export.params.export_materials = false; compare_obj_export_to_golden( "io_tests/blend_geometry/edges.blend", "io_tests/obj/edges.obj", "", _export.params); @@ -331,8 +335,8 @@ TEST_F(obj_exporter_regression_test, edges) TEST_F(obj_exporter_regression_test, vertices) { OBJExportParamsDefault _export; - _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; - _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.forward_axis = IO_AXIS_Y; + _export.params.up_axis = IO_AXIS_Z; _export.params.export_materials = false; compare_obj_export_to_golden( "io_tests/blend_geometry/vertices.blend", "io_tests/obj/vertices.obj", "", _export.params); @@ -351,8 +355,8 @@ TEST_F(obj_exporter_regression_test, non_uniform_scale) TEST_F(obj_exporter_regression_test, nurbs_as_nurbs) { OBJExportParamsDefault _export; - _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; - _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.forward_axis = IO_AXIS_Y; + _export.params.up_axis = IO_AXIS_Z; _export.params.export_materials = false; _export.params.export_curves_as_nurbs = true; compare_obj_export_to_golden( @@ -362,8 +366,8 @@ TEST_F(obj_exporter_regression_test, nurbs_as_nurbs) TEST_F(obj_exporter_regression_test, nurbs_curves_as_nurbs) { OBJExportParamsDefault _export; - _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; - _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.forward_axis = IO_AXIS_Y; + _export.params.up_axis = IO_AXIS_Z; _export.params.export_materials = false; _export.params.export_curves_as_nurbs = true; compare_obj_export_to_golden("io_tests/blend_geometry/nurbs_curves.blend", @@ -375,8 +379,8 @@ TEST_F(obj_exporter_regression_test, nurbs_curves_as_nurbs) TEST_F(obj_exporter_regression_test, nurbs_as_mesh) { OBJExportParamsDefault _export; - _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; - _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.forward_axis = IO_AXIS_Y; + _export.params.up_axis = IO_AXIS_Z; _export.params.export_materials = false; _export.params.export_curves_as_nurbs = false; compare_obj_export_to_golden( @@ -386,8 +390,8 @@ TEST_F(obj_exporter_regression_test, nurbs_as_mesh) TEST_F(obj_exporter_regression_test, cube_all_data_triangulated) { OBJExportParamsDefault _export; - _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; - _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.forward_axis = IO_AXIS_Y; + _export.params.up_axis = IO_AXIS_Z; _export.params.export_materials = false; _export.params.export_triangulated_mesh = true; compare_obj_export_to_golden("io_tests/blend_geometry/cube_all_data.blend", @@ -399,8 +403,8 @@ TEST_F(obj_exporter_regression_test, cube_all_data_triangulated) TEST_F(obj_exporter_regression_test, cube_normal_edit) { OBJExportParamsDefault _export; - _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; - _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.forward_axis = IO_AXIS_Y; + _export.params.up_axis = IO_AXIS_Z; _export.params.export_materials = false; compare_obj_export_to_golden("io_tests/blend_geometry/cube_normal_edit.blend", "io_tests/obj/cube_normal_edit.obj", @@ -432,24 +436,44 @@ TEST_F(obj_exporter_regression_test, cubes_positioned) _export.params); } -/* Note: texture paths in the resulting mtl file currently are always - * as they are stored in the source .blend file; not relative to where - * the export is done. When that is properly fixed, the expected .mtl - * file should be updated. */ -TEST_F(obj_exporter_regression_test, cubes_with_textures) +TEST_F(obj_exporter_regression_test, cubes_vertex_colors) +{ + OBJExportParamsDefault _export; + _export.params.export_colors = true; + _export.params.export_normals = false; + _export.params.export_uv = false; + _export.params.export_materials = false; + compare_obj_export_to_golden("io_tests/blend_geometry/cubes_vertex_colors.blend", + "io_tests/obj/cubes_vertex_colors.obj", + "", + _export.params); +} + +TEST_F(obj_exporter_regression_test, cubes_with_textures_strip) { OBJExportParamsDefault _export; + _export.params.path_mode = PATH_REFERENCE_STRIP; compare_obj_export_to_golden("io_tests/blend_geometry/cubes_with_textures.blend", "io_tests/obj/cubes_with_textures.obj", "io_tests/obj/cubes_with_textures.mtl", _export.params); } +TEST_F(obj_exporter_regression_test, cubes_with_textures_relative) +{ + OBJExportParamsDefault _export; + _export.params.path_mode = PATH_REFERENCE_RELATIVE; + compare_obj_export_to_golden("io_tests/blend_geometry/cubes_with_textures.blend", + "io_tests/obj/cubes_with_textures_rel.obj", + "io_tests/obj/cubes_with_textures_rel.mtl", + _export.params); +} + TEST_F(obj_exporter_regression_test, suzanne_all_data) { OBJExportParamsDefault _export; - _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; - _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.forward_axis = IO_AXIS_Y; + _export.params.up_axis = IO_AXIS_Z; _export.params.export_materials = false; _export.params.export_smooth_groups = true; compare_obj_export_to_golden("io_tests/blend_geometry/suzanne_all_data.blend", @@ -480,9 +504,10 @@ TEST_F(obj_exporter_regression_test, all_curves_as_nurbs) TEST_F(obj_exporter_regression_test, all_objects) { OBJExportParamsDefault _export; - _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; - _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.forward_axis = IO_AXIS_Y; + _export.params.up_axis = IO_AXIS_Z; _export.params.export_smooth_groups = true; + _export.params.export_colors = true; compare_obj_export_to_golden("io_tests/blend_scene/all_objects.blend", "io_tests/obj/all_objects.obj", "io_tests/obj/all_objects.mtl", @@ -492,8 +517,8 @@ TEST_F(obj_exporter_regression_test, all_objects) TEST_F(obj_exporter_regression_test, all_objects_mat_groups) { OBJExportParamsDefault _export; - _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; - _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.forward_axis = IO_AXIS_Y; + _export.params.up_axis = IO_AXIS_Z; _export.params.export_smooth_groups = true; _export.params.export_material_groups = true; compare_obj_export_to_golden("io_tests/blend_scene/all_objects.blend", diff --git a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.hh b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.hh index 6a821e0b1bf..7d3b41ed527 100644 --- a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.hh +++ b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.hh @@ -11,13 +11,14 @@ struct OBJExportParamsDefault { OBJExportParamsDefault() { params.filepath[0] = '\0'; + params.file_base_for_tests[0] = '\0'; params.blen_filepath = ""; params.export_animation = false; params.start_frame = 0; params.end_frame = 1; - params.forward_axis = OBJ_AXIS_NEGATIVE_Z_FORWARD; - params.up_axis = OBJ_AXIS_Y_UP; + params.forward_axis = IO_AXIS_NEGATIVE_Z; + params.up_axis = IO_AXIS_Y; params.scaling_factor = 1.f; params.apply_modifiers = true; @@ -25,7 +26,9 @@ struct OBJExportParamsDefault { params.export_selected_objects = false; params.export_uv = true; params.export_normals = true; + params.export_colors = false; params.export_materials = true; + params.path_mode = PATH_REFERENCE_AUTO; params.export_triangulated_mesh = false; params.export_curves_as_nurbs = false; diff --git a/source/blender/io/wavefront_obj/tests/obj_import_string_utils_tests.cc b/source/blender/io/wavefront_obj/tests/obj_import_string_utils_tests.cc new file mode 100644 index 00000000000..46e093bb8a7 --- /dev/null +++ b/source/blender/io/wavefront_obj/tests/obj_import_string_utils_tests.cc @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include "obj_import_string_utils.hh" + +#include "testing/testing.h" + +namespace blender::io::obj { + +#define EXPECT_STRREF_EQ(str1, str2) EXPECT_STREQ(str1, std::string(str2).c_str()) + +TEST(obj_import_string_utils, read_next_line) +{ + std::string str = "abc\n \n\nline with \\\ncontinuation\nCRLF ending:\r\na"; + StringRef s = str; + EXPECT_STRREF_EQ("abc", read_next_line(s)); + EXPECT_STRREF_EQ(" ", read_next_line(s)); + EXPECT_STRREF_EQ("", read_next_line(s)); + EXPECT_STRREF_EQ("line with \\\ncontinuation", read_next_line(s)); + EXPECT_STRREF_EQ("CRLF ending:\r", read_next_line(s)); + EXPECT_STRREF_EQ("a", read_next_line(s)); + EXPECT_TRUE(s.is_empty()); +} + +static StringRef drop_whitespace(StringRef s) +{ + return StringRef(drop_whitespace(s.begin(), s.end()), s.end()); +} +static StringRef parse_int(StringRef s, int fallback, int &dst, bool skip_space = true) +{ + return StringRef(parse_int(s.begin(), s.end(), fallback, dst, skip_space), s.end()); +} +static StringRef parse_float(StringRef s, float fallback, float &dst, bool skip_space = true) +{ + return StringRef(parse_float(s.begin(), s.end(), fallback, dst, skip_space), s.end()); +} + +TEST(obj_import_string_utils, drop_whitespace) +{ + /* Empty */ + EXPECT_STRREF_EQ("", drop_whitespace("")); + /* Only whitespace */ + EXPECT_STRREF_EQ("", drop_whitespace(" ")); + EXPECT_STRREF_EQ("", drop_whitespace(" ")); + EXPECT_STRREF_EQ("", drop_whitespace(" \t\n\r ")); + /* Drops leading whitespace */ + EXPECT_STRREF_EQ("a", drop_whitespace(" a")); + EXPECT_STRREF_EQ("a b", drop_whitespace(" a b")); + EXPECT_STRREF_EQ("a b ", drop_whitespace(" a b ")); + /* No leading whitespace */ + EXPECT_STRREF_EQ("c", drop_whitespace("c")); + /* Case with backslash, should be treated as whitespace */ + EXPECT_STRREF_EQ("d", drop_whitespace(" \\ d")); +} + +TEST(obj_import_string_utils, parse_int_valid) +{ + std::string str = "1 -10 \t 1234 1234567890 +7 123a"; + StringRef s = str; + int val; + s = parse_int(s, 0, val); + EXPECT_EQ(1, val); + s = parse_int(s, 0, val); + EXPECT_EQ(-10, val); + s = parse_int(s, 0, val); + EXPECT_EQ(1234, val); + s = parse_int(s, 0, val); + EXPECT_EQ(1234567890, val); + s = parse_int(s, 0, val); + EXPECT_EQ(7, val); + s = parse_int(s, 0, val); + EXPECT_EQ(123, val); + EXPECT_STRREF_EQ("a", s); +} + +TEST(obj_import_string_utils, parse_int_invalid) +{ + int val; + /* Invalid syntax */ + EXPECT_STRREF_EQ("--123", parse_int("--123", -1, val)); + EXPECT_EQ(val, -1); + EXPECT_STRREF_EQ("foobar", parse_int("foobar", -2, val)); + EXPECT_EQ(val, -2); + /* Out of integer range */ + EXPECT_STRREF_EQ(" a", parse_int("1234567890123 a", -3, val)); + EXPECT_EQ(val, -3); + /* Has leading white-space when we don't expect it */ + EXPECT_STRREF_EQ(" 1", parse_int(" 1", -4, val, false)); + EXPECT_EQ(val, -4); +} + +TEST(obj_import_string_utils, parse_float_valid) +{ + std::string str = "1 -10 123.5 -17.125 0.1 1e6 50.0e-1"; + StringRef s = str; + float val; + s = parse_float(s, 0, val); + EXPECT_EQ(1.0f, val); + s = parse_float(s, 0, val); + EXPECT_EQ(-10.0f, val); + s = parse_float(s, 0, val); + EXPECT_EQ(123.5f, val); + s = parse_float(s, 0, val); + EXPECT_EQ(-17.125f, val); + s = parse_float(s, 0, val); + EXPECT_EQ(0.1f, val); + s = parse_float(s, 0, val); + EXPECT_EQ(1.0e6f, val); + s = parse_float(s, 0, val); + EXPECT_EQ(5.0f, val); + EXPECT_TRUE(s.is_empty()); +} + +TEST(obj_import_string_utils, parse_float_invalid) +{ + float val; + /* Invalid syntax */ + EXPECT_STRREF_EQ("_0", parse_float("_0", -1.0f, val)); + EXPECT_EQ(val, -1.0f); + EXPECT_STRREF_EQ("..5", parse_float("..5", -2.0f, val)); + EXPECT_EQ(val, -2.0f); + /* Out of float range. Current float parser (fast_float) + * clamps out of range numbers to +/- infinity, so this + * one gets a +inf instead of fallback -3.0. */ + EXPECT_STRREF_EQ(" a", parse_float("9.0e500 a", -3.0f, val)); + EXPECT_EQ(val, std::numeric_limits<float>::infinity()); + /* Has leading white-space when we don't expect it */ + EXPECT_STRREF_EQ(" 1", parse_float(" 1", -4.0f, val, false)); + EXPECT_EQ(val, -4.0f); +} + +} // 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 index 3d34fb6f9c6..eeb81f5e23e 100644 --- a/source/blender/io/wavefront_obj/tests/obj_importer_tests.cc +++ b/source/blender/io/wavefront_obj/tests/obj_importer_tests.cc @@ -39,6 +39,7 @@ struct Expectation { float3 vert_first, vert_last; float3 normal_first; float2 uv_first; + float4 color_first = {-1, -1, -1, -1}; }; class obj_importer_test : public BlendfileLoadingBaseTest { @@ -55,8 +56,10 @@ class obj_importer_test : public BlendfileLoadingBaseTest { OBJImportParams params; params.clamp_size = 0; - params.forward_axis = OBJ_AXIS_NEGATIVE_Z_FORWARD; - params.up_axis = OBJ_AXIS_Y_UP; + params.forward_axis = IO_AXIS_NEGATIVE_Z; + params.up_axis = IO_AXIS_Y; + params.validate_meshes = true; + params.import_vertex_groups = false; std::string obj_path = blender::tests::flags_test_asset_dir() + "/io_tests/obj/" + path; strncpy(params.filepath, obj_path.c_str(), FILE_MAX - 1); @@ -98,6 +101,15 @@ class obj_importer_test : public BlendfileLoadingBaseTest { 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 (exp.color_first.x >= 0) { + const float4 *colors = (const float4 *)(CustomData_get_layer(&mesh->vdata, + CD_PROP_COLOR)); + EXPECT_TRUE(colors != nullptr); + EXPECT_V4_NEAR(colors[0], exp.color_first, 0.0001f); + } + else { + EXPECT_FALSE(CustomData_has_layer(&mesh->vdata, CD_PROP_COLOR)); + } } if (object->type == OB_CURVES_LEGACY) { Curve *curve = static_cast<Curve *>(DEG_get_evaluated_object(depsgraph, object)->data); @@ -111,7 +123,7 @@ class obj_importer_test : public BlendfileLoadingBaseTest { 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 + /* 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); } @@ -133,7 +145,7 @@ 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", + {"OBcube", OB_MESH, 8, 12, @@ -168,7 +180,7 @@ 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", + {"OBnurbs", OB_CURVES_LEGACY, 12, 0, @@ -184,7 +196,7 @@ 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)}, + {"OBnurbs_curves", OB_CURVES_LEGACY, 4, 0, 4, 0, float3(2, -2, 0), float3(-2, -2, 0)}, {"OBNurbsCurveDiffWeights", OB_CURVES_LEGACY, 4, @@ -211,7 +223,7 @@ 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", + {"OBnurbs_cyclic", OB_CURVES_LEGACY, 31, 0, @@ -262,7 +274,7 @@ 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)}, + {"OBmaterials", OB_MESH, 8, 12, 6, 24, float3(-1, -1, 1), float3(1, -1, -1)}, }; import_and_check("materials.obj", expect, std::size(expect), 4); } @@ -333,7 +345,7 @@ TEST_F(obj_importer_test, import_invalid_syntax) {"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. */ + 10, /* NOTE: right now parses some invalid obj syntax as valid vertices. */ 3, 1, 3, @@ -434,7 +446,17 @@ TEST_F(obj_importer_test, import_all_objects) 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)}, + {"OBVColCube", + OB_MESH, + 8, + 13, + 7, + 26, + float3(13, 1, -1), + float3(11, 1, 1), + float3(0, 0, 1), + float2(0, 0), + float4(0.0f, 0.002125f, 1.0f, 1.0f)}, {"OBUVCube", OB_MESH, 8, @@ -490,4 +512,103 @@ TEST_F(obj_importer_test, import_all_objects) import_and_check("all_objects.obj", expect, std::size(expect), 7); } +TEST_F(obj_importer_test, import_cubes_vertex_colors) +{ + Expectation expect[] = { + {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, + {"OBCubeVertexByte", + OB_MESH, + 8, + 12, + 6, + 24, + float3(1.0f, 1.0f, -1.0f), + float3(-1.0f, -1.0f, 1.0f), + float3(0, 0, 0), + float2(0, 0), + float4(0.846873f, 0.027321f, 0.982123f, 1.0f)}, + {"OBCubeVertexFloat", + OB_MESH, + 8, + 12, + 6, + 24, + float3(3.392028f, 1.0f, -1.0f), + float3(1.392028f, -1.0f, 1.0f), + float3(0, 0, 0), + float2(0, 0), + float4(49.99467f, 0.027321f, 0.982123f, 1.0f)}, + {"OBCubeCornerByte", + OB_MESH, + 8, + 12, + 6, + 24, + float3(1.0f, 1.0f, -3.812445f), + float3(-1.0f, -1.0f, -1.812445f), + float3(0, 0, 0), + float2(0, 0), + float4(0.89627f, 0.036889f, 0.47932f, 1.0f)}, + {"OBCubeCornerFloat", + OB_MESH, + 8, + 12, + 6, + 24, + float3(3.481967f, 1.0f, -3.812445f), + float3(1.481967f, -1.0f, -1.812445f), + float3(0, 0, 0), + float2(0, 0), + float4(1.564582f, 0.039217f, 0.664309f, 1.0f)}, + {"OBCubeMultiColorAttribs", + OB_MESH, + 8, + 12, + 6, + 24, + float3(-4.725068f, -1.0f, 1.0f), + float3(-2.725068f, 1.0f, -1.0f), + float3(0, 0, 0), + float2(0, 0), + float4(0.270498f, 0.47932f, 0.262251f, 1.0f)}, + {"OBCubeNoColors", + OB_MESH, + 8, + 12, + 6, + 24, + float3(-4.550208f, -1.0f, -1.918042f), + float3(-2.550208f, 1.0f, -3.918042f)}, + }; + import_and_check("cubes_vertex_colors.obj", expect, std::size(expect), 0); +} + +TEST_F(obj_importer_test, import_cubes_vertex_colors_mrgb) +{ + Expectation expect[] = {{"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, + {"OBCubeXYZRGB", + OB_MESH, + 8, + 12, + 6, + 24, + float3(1, 1, -1), + float3(-1, -1, 1), + float3(0, 0, 0), + float2(0, 0), + float4(0.6038f, 0.3185f, 0.1329f, 1.0f)}, + {"OBCubeMRGB", + OB_MESH, + 8, + 12, + 6, + 24, + float3(4, 1, -1), + float3(2, -1, 1), + float3(0, 0, 0), + float2(0, 0), + float4(0.8714f, 0.6308f, 0.5271f, 1.0f)}}; + import_and_check("cubes_vertex_colors_mrgb.obj", expect, std::size(expect), 0); +} + } // namespace blender::io::obj |