Last active
October 5, 2021 22:59
-
-
Save ryanfleury/0062f2ffdec07bcda8a4a0ef5c7f8f37 to your computer and use it in GitHub Desktop.
Single-header OBJ Parser (WIP)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ---------------------------------------------------------------------------- | |
// Single Header Wavefront OBJ Parser, by Ryan Fleury | |
// ---------------------------------------------------------------------------- | |
// | |
// This is a single-header Wavefront OBJ parsing library that is both C99+ and | |
// C++ compatible. There are two APIs that you can use. One of which is lower | |
// level and more shippable, and the other is high level and less shippable, | |
// but is nice to get things off the ground. | |
// | |
// To use this file, you must #define OBJ_PARSE_IMPLEMENTATION in at least ONE | |
// C/C++ file before including this file. The file can be included elsewhere | |
// and act just like a regular header. | |
// | |
// ---------------------------------------------------------------------------- | |
// | |
// High Level API: Less shippable, but does almost everything for you. Remember | |
// to free after you're done with the result. | |
#if 0 | |
ParsedOBJ obj = LoadOBJ("data/sponza/sponza.obj"); | |
FreeParsedOBJ(&obj); | |
#endif | |
// | |
// ---------------------------------------------------------------------------- | |
// | |
// Low Level API | |
#if 0 | |
// You begin by filling out an OBJParseInfo structure. | |
OBJParseInfo info = {0}; | |
{ | |
info.obj_data = LoadFile(filename); // This should be a null-terminated OBJ file's contents. | |
info.parse_memory_size = parse_memory_size; // This can be too small for certain models! | |
info.parse_memory = parse_memory; // Parser will work within this memory, and perform 0 allocations. | |
info.filename = filename; // Optional, passed to callbacks | |
info.error_callback = ErrorCallback; // Optional | |
info.warning_callback = WarningCallback; // Optional | |
} | |
// Pass a pointer to this structure to ParseOBJ, which will return a ParsedOBJ | |
// structure, containing all of the information extracted from the .obj file. | |
ParsedOBJ obj = ParseOBJ(&info); | |
// You can also choose to load in the MTL files found in the .obj file. | |
for(unsigned int i = 0; i < obj.material_library_count; ++i) | |
{ | |
char *material_library_name = obj.material_libraries[i]; | |
char *filename = ...; // Use material_library_name to construct a path to the .mtl file | |
// For each MTL file, fill out a MTLParseInfo structure. | |
MTLParseInfo info = {0}; | |
{ | |
info.mtl_data = LoadFile(filename); | |
info.filename = filename; | |
info.error_callback = ErrorCallback; | |
info.warning_callback = WarningCallback; | |
} | |
ParseMTLForOBJ(&info, obj); | |
} | |
// Now, the ParsedOBJ structure will contain an array of "Renderables", which have | |
// a vertex buffer, an index buffer, associated sizes, and materials. Each renderable | |
// corresponds one-to-one with materials found in the file; polygon groups with the | |
// same material are joined. See the definition of the ParsedOBJ structure for | |
// more detailed information. | |
#endif | |
// | |
// ---------------------------------------------------------------------------- | |
// LICENSE INFORMATION IS AT THE END OF THE FILE. (MIT) | |
// ---------------------------------------------------------------------------- | |
#ifndef OBJ_PARSE_H_INCLUDED | |
#define OBJ_PARSE_H_INCLUDED | |
#ifndef OBJ_PARSE_NO_CRT | |
#include <stdlib.h> | |
#include <stdio.h> | |
#include <string.h> | |
#endif | |
#define PARSED_OBJ_MODEL_FLAG_POSITION (1<<0) | |
#define PARSED_OBJ_MODEL_FLAG_UV (1<<1) | |
#define PARSED_OBJ_MODEL_FLAG_NORMAL (1<<2) | |
typedef struct ParsedOBJMaterial ParsedOBJMaterial; | |
struct ParsedOBJMaterial | |
{ | |
char *material_name; | |
float albedo_color[3]; | |
float diffuse_color[3]; | |
float specular_color[3]; | |
float emissive_color[3]; | |
float opacity; | |
int illumination_type; | |
char *albedo_map_path; | |
char *diffuse_map_path; | |
char *specular_map_path; | |
char *dissolve_map_path; | |
char *bump_map_path; | |
}; | |
typedef struct ParsedOBJRenderable ParsedOBJRenderable; | |
struct ParsedOBJRenderable | |
{ | |
ParsedOBJMaterial material; | |
int flags; | |
unsigned int vertex_count; | |
unsigned int floats_per_vertex; | |
float *vertices; | |
unsigned int index_count; | |
int *indices; | |
}; | |
typedef struct OBJParseArena OBJParseArena; | |
typedef struct ParsedOBJ ParsedOBJ; | |
struct ParsedOBJ | |
{ | |
unsigned int renderable_count; | |
ParsedOBJRenderable *renderables; | |
unsigned int material_library_count; | |
char **material_libraries; | |
}; | |
typedef struct OBJParseInfo OBJParseInfo; | |
typedef struct MTLParseInfo MTLParseInfo; | |
ParsedOBJ LoadOBJ(char *filename); | |
void LoadMTLForOBJ(char *filename, ParsedOBJ *obj); | |
ParsedOBJ ParseOBJ(OBJParseInfo *parse_info); | |
void ParseMTLForOBJ(MTLParseInfo *parse_info, ParsedOBJ *obj); | |
void FreeParsedOBJ(ParsedOBJ *parsed_obj); | |
typedef struct OBJParseError OBJParseError; | |
typedef struct OBJParseWarning OBJParseWarning; | |
typedef void OBJParseErrorCallback (OBJParseError error); | |
typedef void OBJParseWarningCallback (OBJParseWarning warning); | |
struct OBJParseInfo | |
{ | |
char *obj_data; | |
void *parse_memory; | |
unsigned int parse_memory_size; | |
char *filename; | |
OBJParseErrorCallback *error_callback; | |
OBJParseWarningCallback *warning_callback; | |
}; | |
struct MTLParseInfo | |
{ | |
char *mtl_data; | |
char *filename; | |
OBJParseErrorCallback *error_callback; | |
OBJParseWarningCallback *warning_callback; | |
}; | |
typedef enum OBJParseErrorType OBJParseErrorType; | |
enum OBJParseErrorType | |
{ | |
OBJ_PARSE_ERROR_TYPE_out_of_memory, | |
OBJ_PARSE_ERROR_TYPE_MAX | |
}; | |
typedef enum OBJParseWarningType OBJParseWarningType; | |
enum OBJParseWarningType | |
{ | |
OBJ_PARSE_WARNING_TYPE_unexpected_token, | |
OBJ_PARSE_WARNING_TYPE_MAX | |
}; | |
struct OBJParseError | |
{ | |
OBJParseErrorType type; | |
char *filename; | |
int line; | |
char *message; | |
}; | |
struct OBJParseWarning | |
{ | |
OBJParseWarningType type; | |
char *filename; | |
int line; | |
char *message; | |
}; | |
/*---------------------------------------------------------------------------------*/ | |
/*---------------------------------------------------------------------------------*/ | |
/*---------------------------------------------------------------------------------*/ | |
#ifdef OBJ_PARSE_IMPLEMENTATION | |
typedef struct OBJParserArena OBJParserArena; | |
struct OBJParserArena | |
{ | |
void *memory; | |
unsigned int memory_alloc_position; | |
unsigned int memory_size; | |
unsigned int memory_left; | |
}; | |
typedef struct OBJParserPersistentState OBJParserPersistentState; | |
struct OBJParserPersistentState | |
{ | |
OBJParserArena parser_arena; | |
unsigned int materials_to_load_count; | |
ParsedOBJMaterial **materials_to_load; | |
void *parse_memory_to_free; | |
OBJParseErrorCallback *ErrorCallback; | |
OBJParseWarningCallback *WarningCallback; | |
}; | |
OBJParserArena | |
OBJParserArenaInit(void *memory, unsigned int memory_size) | |
{ | |
OBJParserArena arena = {0}; | |
arena.memory = memory; | |
arena.memory_alloc_position = 0; | |
arena.memory_size = arena.memory_left = memory_size; | |
return arena; | |
} | |
void * | |
OBJParserArenaAllocate(OBJParserArena *arena, unsigned int size) | |
{ | |
void *memory = 0; | |
if(arena->memory && size <= arena->memory_left) | |
{ | |
memory = (char *)arena->memory + arena->memory_alloc_position; | |
arena->memory_alloc_position += size; | |
arena->memory_left -= size; | |
} | |
return memory; | |
} | |
char * | |
OBJParserArenaAllocateCStringCopy(OBJParserArena *arena, char *string) | |
{ | |
char *result = 0; | |
int string_length = 0; | |
for(; string[string_length]; ++string_length); | |
result = (char *)OBJParserArenaAllocate(arena, string_length + 1); | |
for(int i = 0; i < string_length; ++i) | |
{ | |
result[i] = string[i]; | |
} | |
result[string_length] = 0; | |
return result; | |
} | |
int | |
OBJParserCharToLower(int c) | |
{ | |
if(c >= 'A' && c <= 'Z') | |
{ | |
c += 32; | |
} | |
return c; | |
} | |
int | |
OBJParserCharIsAlpha(int c) | |
{ | |
return ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')); | |
} | |
int | |
OBJParserCharIsDigit(int c) | |
{ | |
return (c >= '0' && c <= '9'); | |
} | |
int | |
OBJParserStringMatchCaseSensitive(char *str1, char *str2) | |
{ | |
int result = 1; | |
if(str1 && str2) | |
{ | |
for(int i = 0;; ++i) | |
{ | |
if(str1[i] != str2[i]) | |
{ | |
result = 0; | |
break; | |
} | |
if(str1[i] == 0 && str2[i] == 0) | |
{ | |
break; | |
} | |
} | |
} | |
else if(str1 || str2) | |
{ | |
result = 0; | |
} | |
return result; | |
} | |
int | |
OBJParserStringMatchCaseInsensitiveN(char *str1, char *str2, int n) | |
{ | |
int result = 1; | |
if(str1 && str2) | |
{ | |
for(int i = 0; i < n; ++i) | |
{ | |
if(OBJParserCharToLower(str1[i]) != OBJParserCharToLower(str2[i])) | |
{ | |
result = 0; | |
break; | |
} | |
if(str1[i] == 0 && str2[i] == 0) | |
{ | |
break; | |
} | |
} | |
} | |
else if(str1 || str2) | |
{ | |
result = 0; | |
} | |
return result; | |
} | |
typedef enum OBJTokenType OBJTokenType; | |
enum OBJTokenType | |
{ | |
OBJ_TOKEN_TYPE_null, | |
MTL_TOKEN_TYPE_null = OBJ_TOKEN_TYPE_null, | |
OBJ_TOKEN_TYPE_object_signifier, | |
OBJ_TOKEN_TYPE_group_signifier, | |
OBJ_TOKEN_TYPE_shading_signifier, | |
OBJ_TOKEN_TYPE_vertex_position_signifier, | |
OBJ_TOKEN_TYPE_vertex_uv_signifier, | |
OBJ_TOKEN_TYPE_vertex_normal_signifier, | |
OBJ_TOKEN_TYPE_face_signifier, | |
OBJ_TOKEN_TYPE_number, | |
OBJ_TOKEN_TYPE_face_index_divider, | |
OBJ_TOKEN_TYPE_material_library_signifier, | |
OBJ_TOKEN_TYPE_use_material_signifier, | |
MTL_TOKEN_TYPE_new_material_signifier, // newmtl | |
MTL_TOKEN_TYPE_ambient_color_signifier, // Ka | |
MTL_TOKEN_TYPE_diffuse_color_signifier, // Kd | |
MTL_TOKEN_TYPE_specular_color_signifier, // Ks | |
MTL_TOKEN_TYPE_emissive_color_signifier, // Ke | |
MTL_TOKEN_TYPE_specular_exponent_signifier, // Ns | |
MTL_TOKEN_TYPE_optical_density_signifier, // Ni | |
MTL_TOKEN_TYPE_dissolve_signifier, // d | |
MTL_TOKEN_TYPE_transparency_signifier, // Tr | |
MTL_TOKEN_TYPE_transmission_filter_signifier, // Tf | |
MTL_TOKEN_TYPE_illumination_type_signifier, // illum | |
MTL_TOKEN_TYPE_albedo_map_signifier, // map_Ka | |
MTL_TOKEN_TYPE_diffuse_map_signifier, // map_Kd | |
MTL_TOKEN_TYPE_specular_map_signifier, // map_Ks | |
MTL_TOKEN_TYPE_dissolve_map_signifier, // map_d | |
MTL_TOKEN_TYPE_bump_map_signifier, // map_bump | |
}; | |
typedef struct OBJToken OBJToken; | |
struct OBJToken | |
{ | |
OBJTokenType type; | |
char *string; | |
int string_length; | |
}; | |
typedef struct OBJTokenizer OBJTokenizer; | |
struct OBJTokenizer | |
{ | |
char *at; | |
char *filename; | |
int line; | |
}; | |
int | |
OBJTokenMatch(OBJToken token, char *string) | |
{ | |
return (OBJParserStringMatchCaseInsensitiveN(token.string, string, token.string_length) && | |
string[token.string_length] == 0); | |
} | |
OBJToken | |
OBJParserGetNextTokenFromBuffer(char *buffer) | |
{ | |
OBJToken token = {0}; | |
enum | |
{ | |
SEARCH_MODE_normal, | |
SEARCH_MODE_ignore_line, | |
}; | |
int search_mode = SEARCH_MODE_normal; | |
for(int i = 0; buffer[i]; ++i) | |
{ | |
if(search_mode == SEARCH_MODE_normal) | |
{ | |
if(buffer[i] == '#') | |
{ | |
search_mode = SEARCH_MODE_ignore_line; | |
} | |
else if(OBJParserCharIsAlpha(buffer[i])) | |
{ | |
int j = i+1; | |
for(; buffer[j] && (OBJParserCharIsAlpha(buffer[j]) || buffer[j] == '_'); ++j); | |
// NOTE(rjf): OBJ tokens | |
if(OBJParserStringMatchCaseInsensitiveN("vn", buffer + i, j - i)) | |
{ | |
token.type = OBJ_TOKEN_TYPE_vertex_normal_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("vt", buffer + i, j - i)) | |
{ | |
token.type = OBJ_TOKEN_TYPE_vertex_uv_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("v", buffer + i, j - i)) | |
{ | |
token.type = OBJ_TOKEN_TYPE_vertex_position_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("f", buffer + i, j - i)) | |
{ | |
token.type = OBJ_TOKEN_TYPE_face_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("o", buffer + i, j - i)) | |
{ | |
token.type = OBJ_TOKEN_TYPE_object_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("g", buffer + i, j - i)) | |
{ | |
token.type = OBJ_TOKEN_TYPE_group_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("s", buffer + i, j - i)) | |
{ | |
token.type = OBJ_TOKEN_TYPE_shading_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("mtllib", buffer + i, j - i)) | |
{ | |
token.type = OBJ_TOKEN_TYPE_material_library_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("usemtl", buffer + i, j - i)) | |
{ | |
token.type = OBJ_TOKEN_TYPE_use_material_signifier; | |
} | |
// NOTE(rjf): MTL tokens | |
else if(OBJParserStringMatchCaseInsensitiveN("newmtl", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_new_material_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("Ka", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_ambient_color_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("Kd", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_diffuse_color_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("Ks", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_specular_color_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("Ke", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_emissive_color_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("Ns", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_specular_exponent_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("Ni", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_optical_density_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("d", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_dissolve_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("Tr", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_transparency_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("Tf", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_transmission_filter_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("illum", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_illumination_type_signifier; | |
} | |
// NOTE(rjf): Maps | |
else if(OBJParserStringMatchCaseInsensitiveN("map_Ka", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_albedo_map_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("map_Kd", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_diffuse_map_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("map_Ks", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_specular_map_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("map_d", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_dissolve_map_signifier; | |
} | |
else if(OBJParserStringMatchCaseInsensitiveN("map_bump", buffer + i, j - i)) | |
{ | |
token.type = MTL_TOKEN_TYPE_bump_map_signifier; | |
} | |
token.string = buffer+i; | |
token.string_length = j-i; | |
break; | |
} | |
else if(OBJParserCharIsDigit(buffer[i]) || buffer[i] == '-') | |
{ | |
int j = i+1; | |
token.type = OBJ_TOKEN_TYPE_number; | |
for(; buffer[j] && (OBJParserCharIsDigit(buffer[j]) || buffer[j] == '.' || OBJParserCharIsAlpha(buffer[j])); ++j); | |
token.string = buffer+i; | |
token.string_length = j-i; | |
break; | |
} | |
else if(buffer[i] == '/') | |
{ | |
token.string = buffer+i; | |
token.string_length = 1; | |
token.type = OBJ_TOKEN_TYPE_face_index_divider; | |
break; | |
} | |
} | |
else if(search_mode == SEARCH_MODE_ignore_line) | |
{ | |
if(buffer[i] == '\n') | |
{ | |
search_mode = SEARCH_MODE_normal; | |
} | |
} | |
} | |
return token; | |
} | |
OBJToken | |
OBJNextToken(OBJTokenizer *tokenizer) | |
{ | |
OBJToken token = OBJParserGetNextTokenFromBuffer(tokenizer->at); | |
if(token.type != OBJ_TOKEN_TYPE_null) | |
{ | |
tokenizer->at = token.string + token.string_length; | |
} | |
return token; | |
} | |
void | |
OBJSkipUntilNextLine(OBJTokenizer *tokenizer) | |
{ | |
for(int i = 0; tokenizer->at[i]; ++i) | |
{ | |
if(tokenizer->at[i] == '\n') | |
{ | |
tokenizer->at += ++i; | |
break; | |
} | |
} | |
} | |
void | |
OBJReadTrimmedStringUntilNextLineToFixSizedBuffer(OBJTokenizer *tokenizer, char *destination, int destination_max) | |
{ | |
int destination_write_pos = 0; | |
int found_non_whitespace = 0; | |
char *one_past_last_non_whitespace_character = tokenizer->at; | |
for(int i = 0; tokenizer->at[i] && destination_write_pos < destination_max - 1; ++i) | |
{ | |
if(tokenizer->at[i] == '\n') | |
{ | |
break; | |
} | |
else | |
{ | |
if(found_non_whitespace) | |
{ | |
destination[destination_write_pos++] = tokenizer->at[i]; | |
one_past_last_non_whitespace_character = destination + destination_write_pos; | |
} | |
else | |
{ | |
if(tokenizer->at[i] > 32) | |
{ | |
found_non_whitespace = 1; | |
--i; | |
} | |
} | |
} | |
} | |
*one_past_last_non_whitespace_character = 0; | |
destination[destination_write_pos++] = 0; | |
} | |
OBJToken | |
OBJPeekToken(OBJTokenizer *tokenizer) | |
{ | |
OBJToken token = OBJParserGetNextTokenFromBuffer(tokenizer->at); | |
return token; | |
} | |
int | |
OBJRequireToken(OBJTokenizer *tokenizer, char *string, OBJToken *token_ptr) | |
{ | |
int match = 0; | |
OBJToken token = OBJPeekToken(tokenizer); | |
if(OBJTokenMatch(token, string)) | |
{ | |
match = 1; | |
tokenizer->at = token.string + token.string_length; | |
if(token_ptr) | |
{ | |
*token_ptr = token; | |
} | |
} | |
return match; | |
} | |
int | |
OBJRequireTokenType(OBJTokenizer *tokenizer, OBJTokenType type, OBJToken *token_ptr) | |
{ | |
int match = 0; | |
OBJToken token = OBJPeekToken(tokenizer); | |
if(token.type == type) | |
{ | |
match = 1; | |
tokenizer->at = token.string + token.string_length; | |
if(token_ptr) | |
{ | |
*token_ptr = token; | |
} | |
} | |
return match; | |
} | |
#define OBJParseCStringToInt(i) (atoi(i)) | |
#define OBJParseCStringToFloat(f) ((float)atof(f)) | |
float | |
OBJTokenToFloat(OBJToken token) | |
{ | |
int float_str_write_pos = 0; | |
char float_str[64] = {0}; | |
float val = 0.f; | |
for(int i = 0; i < token.string_length; ++i) | |
{ | |
if(token.string[i] == '-' || OBJParserCharIsDigit(token.string[i])) | |
{ | |
for(int j = i; j < token.string_length && token.string[j] && | |
(token.string[j] == '-' || OBJParserCharIsDigit(token.string[j]) || token.string[j] == '.'); ++j) | |
{ | |
if(float_str_write_pos < sizeof(float_str)) | |
{ | |
float_str[float_str_write_pos++] = token.string[j]; | |
} | |
else | |
{ | |
break; | |
} | |
} | |
break; | |
} | |
} | |
val = OBJParseCStringToFloat(float_str); | |
return val; | |
} | |
int | |
OBJTokenToInt(OBJToken token) | |
{ | |
int int_str_write_pos = 0; | |
char int_str[64] = {0}; | |
int val = 0; | |
if(token.string) | |
{ | |
for(int i = 0; i < token.string_length; ++i) | |
{ | |
if(token.string[i] == '-' || OBJParserCharIsDigit(token.string[i])) | |
{ | |
for(int j = i; j < token.string_length && token.string[j] && | |
(token.string[j] == '-' || OBJParserCharIsDigit(token.string[j])); ++j) | |
{ | |
if(int_str_write_pos < sizeof(int_str)) | |
{ | |
int_str[int_str_write_pos++] = token.string[j]; | |
} | |
else | |
{ | |
break; | |
} | |
} | |
break; | |
} | |
} | |
val = OBJParseCStringToInt(int_str); | |
} | |
return val; | |
} | |
#ifndef OBJ_PARSE_NO_CRT | |
char * | |
OBJParseLoadEntireFileAndNullTerminate(char *filename) | |
{ | |
char *result = 0; | |
FILE *file = fopen(filename, "rb"); | |
if(file) | |
{ | |
fseek(file, 0, SEEK_END); | |
unsigned int file_size = ftell(file); | |
fseek(file, 0, SEEK_SET); | |
result = (char *)malloc(file_size+1); | |
if(result) | |
{ | |
fread(result, 1, file_size, file); | |
result[file_size] = 0; | |
} | |
fclose(file); | |
} | |
return result; | |
} | |
void | |
OBJParseFreeFileData(void *file_data) | |
{ | |
free(file_data); | |
} | |
void | |
OBJParseDefaultCRTErrorCallback(OBJParseError error) | |
{ | |
fprintf(stderr, "OBJ PARSE ERROR (%s:%i): %s\n", error.filename, error.line, error.message); | |
} | |
void | |
OBJParseDefaultCRTWarningCallback(OBJParseWarning warning) | |
{ | |
fprintf(stderr, "OBJ PARSE WARNING (%s:%i): %s\n", warning.filename, warning.line, warning.message); | |
} | |
ParsedOBJ | |
LoadOBJ(char *filename) | |
{ | |
OBJParseInfo info = {0}; | |
{ | |
info.obj_data = OBJParseLoadEntireFileAndNullTerminate(filename); | |
info.parse_memory_size = 1024*1024*1024; | |
info.parse_memory = malloc(info.parse_memory_size); | |
if(!info.parse_memory) | |
{ | |
info.parse_memory_size = 0; | |
} | |
info.filename = filename; | |
info.error_callback = OBJParseDefaultCRTErrorCallback; | |
info.warning_callback = OBJParseDefaultCRTWarningCallback; | |
} | |
ParsedOBJ obj = {0}; | |
if(info.obj_data) | |
{ | |
obj = ParseOBJ(&info); | |
OBJParseFreeFileData(info.obj_data); | |
OBJParserPersistentState *persistent_state = (OBJParserPersistentState *)((char *)obj.renderables - sizeof(OBJParserPersistentState)); | |
persistent_state->parse_memory_to_free = info.parse_memory; | |
} | |
// NOTE(rjf): Load in material libraries. | |
{ | |
char obj_folder[256] = {0}; | |
snprintf(obj_folder, sizeof(obj_folder), "%s", filename); | |
char *one_past_last_slash = obj_folder; | |
for(int i = 0; obj_folder[i]; ++i) | |
{ | |
if(obj_folder[i] == '/' || obj_folder[i] == '\\') | |
{ | |
one_past_last_slash = obj_folder + i + 1; | |
} | |
} | |
*one_past_last_slash = 0; | |
for(unsigned int i = 0; i < obj.material_library_count; ++i) | |
{ | |
char mtl_filename[256] = {0}; | |
snprintf(mtl_filename, sizeof(mtl_filename), "%s%s", obj_folder, obj.material_libraries[i]); | |
LoadMTLForOBJ(mtl_filename, &obj); | |
} | |
} | |
return obj; | |
} | |
void | |
LoadMTLForOBJ(char *filename, ParsedOBJ *obj) | |
{ | |
OBJParserPersistentState *persistent_state = (OBJParserPersistentState *)((char *)obj->renderables - sizeof(OBJParserPersistentState)); | |
MTLParseInfo info = {0}; | |
{ | |
info.mtl_data = OBJParseLoadEntireFileAndNullTerminate(filename); | |
info.filename = filename; | |
info.error_callback = persistent_state->ErrorCallback; | |
info.warning_callback = persistent_state->WarningCallback; | |
} | |
if(info.mtl_data) | |
{ | |
ParseMTLForOBJ(&info, obj); | |
OBJParseFreeFileData(info.mtl_data); | |
} | |
} | |
void | |
FreeParsedOBJ(ParsedOBJ *obj) | |
{ | |
if(obj->renderables) | |
{ | |
OBJParserPersistentState *persistent_state = (OBJParserPersistentState *)((char *)obj->renderables - sizeof(OBJParserPersistentState)); | |
if(persistent_state->parse_memory_to_free) | |
{ | |
free(persistent_state->parse_memory_to_free); | |
} | |
} | |
} | |
#endif // OBJ_PARSE_NO_CRT | |
ParsedOBJ | |
ParseOBJ(OBJParseInfo *info) | |
{ | |
char *obj_data = info->obj_data; | |
void *parse_memory = info->parse_memory; | |
unsigned int parse_memory_size = info->parse_memory_size; | |
char *filename = info->filename ? info->filename : ""; | |
OBJParseErrorCallback *ErrorCallback = info->error_callback; | |
OBJParseWarningCallback *WarningCallback = info->warning_callback; | |
ParsedOBJ obj_ = {0}; | |
ParsedOBJ *obj = &obj_; | |
OBJParserArena arena_ = OBJParserArenaInit(parse_memory, parse_memory_size); | |
OBJParserArena *arena = &arena_; | |
OBJTokenizer tokenizer_ = {0}; | |
OBJTokenizer *tokenizer = &tokenizer_; | |
tokenizer->at = obj_data; | |
unsigned int floats_per_vertex = 3 + 2 + 3; | |
unsigned int bytes_per_vertex = sizeof(float)*floats_per_vertex; | |
// NOTE(rjf): Used for storing a linked list of parsed models in the parsing | |
// memory, which can then be converted into a contiguous array for the user. | |
typedef struct OBJParsedGeometryGroupListNode OBJParsedGeometryGroupListNode; | |
struct OBJParsedGeometryGroupListNode | |
{ | |
unsigned int face_vertex_start; | |
unsigned int face_vertex_end; | |
unsigned int lowest_position_index; | |
OBJParsedGeometryGroupListNode *next; | |
}; | |
typedef struct OBJParsedRenderableListNode OBJParsedRenderableListNode; | |
struct OBJParsedRenderableListNode | |
{ | |
ParsedOBJMaterial material; | |
OBJParsedGeometryGroupListNode *first_geometry_group; | |
OBJParsedGeometryGroupListNode **current_target_geometry_group; | |
unsigned int geometry_group_count; | |
OBJParsedRenderableListNode *next; | |
}; | |
unsigned int renderable_list_node_count = 0; | |
OBJParsedRenderableListNode *first_renderable_list_node = 0; | |
OBJParsedRenderableListNode **target_renderable_list_node = &first_renderable_list_node; | |
OBJParsedRenderableListNode *active_renderable = 0; | |
OBJParsedGeometryGroupListNode *active_geometry_group = 0; | |
// NOTE(rjf): Similar to above linked lists, except for material libraries. | |
typedef struct OBJParsedMaterialLibraryNode OBJParsedMaterialLibraryNode; | |
struct OBJParsedMaterialLibraryNode | |
{ | |
char *name; | |
OBJParsedMaterialLibraryNode *next; | |
}; | |
unsigned int material_library_list_node_count = 0; | |
OBJParsedMaterialLibraryNode *first_material_library_list_node = 0; | |
OBJParsedMaterialLibraryNode **target_material_library_list_node = &first_material_library_list_node; | |
ParsedOBJMaterial global_material = {0}; | |
ParsedOBJMaterial active_material = global_material; | |
// NOTE(rjf): Calculate size of and allocate data for all vertices/UVs/normals read | |
// directly from the .obj file. | |
float *vertex_positions = 0; | |
float *vertex_uvs = 0; | |
float *vertex_normals = 0; | |
int *face_vertices = 0; | |
{ | |
unsigned int num_positions = 0; | |
unsigned int num_uvs = 0; | |
unsigned int num_normals = 0; | |
unsigned int num_face_vertices = 0; | |
for(;;) | |
{ | |
OBJToken token = OBJPeekToken(tokenizer); | |
if(token.type == OBJ_TOKEN_TYPE_null) | |
{ | |
break; | |
} | |
if(OBJRequireToken(tokenizer, "v", 0)) | |
{ | |
if(OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, 0) && | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, 0) && | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, 0)) | |
{ | |
num_positions += 1; | |
} | |
} | |
else if(OBJRequireToken(tokenizer, "vn", 0)) | |
{ | |
if(OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, 0) && | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, 0) && | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, 0)) | |
{ | |
num_normals += 1; | |
} | |
} | |
else if(OBJRequireToken(tokenizer, "vt", 0)) | |
{ | |
if(OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, 0) && | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, 0)) | |
{ | |
num_uvs += 1; | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, 0); | |
} | |
} | |
else if(OBJRequireToken(tokenizer, "f", 0)) | |
{ | |
int face_vertex_count = 0; | |
for(;;) | |
{ | |
if(OBJPeekToken(tokenizer).type == OBJ_TOKEN_TYPE_number) | |
{ | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, 0); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_face_index_divider, 0); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, 0); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_face_index_divider, 0); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, 0); | |
++num_face_vertices; | |
++face_vertex_count; | |
} | |
else | |
{ | |
break; | |
} | |
} | |
// NOTE(rjf): We will perform a triangulation step on faces if a face | |
// is listed with more than 3 vertices. This is the memory usage calculation | |
// stage, so we just need to calculate how many more vertices will be used by | |
// the triangulation step. The triangulation method used is just the | |
// simple fan-triangulation step: | |
// | |
// | |
// XXXXXXXXXXX | |
// XXXXXXXX XX 1. Start with 8 vertices | |
// XXXXXXX XXXXXXX XX | |
// XX XXXX XX XXXXXX 2. Connect first vertex with each other non-adjacent vertex | |
// X X X XX XX X | |
// X X XX XX XX X 3. Profit | |
// X X X XX XX X | |
// X X X XX XX X | |
// X X X XX XXX X | |
// X X X XX XX X | |
// XX X XX XX | |
// XX X XX XX | |
// XXX XXXX | |
// XXXXXXXXXXX | |
// | |
// In this simple algorithm, if we have n input vertices, we'll actually | |
// be generating 3 * (n - 2) vertices. For example, with n=8 we'll get | |
// 6 triangles, or 6*3 = 18 vertices, which fits expectations (see above | |
// diagram), as 3 * (n - 2) = 3 * (8 - 2) = 3 * 6 = 18. | |
if(face_vertex_count > 3) | |
{ | |
// NOTE(rjf): We've already added n, so just add what we expect minus that. | |
int n = face_vertex_count; | |
num_face_vertices += (3 * (n - 2)) - n; | |
} | |
} | |
else | |
{ | |
OBJSkipUntilNextLine(tokenizer); | |
} | |
} | |
// NOTE(rjf): Allocate the memory we need for storing the initial vertex information | |
// from the OBJ file. | |
void *memory = 0; | |
{ | |
unsigned int needed_memory_for_initial_obj_read = | |
((num_positions * 3) + (num_uvs * 2) + (num_normals * 3)) * sizeof(float) + | |
(num_face_vertices * 3) * sizeof(int); | |
memory = OBJParserArenaAllocate(arena, needed_memory_for_initial_obj_read); | |
if(!memory) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
} | |
vertex_positions = (float *)memory; | |
vertex_uvs = vertex_positions + num_positions * 3; | |
vertex_normals = vertex_uvs + num_uvs * 2; | |
face_vertices = (int *)(vertex_normals + num_normals*3); | |
} | |
// NOTE(rjf): Do the parse. | |
tokenizer->at = info->obj_data; | |
unsigned int vertex_position_write_pos = 0; | |
unsigned int vertex_uv_write_pos = 0; | |
unsigned int vertex_normal_write_pos = 0; | |
unsigned int face_vertex_write_pos = 0; | |
{ | |
// NOTE(rjf): Allocate the first renderable and geometry group. | |
{ | |
OBJParsedRenderableListNode *renderable = (OBJParsedRenderableListNode *)OBJParserArenaAllocate(arena, sizeof(*renderable)); | |
OBJParsedGeometryGroupListNode *geometry_group = (OBJParsedGeometryGroupListNode *)OBJParserArenaAllocate(arena, sizeof(*geometry_group)); | |
if(!renderable || !geometry_group) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
renderable->material.material_name = 0; | |
renderable->first_geometry_group = geometry_group; | |
renderable->current_target_geometry_group = &renderable->first_geometry_group->next; | |
renderable->geometry_group_count = 1; | |
geometry_group->face_vertex_start = 0; | |
geometry_group->face_vertex_end = 0; | |
geometry_group->lowest_position_index = 0; | |
geometry_group->next = 0; | |
renderable->next = 0; | |
*target_renderable_list_node = renderable; | |
target_renderable_list_node = &(*target_renderable_list_node)->next; | |
++renderable_list_node_count; | |
active_renderable = renderable; | |
active_geometry_group = geometry_group; | |
} | |
for(;;) | |
{ | |
OBJToken token = OBJPeekToken(tokenizer); | |
if(token.type == OBJ_TOKEN_TYPE_null) | |
{ | |
break; | |
} | |
// NOTE(rjf): Handle material library listings at the global level. | |
else if(token.type == OBJ_TOKEN_TYPE_material_library_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
char material_library_path[256] = {0}; | |
OBJReadTrimmedStringUntilNextLineToFixSizedBuffer(tokenizer, material_library_path, sizeof(material_library_path)); | |
OBJSkipUntilNextLine(tokenizer); | |
if(material_library_path[0]) | |
{ | |
OBJParsedMaterialLibraryNode *material_library = | |
(OBJParsedMaterialLibraryNode *)OBJParserArenaAllocate(arena, sizeof(OBJParsedMaterialLibraryNode)); | |
material_library->name = OBJParserArenaAllocateCStringCopy(arena, material_library_path); | |
if(!material_library) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
material_library->next = 0; | |
*target_material_library_list_node = material_library; | |
target_material_library_list_node = &(*target_material_library_list_node)->next; | |
++material_library_list_node_count; | |
} | |
} | |
// NOTE(rjf): Handle material usage listings at the global level. | |
else if(token.type == OBJ_TOKEN_TYPE_use_material_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
char material_name[256] = {0}; | |
OBJReadTrimmedStringUntilNextLineToFixSizedBuffer(tokenizer, material_name, sizeof(material_name)); | |
OBJSkipUntilNextLine(tokenizer); | |
global_material.material_name = OBJParserArenaAllocateCStringCopy(arena, material_name); | |
active_material = global_material; | |
if(active_renderable) | |
{ | |
if(active_renderable->material.material_name) | |
{ | |
// NOTE(rjf): Find out if this material is already being used by a renderable. | |
int is_unseen_material = 1; | |
OBJParsedRenderableListNode *renderable_with_material = 0; | |
for(OBJParsedRenderableListNode *node = first_renderable_list_node; node; node = node->next) | |
{ | |
if(OBJParserStringMatchCaseSensitive(material_name, node->material.material_name)) | |
{ | |
is_unseen_material = 0; | |
renderable_with_material = node; | |
break; | |
} | |
} | |
OBJParsedGeometryGroupListNode *geometry_group = (OBJParsedGeometryGroupListNode *)OBJParserArenaAllocate(arena, sizeof(*geometry_group)); | |
geometry_group->face_vertex_start = face_vertex_write_pos; | |
geometry_group->face_vertex_end = face_vertex_write_pos; | |
geometry_group->lowest_position_index = 0; | |
geometry_group->next = 0; | |
if(is_unseen_material) | |
{ | |
// NOTE(rjf): Allocate a new renderable for this material. | |
{ | |
OBJParsedRenderableListNode *renderable = (OBJParsedRenderableListNode *) | |
OBJParserArenaAllocate(arena, sizeof(*renderable)); | |
if(!renderable) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
renderable->material = active_material; | |
renderable->first_geometry_group = geometry_group; | |
renderable->current_target_geometry_group = &renderable->first_geometry_group->next; | |
renderable->geometry_group_count = 1; | |
renderable->next = 0; | |
*target_renderable_list_node = renderable; | |
target_renderable_list_node = &(*target_renderable_list_node)->next; | |
++renderable_list_node_count; | |
active_renderable = renderable; | |
active_geometry_group = geometry_group; | |
} | |
} | |
else | |
{ | |
// NOTE(rjf): This material has already been used by a renderable, so we want to | |
// add a new geometry group to that renderable. | |
{ | |
active_renderable = renderable_with_material; | |
active_geometry_group = geometry_group; | |
*(renderable_with_material->current_target_geometry_group) = geometry_group; | |
renderable_with_material->current_target_geometry_group = &(*renderable_with_material->current_target_geometry_group)->next; | |
++renderable_with_material->geometry_group_count; | |
} | |
} | |
} | |
else | |
{ | |
active_renderable->material = active_material; | |
} | |
} | |
} | |
// NOTE(rjf): Vertex position | |
else if(OBJRequireToken(tokenizer, "v", 0)) | |
{ | |
OBJToken p1; | |
OBJToken p2; | |
OBJToken p3; | |
if(OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &p1) && | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &p2) && | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &p3)) | |
{ | |
vertex_positions[vertex_position_write_pos++] = OBJTokenToFloat(p1); | |
vertex_positions[vertex_position_write_pos++] = OBJTokenToFloat(p2); | |
vertex_positions[vertex_position_write_pos++] = OBJTokenToFloat(p3); | |
} | |
} | |
// NOTE(rjf): Vertex UV | |
else if(OBJRequireToken(tokenizer, "vt", 0)) | |
{ | |
OBJToken u; | |
OBJToken v; | |
OBJToken w; | |
if(OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &u) && | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &v)) | |
{ | |
vertex_uvs[vertex_uv_write_pos++] = OBJTokenToFloat(u); | |
vertex_uvs[vertex_uv_write_pos++] = OBJTokenToFloat(v); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &w); | |
} | |
} | |
// NOTE(rjf): Vertex normal | |
else if(OBJRequireToken(tokenizer, "vn", 0)) | |
{ | |
OBJToken n1; | |
OBJToken n2; | |
OBJToken n3; | |
if(OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &n1) && | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &n2) && | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &n3)) | |
{ | |
vertex_normals[vertex_normal_write_pos++] = OBJTokenToFloat(n1); | |
vertex_normals[vertex_normal_write_pos++] = OBJTokenToFloat(n2); | |
vertex_normals[vertex_normal_write_pos++] = OBJTokenToFloat(n3); | |
} | |
} | |
// NOTE(rjf): Face definition | |
else if(OBJRequireToken(tokenizer, "f", 0)) | |
{ | |
int face_vertex_count = 0; | |
int *this_face_vertices_ptr = face_vertices + face_vertex_write_pos*3; | |
unsigned int last_face_vertex_write_pos = face_vertex_write_pos; | |
unsigned int actual_vertices_added = 0; | |
for(;;) | |
{ | |
OBJToken position = {0}; | |
OBJToken uv = {0}; | |
OBJToken normal = {0}; | |
if(OBJPeekToken(tokenizer).type == OBJ_TOKEN_TYPE_number) | |
{ | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &position); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_face_index_divider, 0); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &uv); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_face_index_divider, 0); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &normal); | |
int position_index = OBJTokenToInt(position); | |
int uv_index = OBJTokenToInt(uv); | |
int normal_index = OBJTokenToInt(normal); | |
if(position_index < 0) | |
{ | |
position_index = (int)vertex_position_write_pos/3 + position_index + 1; | |
} | |
if(uv_index < 0) | |
{ | |
uv_index = (int)vertex_uv_write_pos/2 + uv_index + 1; | |
} | |
if(normal_index < 0) | |
{ | |
normal_index = (int)vertex_normal_write_pos/3 + normal_index + 1; | |
} | |
face_vertices[face_vertex_write_pos*3 + 0] = position_index; | |
face_vertices[face_vertex_write_pos*3 + 1] = uv_index; | |
face_vertices[face_vertex_write_pos*3 + 2] = normal_index; | |
if(active_geometry_group->lowest_position_index == 0 || position_index < active_geometry_group->lowest_position_index) | |
{ | |
active_geometry_group->lowest_position_index = position_index; | |
} | |
++face_vertex_write_pos; | |
++face_vertex_count; | |
++actual_vertices_added; | |
} | |
else | |
{ | |
break; | |
} | |
} | |
// NOTE(rjf): We will perform a triangulation step on faces if a face | |
// is listed with more than 3 vertices. | |
// | |
// | |
// XXXXXXXXXXX | |
// XXXXXXXX XX 1. Start with 8 vertices | |
// XXXXXXX XXXXXXX XX | |
// XX XXXX XX XXXXXX 2. Connect first vertex with each other non-adjacent vertex | |
// X X X XX XX X | |
// X X XX XX XX X 3. Profit | |
// X X X XX XX X | |
// X X X XX XX X | |
// X X X XX XXX X | |
// X X X XX XX X | |
// XX X XX XX | |
// XX X XX XX | |
// XXX XXXX | |
// XXXXXXXXXXX | |
if(face_vertex_count > 3) | |
{ | |
// HACK(rjf): Right now we're going to go ahead and assume that | |
// we'll never have a face with 256 vertices, which would be | |
// ridiculous. This logic will prevent such a face from crashing | |
// the parser, but will not correctly triangulate it. Seriously, | |
// though, why do you have a face with 256 vertices? | |
if(face_vertex_count > 256) | |
{ | |
face_vertex_count = 256; | |
} | |
face_vertex_write_pos = last_face_vertex_write_pos; | |
int this_face_vertices[256*3]; | |
for(int i = 0; i < face_vertex_count; ++i) | |
{ | |
this_face_vertices[i*3 + 0] = this_face_vertices_ptr[i*3 + 0]; | |
this_face_vertices[i*3 + 1] = this_face_vertices_ptr[i*3 + 1]; | |
this_face_vertices[i*3 + 2] = this_face_vertices_ptr[i*3 + 2]; | |
} | |
actual_vertices_added = 0; | |
// NOTE(rjf): We start at 2, because we're skipping both the | |
// first vertex listed, AND the one immediately following it | |
// (adjacent to the first vertex). | |
for(int i = 2; i < face_vertex_count; ++i) | |
{ | |
int triangle_p1[3] = { | |
this_face_vertices[0], | |
this_face_vertices[1], | |
this_face_vertices[2], | |
}; | |
int triangle_p2[3] = { | |
this_face_vertices[(i-1)*3+0], | |
this_face_vertices[(i-1)*3+1], | |
this_face_vertices[(i-1)*3+2], | |
}; | |
int triangle_p3[3] = { | |
this_face_vertices[(i)*3+0], | |
this_face_vertices[(i)*3+1], | |
this_face_vertices[(i)*3+2], | |
}; | |
face_vertices[face_vertex_write_pos*3 + 0] = triangle_p1[0]; | |
face_vertices[face_vertex_write_pos*3 + 1] = triangle_p1[1]; | |
face_vertices[face_vertex_write_pos*3 + 2] = triangle_p1[2]; | |
++face_vertex_write_pos; | |
++actual_vertices_added; | |
face_vertices[face_vertex_write_pos*3 + 0] = triangle_p2[0]; | |
face_vertices[face_vertex_write_pos*3 + 1] = triangle_p2[1]; | |
face_vertices[face_vertex_write_pos*3 + 2] = triangle_p2[2]; | |
++face_vertex_write_pos; | |
++actual_vertices_added; | |
face_vertices[face_vertex_write_pos*3 + 0] = triangle_p3[0]; | |
face_vertices[face_vertex_write_pos*3 + 1] = triangle_p3[1]; | |
face_vertices[face_vertex_write_pos*3 + 2] = triangle_p3[2]; | |
++face_vertex_write_pos; | |
++actual_vertices_added; | |
} | |
} | |
if(active_geometry_group) | |
{ | |
active_geometry_group->face_vertex_end += actual_vertices_added; | |
} | |
} | |
// NOTE(rjf): Anything else (ignore) | |
else | |
{ | |
OBJNextToken(tokenizer); | |
OBJSkipUntilNextLine(tokenizer); | |
} | |
} | |
} | |
// NOTE(rjf): Allocate the persistent state before the model array, so we | |
// can access it by taking the base of the models array and subtracting | |
// sizeof(OBJParserPersistentState) bytes. | |
OBJParserPersistentState *persistent_state = 0; | |
{ | |
persistent_state = (OBJParserPersistentState *)OBJParserArenaAllocate(arena, sizeof(OBJParserPersistentState)); | |
if(!persistent_state) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
} | |
// NOTE(rjf): Make renderables. | |
{ | |
obj->renderables = (ParsedOBJRenderable *)OBJParserArenaAllocate(arena, renderable_list_node_count*sizeof(ParsedOBJRenderable)); | |
if(!obj->renderables) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
obj->renderable_count = 0; | |
for(OBJParsedRenderableListNode *node = first_renderable_list_node; | |
node; node = node->next) | |
{ | |
ParsedOBJRenderable *renderable = obj->renderables + obj->renderable_count; | |
++obj->renderable_count; | |
renderable->material = node->material; | |
typedef struct VertexUVAndNormalIndices VertexUVAndNormalIndices; | |
struct VertexUVAndNormalIndices | |
{ | |
int position_index; | |
int uv_index; | |
int normal_index; | |
}; | |
typedef struct GeometryGroupFinalData GeometryGroupFinalData; | |
struct GeometryGroupFinalData | |
{ | |
unsigned int num_unique_vertices; | |
unsigned int num_face_vertices_with_duplicates; | |
unsigned int face_vertex_count; | |
int *face_vertices; | |
VertexUVAndNormalIndices *vertex_uv_and_normal_indices_buffer; | |
unsigned int lowest_position_index; | |
}; | |
GeometryGroupFinalData *geometry_groups_final_data = (GeometryGroupFinalData *)OBJParserArenaAllocate(arena, sizeof(GeometryGroupFinalData)*node->geometry_group_count); | |
if(!geometry_groups_final_data) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
unsigned int renderable_total_unique_vertices = 0; | |
unsigned int renderable_total_face_vertices_with_duplicates = 0; | |
unsigned int group_node_index = 0; | |
for(OBJParsedGeometryGroupListNode *geometry_group = node->first_geometry_group; | |
geometry_group; geometry_group = geometry_group->next, ++group_node_index) | |
{ | |
int *geometry_group_face_vertices = face_vertices + geometry_group->face_vertex_start*3; | |
unsigned int geometry_group_face_vertex_count = geometry_group->face_vertex_end - geometry_group->face_vertex_start; | |
VertexUVAndNormalIndices *vertex_uv_and_normal_indices_buffer = 0; | |
unsigned int num_face_vertices_with_duplicates = 0; | |
unsigned int num_unique_vertices = 0; | |
// NOTE(rjf): Finish up vertices and generate final vertex/index buffer. | |
{ | |
// NOTE(rjf): OBJs store indices for positions, normals, and UVs, but OpenGL and | |
// other APIs only allow us to have a single index buffer. So, when we get cases | |
// like 2/3/4 and 2/3/5, we need to duplicate vertex information. We'll do this | |
// in a vertex duplication step, where we'll keep track of position/UV/normal triplets, | |
// and in cases where we find duplicates, we'll explicitly allocate storage for | |
// duplicates, and fix up the face indices so that they point to the correct | |
// indices. | |
{ | |
vertex_uv_and_normal_indices_buffer = | |
(VertexUVAndNormalIndices *)OBJParserArenaAllocate(arena, geometry_group_face_vertex_count * sizeof(VertexUVAndNormalIndices)); | |
if(!vertex_uv_and_normal_indices_buffer) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
for(unsigned int i = 0; i < geometry_group_face_vertex_count; ++i) | |
{ | |
vertex_uv_and_normal_indices_buffer[i].position_index = 0; | |
vertex_uv_and_normal_indices_buffer[i].uv_index = 0; | |
vertex_uv_and_normal_indices_buffer[i].normal_index = 0; | |
} | |
num_face_vertices_with_duplicates = geometry_group_face_vertex_count; | |
num_unique_vertices = geometry_group_face_vertex_count; | |
for(unsigned int i = 0; i < geometry_group_face_vertex_count; ++i) | |
{ | |
// NOTE(rjf): We subtract 1 because the OBJ spec has 1-based indices. | |
int position_index = geometry_group_face_vertices[i*3 + 0] - 1; | |
int uv_index = geometry_group_face_vertices[i*3 + 1] - 1; | |
int normal_index = geometry_group_face_vertices[i*3 + 2] - 1; | |
int geometry_group_position_index = position_index + 1 - geometry_group->lowest_position_index; | |
// NOTE(rjf): We've already written to this index, which means this vertex is a duplicate. | |
if(vertex_uv_and_normal_indices_buffer[geometry_group_position_index].position_index || | |
vertex_uv_and_normal_indices_buffer[geometry_group_position_index].uv_index || | |
vertex_uv_and_normal_indices_buffer[geometry_group_position_index].normal_index) | |
{ | |
if(vertex_uv_and_normal_indices_buffer[geometry_group_position_index].position_index != position_index+1 || | |
vertex_uv_and_normal_indices_buffer[geometry_group_position_index].uv_index != uv_index+1 || | |
vertex_uv_and_normal_indices_buffer[geometry_group_position_index].normal_index != normal_index+1) | |
{ | |
// NOTE(rjf): This is a duplicate position, but it has different UV's or normals, so we | |
// need to duplicate this vertex. | |
{ | |
VertexUVAndNormalIndices *duplicate = (VertexUVAndNormalIndices *)OBJParserArenaAllocate(arena, sizeof(VertexUVAndNormalIndices)); | |
if(!duplicate) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
duplicate->position_index = position_index+1; | |
duplicate->uv_index = uv_index+1; | |
duplicate->normal_index = normal_index+1; | |
} | |
// NOTE(rjf): Fix up the reference to this position. | |
{ | |
geometry_group_face_vertices[i*3 + 0] = geometry_group->lowest_position_index + num_face_vertices_with_duplicates; | |
} | |
++num_face_vertices_with_duplicates; | |
} | |
else | |
{ | |
// NOTE(rjf): Vertex is a complete duplicate, which means we do not need to duplicate it, and can | |
// already reference the same information. Decrease the number of unique vertices. | |
--num_unique_vertices; | |
} | |
} | |
else | |
{ | |
// NOTE(rjf): We add 1 because the OBJ spec has 1-based indices, and we are checking | |
// for 0's. | |
vertex_uv_and_normal_indices_buffer[geometry_group_position_index].position_index = position_index+1; | |
vertex_uv_and_normal_indices_buffer[geometry_group_position_index].uv_index = uv_index+1; | |
vertex_uv_and_normal_indices_buffer[geometry_group_position_index].normal_index = normal_index+1; | |
} | |
} | |
} | |
} | |
geometry_groups_final_data[group_node_index].num_unique_vertices = num_unique_vertices; | |
geometry_groups_final_data[group_node_index].num_face_vertices_with_duplicates = num_face_vertices_with_duplicates; | |
geometry_groups_final_data[group_node_index].face_vertex_count = geometry_group_face_vertex_count; | |
geometry_groups_final_data[group_node_index].face_vertices = geometry_group_face_vertices; | |
geometry_groups_final_data[group_node_index].vertex_uv_and_normal_indices_buffer = vertex_uv_and_normal_indices_buffer; | |
geometry_groups_final_data[group_node_index].lowest_position_index = geometry_group->lowest_position_index; | |
renderable_total_unique_vertices += num_unique_vertices; | |
renderable_total_face_vertices_with_duplicates += num_face_vertices_with_duplicates; | |
} | |
// NOTE(rjf): Now that we've duplicated vertices where necessary, we can create | |
// final vertex and index buffers, where everything is correct for rendering. | |
int final_vertex_buffer_write_number = 0; | |
unsigned int bytes_needed_for_final_vertex_buffer = sizeof(float) * 8 * renderable_total_unique_vertices; | |
float *final_vertex_buffer = (float *)OBJParserArenaAllocate(arena, bytes_needed_for_final_vertex_buffer); | |
int final_index_buffer_write_number = 0; | |
unsigned int bytes_needed_for_final_index_buffer = sizeof(int) * renderable_total_face_vertices_with_duplicates; | |
int *final_index_buffer = (int *)OBJParserArenaAllocate(arena, bytes_needed_for_final_index_buffer); | |
if(!final_vertex_buffer || !final_index_buffer) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
unsigned int group_index_offset = 0; | |
for(unsigned int group_index = 0; group_index < node->geometry_group_count; ++group_index) | |
{ | |
GeometryGroupFinalData *group_final_data = geometry_groups_final_data + group_index; | |
for(unsigned int i = 0; i < group_final_data->face_vertex_count; ++i) | |
{ | |
int position_index = group_final_data->face_vertices[i*3 + 0] - group_final_data->lowest_position_index; | |
VertexUVAndNormalIndices *vertex_data = group_final_data->vertex_uv_and_normal_indices_buffer + position_index; | |
float position[3] = { | |
vertex_positions[(vertex_data->position_index-1)*3+0], | |
vertex_positions[(vertex_data->position_index-1)*3+1], | |
vertex_positions[(vertex_data->position_index-1)*3+2], | |
}; | |
float uv[2] = { | |
vertex_uvs[(vertex_data->uv_index-1)*2+0], | |
vertex_uvs[(vertex_data->uv_index-1)*2+1], | |
}; | |
float normal[3] = { | |
vertex_normals[(vertex_data->normal_index-1)*3+0], | |
vertex_normals[(vertex_data->normal_index-1)*3+1], | |
vertex_normals[(vertex_data->normal_index-1)*3+2], | |
}; | |
int geometry_group_position_index = vertex_data->position_index - group_final_data->lowest_position_index + group_index_offset; | |
final_vertex_buffer[(geometry_group_position_index)*8+0] = position[0]; | |
final_vertex_buffer[(geometry_group_position_index)*8+1] = position[1]; | |
final_vertex_buffer[(geometry_group_position_index)*8+2] = position[2]; | |
final_vertex_buffer[(geometry_group_position_index)*8+3] = uv[0]; | |
final_vertex_buffer[(geometry_group_position_index)*8+4] = uv[1]; | |
final_vertex_buffer[(geometry_group_position_index)*8+5] = normal[0]; | |
final_vertex_buffer[(geometry_group_position_index)*8+6] = normal[1]; | |
final_vertex_buffer[(geometry_group_position_index)*8+7] = normal[2]; | |
final_index_buffer[final_index_buffer_write_number++] = geometry_group_position_index; | |
final_vertex_buffer_write_number += 8; | |
} | |
group_index_offset += group_final_data->num_unique_vertices; | |
} | |
renderable->vertices = final_vertex_buffer; | |
renderable->vertex_count = renderable_total_unique_vertices; | |
// NOTE(rjf): This is hard-coded, while we still only support interleaved data. | |
renderable->floats_per_vertex = 8; | |
renderable->indices = final_index_buffer; | |
renderable->index_count = final_index_buffer_write_number; | |
} | |
} | |
// NOTE(rjf): Form the contiguous material libraries array for the user. | |
{ | |
obj->material_libraries = (char **)OBJParserArenaAllocate(arena, sizeof(char *)*material_library_list_node_count); | |
if(!obj->material_libraries) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
obj->material_library_count = 0; | |
for(OBJParsedMaterialLibraryNode *node = first_material_library_list_node; | |
node; node = node->next) | |
{ | |
obj->material_libraries[obj->material_library_count] = node->name; | |
++obj->material_library_count; | |
} | |
} | |
// NOTE(rjf): Fill out state that needs to persist between library calls. | |
{ | |
unsigned int materials_to_load_count = obj->renderable_count; | |
persistent_state->materials_to_load = (ParsedOBJMaterial **)OBJParserArenaAllocate(arena, sizeof(ParsedOBJMaterial *) * materials_to_load_count); | |
if(!persistent_state->materials_to_load) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
persistent_state->materials_to_load_count = 0; | |
for(unsigned int i = 0; i < materials_to_load_count; ++i) | |
{ | |
persistent_state->materials_to_load[persistent_state->materials_to_load_count] = &obj->renderables[i].material; | |
++persistent_state->materials_to_load_count; | |
} | |
persistent_state->parser_arena = arena_; | |
persistent_state->ErrorCallback = ErrorCallback; | |
persistent_state->WarningCallback = WarningCallback; | |
// NOTE(rjf): We'll set this to 0 as the default. The higher-level functions that allocate | |
// their own parsing buffers can set this to something else. | |
persistent_state->parse_memory_to_free = 0; | |
} | |
end_parse:; | |
return obj_; | |
} | |
void | |
ParseMTLForOBJ(MTLParseInfo *info, ParsedOBJ *obj) | |
{ | |
char *mtl_data = info->mtl_data; | |
char *filename = info->filename ? info->filename : ""; | |
OBJParseErrorCallback *ErrorCallback = info->error_callback; | |
OBJParseWarningCallback *WarningCallback = info->warning_callback; | |
OBJParserPersistentState *persistent_state = (OBJParserPersistentState *)((char *)obj->renderables - sizeof(OBJParserPersistentState)); | |
OBJParserArena *arena = &persistent_state->parser_arena; | |
OBJTokenizer tokenizer_ = {0}; | |
OBJTokenizer *tokenizer = &tokenizer_; | |
tokenizer->at = mtl_data; | |
unsigned int materials_to_load_count = persistent_state->materials_to_load_count; | |
ParsedOBJMaterial **materials_to_load = persistent_state->materials_to_load; | |
if(!obj->renderables) | |
{ | |
goto end_parse; | |
} | |
// NOTE(rjf): Loop through all materials. | |
for(;;) | |
{ | |
OBJToken token = OBJPeekToken(tokenizer); | |
if(token.type == MTL_TOKEN_TYPE_null && !token.string) | |
{ | |
break; | |
} | |
else | |
{ | |
if(token.type == MTL_TOKEN_TYPE_new_material_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
char material_name[256] = {0}; | |
OBJReadTrimmedStringUntilNextLineToFixSizedBuffer(tokenizer, material_name, sizeof(material_name)); | |
OBJSkipUntilNextLine(tokenizer); | |
ParsedOBJMaterial material = {0}; | |
material.material_name = OBJParserArenaAllocateCStringCopy(arena, material_name); | |
if(!material.material_name) | |
{ | |
// TODO(rjf): ERROR: Out of memory. | |
goto end_parse; | |
} | |
// NOTE(rjf): Loop through all material attributes. | |
for(;;) | |
{ | |
token = OBJPeekToken(tokenizer); | |
if(token.type == MTL_TOKEN_TYPE_null || token.type == MTL_TOKEN_TYPE_new_material_signifier) | |
{ | |
break; | |
} | |
else | |
{ | |
if(token.type == MTL_TOKEN_TYPE_ambient_color_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
OBJToken red = {0}; | |
OBJToken green = {0}; | |
OBJToken blue = {0}; | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &red); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &green); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &blue); | |
material.albedo_color[0] = OBJTokenToFloat(red); | |
material.albedo_color[1] = OBJTokenToFloat(green); | |
material.albedo_color[2] = OBJTokenToFloat(blue); | |
} | |
else if(token.type == MTL_TOKEN_TYPE_diffuse_color_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
OBJToken red = {0}; | |
OBJToken green = {0}; | |
OBJToken blue = {0}; | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &red); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &green); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &blue); | |
material.diffuse_color[0] = OBJTokenToFloat(red); | |
material.diffuse_color[1] = OBJTokenToFloat(green); | |
material.diffuse_color[2] = OBJTokenToFloat(blue); | |
} | |
else if(token.type == MTL_TOKEN_TYPE_specular_color_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
OBJToken red = {0}; | |
OBJToken green = {0}; | |
OBJToken blue = {0}; | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &red); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &green); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &blue); | |
material.specular_color[0] = OBJTokenToFloat(red); | |
material.specular_color[1] = OBJTokenToFloat(green); | |
material.specular_color[2] = OBJTokenToFloat(blue); | |
} | |
else if(token.type == MTL_TOKEN_TYPE_emissive_color_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
OBJToken red = {0}; | |
OBJToken green = {0}; | |
OBJToken blue = {0}; | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &red); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &green); | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &blue); | |
material.emissive_color[0] = OBJTokenToFloat(red); | |
material.emissive_color[1] = OBJTokenToFloat(green); | |
material.emissive_color[2] = OBJTokenToFloat(blue); | |
} | |
else if(token.type == MTL_TOKEN_TYPE_dissolve_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
OBJToken value = {0}; | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &value); | |
material.opacity = OBJTokenToFloat(value); | |
} | |
else if(token.type == MTL_TOKEN_TYPE_transparency_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
OBJToken value = {0}; | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &value); | |
material.opacity = 1 - OBJTokenToFloat(value); | |
} | |
else if(token.type == MTL_TOKEN_TYPE_illumination_type_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
OBJToken value = {0}; | |
OBJRequireTokenType(tokenizer, OBJ_TOKEN_TYPE_number, &value); | |
material.illumination_type = OBJTokenToInt(value); | |
} | |
else if(token.type == MTL_TOKEN_TYPE_albedo_map_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
char path[256] = {0}; | |
OBJReadTrimmedStringUntilNextLineToFixSizedBuffer(tokenizer, path, sizeof(path)); | |
OBJSkipUntilNextLine(tokenizer); | |
material.albedo_map_path = OBJParserArenaAllocateCStringCopy(arena, path); | |
} | |
else if(token.type == MTL_TOKEN_TYPE_diffuse_map_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
char path[256] = {0}; | |
OBJReadTrimmedStringUntilNextLineToFixSizedBuffer(tokenizer, path, sizeof(path)); | |
OBJSkipUntilNextLine(tokenizer); | |
material.diffuse_map_path = OBJParserArenaAllocateCStringCopy(arena, path); | |
} | |
else if(token.type == MTL_TOKEN_TYPE_specular_map_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
char path[256] = {0}; | |
OBJReadTrimmedStringUntilNextLineToFixSizedBuffer(tokenizer, path, sizeof(path)); | |
OBJSkipUntilNextLine(tokenizer); | |
material.specular_map_path = OBJParserArenaAllocateCStringCopy(arena, path); | |
} | |
else if(token.type == MTL_TOKEN_TYPE_dissolve_map_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
char path[256] = {0}; | |
OBJReadTrimmedStringUntilNextLineToFixSizedBuffer(tokenizer, path, sizeof(path)); | |
OBJSkipUntilNextLine(tokenizer); | |
material.dissolve_map_path = OBJParserArenaAllocateCStringCopy(arena, path); | |
} | |
else if(token.type == MTL_TOKEN_TYPE_bump_map_signifier) | |
{ | |
OBJNextToken(tokenizer); | |
char path[256] = {0}; | |
OBJReadTrimmedStringUntilNextLineToFixSizedBuffer(tokenizer, path, sizeof(path)); | |
OBJSkipUntilNextLine(tokenizer); | |
material.bump_map_path = OBJParserArenaAllocateCStringCopy(arena, path); | |
} | |
else | |
{ | |
// TODO(rjf): WARNING: Unexpected token. | |
OBJNextToken(tokenizer); | |
} | |
} | |
} | |
// NOTE(rjf): For now, we'll do a linear search through all of the materials to | |
// load so that we can patch in the values we've parsed. This can probably be | |
// done better, but alas, the OBJ format decided to use strings. | |
{ | |
for(unsigned int i = 0; i < materials_to_load_count; ++i) | |
{ | |
if(OBJParserStringMatchCaseSensitive(materials_to_load[i]->material_name, material.material_name)) | |
{ | |
*(materials_to_load[i]) = material; | |
} | |
} | |
} | |
} | |
else | |
{ | |
OBJSkipUntilNextLine(tokenizer); | |
} | |
} | |
} | |
end_parse:; | |
} | |
#endif // OBJ_PARSE_IMPLEMENTATION | |
#endif // OBJ_PARSE_H_INCLUDED | |
// The MIT License | |
// | |
// Copyright (c) 2019 Ryan Fleury. http://ryanfleury.net | |
// | |
// 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. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
In case you wanted to know there is a bug I've found in this loop here:
'num_unqiue_vertices' will be incorrect because you only search for exact duplicates (e.g. 3/2/2 & 3/2/2) once, at the first index of 'vertex_uv_and_normal_indices_buffer' where the positions match. But you need to also check the duplicates themselves for any exact matches. Ex. Incoming vertices to check against 'vertex_uv_and_normal_indices_buffer' are {3, 2, 2} and you check against 'vertex_uv_and_normal_indices_buffer[2] = {3/2/1}' in the first if statement but if there is another {3,2,2} located at 'vertex_uv_and_normal_indices_buffer[36]' because it was stored there as a duplicate with different UVs/normals, it will never be checked and those 'num_unique_vertices' will never get decremented.