diff options
Diffstat (limited to 'source/blender/io/wavefront_obj/tests')
-rw-r--r-- | source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc | 417 | ||||
-rw-r--r-- | source/blender/io/wavefront_obj/tests/obj_exporter_tests.hh | 149 |
2 files changed, 566 insertions, 0 deletions
diff --git a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc new file mode 100644 index 00000000000..f12bfd0cea5 --- /dev/null +++ b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc @@ -0,0 +1,417 @@ +/* Apache License, Version 2.0 */ + +#include <fstream> +#include <gtest/gtest.h> +#include <ios> +#include <memory> +#include <sstream> +#include <string> +#include <system_error> + +#include "testing/testing.h" +#include "tests/blendfile_loading_base_test.h" + +#include "BKE_appdir.h" +#include "BKE_blender_version.h" + +#include "BLI_fileops.h" +#include "BLI_index_range.hh" +#include "BLI_string_utf8.h" +#include "BLI_vector.hh" + +#include "DEG_depsgraph.h" + +#include "obj_export_file_writer.hh" +#include "obj_export_mesh.hh" +#include "obj_export_nurbs.hh" +#include "obj_exporter.hh" + +#include "obj_exporter_tests.hh" + +namespace blender::io::obj { + +/* This is also the test name. */ +class obj_exporter_test : public BlendfileLoadingBaseTest { + public: + /** + * \param filepath: relative to "tests" directory. + */ + bool load_file_and_depsgraph(const std::string &filepath, + const eEvaluationMode eval_mode = DAG_EVAL_VIEWPORT) + { + if (!blendfile_load(filepath.c_str())) { + return false; + } + depsgraph_create(eval_mode); + return true; + } +}; + +const std::string all_objects_file = "io_tests/blend_scene/all_objects.blend"; +const std::string all_curve_objects_file = "io_tests/blend_scene/all_curves.blend"; + +TEST_F(obj_exporter_test, filter_objects_curves_as_mesh) +{ + OBJExportParamsDefault _export; + if (!load_file_and_depsgraph(all_objects_file)) { + ADD_FAILURE(); + return; + } + auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)}; + EXPECT_EQ(objmeshes.size(), 17); + EXPECT_EQ(objcurves.size(), 0); +} + +TEST_F(obj_exporter_test, filter_objects_curves_as_nurbs) +{ + OBJExportParamsDefault _export; + if (!load_file_and_depsgraph(all_objects_file)) { + ADD_FAILURE(); + return; + } + _export.params.export_curves_as_nurbs = true; + auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)}; + EXPECT_EQ(objmeshes.size(), 16); + EXPECT_EQ(objcurves.size(), 2); +} + +TEST_F(obj_exporter_test, filter_objects_selected) +{ + OBJExportParamsDefault _export; + if (!load_file_and_depsgraph(all_objects_file)) { + ADD_FAILURE(); + return; + } + _export.params.export_selected_objects = true; + _export.params.export_curves_as_nurbs = true; + auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)}; + EXPECT_EQ(objmeshes.size(), 1); + EXPECT_EQ(objcurves.size(), 0); +} + +TEST(obj_exporter_utils, append_negative_frame_to_filename) +{ + const char path_original[FILE_MAX] = "/my_file.obj"; + const char path_truth[FILE_MAX] = "/my_file-123.obj"; + const int frame = -123; + char path_with_frame[FILE_MAX] = {0}; + const bool ok = append_frame_to_filename(path_original, frame, path_with_frame); + EXPECT_TRUE(ok); + EXPECT_EQ_ARRAY(path_with_frame, path_truth, BLI_strlen_utf8(path_truth)); +} + +TEST(obj_exporter_utils, append_positive_frame_to_filename) +{ + const char path_original[FILE_MAX] = "/my_file.obj"; + const char path_truth[FILE_MAX] = "/my_file123.obj"; + const int frame = 123; + char path_with_frame[FILE_MAX] = {0}; + const bool ok = append_frame_to_filename(path_original, frame, path_with_frame); + EXPECT_TRUE(ok); + EXPECT_EQ_ARRAY(path_with_frame, path_truth, BLI_strlen_utf8(path_truth)); +} + +TEST_F(obj_exporter_test, curve_nurbs_points) +{ + if (!load_file_and_depsgraph(all_curve_objects_file)) { + ADD_FAILURE(); + return; + } + + OBJExportParamsDefault _export; + _export.params.export_curves_as_nurbs = true; + auto [objmeshes_unused, objcurves]{filter_supported_objects(depsgraph, _export.params)}; + + for (auto &objcurve : objcurves) { + if (all_nurbs_truth.count(objcurve->get_curve_name()) != 1) { + ADD_FAILURE(); + return; + } + const NurbsObject *const nurbs_truth = all_nurbs_truth.at(objcurve->get_curve_name()).get(); + EXPECT_EQ(objcurve->total_splines(), nurbs_truth->total_splines()); + for (int spline_index : IndexRange(objcurve->total_splines())) { + EXPECT_EQ(objcurve->total_spline_vertices(spline_index), + nurbs_truth->total_spline_vertices(spline_index)); + EXPECT_EQ(objcurve->get_nurbs_degree(spline_index), + nurbs_truth->get_nurbs_degree(spline_index)); + EXPECT_EQ(objcurve->total_spline_control_points(spline_index), + nurbs_truth->total_spline_control_points(spline_index)); + } + } +} + +TEST_F(obj_exporter_test, curve_coordinates) +{ + if (!load_file_and_depsgraph(all_curve_objects_file)) { + ADD_FAILURE(); + return; + } + + OBJExportParamsDefault _export; + _export.params.export_curves_as_nurbs = true; + auto [objmeshes_unused, objcurves]{filter_supported_objects(depsgraph, _export.params)}; + + for (auto &objcurve : objcurves) { + if (all_nurbs_truth.count(objcurve->get_curve_name()) != 1) { + ADD_FAILURE(); + return; + } + const NurbsObject *const nurbs_truth = all_nurbs_truth.at(objcurve->get_curve_name()).get(); + EXPECT_EQ(objcurve->total_splines(), nurbs_truth->total_splines()); + for (int spline_index : IndexRange(objcurve->total_splines())) { + for (int vertex_index : IndexRange(objcurve->total_spline_vertices(spline_index))) { + EXPECT_V3_NEAR(objcurve->vertex_coordinates( + spline_index, vertex_index, _export.params.scaling_factor), + nurbs_truth->vertex_coordinates(spline_index, vertex_index), + 0.000001f); + } + } + } +} + +static std::unique_ptr<OBJWriter> init_writer(const OBJExportParams ¶ms, + const std::string out_filepath) +{ + try { + auto writer = std::make_unique<OBJWriter>(out_filepath.c_str(), params); + return writer; + } + catch (const std::system_error &ex) { + std::cerr << ex.code().category().name() << ": " << ex.what() << ": " << ex.code().message() + << std::endl; + return nullptr; + } +} + +/* The following is relative to BKE_tempdir_base. */ +const char *const temp_file_path = "output.OBJ"; + +static std::string read_temp_file_in_string(const std::string &file_path) +{ + std::ifstream temp_stream(file_path); + std::ostringstream input_ss; + input_ss << temp_stream.rdbuf(); + return input_ss.str(); +} + +TEST(obj_exporter_writer, header) +{ + /* Because testing doesn't fully initialize Blender, we need the following. */ + BKE_tempdir_init(NULL); + std::string out_file_path = blender::tests::flags_test_release_dir() + "/" + temp_file_path; + { + OBJExportParamsDefault _export; + std::unique_ptr<OBJWriter> writer = init_writer(_export.params, out_file_path); + if (!writer) { + ADD_FAILURE(); + return; + } + writer->write_header(); + } + const std::string result = read_temp_file_in_string(out_file_path); + using namespace std::string_literals; + ASSERT_EQ(result, "# Blender "s + BKE_blender_version_string() + "\n" + "# www.blender.org\n"); + BLI_delete(out_file_path.c_str(), false, false); +} + +TEST(obj_exporter_writer, mtllib) +{ + std::string out_file_path = blender::tests::flags_test_release_dir() + "/" + temp_file_path; + { + OBJExportParamsDefault _export; + std::unique_ptr<OBJWriter> writer = init_writer(_export.params, out_file_path); + if (!writer) { + ADD_FAILURE(); + return; + } + writer->write_mtllib_name("/Users/blah.mtl"); + writer->write_mtllib_name("\\C:\\blah.mtl"); + } + const std::string result = read_temp_file_in_string(out_file_path); + ASSERT_EQ(result, "mtllib blah.mtl\nmtllib blah.mtl\n"); +} + +/* Return true if string #a and string #b are equal after their first newline. */ +static bool strings_equal_after_first_lines(const std::string &a, const std::string &b) +{ + /* If `dbg_level > 0` then a failing test will print context around the first mismatch. */ + const bool dbg_level = 0; + const size_t a_len = a.size(); + const size_t b_len = b.size(); + const size_t a_next = a.find_first_of('\n'); + const size_t b_next = b.find_first_of('\n'); + if (a_next == std::string::npos || b_next == std::string::npos) { + if (dbg_level > 0) { + std::cout << "Couldn't find newline in one of args\n"; + } + return false; + } + if (dbg_level > 0) { + if (a.compare(a_next, a_len - a_next, b, b_next, b_len - b_next) != 0) { + for (int i = 0; i < a_len - a_next && i < b_len - b_next; ++i) { + if (a[a_next + i] != b[b_next + i]) { + std::cout << "Difference found at pos " << a_next + i << " of a\n"; + std::cout << "a: " << a.substr(a_next + i, 100) << " ...\n"; + std::cout << "b: " << b.substr(b_next + i, 100) << " ... \n"; + return false; + } + } + } + else { + return true; + } + } + return a.compare(a_next, a_len - a_next, b, b_next, b_len - b_next) == 0; +} + +/* From here on, tests are whole file tests, testing for golden output. */ +class obj_exporter_regression_test : public obj_exporter_test { + public: + /** + * Export the given blend file with the given parameters and + * test to see if it matches a golden file (ignoring any difference in Blender version number). + * \param blendfile: input, relative to "tests" directory. + * \param golden_obj: expected output, relative to "tests" directory. + * \param params: the parameters to be used for export. + */ + void compare_obj_export_to_golden(const std::string &blendfile, + const std::string &golden_obj, + const std::string &golden_mtl, + OBJExportParams ¶ms) + { + if (!load_file_and_depsgraph(blendfile)) { + return; + } + /* Because testing doesn't fully initialize Blender, we need the following. */ + BKE_tempdir_init(NULL); + std::string tempdir = std::string(BKE_tempdir_base()); + std::string out_file_path = tempdir + BLI_path_basename(golden_obj.c_str()); + strncpy(params.filepath, out_file_path.c_str(), FILE_MAX); + params.blen_filepath = blendfile.c_str(); + export_frame(depsgraph, params, out_file_path.c_str()); + std::string output_str = read_temp_file_in_string(out_file_path); + + std::string golden_file_path = blender::tests::flags_test_asset_dir() + "/" + golden_obj; + std::string golden_str = read_temp_file_in_string(golden_file_path); + ASSERT_TRUE(strings_equal_after_first_lines(output_str, golden_str)); + BLI_delete(out_file_path.c_str(), false, false); + if (!golden_mtl.empty()) { + std::string out_mtl_file_path = tempdir + BLI_path_basename(golden_mtl.c_str()); + std::string output_mtl_str = read_temp_file_in_string(out_mtl_file_path); + std::string golden_mtl_file_path = blender::tests::flags_test_asset_dir() + "/" + golden_mtl; + std::string golden_mtl_str = read_temp_file_in_string(golden_mtl_file_path); + ASSERT_TRUE(strings_equal_after_first_lines(output_mtl_str, golden_mtl_str)); + BLI_delete(out_mtl_file_path.c_str(), false, false); + } + } +}; + +TEST_F(obj_exporter_regression_test, all_tris) +{ + OBJExportParamsDefault _export; + compare_obj_export_to_golden("io_tests/blend_geometry/all_tris.blend", + "io_tests/obj/all_tris.obj", + "io_tests/obj/all_tris.mtl", + _export.params); +} + +TEST_F(obj_exporter_regression_test, all_quads) +{ + OBJExportParamsDefault _export; + _export.params.scaling_factor = 2.0f; + _export.params.export_materials = false; + compare_obj_export_to_golden( + "io_tests/blend_geometry/all_quads.blend", "io_tests/obj/all_quads.obj", "", _export.params); +} + +TEST_F(obj_exporter_regression_test, fgons) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + compare_obj_export_to_golden( + "io_tests/blend_geometry/fgons.blend", "io_tests/obj/fgons.obj", "", _export.params); +} + +TEST_F(obj_exporter_regression_test, edges) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + compare_obj_export_to_golden( + "io_tests/blend_geometry/edges.blend", "io_tests/obj/edges.obj", "", _export.params); +} + +TEST_F(obj_exporter_regression_test, vertices) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + compare_obj_export_to_golden( + "io_tests/blend_geometry/vertices.blend", "io_tests/obj/vertices.obj", "", _export.params); +} + +TEST_F(obj_exporter_regression_test, nurbs_as_nurbs) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + _export.params.export_curves_as_nurbs = true; + compare_obj_export_to_golden( + "io_tests/blend_geometry/nurbs.blend", "io_tests/obj/nurbs.obj", "", _export.params); +} + +TEST_F(obj_exporter_regression_test, nurbs_as_mesh) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + _export.params.export_curves_as_nurbs = false; + compare_obj_export_to_golden( + "io_tests/blend_geometry/nurbs.blend", "io_tests/obj/nurbs_mesh.obj", "", _export.params); +} + +TEST_F(obj_exporter_regression_test, cube_all_data_triangulated) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + _export.params.export_triangulated_mesh = true; + compare_obj_export_to_golden("io_tests/blend_geometry/cube_all_data.blend", + "io_tests/obj/cube_all_data_triangulated.obj", + "", + _export.params); +} + +TEST_F(obj_exporter_regression_test, suzanne_all_data) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_materials = false; + _export.params.export_smooth_groups = true; + compare_obj_export_to_golden("io_tests/blend_geometry/suzanne_all_data.blend", + "io_tests/obj/suzanne_all_data.obj", + "", + _export.params); +} + +TEST_F(obj_exporter_regression_test, all_objects) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = OBJ_AXIS_Y_FORWARD; + _export.params.up_axis = OBJ_AXIS_Z_UP; + _export.params.export_smooth_groups = true; + compare_obj_export_to_golden("io_tests/blend_scene/all_objects.blend", + "io_tests/obj/all_objects.obj", + "io_tests/obj/all_objects.mtl", + _export.params); +} + +} // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.hh b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.hh new file mode 100644 index 00000000000..def70eff0ee --- /dev/null +++ b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.hh @@ -0,0 +1,149 @@ +/* Apache License, Version 2.0 */ + +/** + * This file contains default values for several items like + * vertex coordinates, export parameters, MTL values etc. + */ + +#pragma once + +#include <array> +#include <gtest/gtest.h> +#include <string> +#include <vector> + +#include "IO_wavefront_obj.h" + +namespace blender::io::obj { + +using array_float_3 = std::array<float, 3>; + +/** + * This matches #OBJCurve's member functions, except that all the numbers and names are known + * constants. Used to store expected values of NURBS Curve sobjects. + */ +class NurbsObject { + private: + std::string nurbs_name_; + /* The indices in these vectors are spline indices. */ + std::vector<std::vector<array_float_3>> coordinates_; + std::vector<int> degrees_; + std::vector<int> control_points_; + + public: + NurbsObject(const std::string nurbs_name, + const std::vector<std::vector<array_float_3>> coordinates, + const std::vector<int> degrees, + const std::vector<int> control_points) + : nurbs_name_(nurbs_name), + coordinates_(coordinates), + degrees_(degrees), + control_points_(control_points) + { + } + + int total_splines() const + { + return coordinates_.size(); + } + + int total_spline_vertices(const int spline_index) const + { + if (spline_index >= coordinates_.size()) { + ADD_FAILURE(); + return 0; + } + return coordinates_[spline_index].size(); + } + + const float *vertex_coordinates(const int spline_index, const int vertex_index) const + { + return coordinates_[spline_index][vertex_index].data(); + } + + int get_nurbs_degree(const int spline_index) const + { + return degrees_[spline_index]; + } + + int total_spline_control_points(const int spline_index) const + { + return control_points_[spline_index]; + } +}; + +struct OBJExportParamsDefault { + OBJExportParams params; + OBJExportParamsDefault() + { + params.filepath[0] = '\0'; + params.blen_filepath = ""; + params.export_animation = false; + params.start_frame = 0; + params.end_frame = 1; + + params.forward_axis = OBJ_AXIS_NEGATIVE_Z_FORWARD; + params.up_axis = OBJ_AXIS_Y_UP; + params.scaling_factor = 1.f; + + params.export_eval_mode = DAG_EVAL_VIEWPORT; + params.export_selected_objects = false; + params.export_uv = true; + params.export_normals = true; + params.export_materials = true; + params.export_triangulated_mesh = false; + params.export_curves_as_nurbs = false; + + params.export_object_groups = false; + params.export_material_groups = false; + params.export_vertex_groups = false; + params.export_smooth_groups = true; + params.smooth_groups_bitflags = false; + } +}; + +const std::vector<std::vector<array_float_3>> coordinates_NurbsCurve{ + {{6.94742, 0.000000, 0.000000}, + {7.44742, 0.000000, -1.000000}, + {9.44742, 0.000000, -1.000000}, + {9.94742, 0.000000, 0.000000}}}; +const std::vector<std::vector<array_float_3>> coordinates_NurbsCircle{ + {{11.463165, 0.000000, 1.000000}, + {10.463165, 0.000000, 1.000000}, + {10.463165, 0.000000, 0.000000}, + {10.463165, 0.000000, -1.000000}, + {11.463165, 0.000000, -1.000000}, + {12.463165, 0.000000, -1.000000}, + {12.463165, 0.000000, 0.000000}, + {12.463165, 0.000000, 1.000000}}}; +const std::vector<std::vector<array_float_3>> coordinates_NurbsPathCurve{ + {{13.690557, 0.000000, 0.000000}, + {14.690557, 0.000000, 0.000000}, + {15.690557, 0.000000, 0.000000}, + {16.690557, 0.000000, 0.000000}, + {17.690557, 0.000000, 0.000000}}, + {{14.192808, 0.000000, 0.000000}, + {14.692808, 0.000000, -1.000000}, + {16.692808, 0.000000, -1.000000}, + {17.192808, 0.000000, 0.000000}}}; + +const std::map<std::string, std::unique_ptr<NurbsObject>> all_nurbs_truth = []() { + std::map<std::string, std::unique_ptr<NurbsObject>> all_nurbs; + all_nurbs.emplace( + "NurbsCurve", + /* Name, coordinates, degrees of splines, control points of splines. */ + std::make_unique<NurbsObject>( + "NurbsCurve", coordinates_NurbsCurve, std::vector<int>{3}, std::vector<int>{4})); + all_nurbs.emplace( + "NurbsCircle", + std::make_unique<NurbsObject>( + "NurbsCircle", coordinates_NurbsCircle, std::vector<int>{3}, std::vector<int>{11})); + /* This is actually an Object containing a NurbsPath and a NurbsCurve spline. */ + all_nurbs.emplace("NurbsPathCurve", + std::make_unique<NurbsObject>("NurbsPathCurve", + coordinates_NurbsPathCurve, + std::vector<int>{3, 3}, + std::vector<int>{5, 4})); + return all_nurbs; +}(); +} // namespace blender::io::obj |