//-------------------------------------------------------------------------------------- // File: WaveFrontReader.h // // Code for loading basic mesh data from a WaveFront OBJ file // // http://en.wikipedia.org/wiki/Wavefront_.obj_file // // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. // // http://go.microsoft.com/fwlink/?LinkID=324981 //-------------------------------------------------------------------------------------- #pragma once #ifdef _WIN32 #ifdef _MSC_VER #pragma warning(push) #pragma warning(disable : 4005) #endif #define WIN32_LEAN_AND_MEAN #ifndef NOMINMAX #define NOMINMAX 1 #endif #define NODRAWTEXT #define NOGDI #define NOMCX #define NOSERVICE #define NOHELP #ifdef _MSC_VER #pragma warning(pop) #endif #include #ifdef __MINGW32__ #include #endif #else // !WIN32 #include #include #ifndef MAX_PATH #define MAX_PATH 4096 #endif #endif #include #include #include #include #include #include #include #ifndef _WIN32 #include #endif #include #include namespace DX { template class WaveFrontReader { public: struct Vertex { DirectX::XMFLOAT3 position; DirectX::XMFLOAT3 normal; DirectX::XMFLOAT2 textureCoordinate; }; WaveFrontReader() noexcept : hasNormals(false), hasTexcoords(false) {} HRESULT Load(_In_z_ const wchar_t* szFileName, bool ccw = true) { Clear(); if (!szFileName) return E_INVALIDARG; constexpr size_t MAX_POLY = 64; using namespace DirectX; std::wifstream InFile(szFileName); if (!InFile) return /* HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) */ static_cast(0x80070002L); InFile.imbue(std::locale::classic()); #ifdef _WIN32 wchar_t fname[_MAX_FNAME] = {}; _wsplitpath_s(szFileName, nullptr, 0, nullptr, 0, fname, _MAX_FNAME, nullptr, 0); name = fname; #else auto path = std::filesystem::path(szFileName); name = path.filename().c_str(); #endif std::vector positions; std::vector normals; std::vector texCoords; VertexCache vertexCache; Material defmat; wcscpy_s(defmat.strName, L"default"); materials.emplace_back(defmat); uint32_t curSubset = 0; wchar_t strMaterialFilename[MAX_PATH] = {}; for (;; ) { std::wstring strCommand; InFile.width(MAX_PATH); InFile >> strCommand; if (!InFile) break; if (*strCommand.c_str() == L'#') { // Comment } else if (0 == wcscmp(strCommand.c_str(), L"o")) { // Object name ignored } else if (0 == wcscmp(strCommand.c_str(), L"g")) { // Group name ignored } else if (0 == wcscmp(strCommand.c_str(), L"s")) { // Smoothing group ignored } else if (0 == wcscmp(strCommand.c_str(), L"v")) { // Vertex Position float x, y, z; InFile >> x >> y >> z; positions.emplace_back(XMFLOAT3(x, y, z)); } else if (0 == wcscmp(strCommand.c_str(), L"vt")) { // Vertex TexCoord float u, v; InFile >> u >> v; texCoords.emplace_back(XMFLOAT2(u, v)); hasTexcoords = true; } else if (0 == wcscmp(strCommand.c_str(), L"vn")) { // Vertex Normal float x, y, z; InFile >> x >> y >> z; normals.emplace_back(XMFLOAT3(x, y, z)); hasNormals = true; } else if (0 == wcscmp(strCommand.c_str(), L"f")) { // Face INT iPosition, iTexCoord, iNormal; Vertex vertex; uint32_t faceIndex[MAX_POLY]; size_t iFace = 0; for (;;) { if (iFace >= MAX_POLY) { // Too many polygon verts for the reader return E_FAIL; } memset(&vertex, 0, sizeof(vertex)); InFile >> iPosition; uint32_t vertexIndex = 0; if (!iPosition) { // 0 is not allowed for index return E_UNEXPECTED; } else if (iPosition < 0) { // Negative values are relative indices vertexIndex = uint32_t(ptrdiff_t(positions.size()) + iPosition); } else { // OBJ format uses 1-based arrays vertexIndex = uint32_t(iPosition - 1); } if (vertexIndex >= positions.size()) return E_FAIL; vertex.position = positions[vertexIndex]; if ('/' == InFile.peek()) { InFile.ignore(); if ('/' != InFile.peek()) { // Optional texture coordinate InFile >> iTexCoord; uint32_t coordIndex = 0; if (!iTexCoord) { // 0 is not allowed for index return E_UNEXPECTED; } else if (iTexCoord < 0) { // Negative values are relative indices coordIndex = uint32_t(ptrdiff_t(texCoords.size()) + iTexCoord); } else { // OBJ format uses 1-based arrays coordIndex = uint32_t(iTexCoord - 1); } if (coordIndex >= texCoords.size()) return E_FAIL; vertex.textureCoordinate = texCoords[coordIndex]; } if ('/' == InFile.peek()) { InFile.ignore(); // Optional vertex normal InFile >> iNormal; uint32_t normIndex = 0; if (!iNormal) { // 0 is not allowed for index return E_UNEXPECTED; } else if (iNormal < 0) { // Negative values are relative indices normIndex = uint32_t(ptrdiff_t(normals.size()) + iNormal); } else { // OBJ format uses 1-based arrays normIndex = uint32_t(iNormal - 1); } if (normIndex >= normals.size()) return E_FAIL; vertex.normal = normals[normIndex]; } } // If a duplicate vertex doesn't exist, add this vertex to the Vertices // list. Store the index in the Indices array. The Vertices and Indices // lists will eventually become the Vertex Buffer and Index Buffer for // the mesh. const uint32_t index = AddVertex(vertexIndex, &vertex, vertexCache); if (index == uint32_t(-1)) return E_OUTOFMEMORY; constexpr uint32_t maxIndex = (sizeof(index_t) == 2) ? UINT16_MAX : UINT32_MAX; if (index >= maxIndex) { // Too many indices for IB! return E_FAIL; } faceIndex[iFace] = index; ++iFace; // Check for more face data or end of the face statement bool faceEnd = false; for (;;) { const wchar_t p = InFile.peek(); if ('\n' == p || !InFile) { faceEnd = true; break; } else if (isdigit(p) || p == '-' || p == '+') break; InFile.ignore(); } if (faceEnd) break; } if (iFace < 3) { // Need at least 3 points to form a triangle return E_FAIL; } // Convert polygons to triangles const uint32_t i0 = faceIndex[0]; uint32_t i1 = faceIndex[1]; for (size_t j = 2; j < iFace; ++j) { const uint32_t index = faceIndex[j]; indices.emplace_back(static_cast(i0)); if (ccw) { indices.emplace_back(static_cast(i1)); indices.emplace_back(static_cast(index)); } else { indices.emplace_back(static_cast(index)); indices.emplace_back(static_cast(i1)); } attributes.emplace_back(curSubset); i1 = index; } assert(attributes.size() * 3 == indices.size()); } else if (0 == wcscmp(strCommand.c_str(), L"mtllib")) { // Material library InFile.width(MAX_PATH); InFile >> strMaterialFilename; } else if (0 == wcscmp(strCommand.c_str(), L"usemtl")) { // Material wchar_t strName[MAX_PATH] = {}; InFile.width(MAX_PATH); InFile >> strName; bool bFound = false; uint32_t count = 0; for (auto it = materials.cbegin(); it != materials.cend(); ++it, ++count) { if (0 == wcscmp(it->strName, strName)) { bFound = true; curSubset = count; break; } } if (!bFound) { Material mat; curSubset = static_cast(materials.size()); wcscpy_s(mat.strName, MAX_PATH - 1, strName); materials.emplace_back(mat); } } else { #ifdef _DEBUG // Unimplemented or unrecognized command OutputDebugStringW(strCommand.c_str()); #endif } InFile.ignore(1000, L'\n'); } if (positions.empty()) return E_FAIL; // Cleanup InFile.close(); BoundingBox::CreateFromPoints(bounds, positions.size(), positions.data(), sizeof(XMFLOAT3)); // If an associated material file was found, read that in as well. if (*strMaterialFilename) { #ifdef _WIN32 wchar_t ext[_MAX_EXT] = {}; _wsplitpath_s(strMaterialFilename, nullptr, 0, nullptr, 0, fname, _MAX_FNAME, ext, _MAX_EXT); wchar_t drive[_MAX_DRIVE] = {}; wchar_t dir[_MAX_DIR] = {}; _wsplitpath_s(szFileName, drive, _MAX_DRIVE, dir, _MAX_DIR, nullptr, 0, nullptr, 0); wchar_t szPath[MAX_PATH] = {}; _wmakepath_s(szPath, MAX_PATH, drive, dir, fname, ext); HRESULT hr = LoadMTL(szPath); if (FAILED(hr)) return hr; #else auto path = std::filesystem::path(szFileName); auto mtlpath = std::filesystem::path(strMaterialFilename); path.replace_filename(mtlpath.filename()); path.replace_extension(mtlpath.extension()); HRESULT hr = LoadMTL(path.c_str()); if (FAILED(hr)) return hr; #endif } return S_OK; } HRESULT LoadMTL(_In_z_ const wchar_t* szFileName) { if (!szFileName) return E_INVALIDARG; using namespace DirectX; // Assumes MTL is in CWD along with OBJ std::wifstream InFile(szFileName); if (!InFile) return /* HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) */ static_cast(0x80070002L); InFile.imbue(std::locale::classic()); auto curMaterial = materials.end(); for (;; ) { std::wstring strCommand; InFile >> strCommand; if (!InFile) break; if (0 == wcscmp(strCommand.c_str(), L"newmtl")) { // Switching active materials wchar_t strName[MAX_PATH] = {}; InFile.width(MAX_PATH); InFile >> strName; curMaterial = materials.end(); for (auto it = materials.begin(); it != materials.end(); ++it) { if (0 == wcscmp(it->strName, strName)) { curMaterial = it; break; } } } // The rest of the commands rely on an active material if (curMaterial == materials.end()) continue; if (0 == wcscmp(strCommand.c_str(), L"#")) { // Comment } else if (0 == wcscmp(strCommand.c_str(), L"Ka")) { // Ambient color float r, g, b; InFile >> r >> g >> b; curMaterial->vAmbient = XMFLOAT3(r, g, b); } else if (0 == wcscmp(strCommand.c_str(), L"Kd")) { // Diffuse color float r, g, b; InFile >> r >> g >> b; curMaterial->vDiffuse = XMFLOAT3(r, g, b); } else if (0 == wcscmp(strCommand.c_str(), L"Ks")) { // Specular color float r, g, b; InFile >> r >> g >> b; curMaterial->vSpecular = XMFLOAT3(r, g, b); } else if (0 == wcscmp(strCommand.c_str(), L"Ke")) { // Emissive color float r, g, b; InFile >> r >> g >> b; curMaterial->vEmissive = XMFLOAT3(r, g, b); if (r > 0.f || g > 0.f || b > 0.f) { curMaterial->bEmissive = true; } } else if (0 == wcscmp(strCommand.c_str(), L"d")) { // Alpha float alpha; InFile >> alpha; curMaterial->fAlpha = std::min(1.f, std::max(0.f, alpha)); } else if (0 == wcscmp(strCommand.c_str(), L"Tr")) { // Transparency (inverse of alpha) float invAlpha; InFile >> invAlpha; curMaterial->fAlpha = std::min(1.f, std::max(0.f, 1.f - invAlpha)); } else if (0 == wcscmp(strCommand.c_str(), L"Ns")) { // Shininess int nShininess; InFile >> nShininess; curMaterial->nShininess = uint32_t(nShininess); } else if (0 == wcscmp(strCommand.c_str(), L"illum")) { // Specular on/off int illumination; InFile >> illumination; curMaterial->bSpecular = (illumination == 2); } else if (0 == wcscmp(strCommand.c_str(), L"map_Kd")) { // Diffuse texture LoadTexturePath(InFile, curMaterial->strTexture, MAX_PATH); } else if (0 == wcscmp(strCommand.c_str(), L"map_Ks")) { // Specular texture LoadTexturePath(InFile, curMaterial->strSpecularTexture, MAX_PATH); } else if (0 == wcscmp(strCommand.c_str(), L"map_Kn") || 0 == wcscmp(strCommand.c_str(), L"norm")) { // Normal texture LoadTexturePath(InFile, curMaterial->strNormalTexture, MAX_PATH); } else if (0 == wcscmp(strCommand.c_str(), L"map_Ke") || 0 == wcscmp(strCommand.c_str(), L"map_emissive")) { // Emissive texture LoadTexturePath(InFile, curMaterial->strEmissiveTexture, MAX_PATH); curMaterial->bEmissive = true; } else if (0 == wcscmp(strCommand.c_str(), L"map_RMA") || 0 == wcscmp(strCommand.c_str(), L"map_ORM")) { // RMA texture LoadTexturePath(InFile, curMaterial->strRMATexture, MAX_PATH); } else { // Unimplemented or unrecognized command } InFile.ignore(1000, L'\n'); } InFile.close(); return S_OK; } void Clear() { vertices.clear(); indices.clear(); attributes.clear(); materials.clear(); name.clear(); hasNormals = false; hasTexcoords = false; bounds.Center.x = bounds.Center.y = bounds.Center.z = 0.f; bounds.Extents.x = bounds.Extents.y = bounds.Extents.z = 0.f; } HRESULT LoadVBO(_In_z_ const wchar_t* szFileName) { Clear(); if (!szFileName) return E_INVALIDARG; using namespace DirectX; #ifdef _WIN32 wchar_t fname[_MAX_FNAME] = {}; _wsplitpath_s(szFileName, nullptr, 0, nullptr, 0, fname, _MAX_FNAME, nullptr, 0); name = fname; #else auto path = std::filesystem::path(szFileName); name = path.filename().c_str(); #endif Material defmat; wcscpy_s(defmat.strName, L"default"); materials.emplace_back(defmat); std::ifstream vboFile(szFileName, std::ifstream::in | std::ifstream::binary); if (!vboFile.is_open()) return /* HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) */ static_cast(0x80070002L); hasNormals = hasTexcoords = true; uint32_t numVertices = 0; uint32_t numIndices = 0; vboFile.read(reinterpret_cast(&numVertices), sizeof(uint32_t)); if (!numVertices) return E_FAIL; vboFile.read(reinterpret_cast(&numIndices), sizeof(uint32_t)); if (!numIndices) return E_FAIL; vertices.resize(numVertices); vboFile.read(reinterpret_cast(vertices.data()), sizeof(Vertex) * numVertices); #if (__cplusplus >= 201703L) if constexpr (sizeof(index_t) == 2) #else #pragma warning( suppress : 4127 ) if (sizeof(index_t) == 2) #endif { indices.resize(numIndices); vboFile.read(reinterpret_cast(indices.data()), sizeof(uint16_t) * numIndices); } else { std::vector tmp; tmp.resize(numIndices); vboFile.read(reinterpret_cast(tmp.data()), sizeof(uint16_t) * numIndices); indices.reserve(numIndices); for (const auto it : tmp) { indices.emplace_back(it); } } BoundingBox::CreateFromPoints(bounds, vertices.size(), reinterpret_cast(vertices.data()), sizeof(Vertex)); vboFile.close(); return S_OK; } struct Material { DirectX::XMFLOAT3 vAmbient; DirectX::XMFLOAT3 vDiffuse; DirectX::XMFLOAT3 vSpecular; DirectX::XMFLOAT3 vEmissive; uint32_t nShininess; float fAlpha; bool bSpecular; bool bEmissive; wchar_t strName[MAX_PATH]; wchar_t strTexture[MAX_PATH]; wchar_t strNormalTexture[MAX_PATH]; wchar_t strSpecularTexture[MAX_PATH]; wchar_t strEmissiveTexture[MAX_PATH]; wchar_t strRMATexture[MAX_PATH]; Material() noexcept : vAmbient(0.2f, 0.2f, 0.2f), vDiffuse(0.8f, 0.8f, 0.8f), vSpecular(1.0f, 1.0f, 1.0f), vEmissive(0.f, 0.f, 0.f), nShininess(0), fAlpha(1.f), bSpecular(false), bEmissive(false), strName{}, strTexture{}, strNormalTexture{}, strSpecularTexture{}, strEmissiveTexture{}, strRMATexture{} { } }; std::vector vertices; std::vector indices; std::vector attributes; std::vector materials; std::wstring name; bool hasNormals; bool hasTexcoords; DirectX::BoundingBox bounds; private: using VertexCache = std::unordered_multimap; uint32_t AddVertex(uint32_t hash, const Vertex* pVertex, VertexCache& cache) { auto f = cache.equal_range(hash); for (auto it = f.first; it != f.second; ++it) { auto& tv = vertices[it->second]; if (0 == memcmp(pVertex, &tv, sizeof(Vertex))) { return it->second; } } auto index = static_cast(vertices.size()); vertices.emplace_back(*pVertex); VertexCache::value_type entry(hash, index); cache.insert(entry); return index; } void LoadTexturePath(std::wifstream& InFile, _Out_writes_(maxChar) wchar_t* texture, size_t maxChar) { wchar_t buff[1024] = {}; InFile.getline(buff, 1024, L'\n'); InFile.putback(L'\n'); std::wstring path = buff; // Ignore any end-of-line comment size_t pos = path.find_first_of(L'#'); if (pos != std::wstring::npos) { path = path.substr(0, pos); } // Trim any trailing whitespace pos = path.find_last_not_of(L" \t"); if (pos != std::wstring::npos) { path = path.substr(0, pos + 1); } // Texture path should be last element in line pos = path.find_last_of(' '); if (pos != std::wstring::npos) { path = path.substr(pos + 1); } if (!path.empty()) { #ifdef _WIN32 wcscpy_s(texture, maxChar, path.c_str()); #else wcscpy(texture, path.c_str()); #endif } } }; }