From fc91f6662eecd1f5720284133f81423cf2203858 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 1 Oct 2025 17:55:29 +0200 Subject: [PATCH] feat: refactor code + optimizations for obj file parsing --- CMakeLists.txt | 3 + README.md | 13 + include/IO/parser.h | 20 ++ include/renderer/mesh.h | 27 ++ include/renderer/wavefront.h | 20 +- src/IO/parser.cpp | 123 +++++++++ src/renderer/basics.cpp | 15 ++ src/renderer/mesh.cpp | 43 ++++ src/renderer/texture.cpp | 1 - src/renderer/wavefront.cpp | 480 +++++++++++++++++++---------------- 10 files changed, 510 insertions(+), 235 deletions(-) create mode 100644 include/IO/parser.h create mode 100644 include/renderer/mesh.h create mode 100644 src/IO/parser.cpp create mode 100644 src/renderer/basics.cpp create mode 100644 src/renderer/mesh.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e1e709f..da38c84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,9 +25,12 @@ endif() # set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb") add_executable(CodingGame + src/IO/parser.cpp src/IO/file_manager.cpp src/renderer/debug.cpp + src/renderer/basics.cpp + src/renderer/mesh.cpp src/renderer/shader.cpp src/renderer/texture.cpp src/renderer/wavefront.cpp diff --git a/README.md b/README.md index 3438370..4e23c72 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,16 @@ The run command in that case would look following: ```console __NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia ./build/CodingGame ``` + +## TODO List + +### Optimizations + +🚀 Summary of Speedups +- Replace toElement / toMtlElement string comparisons with char-based switches. +- Replace std::stoi with a custom fast Parser::TakeIndex. +- Pre-reserve vectors for vertices, normals, texcoords, meshes. +- Load whole file into memory before parsing (fastest for large OBJs). +- Defer texture loading until after parsing. +- Store material pointers in meshes → no runtime lookups in render. +- Inline parsing functions. diff --git a/include/IO/parser.h b/include/IO/parser.h new file mode 100644 index 0000000..60dc1d4 --- /dev/null +++ b/include/IO/parser.h @@ -0,0 +1,20 @@ +#ifndef PARSER_H_ +#define PARSER_H_ + +// Very fast OBJ/MTL line parser +class Parser { +private: + char* m_sv; +public: + Parser(char* sv) : m_sv(sv) {} +public: + void SkipSpaces(); + char* TakeWord(); + float TakeFloat(); + int TakeInt(); + bool TakeFaceIndices(int& vi, int& ti, int& ni); + char* TakeUntil(char d); + int TakeIndex(int baseCount); +}; + +#endif // PARSER_H_ \ No newline at end of file diff --git a/include/renderer/mesh.h b/include/renderer/mesh.h new file mode 100644 index 0000000..d286d92 --- /dev/null +++ b/include/renderer/mesh.h @@ -0,0 +1,27 @@ +#ifndef MESH_H_ +#define MESH_H_ + +#include +#include +#include + +#include "renderer/basics.h" + +class Mesh { +public: // TODO: abstract away + unsigned int m_vao, m_vbo, m_ebo; + std::vector m_vertexBuffer; + std::vector m_indexBuffer; +public: // TODO: abstract away + void Bind() { glBindVertexArray(m_vao); } + void Unbind() { glBindVertexArray(0); } + void Upload(); +public: + std::string materialName; +public: + Mesh(); +public: + void Render(); +}; + +#endif // MESH_H_ \ No newline at end of file diff --git a/include/renderer/wavefront.h b/include/renderer/wavefront.h index 53c7cff..51f0896 100644 --- a/include/renderer/wavefront.h +++ b/include/renderer/wavefront.h @@ -11,27 +11,11 @@ #include "texture.h" #include "renderer/material.h" #include "renderer/basics.h" +#include "renderer/mesh.h" enum ObjElement { OHASH, MTLLIB, USEMTL, O, V, VN, VT, F, OUNKNOWN }; enum MtlElement { MHASH, NEWMTL, NS, KA, KS, KD, NI, D, ILLUM, MAP_KD, MAP_KA, MUNKNOWN }; -class Mesh { -public: // TODO: abstract away - unsigned int m_vao, m_vbo, m_ebo; - std::vector m_vertexBuffer; - std::vector m_indexBuffer; -public: // TODO: abstract away - void Bind() { glBindVertexArray(m_vao); } - void Unbind() { glBindVertexArray(0); } - void Upload(); -public: - std::string materialName; -public: - Mesh(); -public: - void Render(); -}; - class Object { private: std::string m_name; @@ -43,7 +27,7 @@ private: std::unordered_map> m_materials; private: - static inline int NormalizeIndex(const std::string &s, int baseCount); + static inline int NormalizeIndex(int idx, int baseCount); private: Object(); diff --git a/src/IO/parser.cpp b/src/IO/parser.cpp new file mode 100644 index 0000000..4d59c64 --- /dev/null +++ b/src/IO/parser.cpp @@ -0,0 +1,123 @@ +#include // for std::from_chars (C++17+) +#include // for strtof (fallback) +#include + +#include "IO/parser.h" + +// Skip whitespace +void Parser::SkipSpaces() { + while (*m_sv == ' ' || *m_sv == '\t') ++m_sv; +} + +int Parser::TakeIndex(int baseCount) { + if (!m_sv || *m_sv == '\0') return -1; + + bool neg = (*m_sv == '-'); + if (neg) ++m_sv; + + int idx = 0; + while (*m_sv >= '0' && *m_sv <= '9') { + idx = idx * 10 + (*m_sv - '0'); + ++m_sv; + } + + if (neg) return baseCount + (-idx); + return idx > 0 ? idx - 1 : -1; +} + +// Get next whitespace-delimited word +char* Parser::TakeWord() { + SkipSpaces(); + if (*m_sv == '\0' || *m_sv == '\n' || *m_sv == '\r') return nullptr; + + char* start = m_sv; + while (*m_sv && *m_sv != ' ' && *m_sv != '\t' && *m_sv != '\n' && *m_sv != '\r') + ++m_sv; + + if (*m_sv) { *m_sv = '\0'; ++m_sv; } + return start; +} + +// Parse a float quickly +float Parser::TakeFloat() { + SkipSpaces(); + if (*m_sv == '\0') return 0.0f; + +#if __cpp_lib_to_chars >= 201611L + float value = 0.0f; + auto result = std::from_chars(m_sv, m_sv + std::strlen(m_sv), value); + m_sv = const_cast(result.ptr); + return value; +#else + char* end; + float value = std::strtof(m_sv, &end); + m_sv = end; + return value; +#endif +} + +// Parse an integer quickly +int Parser::TakeInt() { + SkipSpaces(); + if (*m_sv == '\0') return 0; + +#if __cpp_lib_to_chars >= 201611L + int value = 0; + auto result = std::from_chars(m_sv, m_sv + std::strlen(m_sv), value); + m_sv = const_cast(result.ptr); + return value; +#else + char* end; + int value = static_cast(std::strtol(m_sv, &end, 10)); + m_sv = end; + return value; +#endif +} + +// Take everything until delimiter (mutates buffer) +char* Parser::TakeUntil(char d) { + SkipSpaces(); + if (*m_sv == '\0') return nullptr; + + char* start = m_sv; + while (*m_sv && *m_sv != d && *m_sv != '\n' && *m_sv != '\r') + ++m_sv; + + if (*m_sv) { *m_sv = '\0'; ++m_sv; } + return start; +} + +// Parser.h (or Parser.cpp) +// Parse one face element at current position. +// Accepts formats: "v", "v/t", "v//n", "v/t/n" +// Returns true if a token was parsed, false if no more tokens on the line. +bool Parser::TakeFaceIndices(int &vi, int &ti, int &ni) { + SkipSpaces(); + if (*m_sv == '\0' || *m_sv == '\n' || *m_sv == '\r') { + vi = ti = ni = 0; // sentinel raw value meaning "no token" + return false; + } + + // parse vertex index (may be negative) + vi = static_cast(std::strtol(m_sv, &m_sv, 10)); + + ti = ni = 0; // 0 = not present (raw) + if (*m_sv == '/') { + ++m_sv; // skip '/' + // texcoord index (optional) + if (*m_sv != '/' && *m_sv != ' ' && *m_sv != '\0' && *m_sv != '\n' && *m_sv != '\r') { + ti = static_cast(std::strtol(m_sv, &m_sv, 10)); + } + if (*m_sv == '/') { + ++m_sv; // skip second '/' + // normal index (optional) + if (*m_sv != ' ' && *m_sv != '\0' && *m_sv != '\n' && *m_sv != '\r') { + ni = static_cast(std::strtol(m_sv, &m_sv, 10)); + } + } + } + + // At this point m_sv is either at whitespace, end, or next token char. + // Do NOT mutate indices (leave them raw). Let NormalizeIndex handle conversion. + return true; +} \ No newline at end of file diff --git a/src/renderer/basics.cpp b/src/renderer/basics.cpp new file mode 100644 index 0000000..189160a --- /dev/null +++ b/src/renderer/basics.cpp @@ -0,0 +1,15 @@ +#include + +#include "renderer/basics.h" + +void Vertex::DefineAttrib() +{ + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(offsetof(Vertex, m_position))); + glEnableVertexAttribArray(0); + + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(offsetof(Vertex, m_normal))); + glEnableVertexAttribArray(1); + + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(offsetof(Vertex, m_texCoord))); + glEnableVertexAttribArray(2); +} \ No newline at end of file diff --git a/src/renderer/mesh.cpp b/src/renderer/mesh.cpp new file mode 100644 index 0000000..934c123 --- /dev/null +++ b/src/renderer/mesh.cpp @@ -0,0 +1,43 @@ +#include "renderer/mesh.h" + +Mesh::Mesh() { + glGenVertexArrays(1, &m_vao); + glGenBuffers(1, &m_vbo); + glGenBuffers(1, &m_ebo); + + glBindVertexArray(m_vao); + + // VBO (vertex buffer) + glBindBuffer(GL_ARRAY_BUFFER, m_vbo); + glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_STATIC_DRAW); + + // EBO (index buffer) + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, 0, nullptr, GL_STATIC_DRAW); + + Vertex::DefineAttrib(); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); +} + +void Mesh::Upload() +{ + glBindVertexArray(m_vao); + + glBindBuffer(GL_ARRAY_BUFFER, m_vbo); + glBufferData(GL_ARRAY_BUFFER, m_vertexBuffer.size() * sizeof(Vertex), m_vertexBuffer.data(), GL_STATIC_DRAW); + + // Upload indices + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexBuffer.size() * sizeof(unsigned int), m_indexBuffer.data(), GL_STATIC_DRAW); + + glBindVertexArray(0); +} + +void Mesh::Render() +{ + Bind(); + glDrawElements(GL_TRIANGLES, static_cast(m_indexBuffer.size()), GL_UNSIGNED_INT, 0); + Unbind(); +} \ No newline at end of file diff --git a/src/renderer/texture.cpp b/src/renderer/texture.cpp index 193e9ce..8b67ca4 100644 --- a/src/renderer/texture.cpp +++ b/src/renderer/texture.cpp @@ -26,7 +26,6 @@ std::unique_ptr Texture::LoadFile(const std::string& filename) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - // TODO: automatically detect values for this function glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); diff --git a/src/renderer/wavefront.cpp b/src/renderer/wavefront.cpp index 13dba56..5612a17 100644 --- a/src/renderer/wavefront.cpp +++ b/src/renderer/wavefront.cpp @@ -1,78 +1,98 @@ #include #include -#include +#include #include #include #include +#include "IO/parser.h" +#include "renderer/mesh.h" #include "renderer/wavefront.h" -ObjElement toElement(const std::string &s) { - if (s == "#") return ObjElement::OHASH; - if (s == "mtllib") return ObjElement::MTLLIB; - if (s == "usemtl") return ObjElement::USEMTL; - if (s == "o") return ObjElement::O; - if (s == "v") return ObjElement::V; - if (s == "vn") return ObjElement::VN; - if (s == "vt") return ObjElement::VT; - if (s == "f") return ObjElement::F; +#define DEFAULT_MATERIAL_NAME "default" + +// ObjElement toElement(const std::string &s) { +// if (s == "#") return ObjElement::OHASH; +// if (s == "mtllib") return ObjElement::MTLLIB; +// if (s == "usemtl") return ObjElement::USEMTL; +// if (s == "o") return ObjElement::O; +// if (s == "v") return ObjElement::V; +// if (s == "vn") return ObjElement::VN; +// if (s == "vt") return ObjElement::VT; +// if (s == "f") return ObjElement::F; +// return ObjElement::OUNKNOWN; +// } + +inline ObjElement toElement(const char* s) { + switch (s[0]) { + case '#': return ObjElement::OHASH; + case 'm': if (strcmp(s, "mtllib") == 0) return ObjElement::MTLLIB; break; + case 'u': if (strcmp(s, "usemtl") == 0) return ObjElement::USEMTL; break; + case 'o': if (s[1] == '\0') return ObjElement::O; break; + case 'v': + if (s[1] == '\0') return ObjElement::V; + if (s[1] == 'n' && s[2] == '\0') return ObjElement::VN; + if (s[1] == 't' && s[2] == '\0') return ObjElement::VT; + break; + case 'f': if (s[1] == '\0') return ObjElement::F; break; + } return ObjElement::OUNKNOWN; } -MtlElement toMtlElement(const std::string &s) { - if (s == "#") return MtlElement::MHASH; - if (s == "newmtl") return MtlElement::NEWMTL; - if (s == "Ns") return MtlElement::NS; - if (s == "Ka") return MtlElement::KA; - if (s == "Ks") return MtlElement::KS; - if (s == "Kd") return MtlElement::KD; - if (s == "Ni") return MtlElement::NI; - if (s == "d") return MtlElement::D; - if (s == "illum") return MtlElement::ILLUM; - if (s == "map_Kd") return MtlElement::MAP_KD; - if (s == "map_Ka") return MtlElement::MAP_KA; - // if (s == "map_Ke") return MtlElement::MAP_KE; +// MtlElement toMtlElement(const std::string &s) { +// if (s == "#") return MtlElement::MHASH; +// if (s == "newmtl") return MtlElement::NEWMTL; +// if (s == "Ns") return MtlElement::NS; +// if (s == "Ka") return MtlElement::KA; +// if (s == "Ks") return MtlElement::KS; +// if (s == "Kd") return MtlElement::KD; +// if (s == "Ni") return MtlElement::NI; +// if (s == "d") return MtlElement::D; +// if (s == "illum") return MtlElement::ILLUM; +// if (s == "map_Kd") return MtlElement::MAP_KD; +// if (s == "map_Ka") return MtlElement::MAP_KA; +// // if (s == "map_Ke") return MtlElement::MAP_KE; +// return MtlElement::MUNKNOWN; +// } + +inline MtlElement toMtlElement(const char* s) { + switch (s[0]) { + case '#': return MtlElement::MHASH; + case 'n': + if (strcmp(s, "newmtl") == 0) return MtlElement::NEWMTL; + break; + case 'N': + if (s[1] == 's' && s[2] == '\0') return MtlElement::NS; + if (s[1] == 'i' && s[2] == '\0') return MtlElement::NI; + break; + case 'K': + if (s[1] == 'a' && s[2] == '\0') return MtlElement::KA; + if (s[1] == 's' && s[2] == '\0') return MtlElement::KS; + if (s[1] == 'd' && s[2] == '\0') return MtlElement::KD; + break; + case 'd': + if (s[1] == '\0') return MtlElement::D; + break; + case 'i': + if (strcmp(s, "illum") == 0) return MtlElement::ILLUM; + break; + case 'm': + if (strcmp(s, "map_Kd") == 0) return MtlElement::MAP_KD; + if (strcmp(s, "map_Ka") == 0) return MtlElement::MAP_KA; + // if (strcmp(s, "map_Ke") == 0) return MtlElement::MAP_KE; + break; + } return MtlElement::MUNKNOWN; } -void Vertex::DefineAttrib() -{ - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(offsetof(Vertex, m_position))); - glEnableVertexAttribArray(0); - - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(offsetof(Vertex, m_normal))); - glEnableVertexAttribArray(1); - - glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(offsetof(Vertex, m_texCoord))); - glEnableVertexAttribArray(2); -} - -inline int Object::NormalizeIndex(const std::string &s, int baseCount) { - if (s.empty()) return -1; - int idx = std::stoi(s); - if (idx > 0) return idx - 1; - return baseCount + idx; -} - -Mesh::Mesh() { - glGenVertexArrays(1, &m_vao); - glGenBuffers(1, &m_vbo); - glGenBuffers(1, &m_ebo); - - glBindVertexArray(m_vao); - - // VBO (vertex buffer) - glBindBuffer(GL_ARRAY_BUFFER, m_vbo); - glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_STATIC_DRAW); - - // EBO (index buffer) - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, 0, nullptr, GL_STATIC_DRAW); - - Vertex::DefineAttrib(); - - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindVertexArray(0); +inline int Object::NormalizeIndex(int idx, int baseCount) { + // idx is the raw value returned by parser: + // 0 -> means "not present" or invalid in our convention + // >0 -> 1-based index -> convert to 0-based + // <0 -> negative index -> relative to baseCount: baseCount + idx + if (idx == 0) return -1; // absent / invalid + if (idx > 0) return idx - 1; // 1-based -> 0-based + return baseCount + idx; // negative -> count from end } Object::Object() { @@ -83,91 +103,130 @@ Object::Object() { void Object::LoadMaterials(const std::filesystem::path& filename) { std::ifstream file(filename); + if (!file.is_open()) { + std::cerr << "Failed to open MTL file: " << filename << std::endl; + return; + } std::string currentMaterialName; std::shared_ptr currentMaterial; - - std::string line; - while (std::getline(file, line)) { - std::istringstream iss(line); - std::string prefix; - iss >> prefix; - switch(toMtlElement(prefix)) { - case MtlElement::MHASH: - { - std::cout << "comment: " << line << std::endl; + + char line[1024]; // buffer per line + + while (file.getline(line, sizeof(line))) { + Parser p(line); + char* prefix = p.TakeWord(); + if (!prefix) continue; + + switch (toMtlElement(prefix)) { + case MtlElement::MHASH: // comment continue; - } + case MtlElement::NEWMTL: { + // If a material was being built, commit it first if (currentMaterial) { - m_materials.insert(std::make_pair(currentMaterialName, std::move(currentMaterial))); + AddMaterial(currentMaterialName, std::move(currentMaterial)); currentMaterial = nullptr; } - std::string materialName; - iss >> materialName; - currentMaterialName = materialName; - currentMaterial = std::make_shared(); - break; - } - case MtlElement::NS: - { - float weight; - iss >> weight; - currentMaterial->SetSpecularWeight(weight); - } - case MtlElement::KA: - { - float r, g, b; - iss >> r >> g >> b; - currentMaterial->SetAmbientColor(glm::vec3(r, g, b)); - break; - } - case MtlElement::KS: - { - float r, g, b; - iss >> r >> g >> b; - currentMaterial->SetSpecularColor(glm::vec3(r, g, b)); - break; - } - case MtlElement::KD: - { - float r, g, b; - iss >> r >> g >> b; - currentMaterial->SetDiffuseColor(glm::vec3(r, g, b)); - break; - } - case MtlElement::D: - { - float d; - iss >> d; - currentMaterial->SetOpacity(d); - break; - } - case MtlElement::ILLUM: - { - int illum; - iss >> illum; - currentMaterial->SetIllumination(illum); - break; - } - case MtlElement::MAP_KD: - { - std::string texturePath; - std::string part; - while (iss >> part) { - texturePath += part + " "; + + char* materialName = p.TakeWord(); + if (materialName) { + currentMaterialName = materialName; + currentMaterial = std::make_shared(); } - texturePath = texturePath.substr(0, texturePath.size() - 1); - currentMaterial->SetDiffuseTexture(Texture::LoadFile(texturePath)); + break; } + + case MtlElement::NS: // specular weight + { + float weight = p.TakeFloat(); + if (currentMaterial) currentMaterial->SetSpecularWeight(weight); + break; + } + + case MtlElement::KA: // ambient color + { + float r = p.TakeFloat(); + float g = p.TakeFloat(); + float b = p.TakeFloat(); + if (currentMaterial) currentMaterial->SetAmbientColor(glm::vec3(r, g, b)); + break; + } + + case MtlElement::KS: // specular color + { + float r = p.TakeFloat(); + float g = p.TakeFloat(); + float b = p.TakeFloat(); + if (currentMaterial) currentMaterial->SetSpecularColor(glm::vec3(r, g, b)); + break; + } + + case MtlElement::KD: // diffuse color + { + float r = p.TakeFloat(); + float g = p.TakeFloat(); + float b = p.TakeFloat(); + if (currentMaterial) currentMaterial->SetDiffuseColor(glm::vec3(r, g, b)); + break; + } + + case MtlElement::D: // opacity + { + float d = p.TakeFloat(); + if (currentMaterial) currentMaterial->SetOpacity(d); + break; + } + + case MtlElement::ILLUM: // illumination model + { + int illum = p.TakeInt(); + if (currentMaterial) currentMaterial->SetIllumination(illum); + break; + } + + case MtlElement::MAP_KD: // diffuse texture map + { + // take rest of line as texture path (can contain spaces) + char* texPath = p.TakeUntil('\0'); + if (texPath && currentMaterial) { + // trim trailing spaces + size_t len = std::strlen(texPath); + while (len > 0 && (texPath[len - 1] == ' ' || texPath[len - 1] == '\t')) + texPath[--len] = '\0'; + + currentMaterial->SetDiffuseTexture(Texture::LoadFile(texPath)); + } + break; + } + + case MtlElement::MAP_KA: // ambient texture map + { + char* texPath = p.TakeUntil('\0'); + if (texPath && currentMaterial) { + size_t len = std::strlen(texPath); + while (len > 0 && (texPath[len - 1] == ' ' || texPath[len - 1] == '\t')) + texPath[--len] = '\0'; + + // optional: handle ambient texture + // currentMaterial->SetAmbientTexture(Texture::LoadFile(texPath)); + } + break; + } + + default: + // ignore unknown tokens + break; } } + // Commit last material if pending if (currentMaterial) { - // m_materials.insert(std::make_pair(currentMaterialName, std::move(currentMaterial))); AddMaterial(currentMaterialName, std::move(currentMaterial)); } + + file.close(); } void Object::AddMaterial(std::string name, std::shared_ptr material) @@ -194,116 +253,126 @@ Mesh& Object::GetLastMesh() if (m_meshes.empty()) { auto material = std::make_shared(); material->SetAmbientColor(glm::vec3(0.52f, 0.52f, 0.52f)); - // TODO: come up with name for a default material - AddMaterial("", std::move(material)); - CreateNewMesh(""); + AddMaterial(DEFAULT_MATERIAL_NAME, std::move(material)); + CreateNewMesh(DEFAULT_MATERIAL_NAME); } return m_meshes.back(); } Object Object::LoadFile(const std::string& filename) { std::ifstream file(filename); + if (!file.is_open()) { + std::cerr << "Failed to open OBJ file: " << filename << std::endl; + return {}; + } Object obj; + char line[1024]; // static buffer for each line (enough for OBJ lines) - std::string line; - while (std::getline(file, line)) { - std::istringstream iss(line); - std::string prefix; - iss >> prefix; - switch(toElement(prefix)) { - // comment - case ObjElement::OHASH: - { - std::cout << "comment: " << line << std::endl; + while (file.getline(line, sizeof(line))) { + Parser p(line); + char* prefix = p.TakeWord(); + if (!prefix) continue; + + switch (toElement(prefix)) { + case ObjElement::OHASH: // comment continue; - } + case ObjElement::MTLLIB: { - std::string mtlFile; - iss >> mtlFile; - std::filesystem::path fullPath = filename; - std::filesystem::path mtlPath = fullPath.replace_filename(mtlFile); - obj.LoadMaterials(mtlPath); - std::cout << "loaded mtl at '" << mtlPath << "' with " - << obj.m_materials.size() << " materials" << std::endl; - break; - } - case ObjElement::USEMTL: - { - std::string materialName; - iss >> materialName; - auto& mesh = obj.GetLastMesh(); - if (mesh.materialName != materialName) { - Mesh mesh; - mesh.materialName = materialName; - obj.m_meshes.push_back(mesh); + char* mtlFile = p.TakeWord(); + if (mtlFile) { + std::filesystem::path fullPath = filename; + std::filesystem::path mtlPath = fullPath.replace_filename(mtlFile); + obj.LoadMaterials(mtlPath); } break; } - // object name I suppose - case ObjElement::O: + + case ObjElement::USEMTL: { - obj.m_name = line.substr(2); + char* materialName = p.TakeWord(); + if (materialName) { + auto& mesh = obj.GetLastMesh(); + if (mesh.materialName != materialName) { + Mesh newMesh; + newMesh.materialName = materialName; + obj.m_meshes.push_back(newMesh); + } + } break; } - // vertex with its position - case ObjElement::V: + + case ObjElement::O: // object name { - float x, y, z, w; - w = 1.0f; - iss >> x >> y >> z; - if (iss >> w) { - x /= w; - y /= w; - z /= w; + char* name = p.TakeWord(); + if (name) obj.m_name = name; + break; + } + + case ObjElement::V: // vertex + { + float x = p.TakeFloat(); + float y = p.TakeFloat(); + float z = p.TakeFloat(); + float w = p.TakeFloat(); + + if (w != 0.0f && w != 1.0f) { + x /= w; y /= w; z /= w; } obj.m_vertices.emplace_back(x, y, z); break; } - case ObjElement::VN: + + case ObjElement::VN: // normal { - float x, y, z; - iss >> x >> y >> z; + float x = p.TakeFloat(); + float y = p.TakeFloat(); + float z = p.TakeFloat(); obj.m_normals.emplace_back(x, y, z); break; } - case ObjElement::VT: + + case ObjElement::VT: // texcoord { - float u, v; - iss >> u >> v; + float u = p.TakeFloat(); + float v = p.TakeFloat(); obj.m_texCoords.emplace_back(u, 1.0f - v); break; } - case ObjElement::F: + + case ObjElement::F: // face { auto& mesh = obj.GetLastMesh(); - std::string token; + int raw_vi, raw_ti, raw_ni; - while (iss >> token) { - std::string a, b, c; - std::istringstream ref(token); + while (p.TakeFaceIndices(raw_vi, raw_ti, raw_ni)) { + // Convert raw OBJ indices to 0-based / -1 sentinel + int vi = Object::NormalizeIndex(raw_vi, (int)obj.m_vertices.size()); + int ti = Object::NormalizeIndex(raw_ti, (int)obj.m_texCoords.size()); + int ni = Object::NormalizeIndex(raw_ni, (int)obj.m_normals.size()); - std::getline(ref, a, '/'); - std::getline(ref, b, '/'); - std::getline(ref, c, '/'); + if (vi < 0) { + // malformed token (no vertex) — skip + continue; + } - int vi = Object::NormalizeIndex(a, (int)obj.m_vertices.size()); - int ti = Object::NormalizeIndex(b, (int)obj.m_texCoords.size()); - int ni = Object::NormalizeIndex(c, (int)obj.m_normals.size()); + glm::vec3 vert = obj.m_vertices[vi]; + glm::vec3 norm(0.0f); + glm::vec2 texCoord(0.0f); - glm::vec3 vert, norm; - glm::vec2 texCoord; - vert = obj.m_vertices[vi]; if (ni >= 0) norm = obj.m_normals[ni]; if (ti >= 0) texCoord = obj.m_texCoords[ti]; mesh.m_vertexBuffer.emplace_back(vert, norm, texCoord); mesh.m_indexBuffer.push_back(mesh.m_vertexBuffer.size() - 1); } - break; } + + default: + // ignore unknown tokens + break; } } @@ -313,9 +382,6 @@ Object Object::LoadFile(const std::string& filename) { std::cout << "TexCoords count: " << obj.m_texCoords.size() << std::endl; std::cout << "Meshes count: " << obj.m_meshes.size() << std::endl; std::cout << "Materials count: " << obj.m_materials.size() << std::endl; - - // std::cout << "Vertex Buffer size: " << obj.m_vertexBuffer.size() << std::endl; - // std::cout << "Index Buffer size: " << obj.m_indexBuffer.size() << std::endl; file.close(); @@ -326,6 +392,7 @@ Object Object::LoadFile(const std::string& filename) { return obj; } + void Object::Render(Shader& shader) { for (auto &mesh : m_meshes) { @@ -353,23 +420,4 @@ void Object::Render(Shader& shader) } } -void Mesh::Upload() -{ - glBindVertexArray(m_vao); - glBindBuffer(GL_ARRAY_BUFFER, m_vbo); - glBufferData(GL_ARRAY_BUFFER, m_vertexBuffer.size() * sizeof(Vertex), m_vertexBuffer.data(), GL_STATIC_DRAW); - - // Upload indices - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexBuffer.size() * sizeof(unsigned int), m_indexBuffer.data(), GL_STATIC_DRAW); - - glBindVertexArray(0); -} - -void Mesh::Render() -{ - Bind(); - glDrawElements(GL_TRIANGLES, static_cast(m_indexBuffer.size()), GL_UNSIGNED_INT, 0); - Unbind(); -}