From 74f9362cf30a0a24f6acd1a17440973c1a900521 Mon Sep 17 00:00:00 2001 From: Joel Kiptoo Date: Mon, 5 Oct 2026 12:16:12 +0300 Subject: [PATCH 1/4] WaveFrontReader: read per-vertex colors from "v x y z r g b" Some tools, such as MeshLab, write a per-vertex color after the position on 'v' lines. The reader ignored everything after x y z, so these colors were dropped. When a 'v' line has exactly three extra values they are read as an RGB color. A single extra value is still treated as the optional 'w' coordinate and ignored. Positions without a color default to white. The colors go into a new vertexColors array, one entry per vertex, alongside a hasVertexColors flag. Vertex itself is unchanged, so the .vbo format and existing callers are unaffected. --- Utilities/WaveFrontReader.h | 53 ++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/Utilities/WaveFrontReader.h b/Utilities/WaveFrontReader.h index 6633c041..bbb4f71c 100644 --- a/Utilities/WaveFrontReader.h +++ b/Utilities/WaveFrontReader.h @@ -49,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -75,7 +76,8 @@ namespace DX WaveFrontReader() noexcept : hasNormals(false), - hasTexcoords(false) + hasTexcoords(false), + hasVertexColors(false) {} HRESULT Load(_In_z_ const wchar_t* szFileName, bool ccw = true, bool loadmtl = true) @@ -107,6 +109,7 @@ namespace DX std::vector positions; std::vector normals; std::vector texCoords; + std::vector positionColors; VertexCache vertexCache; @@ -152,6 +155,32 @@ namespace DX float x, y, z; InFile >> x >> y >> z; positions.emplace_back(XMFLOAT3(x, y, z)); + + // Some tools (e.g. MeshLab) write a per-vertex color after the position as + // "v x y z r g b". This is not part of the OBJ specification, so only exactly + // three extra values are treated as a color; a single extra value is the + // optional 'w' coordinate and is ignored. + XMFLOAT3 color(1.f, 1.f, 1.f); + + std::wstring rest; + std::getline(InFile, rest, L'\n'); + InFile.putback(L'\n'); + + std::wistringstream extra(rest); + extra.imbue(std::locale::classic()); + + float values[4] = {}; + size_t count = 0; + while (count < std::size(values) && extra >> values[count]) + ++count; + + if (count == 3) + { + color = XMFLOAT3(values[0], values[1], values[2]); + hasVertexColors = true; + } + + positionColors.emplace_back(color); } else if (0 == wcscmp(strCommand.c_str(), L"vt")) { @@ -284,6 +313,12 @@ namespace DX if (index == uint32_t(-1)) return E_OUTOFMEMORY; + if (index == vertexColors.size()) + { + // New vertex: its color comes from the position it was built from + vertexColors.emplace_back(positionColors[vertexIndex]); + } + constexpr uint32_t maxIndex = (sizeof(index_t) == 2) ? UINT16_MAX : UINT32_MAX; if (index >= maxIndex) { @@ -399,6 +434,11 @@ namespace DX if (positions.empty()) return E_FAIL; + if (!hasVertexColors) + { + vertexColors.clear(); + } + // Cleanup InFile.close(); @@ -617,9 +657,11 @@ namespace DX indices.clear(); attributes.clear(); materials.clear(); + vertexColors.clear(); name.clear(); - hasNormals = false; - hasTexcoords = false; + hasNormals = false; + hasTexcoords = false; + hasVertexColors = false; bounds.Center.x = bounds.Center.y = bounds.Center.z = 0.f; bounds.Extents.x = bounds.Extents.y = bounds.Extents.z = 0.f; @@ -739,9 +781,14 @@ namespace DX std::vector attributes; std::vector materials; + // Per-vertex colors from the non-standard "v x y z r g b" extension, one per entry in + // 'vertices'. Only filled in when hasVertexColors is true. + std::vector vertexColors; + std::wstring name; bool hasNormals; bool hasTexcoords; + bool hasVertexColors; DirectX::BoundingBox bounds; From 7cb75cbf002f1dfa13cd34ef9d0a9b642d1d1622 Mon Sep 17 00:00:00 2001 From: Joel Kiptoo Date: Fri, 9 Oct 2026 09:11:03 +0300 Subject: [PATCH 2/4] WaveFrontReader: apply clang-format --- Utilities/WaveFrontReader.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Utilities/WaveFrontReader.h b/Utilities/WaveFrontReader.h index bbb4f71c..d7794abe 100644 --- a/Utilities/WaveFrontReader.h +++ b/Utilities/WaveFrontReader.h @@ -169,14 +169,14 @@ namespace DX std::wistringstream extra(rest); extra.imbue(std::locale::classic()); - float values[4] = {}; - size_t count = 0; + float values[4] = {}; + size_t count = 0; while (count < std::size(values) && extra >> values[count]) ++count; if (count == 3) { - color = XMFLOAT3(values[0], values[1], values[2]); + color = XMFLOAT3(values[0], values[1], values[2]); hasVertexColors = true; } From a095220f6dea0285a45e22d7bc7258f5b94e64d4 Mon Sep 17 00:00:00 2001 From: Joel Kiptoo Date: Wed, 14 Oct 2026 09:11:25 +0300 Subject: [PATCH 3/4] meshconvert: load OBJ per-vertex colors --- Meshconvert/MeshOBJ.cpp | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/Meshconvert/MeshOBJ.cpp b/Meshconvert/MeshOBJ.cpp index be43f754..d4e82600 100644 --- a/Meshconvert/MeshOBJ.cpp +++ b/Meshconvert/MeshOBJ.cpp @@ -91,10 +91,33 @@ HRESULT LoadFromOBJ(const wchar_t* szFilename, std::unique_ptr& inMesh, st { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 }, }; + // Vertex colors are read from a second stream (slot 1) alongside the Vertex data + static const D3D11_INPUT_ELEMENT_DESC s_vboLayoutVC[] = { + { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + }; + + static const D3D11_INPUT_ELEMENT_DESC s_vboLayoutAltVC[] = { + { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + }; + const D3D11_INPUT_ELEMENT_DESC* layout = s_vboLayout; size_t nDecl = std::size(s_vboLayout); - if (!wfReader.hasNormals && !wfReader.hasTexcoords) + const bool hasColors = wfReader.hasVertexColors && (wfReader.vertexColors.size() == wfReader.vertices.size()); + + if (hasColors) + { + layout = (wfReader.hasNormals) ? s_vboLayoutVC : s_vboLayoutAltVC; + nDecl = (wfReader.hasNormals) ? std::size(s_vboLayoutVC) : std::size(s_vboLayoutAltVC); + if (!wfReader.hasTexcoords) + --nDecl; + } + else if (!wfReader.hasNormals && !wfReader.hasTexcoords) { nDecl = 1; } @@ -117,6 +140,21 @@ HRESULT LoadFromOBJ(const wchar_t* szFilename, std::unique_ptr& inMesh, st if (FAILED(hr)) return hr; + std::vector colors; + if (hasColors) + { + // OBJ vertex colors have no alpha, so use opaque + colors.reserve(wfReader.vertexColors.size()); + for (const auto& c : wfReader.vertexColors) + { + colors.emplace_back(c.x, c.y, c.z, 1.f); + } + + hr = vbr.AddStream(colors.data(), colors.size(), 1, sizeof(XMFLOAT4)); + if (FAILED(hr)) + return hr; + } + hr = inMesh->SetVertexData(vbr, wfReader.vertices.size()); if (FAILED(hr)) return hr; From 585b1e91b05a49d5f2fe45d652012fca54fde07a Mon Sep 17 00:00:00 2001 From: Joel Kiptoo Date: Wed, 14 Oct 2026 19:21:00 +0300 Subject: [PATCH 4/4] meshconvert: note that the OBJ layouts match WaveFrontReader::Vertex --- Meshconvert/MeshOBJ.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshconvert/MeshOBJ.cpp b/Meshconvert/MeshOBJ.cpp index d4e82600..c01cae98 100644 --- a/Meshconvert/MeshOBJ.cpp +++ b/Meshconvert/MeshOBJ.cpp @@ -80,6 +80,7 @@ HRESULT LoadFromOBJ(const wchar_t* szFilename, std::unique_ptr& inMesh, st if (FAILED(hr)) return hr; + // These layouts are only used to extract data, so the offsets match WaveFrontReader::Vertex static const D3D11_INPUT_ELEMENT_DESC s_vboLayout[] = { { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },