syoyo / tinyobjloader-c

Header only tiny wavefront .obj loader in pure C99
421 stars 60 forks source link

`obj` parsing doesn't know the file name, but `mtllib` is relative to that file #38

Open martinhath opened 4 years ago

martinhath commented 4 years ago

I have an obj and mtl file exported from blender in a directory, and I'd like to read them both. I'm using tinyobj_parse_obj for this, which takes the .obj file as a char * as input. However, one of the commands in the file is mtllib <filename>.mtl, which it tries to read, and fails, since the files aren't in the same directory as the executable. Since tinyobj_parse_obj doesn't know anything about the location of the obj file it would be unable to locate the .mtl file, since I assume mtllib is supposed to give a relative path.

Is this a bug, or is there some way of handing this?

syoyo commented 4 years ago

It's just an unimplemented feature.

We need to implement a feature to supply search path to .mtl file or a callback function for mtllib to tinyobj_parse_obj API.

PR is welcome!

iyenal commented 4 years ago

@martinhath If you would like a solution to link object's materials to mtllib materials, I created a solution to instead to get the object name, get its material name, and use that material name to then look for its corresponding texture. Let me know if you would like me to upload it here :)

martinhath commented 4 years ago

@syoyo Glad to hear it!

@iyenal I have a folder full of obj and corresponding mtl files, so I think the original route of finding the mtl from the location of the obj would be best for me. Thanks anyways!

I ended up hacking something together in which the user can supply the filename of the obj, extract the directory from that, and append the relative path onto it. Since I'll always supply the filename, I didn't bother (yet) having any error checks, so the code is likely brittle to anyone that doesn't have my exact use-case.

I appreciate the decoupling of file reading and .obj parsing that the current API has, and so I don't think my current solution is fit for integration in the library. This might be what @syoyo is suggesting, but I think a better solution is to have tinyobj_parrse_obj take a char *(*cb)(const char *rel, void *param) and a void *param, where cb would take the relative filename and a pointer to some user data (param, for local state) and return a char * to the full text, or something like this?

If anyone finds this useful, these are my changes:

457c457
<     const char *p = (const char *) memchr(s, 0, n);
---
>     const char *p = memchr(s, 0, n);
1208,1212d1207
< int tinyobj_parse_obj_with_filename(tinyobj_attrib_t *attrib, tinyobj_shape_t **shapes,
<                       size_t *num_shapes, tinyobj_material_t **materials_out,
<                       size_t *num_materials_out, const char *buf, size_t len,
<                       const char *filename, unsigned int flags);
< 
1217,1223d1211
<   return tinyobj_parse_obj_with_filename(attrib, shapes, num_shapes, materials_out, num_materials_out, buf, len, NULL, flags);
< }
< 
< int tinyobj_parse_obj_with_filename(tinyobj_attrib_t *attrib, tinyobj_shape_t **shapes,
<                       size_t *num_shapes, tinyobj_material_t **materials_out,
<                       size_t *num_materials_out, const char *buf, size_t len,
<                       const char *filename, unsigned int flags) {
1329c1317
<     char *relative_filename = my_strndup(commands[mtllib_line_index].mtllib_name,
---
>     char *filename = my_strndup(commands[mtllib_line_index].mtllib_name,
1331,1341d1318
<     char buffer[1024];
<     int filename_len = strlen(filename);
<     memcpy(buffer, filename, filename_len);
<     char *sentinel = buffer + filename_len;
<     while (buffer < sentinel && *sentinel != '/')
<         sentinel--;
<     if (*sentinel == '/')
<         sentinel++;
<     int rel_len = commands[mtllib_line_index].mtllib_name_len;
<     memcpy(sentinel, relative_filename, rel_len);
<     sentinel[rel_len] = 0;
1343c1320
<     int ret = tinyobj_parse_and_index_mtl_file(&materials, &num_materials, buffer, &material_table);
---
>     int ret = tinyobj_parse_and_index_mtl_file(&materials, &num_materials, filename, &material_table);
1347c1324
<       fprintf(stderr, "TINYOBJ: Failed to parse material file '%s': %d\n", buffer, ret);
---
>       fprintf(stderr, "TINYOBJ: Failed to parse material file '%s': %d\n", filename, ret);
1350c1327
<     TINYOBJ_FREE(relative_filename);
---
>     TINYOBJ_FREE(filename);
1381a1359,1372
>         /* @todo
>            if (commands[t][i].material_name &&
>            commands[t][i].material_name_len > 0) {
>            std::string material_name(commands[t][i].material_name,
>            commands[t][i].material_name_len);
> 
>            if (material_map.find(material_name) != material_map.end()) {
>            material_id = material_map[material_name];
>            } else {
>         // Assign invalid material ID
>         material_id = -1;
>         }
>         }
>         */
syoyo commented 4 years ago

We can implement both.

One is supply both filename(for the use of .mtl search) and its content, something like what libjsonnet's evaluateSnippet does:

https://github.com/google/jsonnet/blob/a6979e399ecd0e8d1091506201487a5c390d4d31/include/libjsonnet%2B%2B.h#L111

And another is supply callback functions for resource access(.obj, .mtl, etc)