/* The MIT License (MIT) Copyright (c) 2016 - 2019 Syoyo Fujita and many contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef TINOBJ_LOADER_C_H_ #define TINOBJ_LOADER_C_H_ /* @todo { Remove stddef dependency. size_t? } */ #include #ifdef __cplusplus extern "C" { #endif typedef struct { char *name; float ambient[3]; float diffuse[3]; float specular[3]; float transmittance[3]; float emission[3]; float shininess; float ior; /* index of refraction */ float dissolve; /* 1 == opaque; 0 == fully transparent */ /* illumination model (see http://www.fileformat.info/format/material/) */ int illum; int pad0; char *ambient_texname; /* map_Ka */ char *diffuse_texname; /* map_Kd */ char *specular_texname; /* map_Ks */ char *specular_highlight_texname; /* map_Ns */ char *bump_texname; /* map_bump, bump */ char *displacement_texname; /* disp */ char *alpha_texname; /* map_d */ } tinyobj_material_t; typedef struct { char *name; /* group name or object name. */ unsigned int face_offset; unsigned int length; } tinyobj_shape_t; typedef struct { int v_idx, vt_idx, vn_idx; } tinyobj_vertex_index_t; typedef struct { unsigned int num_vertices; unsigned int num_normals; unsigned int num_texcoords; unsigned int num_faces; unsigned int num_face_num_verts; int pad0; float *vertices; float *normals; float *texcoords; tinyobj_vertex_index_t *faces; int *face_num_verts; int *material_ids; } tinyobj_attrib_t; #define TINYOBJ_FLAG_TRIANGULATE (1 << 0) #define TINYOBJ_INVALID_INDEX (0x80000000) #define TINYOBJ_SUCCESS (0) #define TINYOBJ_ERROR_EMPTY (-1) #define TINYOBJ_ERROR_INVALID_PARAMETER (-2) #define TINYOBJ_ERROR_FILE_OPERATION (-3) /* Provide a callback that can read text file without any parsing or modification. * The obj and mtl parser is going to read all the necessary data: * tinyobj_parse_obj * tinyobj_parse_mtl_file * * @param[in] ctx User provided context. * @param[in] filename Filename to be loaded. * @param[in] is_mtl 1 when the callback is invoked for loading .mtl. 0 for .obj * @param[in] obj_filename .obj filename. Useful when you load .mtl from same location of .obj. When the callback is called to load .obj, `filename` and `obj_filename` are same. * @param[out] buf Content of loaded file * @param[out] len Size of content(file) */ typedef void (*file_reader_callback)(void *ctx, const char *filename, int is_mtl, const char *obj_filename, char **buf, size_t *len); /* Parse wavefront .obj * @param[out] attrib Attibutes * @param[out] shapes Array of parsed shapes * @param[out] num_shapes Array length of `shapes` * @param[out] materials Array of parsed materials * @param[out] num_materials Array length of `materials` * @param[in] file_name File name of .obj * @param[in] file_reader File reader callback function(to read .obj and .mtl). * @param[in] ctx Context pointer passed to the file_reader_callback. * @param[in] flags combination of TINYOBJ_FLAG_*** * * Returns TINYOBJ_SUCCESS if things goes well. * Returns TINYOBJ_ERR_*** when there is an error. */ extern int tinyobj_parse_obj(tinyobj_attrib_t *attrib, tinyobj_shape_t **shapes, size_t *num_shapes, tinyobj_material_t **materials, size_t *num_materials, const char *file_name, file_reader_callback file_reader, void *ctx, unsigned int flags); /* Parse wavefront .mtl * * @param[out] materials_out * @param[out] num_materials_out * @param[in] filename .mtl filename * @param[in] filename of .obj filename. could be NULL if you just want to parse .mtl file. * @param[in] file_reader File reader callback * @param[in[ ctx Context pointer passed to the file_reader callack. * Returns TINYOBJ_SUCCESS if things goes well. * Returns TINYOBJ_ERR_*** when there is an error. */ extern int tinyobj_parse_mtl_file(tinyobj_material_t **materials_out, size_t *num_materials_out, const char *filename, const char *obj_filename, file_reader_callback file_reader, void *ctx); extern void tinyobj_attrib_init(tinyobj_attrib_t *attrib); extern void tinyobj_attrib_free(tinyobj_attrib_t *attrib); extern void tinyobj_shapes_free(tinyobj_shape_t *shapes, size_t num_shapes); extern void tinyobj_materials_free(tinyobj_material_t *materials, size_t num_materials); #ifdef __cplusplus } #endif #ifdef TINYOBJ_LOADER_C_IMPLEMENTATION #include #include #include #include #if defined(TINYOBJ_MALLOC) && defined(TINYOBJ_CALLOC) && defined(TINYOBJ_FREE) && (defined(TINYOBJ_REALLOC) || defined(TINYOBJ_REALLOC_SIZED)) /* ok */ #elif !defined(TINYOBJ_MALLOC) && !defined(TINYOBJ_CALLOC) && !defined(TINYOBJ_FREE) && !defined(TINYOBJ_REALLOC) && !defined(TINYOBJ_REALLOC_SIZED) /* ok */ #else #error "Must define all or none of TINYOBJ_MALLOC, TINYOBJ_CALLOC, TINYOBJ_FREE, and TINYOBJ_REALLOC (or TINYOBJ_REALLOC_SIZED)." #endif #ifndef TINYOBJ_MALLOC #include #define TINYOBJ_MALLOC malloc #define TINYOBJ_REALLOC realloc #define TINYOBJ_CALLOC calloc #define TINYOBJ_FREE free #endif #ifndef TINYOBJ_REALLOC_SIZED #define TINYOBJ_REALLOC_SIZED(p,oldsz,newsz) TINYOBJ_REALLOC(p,newsz) #endif #define TINYOBJ_MAX_FACES_PER_F_LINE (16) #define TINYOBJ_MAX_FILEPATH (8192) #define IS_SPACE(x) (((x) == ' ') || ((x) == '\t')) #define IS_DIGIT(x) ((unsigned int)((x) - '0') < (unsigned int)(10)) #define IS_NEW_LINE(x) (((x) == '\r') || ((x) == '\n') || ((x) == '\0')) static void skip_space(const char **token) { while ((*token)[0] == ' ' || (*token)[0] == '\t') { (*token)++; } } static void skip_space_and_cr(const char **token) { while ((*token)[0] == ' ' || (*token)[0] == '\t' || (*token)[0] == '\r') { (*token)++; } } static int until_space(const char *token) { const char *p = token; while (p[0] != '\0' && p[0] != ' ' && p[0] != '\t' && p[0] != '\r') { p++; } return (int)(p - token); } static size_t length_until_newline(const char *token, size_t n) { size_t len = 0; /* Assume token[n-1] = '\0' */ for (len = 0; len < n - 1; len++) { if (token[len] == '\n') { break; } if ((token[len] == '\r') && ((len < (n - 2)) && (token[len + 1] != '\n'))) { break; } } return len; } static size_t length_until_line_feed(const char *token, size_t n) { size_t len = 0; /* Assume token[n-1] = '\0' */ for (len = 0; len < n; len++) { if ((token[len] == '\n') || (token[len] == '\r')) { break; } } return len; } /* http://stackoverflow.com/questions/5710091/how-does-atoi-function-in-c-work */ static int my_atoi(const char *c) { int value = 0; int sign = 1; if (*c == '+' || *c == '-') { if (*c == '-') sign = -1; c++; } while (((*c) >= '0') && ((*c) <= '9')) { /* isdigit(*c) */ value *= 10; value += (int)(*c - '0'); c++; } return value * sign; } /* Make index zero-base, and also support relative index. */ static int fixIndex(int idx, size_t n) { if (idx > 0) return idx - 1; if (idx == 0) return 0; return (int)n + idx; /* negative value = relative */ } /* Parse raw triples: i, i/j/k, i//k, i/j */ static tinyobj_vertex_index_t parseRawTriple(const char **token) { tinyobj_vertex_index_t vi; /* 0x80000000 = -2147483648 = invalid */ vi.v_idx = (int)(0x80000000); vi.vn_idx = (int)(0x80000000); vi.vt_idx = (int)(0x80000000); vi.v_idx = my_atoi((*token)); while ((*token)[0] != '\0' && (*token)[0] != '/' && (*token)[0] != ' ' && (*token)[0] != '\t' && (*token)[0] != '\r') { (*token)++; } if ((*token)[0] != '/') { return vi; } (*token)++; /* i//k */ if ((*token)[0] == '/') { (*token)++; vi.vn_idx = my_atoi((*token)); while ((*token)[0] != '\0' && (*token)[0] != '/' && (*token)[0] != ' ' && (*token)[0] != '\t' && (*token)[0] != '\r') { (*token)++; } return vi; } /* i/j/k or i/j */ vi.vt_idx = my_atoi((*token)); while ((*token)[0] != '\0' && (*token)[0] != '/' && (*token)[0] != ' ' && (*token)[0] != '\t' && (*token)[0] != '\r') { (*token)++; } if ((*token)[0] != '/') { return vi; } /* i/j/k */ (*token)++; /* skip '/' */ vi.vn_idx = my_atoi((*token)); while ((*token)[0] != '\0' && (*token)[0] != '/' && (*token)[0] != ' ' && (*token)[0] != '\t' && (*token)[0] != '\r') { (*token)++; } return vi; } static int parseInt(const char **token) { int i = 0; skip_space(token); i = my_atoi((*token)); (*token) += until_space((*token)); return i; } /* * Tries to parse a floating point number located at s. * * s_end should be a location in the string where reading should absolutely * stop. For example at the end of the string, to prevent buffer overflows. * * Parses the following EBNF grammar: * sign = "+" | "-" ; * END = ? anything not in digit ? * digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; * integer = [sign] , digit , {digit} ; * decimal = integer , ["." , integer] ; * float = ( decimal , END ) | ( decimal , ("E" | "e") , integer , END ) ; * * Valid strings are for example: * -0 +3.1417e+2 -0.0E-3 1.0324 -1.41 11e2 * * If the parsing is a success, result is set to the parsed value and true * is returned. * * The function is greedy and will parse until any of the following happens: * - a non-conforming character is encountered. * - s_end is reached. * * The following situations triggers a failure: * - s >= s_end. * - parse failure. */ static int tryParseDouble(const char *s, const char *s_end, double *result) { double mantissa = 0.0; /* This exponent is base 2 rather than 10. * However the exponent we parse is supposed to be one of ten, * thus we must take care to convert the exponent/and or the * mantissa to a * 2^E, where a is the mantissa and E is the * exponent. * To get the final double we will use ldexp, it requires the * exponent to be in base 2. */ int exponent = 0; /* NOTE: THESE MUST BE DECLARED HERE SINCE WE ARE NOT ALLOWED * TO JUMP OVER DEFINITIONS. */ char sign = '+'; char exp_sign = '+'; char const *curr = s; /* How many characters were read in a loop. */ int read = 0; /* Tells whether a loop terminated due to reaching s_end. */ int end_not_reached = 0; /* BEGIN PARSING. */ if (s >= s_end) { return 0; /* fail */ } /* Find out what sign we've got. */ if (*curr == '+' || *curr == '-') { sign = *curr; curr++; } else if (IS_DIGIT(*curr)) { /* Pass through. */ } else { goto fail; } /* Read the integer part. */ end_not_reached = (curr != s_end); while (end_not_reached && IS_DIGIT(*curr)) { mantissa *= 10; mantissa += (int)(*curr - 0x30); curr++; read++; end_not_reached = (curr != s_end); } /* We must make sure we actually got something. */ if (read == 0) goto fail; /* We allow numbers of form "#", "###" etc. */ if (!end_not_reached) goto assemble; /* Read the decimal part. */ if (*curr == '.') { curr++; read = 1; end_not_reached = (curr != s_end); while (end_not_reached && IS_DIGIT(*curr)) { /* pow(10.0, -read) */ double frac_value = 1.0; int f; for (f = 0; f < read; f++) { frac_value *= 0.1; } mantissa += (int)(*curr - 0x30) * frac_value; read++; curr++; end_not_reached = (curr != s_end); } } else if (*curr == 'e' || *curr == 'E') { } else { goto assemble; } if (!end_not_reached) goto assemble; /* Read the exponent part. */ if (*curr == 'e' || *curr == 'E') { curr++; /* Figure out if a sign is present and if it is. */ end_not_reached = (curr != s_end); if (end_not_reached && (*curr == '+' || *curr == '-')) { exp_sign = *curr; curr++; } else if (IS_DIGIT(*curr)) { /* Pass through. */ } else { /* Empty E is not allowed. */ goto fail; } read = 0; end_not_reached = (curr != s_end); while (end_not_reached && IS_DIGIT(*curr)) { exponent *= 10; exponent += (int)(*curr - 0x30); curr++; read++; end_not_reached = (curr != s_end); } if (read == 0) goto fail; } assemble : { double a = 1.0; /* = pow(5.0, exponent); */ double b = 1.0; /* = 2.0^exponent */ int i; for (i = 0; i < exponent; i++) { a = a * 5.0; } for (i = 0; i < exponent; i++) { b = b * 2.0; } if (exp_sign == '-') { a = 1.0 / a; b = 1.0 / b; } *result = /* (sign == '+' ? 1 : -1) * ldexp(mantissa * pow(5.0, exponent), exponent); */ (sign == '+' ? 1 : -1) * (mantissa * a * b); } return 1; fail: return 0; } static float parseFloat(const char **token) { const char *end; double val = 0.0; float f = 0.0f; skip_space(token); end = (*token) + until_space((*token)); val = 0.0; tryParseDouble((*token), end, &val); f = (float)(val); (*token) = end; return f; } static void parseFloat2(float *x, float *y, const char **token) { (*x) = parseFloat(token); (*y) = parseFloat(token); } static void parseFloat3(float *x, float *y, float *z, const char **token) { (*x) = parseFloat(token); (*y) = parseFloat(token); (*z) = parseFloat(token); } static size_t my_strnlen(const char *s, size_t n) { const char *p = (char *)memchr(s, 0, n); return p ? (size_t)(p - s) : n; } static char *my_strdup(const char *s, size_t max_length) { char *d; size_t len; if (s == NULL) return NULL; /* Do not consider CRLF line ending(#19) */ len = length_until_line_feed(s, max_length); /* len = strlen(s); */ /* trim line ending and append '\0' */ d = (char *)TINYOBJ_MALLOC(len + 1); /* + '\0' */ memcpy(d, s, (size_t)(len)); d[len] = '\0'; return d; } static char *my_strndup(const char *s, size_t len) { char *d; size_t slen; if (s == NULL) return NULL; if (len == 0) return NULL; slen = my_strnlen(s, len); d = (char *)TINYOBJ_MALLOC(slen + 1); /* + '\0' */ if (!d) { return NULL; } memcpy(d, s, slen); d[slen] = '\0'; return d; } char *dynamic_fgets(char **buf, size_t *size, FILE *file) { char *offset; char *ret; size_t old_size; if (!(ret = fgets(*buf, (int)*size, file))) { return ret; } if (NULL != strchr(*buf, '\n')) { return ret; } do { old_size = *size; *size *= 2; *buf = (char*)TINYOBJ_REALLOC_SIZED(*buf, old_size, *size); offset = &((*buf)[old_size - 1]); ret = fgets(offset, (int)(old_size + 1), file); } while(ret && (NULL == strchr(*buf, '\n'))); return ret; } static void initMaterial(tinyobj_material_t *material) { int i; material->name = NULL; material->ambient_texname = NULL; material->diffuse_texname = NULL; material->specular_texname = NULL; material->specular_highlight_texname = NULL; material->bump_texname = NULL; material->displacement_texname = NULL; material->alpha_texname = NULL; for (i = 0; i < 3; i++) { material->ambient[i] = 0.f; material->diffuse[i] = 0.f; material->specular[i] = 0.f; material->transmittance[i] = 0.f; material->emission[i] = 0.f; } material->illum = 0; material->dissolve = 1.f; material->shininess = 1.f; material->ior = 1.f; } /* Implementation of string to int hashtable */ #define HASH_TABLE_ERROR 1 #define HASH_TABLE_SUCCESS 0 #define HASH_TABLE_DEFAULT_SIZE 10 typedef struct hash_table_entry_t { unsigned long hash; int filled; int pad0; long value; struct hash_table_entry_t* next; } hash_table_entry_t; typedef struct { unsigned long* hashes; hash_table_entry_t* entries; size_t capacity; size_t n; } hash_table_t; static unsigned long hash_djb2(const unsigned char* str) { unsigned long hash = 5381; int c; while ((c = *str++)) { hash = ((hash << 5) + hash) + (unsigned long)(c); } return hash; } static void create_hash_table(size_t start_capacity, hash_table_t* hash_table) { if (start_capacity < 1) start_capacity = HASH_TABLE_DEFAULT_SIZE; hash_table->hashes = (unsigned long*) TINYOBJ_MALLOC(start_capacity * sizeof(unsigned long)); hash_table->entries = (hash_table_entry_t*) TINYOBJ_CALLOC(start_capacity, sizeof(hash_table_entry_t)); hash_table->capacity = start_capacity; hash_table->n = 0; } static void destroy_hash_table(hash_table_t* hash_table) { TINYOBJ_FREE(hash_table->entries); TINYOBJ_FREE(hash_table->hashes); } /* Insert with quadratic probing */ static int hash_table_insert_value(unsigned long hash, long value, hash_table_t* hash_table) { /* Insert value */ size_t start_index = hash % hash_table->capacity; size_t index = start_index; hash_table_entry_t* start_entry = hash_table->entries + start_index; size_t i; hash_table_entry_t* entry; for (i = 1; hash_table->entries[index].filled; i++) { if (i >= hash_table->capacity) return HASH_TABLE_ERROR; index = (start_index + (i * i)) % hash_table->capacity; } entry = hash_table->entries + index; entry->hash = hash; entry->filled = 1; entry->value = value; if (index != start_index) { /* This is a new entry, but not the start entry, hence we need to add a next pointer to our entry */ entry->next = start_entry->next; start_entry->next = entry; } return HASH_TABLE_SUCCESS; } static int hash_table_insert(unsigned long hash, long value, hash_table_t* hash_table) { int ret = hash_table_insert_value(hash, value, hash_table); if (ret == HASH_TABLE_SUCCESS) { hash_table->hashes[hash_table->n] = hash; hash_table->n++; } return ret; } static hash_table_entry_t* hash_table_find(unsigned long hash, hash_table_t* hash_table) { hash_table_entry_t* entry = hash_table->entries + (hash % hash_table->capacity); while (entry) { if (entry->hash == hash && entry->filled) { return entry; } entry = entry->next; } return NULL; } static void hash_table_maybe_grow(size_t new_n, hash_table_t* hash_table) { size_t new_capacity; hash_table_t new_hash_table; size_t i; if (new_n <= hash_table->capacity) { return; } new_capacity = 2 * ((2 * hash_table->capacity) > new_n ? hash_table->capacity : new_n); /* Create a new hash table. We're not calling create_hash_table because we want to realloc the hash array */ new_hash_table.hashes = hash_table->hashes = (unsigned long*) TINYOBJ_REALLOC_SIZED( (void*) hash_table->hashes, sizeof(unsigned long) * hash_table->capacity, sizeof(unsigned long) * new_capacity); new_hash_table.entries = (hash_table_entry_t*) TINYOBJ_CALLOC(new_capacity, sizeof(hash_table_entry_t)); new_hash_table.capacity = new_capacity; new_hash_table.n = hash_table->n; /* Rehash */ for (i = 0; i < hash_table->capacity; i++) { hash_table_entry_t* entry = hash_table_find(hash_table->hashes[i], hash_table); hash_table_insert_value(hash_table->hashes[i], entry->value, &new_hash_table); } TINYOBJ_FREE(hash_table->entries); (*hash_table) = new_hash_table; } static int hash_table_exists(const char* name, hash_table_t* hash_table) { return hash_table_find(hash_djb2((const unsigned char*)name), hash_table) != NULL; } static void hash_table_set(const char* name, size_t val, hash_table_t* hash_table) { /* Hash name */ unsigned long hash = hash_djb2((const unsigned char *)name); hash_table_entry_t* entry = hash_table_find(hash, hash_table); if (entry) { entry->value = (long)val; return; } /* Expand if necessary * Grow until the element has been added */ do { hash_table_maybe_grow(hash_table->n + 1, hash_table); } while (hash_table_insert(hash, (long)val, hash_table) != HASH_TABLE_SUCCESS); } static long hash_table_get(const char* name, hash_table_t* hash_table) { hash_table_entry_t* ret = hash_table_find(hash_djb2((const unsigned char*)(name)), hash_table); return ret->value; } static tinyobj_material_t *tinyobj_material_add(tinyobj_material_t *prev, size_t num_materials, tinyobj_material_t *new_mat) { tinyobj_material_t *dst; size_t num_bytes = sizeof(tinyobj_material_t) * num_materials; dst = (tinyobj_material_t *)TINYOBJ_REALLOC_SIZED( prev, num_bytes, num_bytes + sizeof(tinyobj_material_t)); dst[num_materials] = (*new_mat); /* Just copy pointer for char* members */ return dst; } static int is_line_ending(const char *p, size_t i, size_t end_i) { if (p[i] == '\0') return 1; if (p[i] == '\n') return 1; /* this includes \r\n */ if (p[i] == '\r') { if (((i + 1) < end_i) && (p[i + 1] != '\n')) { /* detect only \r case */ return 1; } } return 0; } typedef struct { size_t pos; size_t len; } LineInfo; /* Find '\n' and create line data. */ static int get_line_infos(const char *buf, size_t buf_len, LineInfo **line_infos, size_t *num_lines) { size_t i = 0; size_t end_idx = buf_len; size_t prev_pos = 0; size_t line_no = 0; size_t last_line_ending = 0; /* Count # of lines. */ for (i = 0; i < end_idx; i++) { if (is_line_ending(buf, i, end_idx)) { (*num_lines)++; last_line_ending = i; } } /* The last char from the input may not be a line * ending character so add an extra line if there * are more characters after the last line ending * that was found. */ if (end_idx - last_line_ending > 0) { (*num_lines)++; } if (*num_lines == 0) return TINYOBJ_ERROR_EMPTY; *line_infos = (LineInfo *)TINYOBJ_MALLOC(sizeof(LineInfo) * (*num_lines)); /* Fill line infos. */ for (i = 0; i < end_idx; i++) { if (is_line_ending(buf, i, end_idx)) { (*line_infos)[line_no].pos = prev_pos; (*line_infos)[line_no].len = i - prev_pos; prev_pos = i + 1; line_no++; } } if (end_idx - last_line_ending > 0) { (*line_infos)[line_no].pos = prev_pos; (*line_infos)[line_no].len = end_idx - 1 - last_line_ending; } return 0; } static int tinyobj_parse_and_index_mtl_file(tinyobj_material_t **materials_out, size_t *num_materials_out, const char *mtl_filename, const char *obj_filename, file_reader_callback file_reader, void *ctx, hash_table_t* material_table) { tinyobj_material_t material; size_t num_materials = 0; tinyobj_material_t *materials = NULL; int has_previous_material = 0; const char *line_end = NULL; size_t num_lines = 0; LineInfo *line_infos = NULL; size_t i = 0; char *buf = NULL; size_t len = 0; if (materials_out == NULL) { return TINYOBJ_ERROR_INVALID_PARAMETER; } if (num_materials_out == NULL) { return TINYOBJ_ERROR_INVALID_PARAMETER; } (*materials_out) = NULL; (*num_materials_out) = 0; file_reader(ctx, mtl_filename, 1, obj_filename, &buf, &len); if (len < 1) return TINYOBJ_ERROR_INVALID_PARAMETER; if (buf == NULL) return TINYOBJ_ERROR_INVALID_PARAMETER; if (get_line_infos(buf, len, &line_infos, &num_lines) != 0) { TINYOBJ_FREE(line_infos); return TINYOBJ_ERROR_EMPTY; } /* Create a default material */ initMaterial(&material); for (i = 0; i < num_lines; i++) { const char *p = &buf[line_infos[i].pos]; size_t p_len = line_infos[i].len; char linebuf[4096]; const char *token; assert(p_len < 4095); memcpy(linebuf, p, p_len); linebuf[p_len] = '\0'; token = linebuf; line_end = token + p_len; /* Skip leading space. */ token += strspn(token, " \t"); assert(token); if (token[0] == '\0') continue; /* empty line */ if (token[0] == '#') continue; /* comment line */ /* new mtl */ if ((0 == strncmp(token, "newmtl", 6)) && IS_SPACE((token[6]))) { char namebuf[4096]; /* flush previous material. */ if (has_previous_material) { materials = tinyobj_material_add(materials, num_materials, &material); num_materials++; } else { has_previous_material = 1; } /* initial temporary material */ initMaterial(&material); /* set new mtl name */ token += 7; #ifdef _MSC_VER sscanf_s(token, "%s", namebuf, (unsigned)_countof(namebuf)); #else sscanf(token, "%s", namebuf); #endif material.name = my_strdup(namebuf, (size_t) (line_end - token)); /* Add material to material table */ if (material_table) hash_table_set(material.name, num_materials, material_table); continue; } /* ambient */ if (token[0] == 'K' && token[1] == 'a' && IS_SPACE((token[2]))) { float r, g, b; token += 2; parseFloat3(&r, &g, &b, &token); material.ambient[0] = r; material.ambient[1] = g; material.ambient[2] = b; continue; } /* diffuse */ if (token[0] == 'K' && token[1] == 'd' && IS_SPACE((token[2]))) { float r, g, b; token += 2; parseFloat3(&r, &g, &b, &token); material.diffuse[0] = r; material.diffuse[1] = g; material.diffuse[2] = b; continue; } /* specular */ if (token[0] == 'K' && token[1] == 's' && IS_SPACE((token[2]))) { float r, g, b; token += 2; parseFloat3(&r, &g, &b, &token); material.specular[0] = r; material.specular[1] = g; material.specular[2] = b; continue; } /* transmittance */ if (token[0] == 'K' && token[1] == 't' && IS_SPACE((token[2]))) { float r, g, b; token += 2; parseFloat3(&r, &g, &b, &token); material.transmittance[0] = r; material.transmittance[1] = g; material.transmittance[2] = b; continue; } /* ior(index of refraction) */ if (token[0] == 'N' && token[1] == 'i' && IS_SPACE((token[2]))) { token += 2; material.ior = parseFloat(&token); continue; } /* emission */ if (token[0] == 'K' && token[1] == 'e' && IS_SPACE(token[2])) { float r, g, b; token += 2; parseFloat3(&r, &g, &b, &token); material.emission[0] = r; material.emission[1] = g; material.emission[2] = b; continue; } /* shininess */ if (token[0] == 'N' && token[1] == 's' && IS_SPACE(token[2])) { token += 2; material.shininess = parseFloat(&token); continue; } /* illum model */ if (0 == strncmp(token, "illum", 5) && IS_SPACE(token[5])) { token += 6; material.illum = parseInt(&token); continue; } /* dissolve */ if ((token[0] == 'd' && IS_SPACE(token[1]))) { token += 1; material.dissolve = parseFloat(&token); continue; } if (token[0] == 'T' && token[1] == 'r' && IS_SPACE(token[2])) { token += 2; /* Invert value of Tr(assume Tr is in range [0, 1]) */ material.dissolve = 1.0f - parseFloat(&token); continue; } /* ambient texture */ if ((0 == strncmp(token, "map_Ka", 6)) && IS_SPACE(token[6])) { token += 7; material.ambient_texname = my_strdup(token, (size_t) (line_end - token)); continue; } /* diffuse texture */ if ((0 == strncmp(token, "map_Kd", 6)) && IS_SPACE(token[6])) { token += 7; material.diffuse_texname = my_strdup(token, (size_t) (line_end - token)); continue; } /* specular texture */ if ((0 == strncmp(token, "map_Ks", 6)) && IS_SPACE(token[6])) { token += 7; material.specular_texname = my_strdup(token, (size_t) (line_end - token)); continue; } /* specular highlight texture */ if ((0 == strncmp(token, "map_Ns", 6)) && IS_SPACE(token[6])) { token += 7; material.specular_highlight_texname = my_strdup(token, (size_t) (line_end - token)); continue; } /* bump texture */ if ((0 == strncmp(token, "map_bump", 8)) && IS_SPACE(token[8])) { token += 9; material.bump_texname = my_strdup(token, (size_t) (line_end - token)); continue; } /* alpha texture */ if ((0 == strncmp(token, "map_d", 5)) && IS_SPACE(token[5])) { token += 6; material.alpha_texname = my_strdup(token, (size_t) (line_end - token)); continue; } /* bump texture */ if ((0 == strncmp(token, "bump", 4)) && IS_SPACE(token[4])) { token += 5; material.bump_texname = my_strdup(token, (size_t) (line_end - token)); continue; } /* displacement texture */ if ((0 == strncmp(token, "disp", 4)) && IS_SPACE(token[4])) { token += 5; material.displacement_texname = my_strdup(token, (size_t) (line_end - token)); continue; } /* @todo { unknown parameter } */ } TINYOBJ_FREE(line_infos); if (material.name) { /* Flush last material element */ materials = tinyobj_material_add(materials, num_materials, &material); num_materials++; } (*num_materials_out) = num_materials; (*materials_out) = materials; return TINYOBJ_SUCCESS; } int tinyobj_parse_mtl_file(tinyobj_material_t **materials_out, size_t *num_materials_out, const char *mtl_filename, const char *obj_filename, file_reader_callback file_reader, void *ctx) { return tinyobj_parse_and_index_mtl_file(materials_out, num_materials_out, mtl_filename, obj_filename, file_reader, ctx, NULL); } typedef enum { COMMAND_EMPTY, COMMAND_V, COMMAND_VN, COMMAND_VT, COMMAND_F, COMMAND_G, COMMAND_O, COMMAND_USEMTL, COMMAND_MTLLIB } CommandType; typedef struct { float vx, vy, vz; float nx, ny, nz; float tx, ty; /* @todo { Use dynamic array } */ tinyobj_vertex_index_t f[TINYOBJ_MAX_FACES_PER_F_LINE]; size_t num_f; int f_num_verts[TINYOBJ_MAX_FACES_PER_F_LINE]; size_t num_f_num_verts; const char *group_name; unsigned int group_name_len; int pad0; const char *object_name; unsigned int object_name_len; int pad1; const char *material_name; unsigned int material_name_len; int pad2; const char *mtllib_name; unsigned int mtllib_name_len; CommandType type; } Command; static int parseLine(Command *command, const char *p, size_t p_len, int triangulate) { char linebuf[4096]; const char *token; assert(p_len < 4095); memcpy(linebuf, p, p_len); linebuf[p_len] = '\0'; token = linebuf; command->type = COMMAND_EMPTY; /* Skip leading space. */ skip_space(&token); assert(token); if (token[0] == '\0') { /* empty line */ return 0; } if (token[0] == '#') { /* comment line */ return 0; } /* vertex */ if (token[0] == 'v' && IS_SPACE((token[1]))) { float x, y, z; token += 2; parseFloat3(&x, &y, &z, &token); command->vx = x; command->vy = y; command->vz = z; command->type = COMMAND_V; return 1; } /* normal */ if (token[0] == 'v' && token[1] == 'n' && IS_SPACE((token[2]))) { float x, y, z; token += 3; parseFloat3(&x, &y, &z, &token); command->nx = x; command->ny = y; command->nz = z; command->type = COMMAND_VN; return 1; } /* texcoord */ if (token[0] == 'v' && token[1] == 't' && IS_SPACE((token[2]))) { float x, y; token += 3; parseFloat2(&x, &y, &token); command->tx = x; command->ty = y; command->type = COMMAND_VT; return 1; } /* face */ if (token[0] == 'f' && IS_SPACE((token[1]))) { size_t num_f = 0; tinyobj_vertex_index_t f[TINYOBJ_MAX_FACES_PER_F_LINE]; token += 2; skip_space(&token); while (!IS_NEW_LINE(token[0])) { tinyobj_vertex_index_t vi = parseRawTriple(&token); skip_space_and_cr(&token); f[num_f] = vi; num_f++; } command->type = COMMAND_F; if (triangulate) { size_t k; size_t n = 0; tinyobj_vertex_index_t i0 = f[0]; tinyobj_vertex_index_t i1; tinyobj_vertex_index_t i2 = f[1]; assert(3 * num_f < TINYOBJ_MAX_FACES_PER_F_LINE); for (k = 2; k < num_f; k++) { i1 = i2; i2 = f[k]; command->f[3 * n + 0] = i0; command->f[3 * n + 1] = i1; command->f[3 * n + 2] = i2; command->f_num_verts[n] = 3; n++; } command->num_f = 3 * n; command->num_f_num_verts = n; } else { size_t k = 0; assert(num_f < TINYOBJ_MAX_FACES_PER_F_LINE); for (k = 0; k < num_f; k++) { command->f[k] = f[k]; } command->num_f = num_f; command->f_num_verts[0] = (int)num_f; command->num_f_num_verts = 1; } return 1; } /* use mtl */ if ((0 == strncmp(token, "usemtl", 6)) && IS_SPACE((token[6]))) { token += 7; skip_space(&token); command->material_name = p + (token - linebuf); command->material_name_len = (unsigned int)length_until_newline( token, (p_len - (size_t)(token - linebuf)) + 1); command->type = COMMAND_USEMTL; return 1; } /* load mtl */ if ((0 == strncmp(token, "mtllib", 6)) && IS_SPACE((token[6]))) { /* By specification, `mtllib` should be appear only once in .obj */ token += 7; skip_space(&token); command->mtllib_name = p + (token - linebuf); command->mtllib_name_len = (unsigned int)length_until_newline( token, p_len - (size_t)(token - linebuf)) + 1; command->type = COMMAND_MTLLIB; return 1; } /* group name */ if (token[0] == 'g' && IS_SPACE((token[1]))) { /* @todo { multiple group name. } */ token += 2; command->group_name = p + (token - linebuf); command->group_name_len = (unsigned int)length_until_newline( token, p_len - (size_t)(token - linebuf)) + 1; command->type = COMMAND_G; return 1; } /* object name */ if (token[0] == 'o' && IS_SPACE((token[1]))) { /* @todo { multiple object name? } */ token += 2; command->object_name = p + (token - linebuf); command->object_name_len = (unsigned int)length_until_newline( token, p_len - (size_t)(token - linebuf)) + 1; command->type = COMMAND_O; return 1; } return 0; } static size_t basename_len(const char *filename, size_t filename_length) { /* Count includes NUL terminator. */ const char *p = &filename[filename_length - 1]; size_t count = 1; /* On Windows, the directory delimiter is '\' and both it and '/' is * reserved by the filesystem. On *nix platforms, only the '/' character * is reserved, so account for the two cases separately. */ #if _WIN32 while (p[-1] != '/' && p[-1] != '\\') { if (p == filename) { count = filename_length; return count; } count++; p--; } p++; return count; #else while (*(--p) != '/') { if (p == filename) { count = filename_length; return count; } count++; } return count; #endif } static char *generate_mtl_filename(const char *obj_filename, size_t obj_filename_length, const char *mtllib_name, size_t mtllib_name_length) { /* Create a dynamically-allocated material filename. This allows the material * and obj files to be separated, however the mtllib name in the OBJ file * must be a relative path to the material file from the OBJ's directory. * This does not support the matllib name as an absolute address. */ char *mtl_filename; char *p; size_t mtl_filename_length; size_t obj_basename_length; /* Calculate required size of mtl_filename and allocate */ obj_basename_length = basename_len(obj_filename, obj_filename_length); mtl_filename_length = (obj_filename_length - obj_basename_length) + mtllib_name_length; mtl_filename = (char *)TINYOBJ_MALLOC(mtl_filename_length); /* Copy over the obj's path */ memcpy(mtl_filename, obj_filename, (obj_filename_length - obj_basename_length)); /* Overwrite the obj basename with the mtllib name, filling the string */ p = &mtl_filename[mtl_filename_length - mtllib_name_length]; strcpy(p, mtllib_name); return mtl_filename; } int tinyobj_parse_obj(tinyobj_attrib_t *attrib, tinyobj_shape_t **shapes, size_t *num_shapes, tinyobj_material_t **materials_out, size_t *num_materials_out, const char *obj_filename, file_reader_callback file_reader, void *ctx, unsigned int flags) { LineInfo *line_infos = NULL; Command *commands = NULL; size_t num_lines = 0; size_t num_v = 0; size_t num_vn = 0; size_t num_vt = 0; size_t num_f = 0; size_t num_faces = 0; int mtllib_line_index = -1; tinyobj_material_t *materials = NULL; size_t num_materials = 0; hash_table_t material_table; char *buf = NULL; size_t len = 0; file_reader(ctx, obj_filename, /* is_mtl */0, obj_filename, &buf, &len); if (len < 1) return TINYOBJ_ERROR_INVALID_PARAMETER; if (attrib == NULL) return TINYOBJ_ERROR_INVALID_PARAMETER; if (shapes == NULL) return TINYOBJ_ERROR_INVALID_PARAMETER; if (num_shapes == NULL) return TINYOBJ_ERROR_INVALID_PARAMETER; if (buf == NULL) return TINYOBJ_ERROR_INVALID_PARAMETER; if (materials_out == NULL) return TINYOBJ_ERROR_INVALID_PARAMETER; if (num_materials_out == NULL) return TINYOBJ_ERROR_INVALID_PARAMETER; tinyobj_attrib_init(attrib); /* 1. create line data */ if (get_line_infos(buf, len, &line_infos, &num_lines) != 0) { return TINYOBJ_ERROR_EMPTY; } commands = (Command *)TINYOBJ_MALLOC(sizeof(Command) * num_lines); create_hash_table(HASH_TABLE_DEFAULT_SIZE, &material_table); /* 2. parse each line */ { size_t i = 0; for (i = 0; i < num_lines; i++) { int ret = parseLine(&commands[i], &buf[line_infos[i].pos], line_infos[i].len, flags & TINYOBJ_FLAG_TRIANGULATE); if (ret) { if (commands[i].type == COMMAND_V) { num_v++; } else if (commands[i].type == COMMAND_VN) { num_vn++; } else if (commands[i].type == COMMAND_VT) { num_vt++; } else if (commands[i].type == COMMAND_F) { num_f += commands[i].num_f; num_faces += commands[i].num_f_num_verts; } if (commands[i].type == COMMAND_MTLLIB) { mtllib_line_index = (int)i; } } } } /* line_infos are not used anymore. Release memory. */ if (line_infos) { TINYOBJ_FREE(line_infos); } /* Load material (if it exists) */ if (mtllib_line_index >= 0 && commands[mtllib_line_index].mtllib_name && commands[mtllib_line_index].mtllib_name_len > 0) { /* Maximum length allowed by Linux - higher than Windows and macOS */ size_t obj_filename_len = my_strnlen(obj_filename, 4096 + 255) + 1; char *mtl_filename; char *mtllib_name; size_t mtllib_name_len = 0; int ret; mtllib_name_len = length_until_line_feed(commands[mtllib_line_index].mtllib_name, commands[mtllib_line_index].mtllib_name_len); mtllib_name = my_strndup(commands[mtllib_line_index].mtllib_name, mtllib_name_len); /* allow for NUL terminator */ mtllib_name_len++; mtl_filename = generate_mtl_filename(obj_filename, obj_filename_len, mtllib_name, mtllib_name_len); ret = tinyobj_parse_and_index_mtl_file(&materials, &num_materials, mtl_filename, obj_filename, file_reader, ctx, &material_table); if (ret != TINYOBJ_SUCCESS) { /* warning. */ fprintf(stderr, "TINYOBJ: Failed to parse material file '%s': %d\n", mtl_filename, ret); } TINYOBJ_FREE(mtl_filename); TINYOBJ_FREE(mtllib_name); } /* Construct attributes */ { size_t v_count = 0; size_t n_count = 0; size_t t_count = 0; size_t f_count = 0; size_t face_count = 0; int material_id = -1; /* -1 = default unknown material. */ size_t i = 0; attrib->vertices = (float *)TINYOBJ_MALLOC(sizeof(float) * num_v * 3); attrib->num_vertices = (unsigned int)num_v; attrib->normals = (float *)TINYOBJ_MALLOC(sizeof(float) * num_vn * 3); attrib->num_normals = (unsigned int)num_vn; attrib->texcoords = (float *)TINYOBJ_MALLOC(sizeof(float) * num_vt * 2); attrib->num_texcoords = (unsigned int)num_vt; attrib->faces = (tinyobj_vertex_index_t *)TINYOBJ_MALLOC( sizeof(tinyobj_vertex_index_t) * num_f); attrib->num_faces = (unsigned int)num_f; attrib->face_num_verts = (int *)TINYOBJ_MALLOC(sizeof(int) * num_faces); attrib->material_ids = (int *)TINYOBJ_MALLOC(sizeof(int) * num_faces); attrib->num_face_num_verts = (unsigned int)num_faces; for (i = 0; i < num_lines; i++) { if (commands[i].type == COMMAND_EMPTY) { continue; } else if (commands[i].type == COMMAND_USEMTL) { /* @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; } } */ if (commands[i].material_name && commands[i].material_name_len >0) { /* Create a null terminated string */ char* material_name_null_term = (char*) TINYOBJ_MALLOC(commands[i].material_name_len + 1); memcpy((void*) material_name_null_term, (const void*) commands[i].material_name, commands[i].material_name_len); material_name_null_term[commands[i].material_name_len] = 0; if (hash_table_exists(material_name_null_term, &material_table)) material_id = (int)hash_table_get(material_name_null_term, &material_table); else material_id = -1; TINYOBJ_FREE(material_name_null_term); } } else if (commands[i].type == COMMAND_V) { attrib->vertices[3 * v_count + 0] = commands[i].vx; attrib->vertices[3 * v_count + 1] = commands[i].vy; attrib->vertices[3 * v_count + 2] = commands[i].vz; v_count++; } else if (commands[i].type == COMMAND_VN) { attrib->normals[3 * n_count + 0] = commands[i].nx; attrib->normals[3 * n_count + 1] = commands[i].ny; attrib->normals[3 * n_count + 2] = commands[i].nz; n_count++; } else if (commands[i].type == COMMAND_VT) { attrib->texcoords[2 * t_count + 0] = commands[i].tx; attrib->texcoords[2 * t_count + 1] = commands[i].ty; t_count++; } else if (commands[i].type == COMMAND_F) { size_t k = 0; for (k = 0; k < commands[i].num_f; k++) { tinyobj_vertex_index_t vi = commands[i].f[k]; int v_idx = fixIndex(vi.v_idx, v_count); int vn_idx = fixIndex(vi.vn_idx, n_count); int vt_idx = fixIndex(vi.vt_idx, t_count); attrib->faces[f_count + k].v_idx = v_idx; attrib->faces[f_count + k].vn_idx = vn_idx; attrib->faces[f_count + k].vt_idx = vt_idx; } for (k = 0; k < commands[i].num_f_num_verts; k++) { attrib->material_ids[face_count + k] = material_id; attrib->face_num_verts[face_count + k] = commands[i].f_num_verts[k]; } f_count += commands[i].num_f; face_count += commands[i].num_f_num_verts; } } } /* 5. Construct shape information. */ { unsigned int face_count = 0; size_t i = 0; size_t n = 0; size_t shape_idx = 0; const char *shape_name = NULL; unsigned int shape_name_len = 0; const char *prev_shape_name = NULL; unsigned int prev_shape_name_len = 0; unsigned int prev_shape_face_offset = 0; unsigned int prev_face_offset = 0; tinyobj_shape_t prev_shape = {NULL, 0, 0}; /* Find the number of shapes in .obj */ for (i = 0; i < num_lines; i++) { if (commands[i].type == COMMAND_O || commands[i].type == COMMAND_G) { n++; } } /* Allocate array of shapes with maximum possible size(+1 for unnamed * group/object). * Actual # of shapes found in .obj is determined in the later */ (*shapes) = (tinyobj_shape_t*)TINYOBJ_MALLOC(sizeof(tinyobj_shape_t) * (n + 1)); for (i = 0; i < num_lines; i++) { if (commands[i].type == COMMAND_O || commands[i].type == COMMAND_G) { if (commands[i].type == COMMAND_O) { shape_name = commands[i].object_name; shape_name_len = commands[i].object_name_len; } else { shape_name = commands[i].group_name; shape_name_len = commands[i].group_name_len; } if (face_count == 0) { /* 'o' or 'g' appears before any 'f' */ prev_shape_name = shape_name; prev_shape_name_len = shape_name_len; prev_shape_face_offset = face_count; prev_face_offset = face_count; } else { if (shape_idx == 0) { /* 'o' or 'g' after some 'v' lines. */ (*shapes)[shape_idx].name = my_strndup( prev_shape_name, prev_shape_name_len); /* may be NULL */ (*shapes)[shape_idx].face_offset = prev_shape.face_offset; (*shapes)[shape_idx].length = face_count - prev_face_offset; shape_idx++; prev_face_offset = face_count; } else { if ((face_count - prev_face_offset) > 0) { (*shapes)[shape_idx].name = my_strndup(prev_shape_name, prev_shape_name_len); (*shapes)[shape_idx].face_offset = prev_face_offset; (*shapes)[shape_idx].length = face_count - prev_face_offset; shape_idx++; prev_face_offset = face_count; } } /* Record shape info for succeeding 'o' or 'g' command. */ prev_shape_name = shape_name; prev_shape_name_len = shape_name_len; prev_shape_face_offset = face_count; } } if (commands[i].type == COMMAND_F) { face_count++; } } if ((face_count - prev_face_offset) > 0) { size_t length = face_count - prev_shape_face_offset; if (length > 0) { (*shapes)[shape_idx].name = my_strndup(prev_shape_name, prev_shape_name_len); (*shapes)[shape_idx].face_offset = prev_face_offset; (*shapes)[shape_idx].length = face_count - prev_face_offset; shape_idx++; } } else { /* Guess no 'v' line occurrence after 'o' or 'g', so discards current * shape information. */ } (*num_shapes) = shape_idx; } if (commands) { TINYOBJ_FREE(commands); } destroy_hash_table(&material_table); (*materials_out) = materials; (*num_materials_out) = num_materials; return TINYOBJ_SUCCESS; } void tinyobj_attrib_init(tinyobj_attrib_t *attrib) { attrib->vertices = NULL; attrib->num_vertices = 0; attrib->normals = NULL; attrib->num_normals = 0; attrib->texcoords = NULL; attrib->num_texcoords = 0; attrib->faces = NULL; attrib->num_faces = 0; attrib->face_num_verts = NULL; attrib->num_face_num_verts = 0; attrib->material_ids = NULL; } void tinyobj_attrib_free(tinyobj_attrib_t *attrib) { if (attrib->vertices) TINYOBJ_FREE(attrib->vertices); if (attrib->normals) TINYOBJ_FREE(attrib->normals); if (attrib->texcoords) TINYOBJ_FREE(attrib->texcoords); if (attrib->faces) TINYOBJ_FREE(attrib->faces); if (attrib->face_num_verts) TINYOBJ_FREE(attrib->face_num_verts); if (attrib->material_ids) TINYOBJ_FREE(attrib->material_ids); } void tinyobj_shapes_free(tinyobj_shape_t *shapes, size_t num_shapes) { size_t i; if (shapes == NULL) return; for (i = 0; i < num_shapes; i++) { if (shapes[i].name) TINYOBJ_FREE(shapes[i].name); } TINYOBJ_FREE(shapes); } void tinyobj_materials_free(tinyobj_material_t *materials, size_t num_materials) { size_t i; if (materials == NULL) return; for (i = 0; i < num_materials; i++) { if (materials[i].name) TINYOBJ_FREE(materials[i].name); if (materials[i].ambient_texname) TINYOBJ_FREE(materials[i].ambient_texname); if (materials[i].diffuse_texname) TINYOBJ_FREE(materials[i].diffuse_texname); if (materials[i].specular_texname) TINYOBJ_FREE(materials[i].specular_texname); if (materials[i].specular_highlight_texname) TINYOBJ_FREE(materials[i].specular_highlight_texname); if (materials[i].bump_texname) TINYOBJ_FREE(materials[i].bump_texname); if (materials[i].displacement_texname) TINYOBJ_FREE(materials[i].displacement_texname); if (materials[i].alpha_texname) TINYOBJ_FREE(materials[i].alpha_texname); } TINYOBJ_FREE(materials); } #endif /* TINYOBJ_LOADER_C_IMPLEMENTATION */ #endif /* TINOBJ_LOADER_C_H_ */