Blender  V3.3
obj_exporter_tests.cc
Go to the documentation of this file.
1 /* SPDX-License-Identifier: Apache-2.0 */
2 
3 #include <gtest/gtest.h>
4 #include <ios>
5 #include <memory>
6 #include <string>
7 #include <system_error>
8 
9 #include "testing/testing.h"
11 
12 #include "BKE_appdir.h"
13 #include "BKE_blender_version.h"
14 #include "BKE_main.h"
15 
16 #include "BLI_fileops.h"
17 #include "BLI_index_range.hh"
18 #include "BLI_string_utf8.h"
19 #include "BLI_vector.hh"
20 
21 #include "BLO_readfile.h"
22 
23 #include "DEG_depsgraph.h"
24 
26 #include "obj_export_mesh.hh"
27 #include "obj_export_nurbs.hh"
28 #include "obj_exporter.hh"
29 
30 #include "obj_exporter_tests.hh"
31 
32 namespace blender::io::obj {
33 /* Set this true to keep comparison-failing test output in temp file directory. */
34 constexpr bool save_failing_test_output = false;
35 
36 /* This is also the test name. */
38  public:
42  bool load_file_and_depsgraph(const std::string &filepath,
43  const eEvaluationMode eval_mode = DAG_EVAL_VIEWPORT)
44  {
45  if (!blendfile_load(filepath.c_str())) {
46  return false;
47  }
48  depsgraph_create(eval_mode);
49  return true;
50  }
51 };
52 
53 const std::string all_objects_file = "io_tests/blend_scene/all_objects.blend";
54 
55 TEST_F(obj_exporter_test, filter_objects_curves_as_mesh)
56 {
57  OBJExportParamsDefault _export;
58  if (!load_file_and_depsgraph(all_objects_file)) {
59  ADD_FAILURE();
60  return;
61  }
62  auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)};
63  EXPECT_EQ(objmeshes.size(), 20);
64  EXPECT_EQ(objcurves.size(), 0);
65 }
66 
67 TEST_F(obj_exporter_test, filter_objects_curves_as_nurbs)
68 {
69  OBJExportParamsDefault _export;
70  if (!load_file_and_depsgraph(all_objects_file)) {
71  ADD_FAILURE();
72  return;
73  }
74  _export.params.export_curves_as_nurbs = true;
75  auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)};
76  EXPECT_EQ(objmeshes.size(), 18);
77  EXPECT_EQ(objcurves.size(), 3);
78 }
79 
80 TEST_F(obj_exporter_test, filter_objects_selected)
81 {
82  OBJExportParamsDefault _export;
83  if (!load_file_and_depsgraph(all_objects_file)) {
84  ADD_FAILURE();
85  return;
86  }
87  _export.params.export_selected_objects = true;
88  _export.params.export_curves_as_nurbs = true;
89  auto [objmeshes, objcurves]{filter_supported_objects(depsgraph, _export.params)};
90  EXPECT_EQ(objmeshes.size(), 1);
91  EXPECT_EQ(objcurves.size(), 0);
92 }
93 
94 TEST(obj_exporter_utils, append_negative_frame_to_filename)
95 {
96  const char path_original[FILE_MAX] = "/my_file.obj";
97  const char path_truth[FILE_MAX] = "/my_file-123.obj";
98  const int frame = -123;
99  char path_with_frame[FILE_MAX] = {0};
100  const bool ok = append_frame_to_filename(path_original, frame, path_with_frame);
101  EXPECT_TRUE(ok);
102  EXPECT_EQ_ARRAY(path_with_frame, path_truth, BLI_strlen_utf8(path_truth));
103 }
104 
105 TEST(obj_exporter_utils, append_positive_frame_to_filename)
106 {
107  const char path_original[FILE_MAX] = "/my_file.obj";
108  const char path_truth[FILE_MAX] = "/my_file123.obj";
109  const int frame = 123;
110  char path_with_frame[FILE_MAX] = {0};
111  const bool ok = append_frame_to_filename(path_original, frame, path_with_frame);
112  EXPECT_TRUE(ok);
113  EXPECT_EQ_ARRAY(path_with_frame, path_truth, BLI_strlen_utf8(path_truth));
114 }
115 
116 static std::string read_temp_file_in_string(const std::string &file_path)
117 {
118  std::string res;
119  size_t buffer_len;
120  void *buffer = BLI_file_read_text_as_mem(file_path.c_str(), 0, &buffer_len);
121  if (buffer != nullptr) {
122  res.assign((const char *)buffer, buffer_len);
123  MEM_freeN(buffer);
124  }
125  return res;
126 }
127 
128 class ObjExporterWriterTest : public testing::Test {
129  protected:
130  void SetUp() override
131  {
132  BKE_tempdir_init("");
133  }
134 
135  void TearDown() override
136  {
138  }
139 
140  std::string get_temp_obj_filename()
141  {
142  /* Use Latin Capital Letter A with Ogonek, Cyrillic Capital Letter Zhe
143  * at the end, to test I/O on non-English file names. */
144  const char *const temp_file_path = "output\xc4\x84\xd0\x96.OBJ";
145 
146  return std::string(BKE_tempdir_session()) + SEP_STR + std::string(temp_file_path);
147  }
148 
149  std::unique_ptr<OBJWriter> init_writer(const OBJExportParams &params,
150  const std::string out_filepath)
151  {
152  try {
153  auto writer = std::make_unique<OBJWriter>(out_filepath.c_str(), params);
154  return writer;
155  }
156  catch (const std::system_error &ex) {
157  std::cerr << ex.code().category().name() << ": " << ex.what() << ": " << ex.code().message()
158  << std::endl;
159  return nullptr;
160  }
161  }
162 };
163 
165 {
166  /* Because testing doesn't fully initialize Blender, we need the following. */
167  BKE_tempdir_init(nullptr);
168  std::string out_file_path = get_temp_obj_filename();
169  {
170  OBJExportParamsDefault _export;
171  std::unique_ptr<OBJWriter> writer = init_writer(_export.params, out_file_path);
172  if (!writer) {
173  ADD_FAILURE();
174  return;
175  }
176  writer->write_header();
177  }
178  const std::string result = read_temp_file_in_string(out_file_path);
179  using namespace std::string_literals;
180  ASSERT_EQ(result, "# Blender "s + BKE_blender_version_string() + "\n" + "# www.blender.org\n");
181 }
182 
184 {
185  std::string out_file_path = get_temp_obj_filename();
186  {
187  OBJExportParamsDefault _export;
188  std::unique_ptr<OBJWriter> writer = init_writer(_export.params, out_file_path);
189  if (!writer) {
190  ADD_FAILURE();
191  return;
192  }
193  writer->write_mtllib_name("/Users/blah.mtl");
194  writer->write_mtllib_name("\\C:\\blah.mtl");
195  }
196  const std::string result = read_temp_file_in_string(out_file_path);
197  ASSERT_EQ(result, "mtllib blah.mtl\nmtllib blah.mtl\n");
198 }
199 
200 TEST(obj_exporter_writer, format_handler_buffer_chunking)
201 {
202  /* Use a tiny buffer chunk size, so that the test below ends up creating several blocks. */
208  h.write<eOBJSyntaxElement::object_name>("012345678901234567890123456789abcd");
214 
215  size_t got_blocks = h.get_block_count();
216  ASSERT_EQ(got_blocks, 7);
217 
218  std::string got_string = h.get_as_string();
219  using namespace std::string_literals;
220  const char *expected = R"(o abc
221 o abcd
222 o abcde
223 o abcdef
224 o 012345678901234567890123456789abcd
225 o 123
226 curv 0.0 1.0
227 parm u 0.0
228 )";
229  ASSERT_EQ(got_string, expected);
230 }
231 
232 /* Return true if string #a and string #b are equal after their first newline. */
233 static bool strings_equal_after_first_lines(const std::string &a, const std::string &b)
234 {
235  const size_t a_len = a.size();
236  const size_t b_len = b.size();
237  const size_t a_next = a.find_first_of('\n');
238  const size_t b_next = b.find_first_of('\n');
239  if (a_next == std::string::npos || b_next == std::string::npos) {
240  std::cout << "Couldn't find newline in one of args\n";
241  return false;
242  }
243  if (a.compare(a_next, a_len - a_next, b, b_next, b_len - b_next) != 0) {
244  for (int i = 0; i < a_len - a_next && i < b_len - b_next; ++i) {
245  if (a[a_next + i] != b[b_next + i]) {
246  std::cout << "Difference found at pos " << a_next + i << " of a\n";
247  std::cout << "a: " << a.substr(a_next + i, 100) << " ...\n";
248  std::cout << "b: " << b.substr(b_next + i, 100) << " ... \n";
249  return false;
250  }
251  }
252  return false;
253  }
254  return true;
255 }
256 
257 /* From here on, tests are whole file tests, testing for golden output. */
259  public:
267  void compare_obj_export_to_golden(const std::string &blendfile,
268  const std::string &golden_obj,
269  const std::string &golden_mtl,
271  {
272  if (!load_file_and_depsgraph(blendfile)) {
273  return;
274  }
275  /* Because testing doesn't fully initialize Blender, we need the following. */
276  BKE_tempdir_init(nullptr);
277  std::string tempdir = std::string(BKE_tempdir_base());
278  std::string out_file_path = tempdir + BLI_path_basename(golden_obj.c_str());
279  strncpy(params.filepath, out_file_path.c_str(), FILE_MAX - 1);
280  params.blen_filepath = bfile->main->filepath;
281  std::string golden_file_path = blender::tests::flags_test_asset_dir() + "/" + golden_obj;
282  BLI_split_dir_part(golden_file_path.c_str(), params.file_base_for_tests, PATH_MAX);
283  export_frame(depsgraph, params, out_file_path.c_str());
284  std::string output_str = read_temp_file_in_string(out_file_path);
285 
286  std::string golden_str = read_temp_file_in_string(golden_file_path);
287  bool are_equal = strings_equal_after_first_lines(output_str, golden_str);
288  if (save_failing_test_output && !are_equal) {
289  printf("failing test output in %s\n", out_file_path.c_str());
290  }
291  ASSERT_TRUE(are_equal);
292  if (!save_failing_test_output || are_equal) {
293  BLI_delete(out_file_path.c_str(), false, false);
294  }
295  if (!golden_mtl.empty()) {
296  std::string out_mtl_file_path = tempdir + BLI_path_basename(golden_mtl.c_str());
297  std::string output_mtl_str = read_temp_file_in_string(out_mtl_file_path);
298  std::string golden_mtl_file_path = blender::tests::flags_test_asset_dir() + "/" + golden_mtl;
299  std::string golden_mtl_str = read_temp_file_in_string(golden_mtl_file_path);
300  are_equal = strings_equal_after_first_lines(output_mtl_str, golden_mtl_str);
301  if (save_failing_test_output && !are_equal) {
302  printf("failing test output in %s\n", out_mtl_file_path.c_str());
303  }
304  ASSERT_TRUE(are_equal);
305  if (!save_failing_test_output || are_equal) {
306  BLI_delete(out_mtl_file_path.c_str(), false, false);
307  }
308  }
309  }
310 };
311 
313 {
314  OBJExportParamsDefault _export;
315  compare_obj_export_to_golden("io_tests/blend_geometry/all_tris.blend",
316  "io_tests/obj/all_tris.obj",
317  "io_tests/obj/all_tris.mtl",
318  _export.params);
319 }
320 
322 {
323  OBJExportParamsDefault _export;
324  _export.params.scaling_factor = 2.0f;
325  _export.params.export_materials = false;
326  compare_obj_export_to_golden(
327  "io_tests/blend_geometry/all_quads.blend", "io_tests/obj/all_quads.obj", "", _export.params);
328 }
329 
331 {
332  OBJExportParamsDefault _export;
333  _export.params.forward_axis = IO_AXIS_Y;
334  _export.params.up_axis = IO_AXIS_Z;
335  _export.params.export_materials = false;
336  compare_obj_export_to_golden(
337  "io_tests/blend_geometry/fgons.blend", "io_tests/obj/fgons.obj", "", _export.params);
338 }
339 
341 {
342  OBJExportParamsDefault _export;
343  _export.params.forward_axis = IO_AXIS_Y;
344  _export.params.up_axis = IO_AXIS_Z;
345  _export.params.export_materials = false;
346  compare_obj_export_to_golden(
347  "io_tests/blend_geometry/edges.blend", "io_tests/obj/edges.obj", "", _export.params);
348 }
349 
351 {
352  OBJExportParamsDefault _export;
353  _export.params.forward_axis = IO_AXIS_Y;
354  _export.params.up_axis = IO_AXIS_Z;
355  _export.params.export_materials = false;
356  compare_obj_export_to_golden(
357  "io_tests/blend_geometry/vertices.blend", "io_tests/obj/vertices.obj", "", _export.params);
358 }
359 
361 {
362  OBJExportParamsDefault _export;
363  _export.params.export_materials = false;
364  compare_obj_export_to_golden("io_tests/blend_geometry/non_uniform_scale.blend",
365  "io_tests/obj/non_uniform_scale.obj",
366  "",
367  _export.params);
368 }
369 
371 {
372  OBJExportParamsDefault _export;
373  _export.params.forward_axis = IO_AXIS_Y;
374  _export.params.up_axis = IO_AXIS_Z;
375  _export.params.export_materials = false;
376  _export.params.export_curves_as_nurbs = true;
377  compare_obj_export_to_golden(
378  "io_tests/blend_geometry/nurbs.blend", "io_tests/obj/nurbs.obj", "", _export.params);
379 }
380 
381 TEST_F(obj_exporter_regression_test, nurbs_curves_as_nurbs)
382 {
383  OBJExportParamsDefault _export;
384  _export.params.forward_axis = IO_AXIS_Y;
385  _export.params.up_axis = IO_AXIS_Z;
386  _export.params.export_materials = false;
387  _export.params.export_curves_as_nurbs = true;
388  compare_obj_export_to_golden("io_tests/blend_geometry/nurbs_curves.blend",
389  "io_tests/obj/nurbs_curves.obj",
390  "",
391  _export.params);
392 }
393 
395 {
396  OBJExportParamsDefault _export;
397  _export.params.forward_axis = IO_AXIS_Y;
398  _export.params.up_axis = IO_AXIS_Z;
399  _export.params.export_materials = false;
400  _export.params.export_curves_as_nurbs = false;
401  compare_obj_export_to_golden(
402  "io_tests/blend_geometry/nurbs.blend", "io_tests/obj/nurbs_mesh.obj", "", _export.params);
403 }
404 
405 TEST_F(obj_exporter_regression_test, cube_all_data_triangulated)
406 {
407  OBJExportParamsDefault _export;
408  _export.params.forward_axis = IO_AXIS_Y;
409  _export.params.up_axis = IO_AXIS_Z;
410  _export.params.export_materials = false;
411  _export.params.export_triangulated_mesh = true;
412  compare_obj_export_to_golden("io_tests/blend_geometry/cube_all_data.blend",
413  "io_tests/obj/cube_all_data_triangulated.obj",
414  "",
415  _export.params);
416 }
417 
419 {
420  OBJExportParamsDefault _export;
421  _export.params.forward_axis = IO_AXIS_Y;
422  _export.params.up_axis = IO_AXIS_Z;
423  _export.params.export_materials = false;
424  compare_obj_export_to_golden("io_tests/blend_geometry/cube_normal_edit.blend",
425  "io_tests/obj/cube_normal_edit.obj",
426  "",
427  _export.params);
428 }
429 
431 {
432  OBJExportParamsDefault _export;
433  _export.params.export_materials = false;
434  _export.params.export_normals = false;
435  _export.params.export_uv = false;
436  _export.params.export_vertex_groups = true;
437  compare_obj_export_to_golden("io_tests/blend_geometry/cube_vertex_groups.blend",
438  "io_tests/obj/cube_vertex_groups.obj",
439  "",
440  _export.params);
441 }
442 
444 {
445  OBJExportParamsDefault _export;
446  _export.params.export_materials = false;
447  _export.params.scaling_factor = 2.0f;
448  compare_obj_export_to_golden("io_tests/blend_geometry/cubes_positioned.blend",
449  "io_tests/obj/cubes_positioned.obj",
450  "",
451  _export.params);
452 }
453 
455 {
456  OBJExportParamsDefault _export;
457  _export.params.export_colors = true;
458  _export.params.export_normals = false;
459  _export.params.export_uv = false;
460  _export.params.export_materials = false;
461  compare_obj_export_to_golden("io_tests/blend_geometry/cubes_vertex_colors.blend",
462  "io_tests/obj/cubes_vertex_colors.obj",
463  "",
464  _export.params);
465 }
466 
467 TEST_F(obj_exporter_regression_test, cubes_with_textures_strip)
468 {
469  OBJExportParamsDefault _export;
471  compare_obj_export_to_golden("io_tests/blend_geometry/cubes_with_textures.blend",
472  "io_tests/obj/cubes_with_textures.obj",
473  "io_tests/obj/cubes_with_textures.mtl",
474  _export.params);
475 }
476 
477 TEST_F(obj_exporter_regression_test, cubes_with_textures_relative)
478 {
479  OBJExportParamsDefault _export;
481  compare_obj_export_to_golden("io_tests/blend_geometry/cubes_with_textures.blend",
482  "io_tests/obj/cubes_with_textures_rel.obj",
483  "io_tests/obj/cubes_with_textures_rel.mtl",
484  _export.params);
485 }
486 
488 {
489  OBJExportParamsDefault _export;
490  _export.params.forward_axis = IO_AXIS_Y;
491  _export.params.up_axis = IO_AXIS_Z;
492  _export.params.export_materials = false;
493  _export.params.export_smooth_groups = true;
494  compare_obj_export_to_golden("io_tests/blend_geometry/suzanne_all_data.blend",
495  "io_tests/obj/suzanne_all_data.obj",
496  "",
497  _export.params);
498 }
499 
501 {
502  OBJExportParamsDefault _export;
503  _export.params.export_materials = false;
504  compare_obj_export_to_golden(
505  "io_tests/blend_scene/all_curves.blend", "io_tests/obj/all_curves.obj", "", _export.params);
506 }
507 
509 {
510  OBJExportParamsDefault _export;
511  _export.params.export_materials = false;
512  _export.params.export_curves_as_nurbs = true;
513  compare_obj_export_to_golden("io_tests/blend_scene/all_curves.blend",
514  "io_tests/obj/all_curves_as_nurbs.obj",
515  "",
516  _export.params);
517 }
518 
520 {
521  OBJExportParamsDefault _export;
522  _export.params.forward_axis = IO_AXIS_Y;
523  _export.params.up_axis = IO_AXIS_Z;
524  _export.params.export_smooth_groups = true;
525  _export.params.export_colors = true;
526  compare_obj_export_to_golden("io_tests/blend_scene/all_objects.blend",
527  "io_tests/obj/all_objects.obj",
528  "io_tests/obj/all_objects.mtl",
529  _export.params);
530 }
531 
532 TEST_F(obj_exporter_regression_test, all_objects_mat_groups)
533 {
534  OBJExportParamsDefault _export;
535  _export.params.forward_axis = IO_AXIS_Y;
536  _export.params.up_axis = IO_AXIS_Z;
537  _export.params.export_smooth_groups = true;
538  _export.params.export_material_groups = true;
539  compare_obj_export_to_golden("io_tests/blend_scene/all_objects.blend",
540  "io_tests/obj/all_objects_mat_groups.obj",
541  "io_tests/obj/all_objects_mat_groups.mtl",
542  _export.params);
543 }
544 
545 } // namespace blender::io::obj
void BKE_tempdir_init(const char *userdir)
Definition: appdir.c:1133
void BKE_tempdir_session_purge(void)
Definition: appdir.c:1159
const char * BKE_tempdir_base(void)
Definition: appdir.c:1154
const char * BKE_blender_version_string(void)
Definition: blender.c:124
EXPECT_EQ(BLI_expr_pylike_eval(expr, nullptr, 0, &result), EXPR_PYLIKE_INVALID)
File and directory operations.
void * BLI_file_read_text_as_mem(const char *filepath, size_t pad_bytes, size_t *r_size)
Definition: storage.c:466
int BLI_delete(const char *file, bool dir, bool recursive) ATTR_NONNULL()
Definition: fileops.c:934
#define PATH_MAX
Definition: BLI_fileops.h:29
void BLI_split_dir_part(const char *string, char *dir, size_t dirlen)
Definition: path_util.c:1490
const char * BLI_path_basename(const char *path) ATTR_NONNULL() ATTR_WARN_UNUSED_RESULT
Definition: path_util.c:1653
#define FILE_MAX
size_t BLI_strlen_utf8(const char *strc) ATTR_NONNULL(1) ATTR_WARN_UNUSED_RESULT
Definition: string_utf8.c:317
external readfile function prototypes.
eEvaluationMode
Definition: DEG_depsgraph.h:44
@ DAG_EVAL_VIEWPORT
Definition: DEG_depsgraph.h:45
@ IO_AXIS_Y
Definition: IO_orientation.h:9
@ IO_AXIS_Z
@ PATH_REFERENCE_RELATIVE
@ PATH_REFERENCE_STRIP
virtual void depsgraph_create(eEvaluationMode depsgraph_evaluation_mode)
bool blendfile_load(const char *filepath)
constexpr void write(T &&...args)
std::string get_as_string() const
std::unique_ptr< OBJWriter > init_writer(const OBJExportParams &params, const std::string out_filepath)
void compare_obj_export_to_golden(const std::string &blendfile, const std::string &golden_obj, const std::string &golden_mtl, OBJExportParams &params)
bool load_file_and_depsgraph(const std::string &filepath, const eEvaluationMode eval_mode=DAG_EVAL_VIEWPORT)
const Depsgraph * depsgraph
uiWidgetBaseParameters params[MAX_WIDGET_BASE_BATCH]
void * BKE_tempdir_session
ccl_global float * buffer
void(* MEM_freeN)(void *vmemh)
Definition: mallocn.c:27
static unsigned a[3]
Definition: RandGen.cpp:78
static bool strings_equal_after_first_lines(const std::string &a, const std::string &b)
TEST_F(obj_exporter_test, filter_objects_curves_as_mesh)
TEST(obj_exporter_utils, append_negative_frame_to_filename)
constexpr bool save_failing_test_output
const std::string all_objects_file
bool append_frame_to_filename(const char *filepath, const int frame, char *r_filepath_with_frames)
static std::string read_temp_file_in_string(const std::string &file_path)
std::pair< Vector< std::unique_ptr< OBJMesh > >, Vector< std::unique_ptr< OBJCurve > > > filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params)
Definition: obj_exporter.cc:88
void export_frame(Depsgraph *depsgraph, const OBJExportParams &export_params, const char *filepath)
static const pxr::TfToken b("b", pxr::TfToken::Immortal)
struct Main * main
Definition: BLO_readfile.h:56
char filepath[1024]
Definition: BKE_main.h:124
ePathReferenceMode path_mode
#define SEP_STR
Definition: unit.c:33