From 5d75cf8fedffd11df53221c66f60125fa346e92d Mon Sep 17 00:00:00 2001 From: wmayer Date: Sat, 16 Aug 2025 14:57:05 +0200 Subject: [PATCH] Mesh: Improve OBJ mesh import This change allows it to load OBJ files created by Blender. Fixes https://github.com/FreeCAD/FreeCAD/issues/19456 --- src/Mod/Mesh/App/Core/IO/ReaderOBJ.cpp | 512 ++++++++++++++++--------- src/Mod/Mesh/App/Core/IO/ReaderOBJ.h | 5 + 2 files changed, 334 insertions(+), 183 deletions(-) diff --git a/src/Mod/Mesh/App/Core/IO/ReaderOBJ.cpp b/src/Mod/Mesh/App/Core/IO/ReaderOBJ.cpp index 84c9606ed3..9b076b1b61 100644 --- a/src/Mod/Mesh/App/Core/IO/ReaderOBJ.cpp +++ b/src/Mod/Mesh/App/Core/IO/ReaderOBJ.cpp @@ -22,14 +22,18 @@ #include "PreCompiled.h" #ifndef _PreComp_ +#include #include -#include #include #include +#include #endif #include "Core/MeshIO.h" #include "Core/MeshKernel.h" +#include +#include +#include #include #include "ReaderOBJ.h" @@ -37,47 +41,322 @@ using namespace MeshCore; +namespace +{ + +class ReaderOBJImp +{ +public: + using string_list = std::vector; + + explicit ReaderOBJImp(Material* material) + : _material {material} + {} + + void Load(const std::string& line) + { + if (Ignore(line)) { + return; + } + + boost::char_separator sep(" /\t"); + boost::tokenizer> tokens(line, sep); + token_results.assign(tokens.begin(), tokens.end()); + if (token_results.size() < 2) { + return; + } + + if (MatchVertex(token_results)) { + LoadVertex(token_results); + } + else if (MatchVertexWithColor(token_results)) { + LoadVertexWithColor(token_results); + } + else if (MatchGroup(token_results)) { + LoadGroup(token_results); + } + else if (MatchLibrary(token_results)) { + LoadLibrary(token_results); + } + else if (MatchUseMaterial(token_results)) { + LoadUseMaterial(token_results); + } + else if (MatchFace(token_results)) { + LoadFace(token_results); + } + else if (MatchQuad(token_results)) { + LoadQuad(token_results); + } + } + + void SetupMaterial() + { + // Add the last added material name + if (!materialName.empty()) { + _materialNames.emplace_back(materialName, countMaterialFacets); + } + + // now get back the colors from the vertex property + if (rgb_value == MeshIO::PER_VERTEX) { + if (_material) { + _material->binding = MeshIO::PER_VERTEX; + _material->diffuseColor.reserve(meshPoints.size()); + + for (const auto& it : meshPoints) { + unsigned long prop = it._ulProp; + Base::Color c; + c.setPackedValue(static_cast(prop)); + _material->diffuseColor.push_back(c); + } + } + } + else if (!materialName.empty()) { + // At this point the materials from the .mtl file are not known and will be read-in by + // the calling instance but the color list is pre-filled with a default value + if (_material) { + _material->binding = MeshIO::PER_FACE; + const float rgb = 0.8F; + _material->diffuseColor.resize(meshFacets.size(), Base::Color(rgb, rgb, rgb)); + } + } + } + +private: + bool Ignore(const std::string& line) const + { + // clang-format off + return boost::starts_with(line, "vn ") || + boost::starts_with(line, "vt ") || + boost::starts_with(line, "s ") || + boost::starts_with(line, "o ") || + boost::starts_with(line, "#"); + // clang-format on + } + + bool MatchVertex(const string_list& tokens) const + { + return tokens[0] == "v" && tokens.size() == 4; + } + + void LoadVertex(const string_list& tokens) + { + float x = std::stof(tokens[1]); + float y = std::stof(tokens[2]); + float z = std::stof(tokens[3]); + meshPoints.push_back(MeshPoint(Base::Vector3f(x, y, z))); + } + + bool MatchVertexWithColor(const string_list& tokens) const + { + return tokens[0] == "v" && tokens.size() == 7; // NOLINT + } + + void LoadVertexWithColor(const string_list& tokens) + { + LoadVertex(tokens); + + // NOLINTBEGIN + float r = std::stof(tokens[4]); + float g = std::stof(tokens[5]); + float b = std::stof(tokens[6]); + if (r > 1.0F || g > 1.0F || b > 1.0F) { + r /= 255.0F; + g /= 255.0F; + b /= 255.0F; + } + // NOLINTEND + + SetVertexColor(Base::Color(r, g, b)); + } + + void SetVertexColor(const Base::Color& c) + { + unsigned long prop = static_cast(c.getPackedValue()); + meshPoints.back().SetProperty(prop); + rgb_value = MeshIO::PER_VERTEX; + } + + bool MatchGroup(const string_list& tokens) const + { + return tokens[0] == "g" && tokens.size() == 2; + } + + void LoadGroup(const string_list& tokens) + { + new_segment = true; + groupName = Base::Tools::escapedUnicodeToUtf8(tokens[1]); + } + + bool MatchLibrary(const string_list& tokens) const + { + return tokens[0] == "mtllib" && tokens.size() == 2; + } + + void LoadLibrary(const string_list& tokens) + { + if (_material) { + _material->library = Base::Tools::escapedUnicodeToUtf8(tokens[1]); + } + } + + bool MatchUseMaterial(const string_list& tokens) const + { + return tokens[0] == "usemtl" && tokens.size() == 2; + } + + void LoadUseMaterial(const string_list& tokens) + { + if (!materialName.empty()) { + _materialNames.emplace_back(materialName, countMaterialFacets); + } + materialName = Base::Tools::escapedUnicodeToUtf8(tokens[1]); + countMaterialFacets = 0; + } + + bool MatchFace(const string_list& tokens) const + { + // NOLINTBEGIN + const auto num = tokens.size(); + return tokens[0] == "f" && (num == 4 || num == 7 || num == 10); + // NOLINTEND + } + + void LoadFace(const string_list& tokens) + { + StartNewSegment(); + + // NOLINTBEGIN + int index1 = 1; + int index2 = 2; + int index3 = 3; + if (tokens.size() == 7) { + index2 = 3; + index3 = 5; + } + else if (tokens.size() == 10) { + index2 = 4; + index3 = 7; + } + // NOLINTEND + + // 3-vertex face + int i1 = std::atoi(tokens[index1].c_str()); + i1 = i1 > 0 ? i1 - 1 : i1 + static_cast(meshPoints.size()); + int i2 = std::atoi(tokens[index2].c_str()); + i2 = i2 > 0 ? i2 - 1 : i2 + static_cast(meshPoints.size()); + int i3 = std::atoi(tokens[index3].c_str()); + i3 = i3 > 0 ? i3 - 1 : i3 + static_cast(meshPoints.size()); + + AddFace(i1, i2, i3); + } + + bool MatchQuad(const string_list& tokens) const + { + // NOLINTBEGIN + const auto num = tokens.size(); + return tokens[0] == "f" && (num == 5 || num == 9 || num == 13); + // NOLINTEND + } + + void LoadQuad(const string_list& tokens) + { + StartNewSegment(); + + // NOLINTBEGIN + int index1 = 1; + int index2 = 2; + int index3 = 3; + int index4 = 4; + if (tokens.size() == 9) { + index2 = 3; + index3 = 5; + index4 = 7; + } + else if (tokens.size() == 13) { + index2 = 4; + index3 = 7; + index4 = 10; + } + // NOLINTEND + + // 4-vertex face + int i1 = std::atoi(tokens[index1].c_str()); + i1 = i1 > 0 ? i1 - 1 : i1 + static_cast(meshPoints.size()); + int i2 = std::atoi(tokens[index2].c_str()); + i2 = i2 > 0 ? i2 - 1 : i2 + static_cast(meshPoints.size()); + int i3 = std::atoi(tokens[index3].c_str()); + i3 = i3 > 0 ? i3 - 1 : i3 + static_cast(meshPoints.size()); + int i4 = std::atoi(tokens[index4].c_str()); + i4 = i4 > 0 ? i4 - 1 : i4 + static_cast(meshPoints.size()); + + AddFace(i1, i2, i3); + AddFace(i3, i4, i1); + } + + void StartNewSegment() + { + // starts a new segment + if (new_segment) { + if (!groupName.empty()) { + _groupNames.push_back(groupName); + groupName.clear(); + } + new_segment = false; + segment++; + } + } + + void AddFace(int i, int j, int k) + { + MeshFacet item; + item.SetVertices(i, j, k); + item.SetProperty(segment); + meshFacets.push_back(item); + countMaterialFacets++; + } + +public: + // NOLINTBEGIN + MeshPointArray meshPoints; + MeshFacetArray meshFacets; + // NOLINTEND + + using MaterialPerSegment = std::pair; + std::vector GetMaterialNames() const + { + return _materialNames; + } + +private: + string_list token_results; + MeshIO::Binding rgb_value = MeshIO::OVERALL; + unsigned long countMaterialFacets = 0; + unsigned long segment = 0; + bool new_segment = true; + + std::string groupName; + std::string materialName; + Material* _material; + std::vector _groupNames; + std::vector _materialNames; +}; + +} // namespace + ReaderOBJ::ReaderOBJ(MeshKernel& kernel, Material* material) : _kernel(kernel) , _material(material) {} +bool ReaderOBJ::Load(const std::string& file) +{ + Base::FileInfo fi(file); + Base::ifstream str(fi, std::ios::in); + return Load(str); +} + bool ReaderOBJ::Load(std::istream& str) { - boost::regex rx_m("^mtllib\\s+(.+)\\s*$"); - boost::regex rx_u(R"(^usemtl\s+([\x21-\x7E]+)\s*$)"); - boost::regex rx_g(R"(^g\s+([\x21-\x7E]+)\s*$)"); - boost::regex rx_p("^v\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)" - "\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)" - "\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)\\s*$"); - boost::regex rx_c("^v\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)" - "\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)" - "\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)" - "\\s+(\\d{1,3})\\s+(\\d{1,3})\\s+(\\d{1,3})\\s*$"); - boost::regex rx_t("^v\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)" - "\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)" - "\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)" - "\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)" - "\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)" - "\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)\\s*$"); - boost::regex rx_f3("^f\\s+([-+]?[0-9]+)/?[-+]?[0-9]*/?[-+]?[0-9]*" - "\\s+([-+]?[0-9]+)/?[-+]?[0-9]*/?[-+]?[0-9]*" - "\\s+([-+]?[0-9]+)/?[-+]?[0-9]*/?[-+]?[0-9]*\\s*$"); - boost::regex rx_f4("^f\\s+([-+]?[0-9]+)/?[-+]?[0-9]*/?[-+]?[0-9]*" - "\\s+([-+]?[0-9]+)/?[-+]?[0-9]*/?[-+]?[0-9]*" - "\\s+([-+]?[0-9]+)/?[-+]?[0-9]*/?[-+]?[0-9]*" - "\\s+([-+]?[0-9]+)/?[-+]?[0-9]*/?[-+]?[0-9]*\\s*$"); - boost::cmatch what; - - unsigned long segment = 0; - MeshPointArray meshPoints; - MeshFacetArray meshFacets; - - std::string line; - float fX {}, fY {}, fZ {}; - int i1 = 1, i2 = 1, i3 = 1, i4 = 1; - MeshFacet item; - if (!str || str.bad()) { return false; } @@ -87,161 +366,30 @@ bool ReaderOBJ::Load(std::istream& str) return false; } - MeshIO::Binding rgb_value = MeshIO::OVERALL; - bool new_segment = true; - std::string groupName; - std::string materialName; - unsigned long countMaterialFacets = 0; - + ReaderOBJImp reader(_material); + std::string line; while (std::getline(str, line)) { - if (boost::regex_match(line.c_str(), what, rx_p)) { - fX = (float)std::atof(what[1].first); - fY = (float)std::atof(what[4].first); - fZ = (float)std::atof(what[7].first); - meshPoints.push_back(MeshPoint(Base::Vector3f(fX, fY, fZ))); - } - else if (boost::regex_match(line.c_str(), what, rx_c)) { - fX = (float)std::atof(what[1].first); - fY = (float)std::atof(what[4].first); - fZ = (float)std::atof(what[7].first); - float r = std::min(std::atof(what[10].first), 255) / 255.0F; - float g = std::min(std::atof(what[11].first), 255) / 255.0F; - float b = std::min(std::atof(what[12].first), 255) / 255.0F; - meshPoints.push_back(MeshPoint(Base::Vector3f(fX, fY, fZ))); - - Base::Color c(r, g, b); - unsigned long prop = static_cast(c.getPackedValue()); - meshPoints.back().SetProperty(prop); - rgb_value = MeshIO::PER_VERTEX; - } - else if (boost::regex_match(line.c_str(), what, rx_t)) { - fX = (float)std::atof(what[1].first); - fY = (float)std::atof(what[4].first); - fZ = (float)std::atof(what[7].first); - float r = static_cast(std::atof(what[10].first)); - float g = static_cast(std::atof(what[13].first)); - float b = static_cast(std::atof(what[16].first)); - meshPoints.push_back(MeshPoint(Base::Vector3f(fX, fY, fZ))); - - Base::Color c(r, g, b); - unsigned long prop = static_cast(c.getPackedValue()); - meshPoints.back().SetProperty(prop); - rgb_value = MeshIO::PER_VERTEX; - } - else if (boost::regex_match(line.c_str(), what, rx_g)) { - new_segment = true; - groupName = Base::Tools::escapedUnicodeToUtf8(what[1].first); - } - else if (boost::regex_match(line.c_str(), what, rx_m)) { - if (_material) { - _material->library = Base::Tools::escapedUnicodeToUtf8(what[1].first); - } - } - else if (boost::regex_match(line.c_str(), what, rx_u)) { - if (!materialName.empty()) { - _materialNames.emplace_back(materialName, countMaterialFacets); - } - materialName = Base::Tools::escapedUnicodeToUtf8(what[1].first); - countMaterialFacets = 0; - } - else if (boost::regex_match(line.c_str(), what, rx_f3)) { - // starts a new segment - if (new_segment) { - if (!groupName.empty()) { - _groupNames.push_back(groupName); - groupName.clear(); - } - new_segment = false; - segment++; - } - - // 3-vertex face - i1 = std::atoi(what[1].first); - i1 = i1 > 0 ? i1 - 1 : i1 + static_cast(meshPoints.size()); - i2 = std::atoi(what[2].first); - i2 = i2 > 0 ? i2 - 1 : i2 + static_cast(meshPoints.size()); - i3 = std::atoi(what[3].first); - i3 = i3 > 0 ? i3 - 1 : i3 + static_cast(meshPoints.size()); - item.SetVertices(i1, i2, i3); - item.SetProperty(segment); - meshFacets.push_back(item); - countMaterialFacets++; - } - else if (boost::regex_match(line.c_str(), what, rx_f4)) { - // starts a new segment - if (new_segment) { - if (!groupName.empty()) { - _groupNames.push_back(groupName); - groupName.clear(); - } - new_segment = false; - segment++; - } - - // 4-vertex face - i1 = std::atoi(what[1].first); - i1 = i1 > 0 ? i1 - 1 : i1 + static_cast(meshPoints.size()); - i2 = std::atoi(what[2].first); - i2 = i2 > 0 ? i2 - 1 : i2 + static_cast(meshPoints.size()); - i3 = std::atoi(what[3].first); - i3 = i3 > 0 ? i3 - 1 : i3 + static_cast(meshPoints.size()); - i4 = std::atoi(what[4].first); - i4 = i4 > 0 ? i4 - 1 : i4 + static_cast(meshPoints.size()); - - item.SetVertices(i1, i2, i3); - item.SetProperty(segment); - meshFacets.push_back(item); - countMaterialFacets++; - - item.SetVertices(i3, i4, i1); - item.SetProperty(segment); - meshFacets.push_back(item); - countMaterialFacets++; - } - } - - // Add the last added material name - if (!materialName.empty()) { - _materialNames.emplace_back(materialName, countMaterialFacets); - } - - // now get back the colors from the vertex property - if (rgb_value == MeshIO::PER_VERTEX) { - if (_material) { - _material->binding = MeshIO::PER_VERTEX; - _material->diffuseColor.reserve(meshPoints.size()); - - for (const auto& it : meshPoints) { - unsigned long prop = it._ulProp; - Base::Color c; - c.setPackedValue(static_cast(prop)); - _material->diffuseColor.push_back(c); - } - } - } - else if (!materialName.empty()) { - // At this point the materials from the .mtl file are not known and will be read-in by the - // calling instance but the color list is pre-filled with a default value - if (_material) { - _material->binding = MeshIO::PER_FACE; - _material->diffuseColor.resize(meshFacets.size(), Base::Color(0.8F, 0.8F, 0.8F)); - } + boost::trim(line); + reader.Load(line); } + reader.SetupMaterial(); + _materialNames = reader.GetMaterialNames(); _kernel.Clear(); // remove all data before - MeshCleanup meshCleanup(meshPoints, meshFacets); + MeshCleanup meshCleanup(reader.meshPoints, reader.meshFacets); if (_material) { meshCleanup.SetMaterial(_material); } meshCleanup.RemoveInvalids(); - MeshPointFacetAdjacency meshAdj(meshPoints.size(), meshFacets); + MeshPointFacetAdjacency meshAdj(reader.meshPoints.size(), reader.meshFacets); meshAdj.SetFacetNeighbourhood(); - _kernel.Adopt(meshPoints, meshFacets); + _kernel.Adopt(reader.meshPoints, reader.meshFacets); return true; } +// NOLINTNEXTLINE bool ReaderOBJ::LoadMaterial(std::istream& str) { std::string line; @@ -272,13 +420,13 @@ bool ReaderOBJ::LoadMaterial(std::istream& str) auto readColor = [](const std::vector& tokens) -> Base::Color { if (tokens.size() == 2) { // If only R is given then G and B will be equal - float r = boost::lexical_cast(tokens[1]); + float r = std::stof(tokens[1]); return Base::Color(r, r, r); } if (tokens.size() == 4) { - float r = boost::lexical_cast(tokens[1]); - float g = boost::lexical_cast(tokens[2]); - float b = boost::lexical_cast(tokens[3]); + float r = std::stof(tokens[1]); + float g = std::stof(tokens[2]); + float b = std::stof(tokens[3]); return Base::Color(r, g, b); } @@ -297,7 +445,7 @@ bool ReaderOBJ::LoadMaterial(std::istream& str) materialName = Base::Tools::escapedUnicodeToUtf8(token_results[1]); } else if (token_results[0] == "d") { - float a = boost::lexical_cast(token_results[1]); + float a = std::stof(token_results[1]); materialTransparency[materialName] = 1.0F - a; } // If only R is given then G and B will be equal @@ -312,8 +460,6 @@ bool ReaderOBJ::LoadMaterial(std::istream& str) } } } - catch (const boost::bad_lexical_cast&) { - } catch (const std::exception&) { } } diff --git a/src/Mod/Mesh/App/Core/IO/ReaderOBJ.h b/src/Mod/Mesh/App/Core/IO/ReaderOBJ.h index e951a959e0..48fc754045 100644 --- a/src/Mod/Mesh/App/Core/IO/ReaderOBJ.h +++ b/src/Mod/Mesh/App/Core/IO/ReaderOBJ.h @@ -42,6 +42,11 @@ public: * \brief ReaderOBJ */ explicit ReaderOBJ(MeshKernel& kernel, Material*); + /*! + * \brief Load the mesh from the file + * \return true on success and false otherwise + */ + bool Load(const std::string& file); /*! * \brief Load the mesh from the input stream * \return true on success and false otherwise