Blender V5.0
obj_import_file_reader.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2023 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
8
9#include "BKE_report.hh"
10
11#include "BLI_fileops.hh"
12#include "BLI_map.hh"
13#include "BLI_math_color.h"
14#include "BLI_math_vector.h"
16#include "BLI_string.h"
17#include "BLI_string_ref.hh"
18#include "BLI_vector.hh"
19
20#include "IO_string_utils.hh"
21
22#include "obj_export_mtl.hh"
24
25#include <algorithm>
26#include <charconv>
27
28#include "CLG_log.h"
29static CLG_LogRef LOG = {"io.obj"};
30
31namespace blender::io::obj {
32
33using std::string;
34
39static Geometry *create_geometry(Geometry *const prev_geometry,
40 const eGeometryType new_type,
42 Vector<std::unique_ptr<Geometry>> &r_all_geometries)
43{
44 auto new_geometry = [&]() {
45 r_all_geometries.append(std::make_unique<Geometry>());
46 Geometry *g = r_all_geometries.last().get();
47 g->geom_type_ = new_type;
48 g->geometry_name_ = name.is_empty() ? "New object" : name;
49 return g;
50 };
51
52 if (prev_geometry && prev_geometry->geom_type_ == GEOM_MESH) {
53 /* After the creation of a Geometry instance, at least one element has been found in the OBJ
54 * file that indicates that it is a mesh (faces or edges). */
55 if (!prev_geometry->face_elements_.is_empty() || !prev_geometry->edges_.is_empty()) {
56 return new_geometry();
57 }
58 if (new_type == GEOM_MESH) {
59 /* A Geometry created initially with a default name now found its name. */
60 prev_geometry->geometry_name_ = name;
61 return prev_geometry;
62 }
63 if (new_type == GEOM_CURVE) {
64 /* The object originally created is not a mesh now that curve data
65 * follows the vertex coordinates list. */
66 prev_geometry->geom_type_ = GEOM_CURVE;
67 return prev_geometry;
68 }
69 }
70
71 return new_geometry();
72}
73
74static void geom_add_vertex(const char *p, const char *end, GlobalVertices &r_global_vertices)
75{
76 r_global_vertices.flush_mrgb_block();
77 float3 vert;
78 p = parse_floats(p, end, 0.0f, vert, 3);
79 r_global_vertices.vertices.append(vert);
80 /* OBJ extension: `xyzrgb` vertex colors, when the vertex position
81 * is followed by 3 more RGB color components. See
82 * http://paulbourke.net/dataformats/obj/colour.html */
83 if (p < end) {
84 float3 srgb;
85 p = parse_floats(p, end, -1.0f, srgb, 3);
86 if (srgb.x >= 0 && srgb.y >= 0 && srgb.z >= 0) {
87 float3 linear;
88 srgb_to_linearrgb_v3_v3(linear, srgb);
89 r_global_vertices.set_vertex_color(r_global_vertices.vertices.size() - 1, linear);
90 }
91 else if (srgb.x > 0) {
92 /* Treats value in srgb.x as weight. */
93 r_global_vertices.set_vertex_weight(r_global_vertices.vertices.size() - 1, srgb.x);
94 }
95 }
96 UNUSED_VARS(p);
97}
98
99static void geom_add_mrgb_colors(const char *p, const char *end, GlobalVertices &r_global_vertices)
100{
101 /* MRGB color extension, in the form of
102 * "#MRGB MMRRGGBBMMRRGGBB ..."
103 * http://paulbourke.net/dataformats/obj/colour.html */
104 p = drop_whitespace(p, end);
105 const int mrgb_length = 8;
106 while (p + mrgb_length <= end) {
107 uint32_t value = 0;
108 std::from_chars_result res = std::from_chars(p, p + mrgb_length, value, 16);
109 if (ELEM(res.ec, std::errc::invalid_argument, std::errc::result_out_of_range)) {
110 return;
111 }
112 uchar srgb[4];
113 srgb[0] = (value >> 16) & 0xFF;
114 srgb[1] = (value >> 8) & 0xFF;
115 srgb[2] = value & 0xFF;
116 srgb[3] = 0xFF;
117 float linear[4];
118 srgb_to_linearrgb_uchar4(linear, srgb);
119
120 r_global_vertices.mrgb_block.append(float3(linear[0], linear[1], linear[2]));
121
122 p += mrgb_length;
123 }
124}
125
126static void geom_add_vertex_normal(const char *p,
127 const char *end,
128 GlobalVertices &r_global_vertices)
129{
130 float3 normal;
131 parse_floats(p, end, 0.0f, normal, 3);
132 /* Normals can be printed with only several digits in the file,
133 * making them ever-so-slightly non unit length. Make sure they are
134 * normalized. */
135 normalize_v3(normal);
136 r_global_vertices.vert_normals.append(normal);
137}
138
139static void geom_add_uv_vertex(const char *p, const char *end, GlobalVertices &r_global_vertices)
140{
141 float2 uv;
142 parse_floats(p, end, 0.0f, uv, 2);
143 r_global_vertices.uv_vertices.append(uv);
144}
145
153static const char *parse_vertex_index(const char *p, const char *end, size_t n_elems, int &r_index)
154{
155 p = parse_int(p, end, INT32_MAX, r_index, false);
156 if (r_index != INT32_MAX) {
157 r_index += r_index < 0 ? n_elems : -1;
158 if (r_index < 0 || r_index >= n_elems) {
159 CLOG_WARN(&LOG, "Invalid vertex index %i (valid range [0, %zu))", r_index, n_elems);
160 r_index = INT32_MAX;
161 }
162 }
163 return p;
164}
165
175static void geom_add_polyline(Geometry *geom,
176 const char *p,
177 const char *end,
178 GlobalVertices &r_global_vertices)
179{
180 int last_vertex_index;
181 p = drop_whitespace(p, end);
182 p = parse_vertex_index(p, end, r_global_vertices.vertices.size(), last_vertex_index);
183
184 if (last_vertex_index == INT32_MAX) {
185 CLOG_WARN(&LOG, "Skipping invalid OBJ polyline.");
186 return;
187 }
188 geom->track_vertex_index(last_vertex_index);
189
190 while (p < end) {
191 int vertex_index;
192
193 /* Lines can contain texture coordinate indices, just ignore them. */
194 p = drop_non_whitespace(p, end);
195 /* Skip whitespace to get to the next vertex. */
196 p = drop_whitespace(p, end);
197
198 p = parse_vertex_index(p, end, r_global_vertices.vertices.size(), vertex_index);
199 if (vertex_index == INT32_MAX) {
200 break;
201 }
202
203 geom->edges_.append({last_vertex_index, vertex_index});
204 geom->track_vertex_index(vertex_index);
205 last_vertex_index = vertex_index;
206 }
207}
208
209static void geom_add_polygon(Geometry *geom,
210 const char *p,
211 const char *end,
212 const GlobalVertices &global_vertices,
213 const int material_index,
214 const int group_index,
215 const bool shaded_smooth)
216{
217 FaceElem curr_face;
218 curr_face.shaded_smooth = shaded_smooth;
219 curr_face.material_index = material_index;
220 if (group_index >= 0) {
221 curr_face.vertex_group_index = group_index;
222 geom->has_vertex_groups_ = true;
223 }
224
225 const int orig_corners_size = geom->face_corners_.size();
226 curr_face.start_index_ = orig_corners_size;
227
228 bool face_valid = true;
229 p = drop_whitespace(p, end);
230 while (p < end && face_valid) {
231 FaceCorner corner;
232 bool got_uv = false, got_normal = false;
233 /* Parse vertex index. */
234 p = parse_int(p, end, INT32_MAX, corner.vert_index, false);
235
236 /* Skip parsing when we reach start of the comment. */
237 if (*p == '#') {
238 break;
239 }
240
241 face_valid &= corner.vert_index != INT32_MAX;
242 if (p < end && *p == '/') {
243 /* Parse UV index. */
244 ++p;
245 if (p < end && *p != '/') {
246 p = parse_int(p, end, INT32_MAX, corner.uv_vert_index, false);
247 got_uv = corner.uv_vert_index != INT32_MAX;
248 }
249 /* Parse normal index. */
250 if (p < end && *p == '/') {
251 ++p;
252 p = parse_int(p, end, INT32_MAX, corner.vertex_normal_index, false);
253 got_normal = corner.vertex_normal_index != INT32_MAX;
254 }
255 }
256 /* Always keep stored indices non-negative and zero-based. */
257 corner.vert_index += corner.vert_index < 0 ? global_vertices.vertices.size() : -1;
258 if (corner.vert_index < 0 || corner.vert_index >= global_vertices.vertices.size()) {
259 CLOG_WARN(&LOG,
260 "Invalid vertex index %i (valid range [0, %zu)), ignoring face",
261 corner.vert_index,
262 size_t(global_vertices.vertices.size()));
263 face_valid = false;
264 }
265 else {
266 geom->track_vertex_index(corner.vert_index);
267 }
268 /* Ignore UV index, if the geometry does not have any UVs (#103212). */
269 if (got_uv && !global_vertices.uv_vertices.is_empty()) {
270 corner.uv_vert_index += corner.uv_vert_index < 0 ? global_vertices.uv_vertices.size() : -1;
271 if (corner.uv_vert_index < 0 || corner.uv_vert_index >= global_vertices.uv_vertices.size()) {
272 CLOG_WARN(&LOG,
273 "Invalid UV index %i (valid range [0, %zu)), ignoring face",
274 corner.uv_vert_index,
275 size_t(global_vertices.uv_vertices.size()));
276 face_valid = false;
277 }
278 }
279 /* Ignore corner normal index, if the geometry does not have any normals.
280 * Some obj files out there do have face definitions that refer to normal indices,
281 * without any normals being present (#98782). */
282 if (got_normal && !global_vertices.vert_normals.is_empty()) {
283 corner.vertex_normal_index += corner.vertex_normal_index < 0 ?
284 global_vertices.vert_normals.size() :
285 -1;
286 if (corner.vertex_normal_index < 0 ||
287 corner.vertex_normal_index >= global_vertices.vert_normals.size())
288 {
289 CLOG_WARN(&LOG,
290 "Invalid normal index %i (valid range [0, %zu)), ignoring face",
291 corner.vertex_normal_index,
292 size_t(global_vertices.vert_normals.size()));
293 face_valid = false;
294 }
295 }
296 geom->face_corners_.append(corner);
297 curr_face.corner_count_++;
298
299 /* Some files contain extra stuff per face (e.g. 4 indices); skip any remainder (#103441). */
300 p = drop_non_whitespace(p, end);
301 /* Skip whitespace to get to the next face corner. */
302 p = drop_whitespace(p, end);
303 }
304
305 if (face_valid) {
306 geom->face_elements_.append(curr_face);
307 geom->total_corner_ += curr_face.corner_count_;
308 }
309 else {
310 /* Remove just-added corners for the invalid face. */
311 geom->face_corners_.resize(orig_corners_size);
312 geom->has_invalid_faces_ = true;
313 }
314}
315
317 const char *p,
318 const char *end,
319 const StringRef group_name,
320 Vector<std::unique_ptr<Geometry>> &r_all_geometries)
321{
322 p = drop_whitespace(p, end);
323 if (!StringRef(p, end).startswith("bspline") && !StringRef(p, end).startswith("rat bspline")) {
324 CLOG_WARN(&LOG, "Curve type not supported: '%s'", string(p, end).c_str());
325 return geom;
326 }
327 geom = create_geometry(geom, GEOM_CURVE, group_name, r_all_geometries);
328 geom->nurbs_element_.group_ = group_name;
329 return geom;
330}
331
332static void geom_set_curve_degree(Geometry *geom, const char *p, const char *end)
333{
334 parse_int(p, end, 3, geom->nurbs_element_.degree);
335}
336
338 const char *p,
339 const char *end,
340 const GlobalVertices &global_vertices)
341{
342 /* Parse curve parameter range. */
343 p = parse_floats(p, end, 0, geom->nurbs_element_.range, 2);
344 /* Parse indices. */
345 while (p < end) {
346 int index;
347 p = parse_int(p, end, INT32_MAX, index);
348 if (index == INT32_MAX) {
349 return;
350 }
351 /* Always keep stored indices non-negative and zero-based. */
352 index += index < 0 ? global_vertices.vertices.size() : -1;
354 }
355}
356
357static void geom_add_curve_parameters(Geometry *geom, const char *p, const char *end)
358{
359 p = drop_whitespace(p, end);
360 if (p == end) {
361 CLOG_ERROR(&LOG, "Invalid OBJ curve parm line");
362 return;
363 }
364 if (*p != 'u') {
365 CLOG_WARN(&LOG, "OBJ curve surfaces are not supported, found '%c'", *p);
366 return;
367 }
368 ++p;
369
370 while (p < end) {
371 float val;
372 p = parse_float(p, end, FLT_MAX, val);
373 if (val != FLT_MAX) {
374 geom->nurbs_element_.parm.append(val);
375 }
376 else {
377 CLOG_ERROR(&LOG, "OBJ curve parm line has invalid number");
378 return;
379 }
380 }
381}
382
383static void geom_update_group(const StringRef rest_line, string &r_group_name)
384{
385 if (rest_line.find("off") != string::npos || rest_line.find("null") != string::npos ||
386 rest_line.find("default") != string::npos)
387 {
388 /* Set group for future elements like faces or curves to empty. */
389 r_group_name = "";
390 return;
391 }
392 r_group_name = rest_line;
393}
394
395static void geom_update_smooth_group(const char *p, const char *end, bool &r_state_shaded_smooth)
396{
397 p = drop_whitespace(p, end);
398 /* Some implementations use "0" and "null" too, in addition to "off". */
399 const StringRef line = StringRef(p, end);
400 if (line == "0" || line.startswith("off") || line.startswith("null")) {
401 r_state_shaded_smooth = false;
402 return;
403 }
404
405 int smooth = 0;
406 parse_int(p, end, 0, smooth);
407 r_state_shaded_smooth = smooth != 0;
408}
409
410static void geom_new_object(const char *p,
411 const char *end,
412 bool &r_state_shaded_smooth,
413 string &r_state_group_name,
414 int &r_state_material_index,
415 Geometry *&r_curr_geom,
416 Vector<std::unique_ptr<Geometry>> &r_all_geometries)
417{
418 r_state_shaded_smooth = false;
419 r_state_group_name = "";
420 /* Reset object-local material index that's used in face information.
421 * NOTE: do not reset the material name; that has to carry over
422 * into the next object if needed. */
423 r_state_material_index = -1;
424 r_curr_geom = create_geometry(
425 r_curr_geom, GEOM_MESH, StringRef(p, end).trim(), r_all_geometries);
426}
427
428OBJParser::OBJParser(const OBJImportParams &import_params, size_t read_buffer_size)
429 : import_params_(import_params), read_buffer_size_(read_buffer_size)
430{
431 obj_file_ = BLI_fopen(import_params_.filepath, "rb");
432 if (!obj_file_) {
433 CLOG_ERROR(&LOG, "Cannot read from OBJ file:'%s'.", import_params_.filepath);
434 BKE_reportf(import_params_.reports,
435 RPT_ERROR,
436 "OBJ Import: Cannot open file '%s'",
437 import_params_.filepath);
438 return;
439 }
440}
441
443{
444 if (obj_file_) {
445 fclose(obj_file_);
446 }
447}
448
449/* If line starts with keyword followed by whitespace, returns true and drops it from the line. */
450static bool parse_keyword(const char *&p, const char *end, StringRef keyword)
451{
452 const size_t keyword_len = keyword.size();
453 if (end - p < keyword_len + 1) {
454 return false;
455 }
456 if (memcmp(p, keyword.data(), keyword_len) != 0) {
457 return false;
458 }
459 /* Treat any ASCII control character as white-space;
460 * don't use `isspace()` for performance reasons. */
461 if (p[keyword_len] > ' ') {
462 return false;
463 }
464 p += keyword_len + 1;
465 return true;
466}
467
468/* Special case: if there were no faces/edges in any geometries,
469 * treat all the vertices as a point cloud. */
471 const Span<std::unique_ptr<Geometry>> all_geometries,
472 const GlobalVertices &global_vertices)
473{
474 if (!global_vertices.vertices.is_empty() && geom && geom->geom_type_ == GEOM_MESH) {
475 if (std::all_of(all_geometries.begin(),
476 all_geometries.end(),
477 [](const std::unique_ptr<Geometry> &g) { return g->get_vertex_count() == 0; }))
478 {
479 geom->track_all_vertices(global_vertices.vertices.size());
480 }
481 }
482}
483
484size_t OBJParser::parse_string_buffer(StringRef &buffer_str,
485 Vector<std::unique_ptr<Geometry>> &r_all_geometries,
486 GlobalVertices &r_global_vertices,
487 Geometry *&curr_geom,
488 bool &state_shaded_smooth,
489 string &state_group_name,
490 int &state_group_index,
491 string &state_material_name,
492 int &state_material_index)
493{
494 size_t read_lines_num = 0;
495 while (!buffer_str.is_empty()) {
496 StringRef line = read_next_line(buffer_str);
497 const char *p = line.begin(), *end = line.end();
498 p = drop_whitespace(p, end);
499 ++read_lines_num;
500 if (p == end) {
501 continue;
502 }
503 /* Most common things that start with 'v': vertices, normals, UVs. */
504 if (*p == 'v') {
505 if (parse_keyword(p, end, "v")) {
506 geom_add_vertex(p, end, r_global_vertices);
507 }
508 else if (parse_keyword(p, end, "vn")) {
509 geom_add_vertex_normal(p, end, r_global_vertices);
510 }
511 else if (parse_keyword(p, end, "vt")) {
512 geom_add_uv_vertex(p, end, r_global_vertices);
513 }
514 }
515 /* Faces. */
516 else if (parse_keyword(p, end, "f")) {
517 /* If we don't have a material index assigned yet, get one.
518 * It means "usemtl" state came from the previous object. */
519 if (state_material_index == -1 && !state_material_name.empty() &&
520 curr_geom->material_indices_.is_empty())
521 {
522 curr_geom->material_indices_.add_new(state_material_name, 0);
523 curr_geom->material_order_.append(state_material_name);
524 state_material_index = 0;
525 }
526
527 geom_add_polygon(curr_geom,
528 p,
529 end,
530 r_global_vertices,
531 state_material_index,
532 state_group_index,
533 state_shaded_smooth);
534 }
535 /* Faces. */
536 else if (parse_keyword(p, end, "l")) {
537 geom_add_polyline(curr_geom, p, end, r_global_vertices);
538 }
539 /* Objects. */
540 else if (parse_keyword(p, end, "o")) {
541 if (import_params_.use_split_objects) {
543 end,
544 state_shaded_smooth,
545 state_group_name,
546 state_material_index,
547 curr_geom,
548 r_all_geometries);
549 }
550 }
551 /* Groups. */
552 else if (parse_keyword(p, end, "g")) {
553 if (import_params_.use_split_groups) {
555 end,
556 state_shaded_smooth,
557 state_group_name,
558 state_material_index,
559 curr_geom,
560 r_all_geometries);
561 }
562 else {
563 geom_update_group(StringRef(p, end).trim(), state_group_name);
564 int new_index = curr_geom->group_indices_.size();
565 state_group_index = curr_geom->group_indices_.lookup_or_add(state_group_name, new_index);
566 if (new_index == state_group_index) {
567 curr_geom->group_order_.append(state_group_name);
568 }
569 }
570 }
571 /* Smoothing groups. */
572 else if (parse_keyword(p, end, "s")) {
573 geom_update_smooth_group(p, end, state_shaded_smooth);
574 }
575 /* Materials and their libraries. */
576 else if (parse_keyword(p, end, "usemtl")) {
577 state_material_name = StringRef(p, end).trim();
578 int new_mat_index = curr_geom->material_indices_.size();
579 state_material_index = curr_geom->material_indices_.lookup_or_add(state_material_name,
580 new_mat_index);
581 if (new_mat_index == state_material_index) {
582 curr_geom->material_order_.append(state_material_name);
583 }
584 }
585 else if (parse_keyword(p, end, "mtllib")) {
586 add_mtl_library(StringRef(p, end).trim());
587 }
588 else if (parse_keyword(p, end, "#MRGB")) {
589 geom_add_mrgb_colors(p, end, r_global_vertices);
590 }
591 /* Comments. */
592 else if (*p == '#') {
593 /* Nothing to do. */
594 }
595 /* Curve related things. */
596 else if (parse_keyword(p, end, "cstype")) {
597 curr_geom = geom_set_curve_type(curr_geom, p, end, state_group_name, r_all_geometries);
598 }
599 else if (parse_keyword(p, end, "deg")) {
600 geom_set_curve_degree(curr_geom, p, end);
601 }
602 else if (parse_keyword(p, end, "curv")) {
603 geom_add_curve_vertex_indices(curr_geom, p, end, r_global_vertices);
604 }
605 else if (parse_keyword(p, end, "parm")) {
606 geom_add_curve_parameters(curr_geom, p, end);
607 }
608 else if (StringRef(p, end).startswith("end")) {
609 /* End of curve definition, nothing else to do. */
610 }
611 else {
612 CLOG_WARN(&LOG, "OBJ element not recognized: '%s'", string(p, end).c_str());
613 }
614 }
615 return read_lines_num;
616}
617
618void OBJParser::parse(Vector<std::unique_ptr<Geometry>> &r_all_geometries,
619 GlobalVertices &r_global_vertices)
620{
621 if (!obj_file_) {
622 return;
623 }
624
625 /* Use the filename as the default name given to the initial object. */
626 char ob_name[FILE_MAXFILE];
627 STRNCPY(ob_name, BLI_path_basename(import_params_.filepath));
629
630 Geometry *curr_geom = create_geometry(nullptr, GEOM_MESH, ob_name, r_all_geometries);
631
632 /* State variables: once set, they remain the same for the remaining
633 * elements in the object. */
634 bool state_shaded_smooth = false;
635 string state_group_name;
636 int state_group_index = -1;
637 string state_material_name;
638 int state_material_index = -1;
639
640 /* Read the input file in chunks. We need up to twice the possible chunk size,
641 * to possibly store remainder of the previous input line that got broken mid-chunk. */
642 Array<char> buffer(read_buffer_size_ * 2);
643
644 size_t buffer_offset = 0;
645 size_t line_number = 0;
646 while (true) {
647 /* Read a chunk of input from the file. */
648 size_t bytes_read = fread(buffer.data() + buffer_offset, 1, read_buffer_size_, obj_file_);
649 if (bytes_read == 0 && buffer_offset == 0) {
650 break; /* No more data to read. */
651 }
652
653 /* Take care of line continuations now (turn them into spaces);
654 * the rest of the parsing code does not need to worry about them anymore. */
655 fixup_line_continuations(buffer.data() + buffer_offset,
656 buffer.data() + buffer_offset + bytes_read);
657
658 /* Ensure buffer ends in a newline. */
659 if (bytes_read < read_buffer_size_) {
660 if (bytes_read == 0 || buffer[buffer_offset + bytes_read - 1] != '\n') {
661 buffer[buffer_offset + bytes_read] = '\n';
662 bytes_read++;
663 }
664 }
665
666 size_t buffer_end = buffer_offset + bytes_read;
667 if (buffer_end == 0) {
668 break;
669 }
670
671 /* Find last newline. */
672 size_t last_nl = buffer_end;
673 while (last_nl > 0) {
674 --last_nl;
675 if (buffer[last_nl] == '\n') {
676 break;
677 }
678 }
679 if (buffer[last_nl] != '\n') {
680 /* Whole line did not fit into our read buffer. Warn and exit. */
682 "OBJ file contains a line #%zu that is too long (max. length %zu)",
683 line_number,
684 read_buffer_size_);
685 break;
686 }
687 ++last_nl;
688
689 /* Parse the buffer (until last newline) that we have so far,
690 * line by line. */
691 StringRef buffer_str{buffer.data(), int64_t(last_nl)};
692 line_number += OBJParser::parse_string_buffer(buffer_str,
693 r_all_geometries,
694 r_global_vertices,
695 curr_geom,
696 state_shaded_smooth,
697 state_group_name,
698 state_group_index,
699 state_material_name,
700 state_material_index);
701
702 /* We might have a line that was cut in the middle by the previous buffer;
703 * copy it over for next chunk reading. */
704 size_t left_size = buffer_end - last_nl;
705 memmove(buffer.data(), buffer.data() + last_nl, left_size);
706 buffer_offset = left_size;
707 }
708
709 r_global_vertices.flush_mrgb_block();
710 use_all_vertices_if_no_faces(curr_geom, r_all_geometries, r_global_vertices);
711 add_default_mtl_library();
712}
713
714static MTLTexMapType mtl_line_start_to_texture_type(const char *&p, const char *end)
715{
716 if (parse_keyword(p, end, "map_Kd")) {
718 }
719 if (parse_keyword(p, end, "map_Ks")) {
721 }
722 if (parse_keyword(p, end, "map_Ns")) {
724 }
725 if (parse_keyword(p, end, "map_d")) {
727 }
728 if (parse_keyword(p, end, "refl") || parse_keyword(p, end, "map_refl")) {
730 }
731 if (parse_keyword(p, end, "map_Ke")) {
733 }
734 if (parse_keyword(p, end, "bump") || parse_keyword(p, end, "map_Bump") ||
735 parse_keyword(p, end, "map_bump"))
736 {
738 }
739 if (parse_keyword(p, end, "map_Pr")) {
741 }
742 if (parse_keyword(p, end, "map_Pm")) {
744 }
745 if (parse_keyword(p, end, "map_Ps")) {
747 }
749}
750
751static const std::pair<StringRef, int> unsupported_texture_options[] = {
752 {"-blendu", 1},
753 {"-blendv", 1},
754 {"-boost", 1},
755 {"-cc", 1},
756 {"-clamp", 1},
757 {"-imfchan", 1},
758 {"-mm", 2},
759 {"-t", 3},
760 {"-texres", 1},
761};
762
763static bool parse_texture_option(const char *&p,
764 const char *end,
765 MTLMaterial *material,
766 MTLTexMap &tex_map)
767{
768 p = drop_whitespace(p, end);
769 if (parse_keyword(p, end, "-o")) {
770 p = parse_floats(p, end, 0.0f, tex_map.translation, 3, true);
771 return true;
772 }
773 if (parse_keyword(p, end, "-s")) {
774 p = parse_floats(p, end, 1.0f, tex_map.scale, 3, true);
775 return true;
776 }
777 if (parse_keyword(p, end, "-bm")) {
778 p = parse_float(p, end, 1.0f, material->normal_strength, true, true);
779 return true;
780 }
781 if (parse_keyword(p, end, "-type")) {
782 p = drop_whitespace(p, end);
783 /* Only sphere is supported. */
785 const StringRef line = StringRef(p, end);
786 if (!line.startswith("sphere")) {
787 CLOG_WARN(&LOG,
788 "Only the 'sphere' MTL projection type is supported, found: '%s'",
789 string(line).c_str());
790 }
791 p = drop_non_whitespace(p, end);
792 return true;
793 }
794 /* Check for unsupported options and skip them. */
795 for (const auto &opt : unsupported_texture_options) {
796 if (parse_keyword(p, end, opt.first)) {
797 /* Drop the arguments. */
798 for (int i = 0; i < opt.second; ++i) {
799 p = drop_whitespace(p, end);
800 p = drop_non_whitespace(p, end);
801 }
802 return true;
803 }
804 }
805
806 return false;
807}
808
809static void parse_texture_map(const char *p,
810 const char *end,
811 MTLMaterial *material,
812 const char *mtl_dir_path)
813{
814 const StringRef line = StringRef(p, end);
815 bool is_map = line.startswith("map_");
816 bool is_refl = line.startswith("refl");
817 bool is_bump = line.startswith("bump");
818 if (!is_map && !is_refl && !is_bump) {
819 return;
820 }
822 if (key == MTLTexMapType::Count) {
823 /* No supported texture map found. */
824 CLOG_WARN(&LOG, "MTL texture map type not supported: '%s'", string(line).c_str());
825 return;
826 }
827 MTLTexMap &tex_map = material->tex_map_of_type(key);
828 tex_map.mtl_dir_path = mtl_dir_path;
829
830 /* Parse texture map options. */
831 while (parse_texture_option(p, end, material, tex_map)) {
832 }
833
834 /* What remains is the image path. */
835 tex_map.image_path = StringRef(p, end).trim();
836}
837
839{
840 return mtl_libraries_;
841}
842
843void OBJParser::add_mtl_library(StringRef path)
844{
845 /* Remove any quotes from start and end (#67266, #97794). */
846 if (path.size() > 2 && path.startswith("\"") && path.endswith("\"")) {
847 path = path.drop_prefix(1).drop_suffix(1);
848 }
849
850 if (!mtl_libraries_.contains(path)) {
851 mtl_libraries_.append(path);
852 }
853}
854
855void OBJParser::add_default_mtl_library()
856{
857 /* Add any existing `.mtl` file that's with the same base name as the `.obj` file
858 * into candidate `.mtl` files to search through. This is not technically following the
859 * spec, but the old python importer was doing it, and there are user files out there
860 * that contain "mtllib bar.mtl" for a foo.obj, and depend on finding materials
861 * from foo.mtl (see #97757). */
862 char mtl_file_path[FILE_MAX];
863 STRNCPY(mtl_file_path, import_params_.filepath);
864 BLI_path_extension_replace(mtl_file_path, sizeof(mtl_file_path), ".mtl");
865 if (BLI_exists(mtl_file_path)) {
866 char mtl_file_base[FILE_MAX];
867 BLI_path_split_file_part(mtl_file_path, mtl_file_base, sizeof(mtl_file_base));
868 add_mtl_library(mtl_file_base);
869 }
870}
871
873{
874 char obj_file_dir[FILE_MAXDIR];
875 BLI_path_split_dir_part(obj_filepath.data(), obj_file_dir, FILE_MAXDIR);
876 BLI_path_join(mtl_file_path_, FILE_MAX, obj_file_dir, mtl_library.data());
877
878 /* Normalize the path to handle different paths pointing to the same file */
879 BLI_path_normalize(mtl_file_path_);
880
881 BLI_path_split_dir_part(mtl_file_path_, mtl_dir_path_, FILE_MAXDIR);
882}
883
884void MTLParser::parse_and_store(Map<string, std::unique_ptr<MTLMaterial>> &r_materials)
885{
886 size_t buffer_len;
887 void *buffer = BLI_file_read_text_as_mem(mtl_file_path_, 0, &buffer_len);
888 if (buffer == nullptr) {
889 CLOG_ERROR(&LOG, "OBJ import: cannot read from MTL file: '%s'", mtl_file_path_);
890 return;
891 }
892
893 MTLMaterial *material = nullptr;
894
895 StringRef buffer_str{(const char *)buffer, int64_t(buffer_len)};
896 while (!buffer_str.is_empty()) {
897 const StringRef line = read_next_line(buffer_str);
898 const char *p = line.begin(), *end = line.end();
899 p = drop_whitespace(p, end);
900 if (p == end) {
901 continue;
902 }
903
904 if (parse_keyword(p, end, "newmtl")) {
905 StringRef mat_name = StringRef(p, end).trim();
906 /* Always try to get or create the material, even if it already exists */
907 material =
908 r_materials.lookup_or_add(string(mat_name), std::make_unique<MTLMaterial>()).get();
909 }
910 else if (material != nullptr) {
911 if (parse_keyword(p, end, "Ns")) {
912 parse_float(p, end, 324.0f, material->spec_exponent);
913 }
914 else if (parse_keyword(p, end, "Ka")) {
915 parse_floats(p, end, 0.0f, material->ambient_color, 3);
916 }
917 else if (parse_keyword(p, end, "Kd")) {
918 parse_floats(p, end, 0.8f, material->color, 3);
919 }
920 else if (parse_keyword(p, end, "Ks")) {
921 parse_floats(p, end, 0.5f, material->spec_color, 3);
922 }
923 else if (parse_keyword(p, end, "Ke")) {
924 parse_floats(p, end, 0.0f, material->emission_color, 3);
925 }
926 else if (parse_keyword(p, end, "Ni")) {
927 parse_float(p, end, 1.45f, material->ior);
928 }
929 else if (parse_keyword(p, end, "d")) {
930 parse_float(p, end, 1.0f, material->alpha);
931 }
932 else if (parse_keyword(p, end, "illum")) {
933 /* Some files incorrectly use a float (#60135). */
934 float val;
935 parse_float(p, end, 1.0f, val);
936 material->illum_mode = val;
937 }
938 else if (parse_keyword(p, end, "Pr")) {
939 parse_float(p, end, 0.5f, material->roughness);
940 }
941 else if (parse_keyword(p, end, "Pm")) {
942 parse_float(p, end, 0.0f, material->metallic);
943 }
944 else if (parse_keyword(p, end, "Ps")) {
945 parse_float(p, end, 0.0f, material->sheen);
946 }
947 else if (parse_keyword(p, end, "Pc")) {
948 parse_float(p, end, 0.0f, material->cc_thickness);
949 }
950 else if (parse_keyword(p, end, "Pcr")) {
951 parse_float(p, end, 0.0f, material->cc_roughness);
952 }
953 else if (parse_keyword(p, end, "aniso")) {
954 parse_float(p, end, 0.0f, material->aniso);
955 }
956 else if (parse_keyword(p, end, "anisor")) {
957 parse_float(p, end, 0.0f, material->aniso_rot);
958 }
959 else if (parse_keyword(p, end, "Kt") || parse_keyword(p, end, "Tf")) {
960 parse_floats(p, end, 0.0f, material->transmit_color, 3);
961 }
962 else {
963 parse_texture_map(p, end, material, mtl_dir_path_);
964 }
965 }
966 }
967
968 MEM_freeN(buffer);
969}
970} // namespace blender::io::obj
void BKE_reportf(ReportList *reports, eReportType type, const char *format,...) ATTR_PRINTF_FORMAT(3
@ RPT_ERROR
Definition BKE_report.hh:39
int BLI_exists(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
Definition storage.cc:360
FILE * BLI_fopen(const char *filepath, const char *mode) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
void * BLI_file_read_text_as_mem(const char *filepath, size_t pad_bytes, size_t *r_size)
Definition storage.cc:511
File and directory operations.
MINLINE void srgb_to_linearrgb_uchar4(float linear[4], const unsigned char srgb[4])
void srgb_to_linearrgb_v3_v3(float linear[3], const float srgb[3])
MINLINE float normalize_v3(float n[3])
bool bool BLI_path_extension_strip(char *path) ATTR_NONNULL(1)
void void void const char * BLI_path_basename(const char *path) ATTR_NONNULL(1) ATTR_WARN_UNUSED_RESULT
#define FILE_MAXFILE
#define FILE_MAX
bool BLI_path_extension_replace(char *path, size_t path_maxncpy, const char *ext) ATTR_NONNULL(1
int BLI_path_normalize(char *path) ATTR_NONNULL(1)
#define BLI_path_join(...)
void void void BLI_path_split_file_part(const char *filepath, char *file, size_t file_maxncpy) ATTR_NONNULL(1
void void BLI_path_split_dir_part(const char *filepath, char *dir, size_t dir_maxncpy) ATTR_NONNULL(1
#define FILE_MAXDIR
char * STRNCPY(char(&dst)[N], const char *src)
Definition BLI_string.h:693
unsigned char uchar
#define UNUSED_VARS(...)
#define ELEM(...)
#define CLOG_ERROR(clg_ref,...)
Definition CLG_log.h:188
#define CLOG_WARN(clg_ref,...)
Definition CLG_log.h:189
@ SHD_PROJ_SPHERE
long long int int64_t
bool contains(const T &value) const
void append(const T &value)
const T * data() const
Definition BLI_array.hh:312
constexpr int64_t find(char c, int64_t pos=0) const
constexpr const char * begin() const
constexpr const char * end() const
constexpr bool is_empty() const
constexpr bool startswith(StringRef prefix) const
constexpr bool endswith(StringRef suffix) const
constexpr int64_t size() const
constexpr StringRef trim() const
constexpr const char * data() const
constexpr StringRef drop_prefix(int64_t n) const
constexpr StringRef drop_suffix(int64_t n) const
void append(const T &value)
void parse_and_store(Map< std::string, std::unique_ptr< MTLMaterial > > &r_materials)
MTLParser(StringRefNull mtl_library_, StringRefNull obj_filepath)
OBJParser(const OBJImportParams &import_params, size_t read_buffer_size)
Span< std::string > mtl_libraries() const
void parse(Vector< std::unique_ptr< Geometry > > &r_all_geometries, GlobalVertices &r_global_vertices)
#define INT32_MAX
#define LOG(level)
Definition log.h:97
void MEM_freeN(void *vmemh)
Definition mallocn.cc:113
static void use_all_vertices_if_no_faces(Geometry *geom, const Span< std::unique_ptr< Geometry > > all_geometries, const GlobalVertices &global_vertices)
static void geom_add_curve_vertex_indices(Geometry *geom, const char *p, const char *end, const GlobalVertices &global_vertices)
static void geom_add_vertex(const char *p, const char *end, GlobalVertices &r_global_vertices)
static void geom_add_mrgb_colors(const char *p, const char *end, GlobalVertices &r_global_vertices)
static void parse_texture_map(const char *p, const char *end, MTLMaterial *material, const char *mtl_dir_path)
static void geom_add_uv_vertex(const char *p, const char *end, GlobalVertices &r_global_vertices)
static const char * parse_vertex_index(const char *p, const char *end, size_t n_elems, int &r_index)
static MTLTexMapType mtl_line_start_to_texture_type(const char *&p, const char *end)
static void geom_add_polyline(Geometry *geom, const char *p, const char *end, GlobalVertices &r_global_vertices)
static bool parse_keyword(const char *&p, const char *end, StringRef keyword)
static bool parse_texture_option(const char *&p, const char *end, MTLMaterial *material, MTLTexMap &tex_map)
static void geom_add_vertex_normal(const char *p, const char *end, GlobalVertices &r_global_vertices)
static void geom_add_curve_parameters(Geometry *geom, const char *p, const char *end)
static void geom_set_curve_degree(Geometry *geom, const char *p, const char *end)
static void geom_update_smooth_group(const char *p, const char *end, bool &r_state_shaded_smooth)
static void geom_new_object(const char *p, const char *end, bool &r_state_shaded_smooth, string &r_state_group_name, int &r_state_material_index, Geometry *&r_curr_geom, Vector< std::unique_ptr< Geometry > > &r_all_geometries)
static void geom_update_group(const StringRef rest_line, string &r_group_name)
static Geometry * geom_set_curve_type(Geometry *geom, const char *p, const char *end, const StringRef group_name, Vector< std::unique_ptr< Geometry > > &r_all_geometries)
static const std::pair< StringRef, int > unsupported_texture_options[]
static void geom_add_polygon(Geometry *geom, const char *p, const char *end, const GlobalVertices &global_vertices, const int material_index, const int group_index, const bool shaded_smooth)
static Geometry * create_geometry(Geometry *const prev_geometry, const eGeometryType new_type, StringRef name, Vector< std::unique_ptr< Geometry > > &r_all_geometries)
const char * parse_float(const char *p, const char *end, float fallback, float &dst, bool skip_space, bool require_trailing_space)
const char * parse_floats(const char *p, const char *end, float fallback, float *dst, int count, bool require_trailing_space)
const char * drop_non_whitespace(const char *p, const char *end)
StringRef read_next_line(StringRef &buffer)
const char * parse_int(const char *p, const char *end, int fallback, int &dst, bool skip_space)
void fixup_line_continuations(char *p, char *end)
const char * drop_whitespace(const char *p, const char *end)
VecBase< float, 2 > float2
VecBase< float, 3 > float3
const char * name
#define FLT_MAX
Definition stdcycles.h:14
char filepath[FILE_MAX]
Vector< FaceCorner > face_corners_
void set_vertex_weight(size_t index, float weight)
void set_vertex_color(size_t index, float3 color)
const MTLTexMap & tex_map_of_type(MTLTexMapType key) const
i
Definition text_draw.cc:230