Visual Computing Library  devel
Loading...
Searching...
No Matches
load.h
1/*****************************************************************************
2 * VCLib *
3 * Visual Computing Library *
4 * *
5 * Copyright(C) 2021-2025 *
6 * Visual Computing Lab *
7 * ISTI - Italian National Research Council *
8 * *
9 * All rights reserved. *
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the Mozilla Public License Version 2.0 as published *
13 * by the Mozilla Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 * This program is distributed in the hope that it will be useful, *
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
19 * Mozilla Public License Version 2.0 *
20 * (https://www.mozilla.org/en-US/MPL/2.0/) for more details. *
21 ****************************************************************************/
22
23#ifndef VCL_IO_MESH_OBJ_LOAD_H
24#define VCL_IO_MESH_OBJ_LOAD_H
25
26#include "material.h"
27
28#include <vclib/io/file_info.h>
29#include <vclib/io/image/load.h>
30#include <vclib/io/mesh/settings.h>
31#include <vclib/io/read.h>
32
33#include <vclib/algorithms/mesh.h>
34#include <vclib/space/complex.h>
35#include <vclib/space/core.h>
36
37#include <algorithm>
38#include <map>
39
40namespace vcl {
41
42namespace detail {
43
44inline void loadObjMaterials(
45 std::map<std::string, ObjMaterial>& materialMap,
46 std::istream& stream)
47{
48 std::string matName;
49 ObjMaterial mat;
50
51 auto manageMaterialArgsFn = [&](auto& token) {
52 while ((*token)[0] == '-') {
53 if (*token == "-o" || *token == "-s" || *token == "-t") {
54 // ignore the argument and the three values
55 ++token;
56 ++token;
57 ++token;
58 ++token;
59 }
60 if (*token == "-mm") {
61 // ignore the argument and the two values
62 ++token;
63 ++token;
64 ++token;
65 }
66 if (*token == "-blendu" || *token == "-blendv" || *token == "-cc" ||
67 *token == "-clamp" || *token == "-texres") {
68 // ignore the argument and the value
69 ++token;
70 ++token;
71 }
72 }
73 };
74
75 do {
76 Tokenizer tokens = readAndTokenizeNextNonEmptyLineNoThrow(stream);
77 if (stream) {
78 // counter for texture images, used when mesh has no texture files
79 uint nt = 0;
80 Tokenizer::iterator token = tokens.begin();
81 std::string header = *token++;
82 if (header == "newmtl") {
83 if (!matName.empty())
84 materialMap[matName] = mat;
85 mat = ObjMaterial();
86 matName = *token;
87 mat.matName = matName;
88 }
89 if (header == "Ka") {
90 if (tokens.size() >= 4) {
91 if (*token != "spectral" && *token != "xyz") {
92 mat.Ka.x() = io::readFloat<float>(token);
93 mat.Ka.y() = io::readFloat<float>(token);
94 mat.Ka.z() = io::readFloat<float>(token);
95 }
96 }
97 }
98 if (header == "Kd") {
99 if (tokens.size() >= 4) {
100 if (*token != "spectral" && *token != "xyz") {
101 mat.Kd.x() = io::readFloat<float>(token);
102 mat.Kd.y() = io::readFloat<float>(token);
103 mat.Kd.z() = io::readFloat<float>(token);
104 }
105 }
106 }
107 if (header == "Ks") {
108 if (tokens.size() >= 4) {
109 if (*token != "spectral" && *token != "xyz") {
110 mat.Ks.x() = io::readFloat<float>(token);
111 mat.Ks.y() = io::readFloat<float>(token);
112 mat.Ks.z() = io::readFloat<float>(token);
113 }
114 }
115 }
116 if (header == "Ke") {
117 if (tokens.size() >= 4) {
118 if (*token != "spectral" && *token != "xyz") {
119 mat.Ke.x() = io::readFloat<float>(token);
120 mat.Ke.y() = io::readFloat<float>(token);
121 mat.Ke.z() = io::readFloat<float>(token);
122 }
123 }
124 }
125 if (header == "d") {
126 if ((*token)[0] == '-')
127 token++;
128 mat.d = io::readFloat<float>(token);
129 }
130 if (header == "Tr") {
131 if ((*token)[0] == '-')
132 token++;
133 mat.d = 1 - io::readFloat<float>(token);
134 }
135 if (header == "Ns") {
136 mat.Ns = io::readFloat<float>(token);
137 }
138 if (header == "illum") {
139 mat.illum = io::readFloat<int>(token);
140 }
141 if (header == "map_Kd") {
142 // need to manage args
143 manageMaterialArgsFn(token);
144 mat.map_Kd = *token;
145 // replace backslashes with slashes - windows compatibility
146 std::ranges::replace(mat.map_Kd, '\\', '/');
147 }
148 if (header == "map_Ke") {
149 // need to manage args
150 manageMaterialArgsFn(token);
151 mat.map_Ke = *token;
152 // replace backslashes with slashes - windows compatibility
153 std::ranges::replace(mat.map_Ke, '\\', '/');
154 }
155 if (header == "map_bump" || header == "bump") {
156 // need to manage args
157 manageMaterialArgsFn(token);
158 mat.map_bump = *token;
159 // replace backslashes with slashes - windows compatibility
160 std::ranges::replace(mat.map_bump, '\\', '/');
161 }
162 }
163 } while (stream);
164 if (!matName.empty())
165 materialMap[matName] = mat;
166}
167
168template<MeshConcept MeshType>
169void loadObjMaterials(
170 std::map<std::string, ObjMaterial>& materialMap,
171 MeshType& mesh,
172 std::istream& stream,
173 MeshInfo& loadedInfo,
174 const LoadSettings& settings)
175{
176 loadObjMaterials(materialMap, stream);
177
178 for (auto& [matName, mat] : materialMap) {
179 if constexpr (HasMaterials<MeshType>) {
180 loadedInfo.setMaterials();
181 Material m = mat.toMaterial();
182 mat.matId = mesh.materialsNumber();
183 mesh.pushMaterial(m);
184 }
185 }
186}
187
188template<MeshConcept MeshType>
189void loadObjMaterials(
190 std::map<std::string, ObjMaterial>& materialMap,
191 MeshType& mesh,
192 const std::string& mtllib,
193 MeshInfo& loadedInfo,
194 const LoadSettings& settings)
195{
196 std::ifstream file = openInputFileStream(mtllib);
197 loadObjMaterials(materialMap, mesh, file, loadedInfo, settings);
198}
199
200template<MeshConcept MeshType>
201void readObjVertex(
202 MeshType& m,
203 Tokenizer::iterator& token,
204 MeshInfo& loadedInfo,
205 const Tokenizer& tokens,
206 const LoadSettings& settings)
207{
208 uint vid = m.addVertex();
209 for (uint i = 0; i < 3; ++i) {
210 m.vertex(vid).position()[i] = io::readDouble<double>(token);
211 }
212 if constexpr (HasPerVertexColor<MeshType>) {
213 if (vid == 0) {
214 // if the file stores the vertex color in the non-standard way
215 // (color values after the positions)
216 if (tokens.size() > 6) {
217 if (settings.enableOptionalComponents) {
218 enableIfPerVertexColorOptional(m);
219 loadedInfo.setPerVertexColor();
220 }
221 else {
222 if (isPerVertexColorAvailable(m))
223 loadedInfo.setPerVertexColor();
224 }
225 }
226 }
227 if (loadedInfo.hasPerVertexColor()) {
228 // the file has the nonstandard way to store vertex colors, after
229 // the positions...
230 if (tokens.size() > 6) {
231 m.vertex(vid).color().setRedF(io::readFloat<float>(token));
232 m.vertex(vid).color().setGreenF(io::readFloat<float>(token));
233 m.vertex(vid).color().setBlueF(io::readFloat<float>(token));
234 }
235 }
236 }
237}
238
239template<FaceMeshConcept MeshType>
240void readObjFace(
241 MeshType& m,
242 MeshInfo& loadedInfo,
243 const Tokenizer& tokens,
244 const std::vector<TexCoordd>& wedgeTexCoords,
245 const ObjMaterial& currentMaterial,
246 const LoadSettings& settings)
247{
248 using FaceType = MeshType::FaceType;
249
250 std::vector<uint> vids;
251 std::vector<uint> wids;
252
253 loadedInfo.updateMeshType(tokens.size() - 1);
254
255 // actual read - load vertex indices and texcoords indices, if present
256 Tokenizer::iterator token = tokens.begin();
257 ++token;
258 vids.resize(tokens.size() - 1);
259 wids.reserve(tokens.size() - 1);
260 for (uint i = 0; i < tokens.size() - 1; ++i) {
261 Tokenizer subt(*token, '/', false);
262 auto t = subt.begin();
263 vids[i] = io::readUInt<uint>(t) - 1;
264 if (subt.size() > 1) {
265 if (!t->empty()) {
266 wids.push_back(io::readUInt<uint>(t) - 1);
267 }
268 }
269 ++token;
270 }
271
272 // add the face
273 uint fid = m.addFace();
274 FaceType& f = m.face(fid);
275
276 // check if we need to split the face we read into triangles
277 bool splitFace = false;
278 // we have a polygonal mesh, no need to split
279 if constexpr (FaceType::VERTEX_NUMBER < 0) {
280 // need to resize to the right number of verts
281 f.resizeVertices(tokens.size() - 1);
282 }
283 else if (FaceType::VERTEX_NUMBER != tokens.size() - 1) {
284 // we have faces with static sizes (triangles), but we are loading faces
285 // with number of verts > 3. Need to split the face we are loading in n
286 // faces!
287 splitFace = true;
288 }
289
290 // create the face in the mesh, for now we manage only vertex indices
291 if (!splitFace) { // no need to split face case
292 for (uint i = 0; i < vids.size(); ++i) {
293 if (vids[i] >= m.vertexNumber()) {
294 throw MalformedFileException(
295 "Bad vertex index for face " + std::to_string(fid));
296 }
297 f.setVertex(i, vids[i]);
298 }
299 }
300 else { // split needed
301 addTriangleFacesFromPolygon(m, f, vids);
302 }
303
304 // material
305 if constexpr (HasPerFaceMaterialIndex<MeshType>) {
306 if (fid == 0 && currentMaterial.matId != UINT_NULL) {
307 if (settings.enableOptionalComponents) {
309 loadedInfo.setPerFaceMaterialIndex();
310 }
311 else {
313 loadedInfo.setPerFaceMaterialIndex();
314 }
315 }
316 }
317 if (loadedInfo.hasPerFaceMaterialIndex()) {
318 if (!splitFace) {
319 f.materialIndex() = currentMaterial.matId;
320 }
321 else {
322 for (uint ff = fid; ff < m.faceNumber(); ++ff) {
323 FaceType& f = m.face(ff);
324 f.materialIndex() = currentMaterial.matId;
325 }
326 }
327 }
328 }
329
330 // wedge texcoords
331 if constexpr (HasPerFaceWedgeTexCoords<MeshType>) {
332 // first, need to check if I can store wedge texcoords in the mesh
333 if (fid == 0) {
334 // if the current face has the right number of wedge texcoords, we
335 // assume that we can load wedge texcoords
336 if (wids.size() == vids.size()) {
337 if (settings.enableOptionalComponents) {
339 loadedInfo.setPerFaceWedgeTexCoords();
340 }
341 else {
343 loadedInfo.setPerFaceWedgeTexCoords();
344 }
345 }
346 }
347 }
348 if (loadedInfo.hasPerFaceWedgeTexCoords()) {
349 if (wids.size() == vids.size()) {
350 if (!splitFace) { // there wasn't a triangulation of the face
351 // it is safe to assign each wedge texcoord to its position
352 // in the face
353 for (uint i = 0; i < wids.size(); ++i) {
354 if (wids[i] >= wedgeTexCoords.size()) {
355 throw MalformedFileException(
356 "Bad texcoord index for face " +
357 std::to_string(fid));
358 }
359 f.wedgeTexCoord(i) =
360 wedgeTexCoords[wids[i]]
361 .cast<typename FaceType::WedgeTexCoordType::
362 ScalarType>();
363 }
364 }
365 else {
366 // take read texcoords and map them in the triangulated
367 // faces for each face of the triangulation of the polygon
368 for (uint ff = fid; ff < m.faceNumber(); ++ff) {
369 FaceType& f = m.face(ff);
370 // for each vertex of the face
371 for (uint i = 0; i < f.vertexNumber(); ++i) {
372 uint vid = m.index(f.vertex(i));
373 // find the position of the vertex in the vids array
374 auto it = std::find(vids.begin(), vids.end(), vid);
375 assert(it != vids.end());
376 uint pos = it - vids.begin();
377 // check that the texcoord id is valid
378 if (wids[pos] >= wedgeTexCoords.size()) {
379 throw MalformedFileException(
380 "Bad texcoord index for face " +
381 std::to_string(fid));
382 }
383 // set the wedge texcoord in the same position of
384 // the vertex
385 f.wedgeTexCoord(i) =
386 wedgeTexCoords[wids[pos]]
387 .cast<typename FaceType::WedgeTexCoordType::
388 ScalarType>();
389 }
390 }
391 }
392 }
393 }
394 }
395}
396
397template<EdgeMeshConcept MeshType>
398void readObjEdge(
399 MeshType& m,
400 MeshInfo& loadedInfo,
401 const Tokenizer& tokens,
402 const ObjMaterial& currentMaterial,
403 const LoadSettings& settings)
404{
405 using EdgeType = MeshType::EdgeType;
406
407 // add the edge
408 uint eid = m.addEdge();
409 EdgeType& e = m.edge(eid);
410
411 // actual read - load vertex indices
412 Tokenizer::iterator token = tokens.begin();
413 ++token;
414 uint vid1 = io::readUInt<uint>(token) - 1;
415 uint vid2 = io::readUInt<uint>(token) - 1;
416 e.setVertices(vid1, vid2);
417}
418
438template<MeshConcept MeshType, LoggerConcept LogType = NullLogger>
439void loadObj(
440 MeshType& m,
441 std::istream& inputObjStream,
442 const std::vector<std::istream*>& inputMtlStreams,
443 MeshInfo& loadedInfo,
444 const std::string& filename = "",
445 bool ignoreMtlLib = false,
446 const LoadSettings& settings = LoadSettings(),
447 LogType& log = nullLogger)
448{
449 loadedInfo.clear();
450
451 // save array of normals, that are stored later
452 std::vector<Point3d> normals;
453
454 // save array of texcoords, that are stored later (into wedges when loading
455 // faces or into vertices as a fallback)
456 std::vector<TexCoordd> texCoords;
457
458 // map of materials loaded
459 std::map<std::string, detail::ObjMaterial> materialMap;
460
461 // load materials from the material files, if any
462 for (auto* stream : inputMtlStreams) {
463 detail::loadObjMaterials(materialMap, m, *stream, loadedInfo, settings);
464 }
465
466 // the current material, set by 'usemtl'
467 detail::ObjMaterial currentMaterial;
468
469 if constexpr (HasMaterials<MeshType>) {
470 m.meshBasePath() = FileInfo::pathWithoutFileName(filename);
471 }
472
473 if constexpr (HasName<MeshType>) {
474 m.name() = FileInfo::fileNameWithoutExtension(filename);
475 }
476
477 inputObjStream.seekg(0, inputObjStream.end);
478 std::size_t fsize = inputObjStream.tellg();
479 inputObjStream.seekg(0, inputObjStream.beg);
480 log.startProgress("Loading OBJ file", fsize);
481
482 // cycle that reads line by line
483 do {
484 Tokenizer tokens =
485 readAndTokenizeNextNonEmptyLineNoThrow(inputObjStream);
486 if (inputObjStream) {
487 Tokenizer::iterator token = tokens.begin();
488 std::string header = *token++;
489 if (header == "mtllib" && !ignoreMtlLib) { // material file
490 // we load the material file if they are not ignored
491 std::string mtlfile =
492 FileInfo::pathWithoutFileName(filename) + *token;
493 try {
494 detail::loadObjMaterials(
495 materialMap, m, mtlfile, loadedInfo, settings);
496 }
497 catch (CannotOpenFileException) {
498 log.log(
499 "Cannot open material file " + mtlfile,
500 LogType::WARNING_LOG);
501 }
502 }
503 // use a new material - change currentMaterial
504 if (header == "usemtl") {
505 std::string matname = *token;
506 auto it = materialMap.find(matname);
507 if (it != materialMap.end()) {
508 currentMaterial = it->second;
509 }
510 else { // material not found - warning
511 log.log(
512 "Material " + matname + " not found.",
513 LogType::WARNING_LOG);
514 }
515 }
516 // read vertex (and for some non-standard obj files, also vertex
517 // color)
518 if (header == "v") {
519 loadedInfo.setVertices();
520 loadedInfo.setPerVertexPosition();
521 detail::readObjVertex(m, token, loadedInfo, tokens, settings);
522 }
523 // read normals and save them in the vector of normasl, we will
524 // store them in the mesh later
525 if (header == "vn") {
526 if constexpr (HasPerVertexNormal<MeshType>) {
527 Point3d n;
528 for (uint i = 0; i < 3; ++i) {
529 n[i] = io::readDouble<double>(token);
530 }
531 normals.push_back(n);
532 }
533 }
534 // read texcoords and save them in the vector of texcoords, we will
535 // store them in the mesh later
536 if (header == "vt") {
537 if constexpr (
538 HasPerVertexTexCoord<MeshType> ||
539 HasPerFaceWedgeTexCoords<MeshType>) {
540 // save the texcoord for later
541 TexCoordd tf;
542 for (uint i = 0; i < 2; ++i) {
543 tf[i] = io::readDouble<double>(token);
544 }
545 texCoords.push_back(tf);
546 }
547 }
548 // read faces and manage:
549 // - color
550 // - eventual texcoords
551 // - possibility to split polygonal face into several triangles
552 if (header == "f") {
553 loadedInfo.setFaces();
554 loadedInfo.setPerFaceVertexReferences();
555 if constexpr (HasFaces<MeshType>) {
556 detail::readObjFace(
557 m,
558 loadedInfo,
559 tokens,
560 texCoords,
561 currentMaterial,
562 settings);
563 }
564 }
565 // read edges and manage their color
566 if (header == "l") {
567 loadedInfo.setEdges();
568 loadedInfo.setPerEdgeVertexReferences();
569 if constexpr (HasEdges<MeshType>) {
570 detail::readObjEdge(
571 m, loadedInfo, tokens, currentMaterial, settings);
572 }
573 }
574 log.progress(inputObjStream.tellg());
575 }
576 } while (inputObjStream);
577
578 if constexpr (HasPerVertexNormal<MeshType>) {
579 using NormalType = typename MeshType::VertexType::NormalType;
580 using NST = typename NormalType::ScalarType;
581 if (settings.enableOptionalComponents) {
582 enableIfPerVertexNormalOptional(m);
583 loadedInfo.setPerVertexNormal();
584 }
585 else {
586 if (isPerVertexNormalAvailable(m))
587 loadedInfo.setPerVertexNormal();
588 }
589
590 if (loadedInfo.hasPerVertexNormal()) {
591 for (uint i = 0; const auto& n : normals) {
592 if (i < m.vertexNumber()) {
593 m.vertex(i).normal() = n.template cast<NST>();
594 }
595 ++i;
596 }
597 }
598 }
599
600 if constexpr (HasPerVertexTexCoord<MeshType>) {
601 if (!loadedInfo.hasPerFaceWedgeTexCoords()) {
602 if (texCoords.size() == m.vertexNumber()) {
603 // load texcoords into vertices only if there are no wedge
604 if (settings.enableOptionalComponents) {
605 enableIfPerVertexTexCoordOptional(m);
606 loadedInfo.setPerVertexTexCoord();
607 }
608 else {
609 if (isPerVertexTexCoordAvailable(m))
610 loadedInfo.setPerVertexTexCoord();
611 }
612 if (loadedInfo.hasPerVertexTexCoord()) {
613 using TexCoordType =
614 typename MeshType::VertexType::TexCoordType;
615 using TCT = typename TexCoordType::ScalarType;
616 for (uint i = 0; i < m.vertexNumber(); ++i) {
617 m.vertex(i).texCoord() =
618 texCoords[i].template cast<TCT>();
619 }
620 }
621
622 // material index
623 if constexpr (HasMaterials<MeshType>) {
624 if (m.materialsNumber() > 0) {
625 if (settings.enableOptionalComponents) {
626 enableIfPerVertexMaterialIndexOptional(m);
627 loadedInfo.setPerVertexMaterialIndex();
628 }
629 else {
630 if (isPerVertexMaterialIndexAvailable(m))
631 loadedInfo.setPerVertexMaterialIndex();
632 }
633 if (loadedInfo.hasPerVertexMaterialIndex()) {
634 for (uint i = 0; i < m.vertexNumber(); ++i) {
635 // assign the first material to all vertices
636 m.vertex(i).materialIndex() = 0;
637 }
638 }
639 }
640 }
641 }
642 }
643 }
644
645 if constexpr (HasMaterials<MeshType>) {
646 if (settings.loadTextureImages) {
647 using enum Material::TextureType;
648 loadTextureImages(m, "", {BASE_COLOR, EMISSIVE}, log);
649 }
650 }
651
652 log.endProgress();
653}
654
655} // namespace detail
656
685template<MeshConcept MeshType, LoggerConcept LogType = NullLogger>
687 MeshType& m,
688 std::istream& inputObjStream,
689 const std::vector<std::istream*>& inputMtlStreams,
691 const LoadSettings& settings = LoadSettings(),
692 LogType& log = nullLogger)
693{
694 detail::loadObj(
695 m,
699 "",
700 true,
701 settings,
702 log);
703}
704
732template<MeshConcept MeshType, LoggerConcept LogType = NullLogger>
734 MeshType& m,
735 const std::string& filename,
737 const LoadSettings& settings = LoadSettings(),
738 LogType& log = nullLogger)
739{
740 std::ifstream file = openInputFileStream(filename);
741
742 // some obj files do not declare the material file name with mtllib, but
743 // they assume that material file has the same name of the obj file.
744 // Therefore, we first load this file if it exists.
747 ".mtl";
748
749 std::ifstream f;
750 std::vector<std::istream*> mtlStreams;
751 try {
752 f = openInputFileStream(stdmtlfile);
753 mtlStreams.push_back(&f);
754 }
756 // nothing to do, this file was missing, but this was a fallback for
757 // some type of files...
758 }
759
760 detail::loadObj(
761 m, file, mtlStreams, loadedInfo, filename, false, settings, log);
762}
763
764} // namespace vcl
765
766#endif // VCL_IO_MESH_OBJ_LOAD_H
A class representing a box in N-dimensional space.
Definition box.h:46
PointT size() const
Computes the size of the box.
Definition box.h:267
Exception thrown when the file cannot be opened.
Definition exceptions.h:58
static std::string fileNameWithoutExtension(const std::string &fullpath)
Get the file name without extension of a file.
Definition file_info.h:220
static std::string pathWithoutFileName(const std::string &fullpath)
Get the path of a file.
Definition file_info.h:200
TextureType
Defines the types of textures used in the PBR material model.
Definition material.h:62
A simple class that allows to store which elements and their components have been imported/loaded or ...
Definition mesh_info.h:76
NullLogger nullLogger
The nullLogger object is an object of type NullLogger that is used as default argument in the functio...
Definition null_logger.h:123
constexpr uint UINT_NULL
The UINT_NULL value represent a null value of uint that is the maximum value that can be represented ...
Definition base.h:48
TexCoord< double, ElementType, OPT > TexCoordd
Definition tex_coord.h:209
bool enableIfPerFaceWedgeTexCoordsOptional(MeshType &m)
If the input mesh has a FaceContainer, and the Face Element has a WedgeTexCoords Component,...
Definition face_requirements.h:936
bool isPerFaceWedgeTexCoordsAvailable(const MeshType &m)
Returns true if the WedgeTexCoords component is available (enabled) in the Face element of the input ...
Definition face_requirements.h:910
bool isPerFaceMaterialIndexAvailable(const MeshType &m)
Returns true if the MaterialIndex component is available (enabled) in the Face element of the input m...
Definition face_requirements.h:608
bool enableIfPerFaceMaterialIndexOptional(MeshType &m)
If the input mesh has a FaceContainer, and the Face Element has a MaterialIndex Component,...
Definition face_requirements.h:633
void loadObj(MeshType &m, std::istream &inputObjStream, const std::vector< std::istream * > &inputMtlStreams, MeshInfo &loadedInfo, const LoadSettings &settings=LoadSettings(), LogType &log=nullLogger)
Loads from the given input obj stream and puts the content into the mesh m.
Definition load.h:686
Point3< double > Point3d
A convenience alias for a 3-dimensional Point with double-precision floating-point components.
Definition point.h:780
The LoadSettings structure contains the settings that can be used to load a mesh from a stream/file.
Definition settings.h:35