• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Razakhel / RaZ / 18062694977

27 Sep 2025 04:20PM UTC coverage: 74.093% (+0.04%) from 74.05%
18062694977

push

github

Razakhel
[Utils/Logger] Added formatted logging overloads

- Formatted calls to logging functions and made use of std::format() in several other places

100 of 170 new or added lines in 36 files covered. (58.82%)

4 existing lines in 2 files now uncovered.

8334 of 11248 relevant lines covered (74.09%)

1757.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.06
/src/RaZ/Data/ObjLoad.cpp
1
#include "RaZ/Data/Color.hpp"
2
#include "RaZ/Data/Image.hpp"
3
#include "RaZ/Data/ImageFormat.hpp"
4
#include "RaZ/Data/Mesh.hpp"
5
#include "RaZ/Data/ObjFormat.hpp"
6
#include "RaZ/Render/MeshRenderer.hpp"
7
#include "RaZ/Utils/FilePath.hpp"
8
#include "RaZ/Utils/FileUtils.hpp"
9
#include "RaZ/Utils/Logger.hpp"
10

11
#include "tracy/Tracy.hpp"
12

13
#include <fstream>
14
#include <map>
15
#include <sstream>
16

17
namespace Raz::ObjFormat {
18

19
namespace {
20

21
inline Texture2DPtr loadTexture(const FilePath& textureFilePath, const Color& defaultColor, bool shouldUseSrgb = false) {
36✔
22
  ZoneScopedN("[ObjLoad]::loadTexture");
23
  ZoneTextF("Path: %s", textureFilePath.toUtf8().c_str());
24

25
  if (!FileUtils::isReadable(textureFilePath)) {
36✔
NEW
26
    Logger::warn("[ObjLoad] Cannot load texture '{}'; either the file does not exist or it cannot be opened", textureFilePath);
×
27
    return Texture2D::create(defaultColor);
×
28
  }
29

30
  // Always apply a vertical flip to imported textures, since OpenGL maps them upside down
31
  return Texture2D::create(ImageFormat::load(textureFilePath, true), true, shouldUseSrgb);
36✔
32
}
33

34
inline void loadMtl(const FilePath& mtlFilePath,
5✔
35
                    std::vector<Material>& materials,
36
                    std::unordered_map<std::string, std::size_t>& materialCorrespIndices) {
37
  ZoneScopedN("[ObjLoad]::loadMtl");
38
  ZoneTextF("Path: %s", mtlFilePath.toUtf8().c_str());
39

40
  Logger::debug("[ObjLoad] Loading MTL file ('{}')...", mtlFilePath);
5✔
41

42
  std::ifstream file(mtlFilePath, std::ios_base::binary);
5✔
43

44
  if (!file) {
5✔
NEW
45
    Logger::error("[ObjLoad] Could not open the MTL file '{}'", mtlFilePath);
×
46
    materials.emplace_back(MaterialType::COOK_TORRANCE);
×
47
    return;
×
48
  }
49

50
  Material material;
5✔
51
  MaterialType materialType = MaterialType::BLINN_PHONG;
5✔
52

53
  while (!file.eof()) {
88✔
54
    std::string tag;
83✔
55
    std::string nextValue;
83✔
56
    file >> tag >> nextValue;
83✔
57

58
    if (tag[0] == 'K') {                 // Standard properties [K*]
83✔
59
      std::string secondValue;
17✔
60
      std::string thirdValue;
17✔
61
      file >> secondValue >> thirdValue;
17✔
62

63
      const Vec3f values(std::stof(nextValue), std::stof(secondValue), std::stof(thirdValue));
17✔
64

65
      if (tag[1] == 'd')                 // Diffuse/albedo factor [Kd]
17✔
66
        material.getProgram().setAttribute(values, MaterialAttribute::BaseColor);
18✔
67
      else if (tag[1] == 'e')            // Emissive factor [Ke]
11✔
68
        material.getProgram().setAttribute(values, MaterialAttribute::Emissive);
15✔
69
      else if (tag[1] == 'a')            // Ambient factor [Ka]
6✔
70
        material.getProgram().setAttribute(values, MaterialAttribute::Ambient);
9✔
71
      else if (tag[1] == 's')            // Specular factor [Ks]
3✔
72
        material.getProgram().setAttribute(values, MaterialAttribute::Specular);
9✔
73
    } else if (tag[0] == 'P') {          // PBR properties [P*]
83✔
74
      const float factor = std::stof(nextValue);
8✔
75

76
      if (tag[1] == 'm') {               // Metallic factor [Pm]
8✔
77
        material.getProgram().setAttribute(factor, MaterialAttribute::Metallic);
9✔
78
      } else if (tag[1] == 'r') {        // Roughness factor [Pr]
5✔
79
        material.getProgram().setAttribute(factor, MaterialAttribute::Roughness);
9✔
80
      } else if (tag[1] == 's') {        // Sheen factors [Ps]
2✔
81
        std::string secondValue;
2✔
82
        std::string thirdValue;
2✔
83
        std::string fourthValue;
2✔
84
        file >> secondValue >> thirdValue >> fourthValue;
2✔
85
        material.getProgram().setAttribute(Vec4f(factor, std::stof(secondValue), std::stof(thirdValue), std::stof(fourthValue)), MaterialAttribute::Sheen);
4✔
86
      }
2✔
87

88
      materialType = MaterialType::COOK_TORRANCE;
8✔
89
    } else if (tag[0] == 'm') {          // Import texture [map_*]
58✔
90
      const FilePath textureFilePath = mtlFilePath.recoverPathToFile() + nextValue;
33✔
91

92
      if (tag[4] == 'K') {               // Standard maps [map_K*]
33✔
93
        if (tag[5] == 'd')               // Diffuse/albedo map [map_Kd]
19✔
94
          material.getProgram().setTexture(loadTexture(textureFilePath, ColorPreset::White, true), MaterialTexture::BaseColor);
18✔
95
        else if (tag[5] == 'e')          // Emissive map [map_Ke]
13✔
96
          material.getProgram().setTexture(loadTexture(textureFilePath, ColorPreset::White, true), MaterialTexture::Emissive);
15✔
97
        else if (tag[5] == 'a')          // Ambient/ambient occlusion map [map_Ka]
8✔
98
          material.getProgram().setTexture(loadTexture(textureFilePath, ColorPreset::White, true), MaterialTexture::Ambient);
15✔
99
        else if (tag[5] == 's')          // Specular map [map_Ks]
3✔
100
          material.getProgram().setTexture(loadTexture(textureFilePath, ColorPreset::White, true), MaterialTexture::Specular);
9✔
101
      } else if (tag[4] == 'P') {        // PBR maps [map_P*]
14✔
102
        if (tag[5] == 'm')               // Metallic map [map_Pm]
8✔
103
          material.getProgram().setTexture(loadTexture(textureFilePath, ColorPreset::Red), MaterialTexture::Metallic);
9✔
104
        else if (tag[5] == 'r')          // Roughness map [map_Pr]
5✔
105
          material.getProgram().setTexture(loadTexture(textureFilePath, ColorPreset::Red), MaterialTexture::Roughness);
9✔
106
        else if (tag[5] == 's')          // Sheen map [map_Ps]
2✔
107
          material.getProgram().setTexture(loadTexture(textureFilePath, ColorPreset::White, true), MaterialTexture::Sheen); // TODO: should be an RGBA texture with an alpha of 1
6✔
108

109
        materialType = MaterialType::COOK_TORRANCE;
8✔
110
      } else if (tag[4] == 'd') {        // Opacity (dissolve) map [map_d]
6✔
111
        Texture2DPtr map = loadTexture(textureFilePath, ColorPreset::White);
3✔
112
        map->setFilter(TextureFilter::NEAREST, TextureFilter::NEAREST, TextureFilter::NEAREST);
3✔
113
        material.getProgram().setTexture(std::move(map), MaterialTexture::Opacity);
6✔
114
      } else if (tag[4] == 'b') {        // Bump map [map_bump]
6✔
115
        material.getProgram().setTexture(loadTexture(textureFilePath, ColorPreset::White), MaterialTexture::Bump);
9✔
116
      }
117
    } else if (tag[0] == 'd') {          // Opacity (dissolve) factor [d]
58✔
118
      material.getProgram().setAttribute(std::stof(nextValue), MaterialAttribute::Opacity);
9✔
119
    } else if (tag[0] == 'T') {
22✔
120
      if (tag[1] == 'r')                 // Transparency factor (alias, 1 - d) [Tr]
×
121
        material.getProgram().setAttribute(1.f - std::stof(nextValue), MaterialAttribute::Opacity);
×
122
    } else if (tag[0] == 'b') {          // Bump map (alias) [bump]
22✔
123
      material.getProgram().setTexture(loadTexture(mtlFilePath.recoverPathToFile() + nextValue, ColorPreset::White), MaterialTexture::Bump);
×
124
    } else if (tag[0] == 'n') {
22✔
125
      if (tag[1] == 'o') {               // Normal map [norm]
9✔
126
        material.getProgram().setTexture(loadTexture(mtlFilePath.recoverPathToFile() + nextValue, ColorPreset::MediumBlue), MaterialTexture::Normal);
9✔
127
      } else if (tag[1] == 'e') {        // New material [newmtl]
6✔
128
        materialCorrespIndices.emplace(nextValue, materialCorrespIndices.size());
6✔
129

130
        if (material.isEmpty())
6✔
131
          continue;
5✔
132

133
        material.loadType(materialType);
1✔
134
        materials.emplace_back(std::move(material));
1✔
135

136
        material     = Material();
1✔
137
        materialType = MaterialType::BLINN_PHONG;
1✔
138
      }
139
    } else {
140
      std::getline(file, tag); // Skip the rest of the line
13✔
141
    }
142
  }
88✔
143

144
  material.loadType(materialType);
5✔
145
  materials.emplace_back(std::move(material));
5✔
146

147
  Logger::debug("[ObjLoad] Loaded MTL file ({} material(s) loaded)", materials.size());
5✔
148
}
5✔
149

150
} // namespace
151

152
std::pair<Mesh, MeshRenderer> load(const FilePath& filePath) {
8✔
153
  ZoneScopedN("ObjFormat::load");
154
  ZoneTextF("Path: %s", filePath.toUtf8().c_str());
155

156
  Logger::debug("[ObjLoad] Loading OBJ file ('{}')...", filePath);
8✔
157

158
  std::ifstream file(filePath, std::ios_base::binary);
8✔
159

160
  if (!file)
8✔
NEW
161
    throw std::invalid_argument(std::format("Error: Could not open the OBJ file '{}'", filePath));
×
162

163
  Mesh mesh;
8✔
164
  MeshRenderer meshRenderer;
8✔
165

166
  mesh.addSubmesh();
8✔
167
  meshRenderer.addSubmeshRenderer();
8✔
168

169
  std::unordered_map<std::string, std::size_t> materialCorrespIndices;
8✔
170

171
  std::vector<Vec3f> positions;
8✔
172
  std::vector<Vec2f> texcoords;
8✔
173
  std::vector<Vec3f> normals;
8✔
174

175
  std::vector<std::vector<int64_t>> posIndices(1);
16✔
176
  std::vector<std::vector<int64_t>> texcoordsIndices(1);
16✔
177
  std::vector<std::vector<int64_t>> normalsIndices(1);
8✔
178

179
  while (!file.eof()) {
3,766✔
180
    std::string line;
3,758✔
181
    file >> line;
3,758✔
182

183
    if (line[0] == 'v') {
3,758✔
184
      if (line[1] == 'n') { // Normal
2,508✔
185
        Vec3f normalTriplet;
794✔
186

187
        file >> normalTriplet.x()
794✔
188
             >> normalTriplet.y()
794✔
189
             >> normalTriplet.z();
794✔
190

191
        normals.push_back(normalTriplet);
794✔
192
      } else if (line[1] == 't') { // Texcoords
1,714✔
193
        Vec2f texcoordsTriplet;
906✔
194

195
        file >> texcoordsTriplet.x()
906✔
196
             >> texcoordsTriplet.y();
906✔
197

198
        texcoords.push_back(texcoordsTriplet);
906✔
199
      } else { // Position
200
        Vec3f positionTriplet;
808✔
201

202
        file >> positionTriplet.x()
808✔
203
             >> positionTriplet.y()
808✔
204
             >> positionTriplet.z();
808✔
205

206
        positions.push_back(positionTriplet);
808✔
207
      }
208
    } else if (line[0] == 'f') { // Faces
1,250✔
209
      std::getline(file, line);
1,212✔
210

211
      constexpr char delim  = '/';
1,212✔
212
      const auto nbVertices = static_cast<uint16_t>(std::count(line.cbegin(), line.cend(), ' '));
1,212✔
213
      const auto nbParts    = static_cast<uint8_t>(std::count(line.cbegin(), line.cend(), delim) / nbVertices + 1);
1,212✔
214
      const bool quadFaces  = (nbVertices == 4);
1,212✔
215

216
      std::stringstream indicesStream(line);
1,212✔
217
      std::vector<int64_t> partIndices(nbParts * nbVertices);
1,212✔
218
      std::string vertex;
1,212✔
219

220
      for (std::size_t vertIndex = 0; vertIndex < nbVertices; ++vertIndex) {
5,208✔
221
        indicesStream >> vertex;
3,996✔
222

223
        std::stringstream vertParts(vertex);
3,996✔
224
        std::string part;
3,996✔
225
        uint8_t partIndex = 0;
3,996✔
226

227
        while (std::getline(vertParts, part, delim)) {
15,984✔
228
          if (!part.empty())
11,988✔
229
            partIndices[partIndex * nbParts + vertIndex + (partIndex * quadFaces)] = std::stol(part);
11,988✔
230

231
          ++partIndex;
11,988✔
232
        }
233
      }
3,996✔
234

235
      if (quadFaces) {
1,212✔
236
        posIndices.back().emplace_back(partIndices[0]);
360✔
237
        posIndices.back().emplace_back(partIndices[2]);
360✔
238
        posIndices.back().emplace_back(partIndices[3]);
360✔
239

240
        texcoordsIndices.back().emplace_back(partIndices[4]);
360✔
241
        texcoordsIndices.back().emplace_back(partIndices[6]);
360✔
242
        texcoordsIndices.back().emplace_back(partIndices[7]);
360✔
243

244
        normalsIndices.back().emplace_back(partIndices[8]);
360✔
245
        normalsIndices.back().emplace_back(partIndices[10]);
360✔
246
        normalsIndices.back().emplace_back(partIndices[11]);
360✔
247
      }
248

249
      posIndices.back().emplace_back(partIndices[0]);
1,212✔
250
      posIndices.back().emplace_back(partIndices[1]);
1,212✔
251
      posIndices.back().emplace_back(partIndices[2]);
1,212✔
252

253
      texcoordsIndices.back().emplace_back(partIndices[3 + quadFaces]);
1,212✔
254
      texcoordsIndices.back().emplace_back(partIndices[4 + quadFaces]);
1,212✔
255
      texcoordsIndices.back().emplace_back(partIndices[5 + quadFaces]);
1,212✔
256

257
      const auto quadStride = static_cast<uint8_t>(quadFaces * 2);
1,212✔
258

259
      normalsIndices.back().emplace_back(partIndices[6 + quadStride]);
1,212✔
260
      normalsIndices.back().emplace_back(partIndices[7 + quadStride]);
1,212✔
261
      normalsIndices.back().emplace_back(partIndices[8 + quadStride]);
1,212✔
262
    } else if (line[0] == 'm') { // Material import (mtllib)
1,250✔
263
      std::string mtlFileName;
5✔
264
      file >> mtlFileName;
5✔
265

266
      const std::string mtlFilePath = filePath.recoverPathToFile() + mtlFileName;
5✔
267
      loadMtl(mtlFilePath, meshRenderer.getMaterials(), materialCorrespIndices);
5✔
268
    } else if (line[0] == 'u') { // Material usage (usemtl)
38✔
269
      if (materialCorrespIndices.empty())
7✔
270
        continue;
1✔
271

272
      std::string materialName;
6✔
273
      file >> materialName;
6✔
274

275
      const auto correspMaterial = materialCorrespIndices.find(materialName);
6✔
276

277
      if (correspMaterial == materialCorrespIndices.cend())
6✔
NEW
278
        Logger::error("[ObjLoad] No corresponding material found with the name '{}'", materialName);
×
279
      else
280
        meshRenderer.getSubmeshRenderers().back().setMaterialIndex(correspMaterial->second);
6✔
281
    } else if (line[0] == 'o' || line[0] == 'g') {
32✔
282
      if (!posIndices.front().empty()) {
12✔
283
        const std::size_t newSize = posIndices.size() + 1;
2✔
284
        posIndices.resize(newSize);
2✔
285
        texcoordsIndices.resize(newSize);
2✔
286
        normalsIndices.resize(newSize);
2✔
287

288
        mesh.addSubmesh();
2✔
289
        meshRenderer.addSubmeshRenderer().setMaterialIndex(std::numeric_limits<std::size_t>::max());
2✔
290
      }
291

292
      std::getline(file, line);
12✔
293
    } else {
294
      std::getline(file, line); // Skip the rest of the line
14✔
295
    }
296
  }
3,758✔
297

298
  const auto posCount  = static_cast<int64_t>(positions.size());
8✔
299
  const auto texCount  = static_cast<int64_t>(texcoords.size());
8✔
300
  const auto normCount = static_cast<int64_t>(normals.size());
8✔
301

302
  std::map<std::array<std::size_t, 3>, unsigned int> indicesMap;
8✔
303

304
  for (std::size_t submeshIndex = 0; submeshIndex < mesh.getSubmeshes().size(); ++submeshIndex) {
18✔
305
    Submesh& submesh = mesh.getSubmeshes()[submeshIndex];
10✔
306
    indicesMap.clear();
10✔
307

308
    for (std::size_t partIndex = 0; partIndex < posIndices[submeshIndex].size(); ++partIndex) {
1,582✔
309
      // Face (vertices indices triplets), containing position/texcoords/normals
310
      // vertIndices[i][j] -> vertex i, feature j (j = 0 -> position, j = 1 -> texcoords, j = 2 -> normal)
311
      std::array<std::array<std::size_t, 3>, 3> vertIndices {};
1,572✔
312

313
      // First vertex information
314
      int64_t tempIndex = posIndices[submeshIndex][partIndex];
1,572✔
315
      vertIndices[0][0] = (tempIndex < 0 ? static_cast<std::size_t>(tempIndex + posCount) : static_cast<std::size_t>(tempIndex - 1));
1,572✔
316

317
      tempIndex = texcoordsIndices[submeshIndex][partIndex];
1,572✔
318
      vertIndices[0][1] = (tempIndex < 0 ? static_cast<std::size_t>(tempIndex + texCount) : static_cast<std::size_t>(tempIndex - 1));
1,572✔
319

320
      tempIndex = normalsIndices[submeshIndex][partIndex];
1,572✔
321
      vertIndices[0][2] = (tempIndex < 0 ? static_cast<std::size_t>(tempIndex + normCount) : static_cast<std::size_t>(tempIndex - 1));
1,572✔
322

323
      ++partIndex;
1,572✔
324

325
      // Second vertex information
326
      tempIndex = posIndices[submeshIndex][partIndex];
1,572✔
327
      vertIndices[1][0] = (tempIndex < 0 ? static_cast<std::size_t>(tempIndex + posCount) : static_cast<std::size_t>(tempIndex - 1));
1,572✔
328

329
      tempIndex = texcoordsIndices[submeshIndex][partIndex];
1,572✔
330
      vertIndices[1][1] = (tempIndex < 0 ? static_cast<std::size_t>(tempIndex + texCount) : static_cast<std::size_t>(tempIndex - 1));
1,572✔
331

332
      tempIndex = normalsIndices[submeshIndex][partIndex];
1,572✔
333
      vertIndices[1][2] = (tempIndex < 0 ? static_cast<std::size_t>(tempIndex + normCount) : static_cast<std::size_t>(tempIndex - 1));
1,572✔
334

335
      ++partIndex;
1,572✔
336

337
      // Third vertex information
338
      tempIndex = posIndices[submeshIndex][partIndex];
1,572✔
339
      vertIndices[2][0] = (tempIndex < 0 ? static_cast<std::size_t>(tempIndex + posCount) : static_cast<std::size_t>(tempIndex - 1));
1,572✔
340

341
      tempIndex = texcoordsIndices[submeshIndex][partIndex];
1,572✔
342
      vertIndices[2][1] = (tempIndex < 0 ? static_cast<std::size_t>(tempIndex + texCount) : static_cast<std::size_t>(tempIndex - 1));
1,572✔
343

344
      tempIndex = normalsIndices[submeshIndex][partIndex];
1,572✔
345
      vertIndices[2][2] = (tempIndex < 0 ? static_cast<std::size_t>(tempIndex + normCount) : static_cast<std::size_t>(tempIndex - 1));
1,572✔
346

347
      const std::array<Vec3f, 3> facePositions = { positions[vertIndices[0][0]],
1,572✔
348
                                                   positions[vertIndices[1][0]],
1,572✔
349
                                                   positions[vertIndices[2][0]] };
3,144✔
350

351
      std::array<Vec2f, 3> faceTexcoords {};
1,572✔
352
      if (!texcoords.empty()) {
1,572✔
353
        faceTexcoords[0] = texcoords[vertIndices[0][1]];
1,572✔
354
        faceTexcoords[1] = texcoords[vertIndices[1][1]];
1,572✔
355
        faceTexcoords[2] = texcoords[vertIndices[2][1]];
1,572✔
356
      }
357

358
      std::array<Vec3f, 3> faceNormals {};
1,572✔
359
      if (!normals.empty()) {
1,572✔
360
        faceNormals[0] = normals[vertIndices[0][2]];
1,572✔
361
        faceNormals[1] = normals[vertIndices[1][2]];
1,572✔
362
        faceNormals[2] = normals[vertIndices[2][2]];
1,572✔
363
      }
364

365
      for (uint8_t vertPartIndex = 0; vertPartIndex < 3; ++vertPartIndex) {
6,288✔
366
        const auto indexIter = indicesMap.find(vertIndices[vertPartIndex]);
4,716✔
367

368
        if (indexIter != indicesMap.cend()) {
4,716✔
369
          submesh.getTriangleIndices().emplace_back(indexIter->second);
3,730✔
370
          continue;
3,730✔
371
        }
372

373
        const Vertex vert {
374
          facePositions[vertPartIndex],
986✔
375
          faceTexcoords[vertPartIndex],
986✔
376
          faceNormals[vertPartIndex]
986✔
377
        };
2,958✔
378

379
        submesh.getTriangleIndices().emplace_back(static_cast<unsigned int>(indicesMap.size()));
986✔
380
        indicesMap.emplace(vertIndices[vertPartIndex], static_cast<unsigned int>(indicesMap.size()));
986✔
381
        submesh.getVertices().emplace_back(vert);
986✔
382
      }
383
    }
384
  }
385

386
  mesh.computeTangents();
8✔
387

388
  // Creating the mesh renderer from the mesh's data
389
  meshRenderer.load(mesh);
8✔
390

391
  Logger::debug("[ObjLoad] Loaded OBJ file ({} submesh(es), {} vertices, {} triangles, {} material(s))",
8✔
392
                mesh.getSubmeshes().size(), mesh.recoverVertexCount(), mesh.recoverTriangleCount(), meshRenderer.getMaterials().size());
8✔
393

394
  return { std::move(mesh), std::move(meshRenderer) };
16✔
395
}
8✔
396

397
} // namespace Raz::ObjFormat
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc