Developer Documentation
OBJReader.cc
1/* ========================================================================= *
2 * *
3 * OpenMesh *
4 * Copyright (c) 2001-2025, RWTH-Aachen University *
5 * Department of Computer Graphics and Multimedia *
6 * All rights reserved. *
7 * www.openmesh.org *
8 * *
9 *---------------------------------------------------------------------------*
10 * This file is part of OpenMesh. *
11 *---------------------------------------------------------------------------*
12 * *
13 * Redistribution and use in source and binary forms, with or without *
14 * modification, are permitted provided that the following conditions *
15 * are met: *
16 * *
17 * 1. Redistributions of source code must retain the above copyright notice, *
18 * this list of conditions and the following disclaimer. *
19 * *
20 * 2. Redistributions in binary form must reproduce the above copyright *
21 * notice, this list of conditions and the following disclaimer in the *
22 * documentation and/or other materials provided with the distribution. *
23 * *
24 * 3. Neither the name of the copyright holder nor the names of its *
25 * contributors may be used to endorse or promote products derived from *
26 * this software without specific prior written permission. *
27 * *
28 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS *
29 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED *
30 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A *
31 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER *
32 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, *
33 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, *
34 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR *
35 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF *
36 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING *
37 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS *
38 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *
39 * *
40 * ========================================================================= */
41
42
43
44
45//== INCLUDES =================================================================
46
47
48// OpenMesh
49#include <OpenMesh/Core/IO/reader/OBJReader.hh>
50#include <OpenMesh/Core/IO/IOManager.hh>
51#include <OpenMesh/Core/Utils/vector_cast.hh>
52#include <OpenMesh/Core/Utils/color_cast.hh>
53// STL
54#if defined(OM_CC_MIPS)
55# include <ctype.h>
57#elif defined(_STLPORT_VERSION) && (_STLPORT_VERSION==0x460)
58# include <cctype>
59#else
60using std::isspace;
61#endif
62
63#ifndef WIN32
64#endif
65
66#include <fstream>
67
68//=== NAMESPACES ==============================================================
69
70
71namespace OpenMesh {
72namespace IO {
73
74
75//=== INSTANCIATE =============================================================
76
77
78_OBJReader_ __OBJReaderInstance;
79_OBJReader_& OBJReader() { return __OBJReaderInstance; }
80
81
82//=== IMPLEMENTATION ==========================================================
83
84//-----------------------------------------------------------------------------
85
86void trimString( std::string& _string) {
87 // Trim Both leading and trailing spaces
88
89 size_t start = _string.find_first_not_of(" \t\r\n");
90 size_t end = _string.find_last_not_of(" \t\r\n");
91
92 if(( std::string::npos == start ) || ( std::string::npos == end))
93 _string = "";
94 else
95 _string = _string.substr( start, end-start+1 );
96}
97
98//-----------------------------------------------------------------------------
99
100// remove duplicated indices from one face
101void remove_duplicated_vertices(BaseImporter::VHandles& _indices)
102{
103 BaseImporter::VHandles::iterator endIter = _indices.end();
104 for (BaseImporter::VHandles::iterator iter = _indices.begin(); iter != endIter; ++iter)
105 endIter = std::remove(iter+1, endIter, *(iter));
106
107 _indices.erase(endIter,_indices.end());
108}
109
110//-----------------------------------------------------------------------------
111
112_OBJReader_::
113_OBJReader_()
114{
116}
117
118
119//-----------------------------------------------------------------------------
120
121
122bool
124read(const std::string& _filename, BaseImporter& _bi, Options& _opt)
125{
126 std::fstream in( _filename.c_str(), std::ios_base::in );
127
128 if (!in.is_open() || !in.good())
129 {
130 omerr() << "[OBJReader] : cannot not open file "
131 << _filename
132 << std::endl;
133 return false;
134 }
135
136 {
137#if defined(WIN32)
138 std::string::size_type dot_pos = _filename.find_last_of("\\/");
139#else
140 std::string::size_type dot_pos = _filename.rfind("/");
141#endif
142 path_ = (dot_pos == std::string::npos)
143 ? "./"
144 : std::string(_filename.substr(0,dot_pos+1));
145 }
146
147 bool result = read(in, _bi, _opt);
148
149 in.close();
150 return result;
151}
152
153//-----------------------------------------------------------------------------
154
155bool
156_OBJReader_::
157read_material(std::fstream& _in)
158{
159 std::string line;
160 std::string keyWrd;
161 std::string textureName;
162
163 std::stringstream stream;
164
165 std::string key;
166 Material mat;
167 float f1,f2,f3;
168 bool indef = false;
169 int textureId = 1;
170
171
172 materials_.clear();
173 mat.cleanup();
174
175 while( _in && !_in.eof() )
176 {
177 std::getline(_in,line);
178 if ( _in.bad() ){
179 omerr() << " Warning! Could not read file properly!\n";
180 return false;
181 }
182
183 if ( line.empty() )
184 continue;
185
186 stream.str(line);
187 stream.clear();
188
189 stream >> keyWrd;
190
191 if (keyWrd == "newmtl") // begin new material definition
192 {
193 // If we are in a material definition (Already reading a material)
194 // And a material name has been set
195 // And the current material definition is valid
196 // Then Store the current material in our lookup table
197 if (indef && !key.empty() && mat.is_valid())
198 {
199 materials_[key] = mat;
200 mat.cleanup();
201 }
202
203 stream >> key;
204 indef = true;
205 }
206
207 else if (keyWrd == "Kd") // diffuse color
208 {
209 stream >> f1; stream >> f2; stream >> f3;
210
211 if( !stream.fail() )
212 mat.set_Kd(f1,f2,f3);
213 }
214
215 else if (keyWrd == "Ka") // ambient color
216 {
217 stream >> f1; stream >> f2; stream >> f3;
218
219 if( !stream.fail() )
220 mat.set_Ka(f1,f2,f3);
221 }
222
223 else if (keyWrd == "Ks") // specular color
224 {
225 stream >> f1; stream >> f2; stream >> f3;
226
227 if( !stream.fail() )
228 mat.set_Ks(f1,f2,f3);
229 }
230#if 0
231 else if (keyWrd == "illum") // diffuse/specular shading model
232 {
233 ; // just skip this
234 }
235
236 else if (keyWrd == "Ns") // Shininess [0..200]
237 {
238 ; // just skip this
239 }
240
241 else if (keyWrd == "map_") // map images
242 {
243 // map_Ks, specular map
244 // map_Ka, ambient map
245 // map_Bump, bump map
246 // map_d, opacity map
247 ; // just skip this
248 }
249#endif
250 else if (keyWrd == "map_Kd" ) {
251 // Get the rest of the line, removing leading or trailing spaces
252 // This will define the filename of the texture
253 std::getline(stream,textureName);
254 trimString(textureName);
255 if ( ! textureName.empty() )
256 mat.set_map_Kd( textureName, textureId++ );
257 }
258 else if (keyWrd == "Tr") // transparency value
259 {
260 stream >> f1;
261
262 if( !stream.fail() )
263 mat.set_Tr(f1);
264 }
265 else if (keyWrd == "d") // transparency value
266 {
267 stream >> f1;
268
269 if( !stream.fail() )
270 mat.set_Tr(f1);
271 }
272
273 if ( _in && indef && mat.is_valid() && !key.empty())
274 materials_[key] = mat;
275 }
276 return true;
277}
278//-----------------------------------------------------------------------------
279
280bool
281_OBJReader_::
282read_vertices(std::istream& _in, BaseImporter& _bi, Options& _opt,
283 std::vector<Vec3f> & normals,
284 std::vector<Vec3f> & colors,
285 std::vector<Vec3f> & texcoords3d,
286 std::vector<Vec2f> & texcoords,
287 std::vector<VertexHandle> & vertexHandles,
288 Options & fileOptions)
289{
290 double x, y, z, u, v, w;
291 double r, g, b;
292
293 std::string line;
294 std::string keyWrd;
295
296 std::stringstream stream;
297
298
299 // Options supplied by the user
300 const Options & userOptions = _opt;
301
302 while( _in && !_in.eof() )
303 {
304 std::getline(_in,line);
305 if ( _in.bad() ){
306 omerr() << " Warning! Could not read file properly!\n";
307 return false;
308 }
309
310 // Trim Both leading and trailing spaces
311 trimString(line);
312
313 // comment
314 if ( line.size() == 0 || line[0] == '#' || isspace(line[0]) ) {
315 continue;
316 }
317
318 stream.str(line);
319 stream.clear();
320
321 stream >> keyWrd;
322
323 // vertex
324 if (keyWrd == "v")
325 {
326 stream >> x; stream >> y; stream >> z;
327
328 if ( !stream.fail() )
329 {
330 vertexHandles.push_back(_bi.add_vertex(OpenMesh::Vec3f(x,y,z)));
331 stream >> r; stream >> g; stream >> b;
332
333 if ( !stream.fail() )
334 {
335 if ( userOptions.vertex_has_color() ) {
336 fileOptions += Options::VertexColor;
337 colors.push_back(OpenMesh::Vec3f(r,g,b));
338 }
339 }
340 }
341 }
342
343 // texture coord
344 else if (keyWrd == "vt")
345 {
346 stream >> u; stream >> v;
347
348 if ( !stream.fail() ){
349
350 if ( userOptions.vertex_has_texcoord() || userOptions.face_has_texcoord() ) {
351 texcoords.push_back(OpenMesh::Vec2f(u, v));
352
353 // Can be used for both!
354 fileOptions += Options::VertexTexCoord;
355 fileOptions += Options::FaceTexCoord;
356
357 // try to read the w component as it is optional
358 stream >> w;
359 if ( !stream.fail() )
360 texcoords3d.push_back(OpenMesh::Vec3f(u, v, w));
361
362 }
363
364 }else{
365 omerr() << "Only single 2D or 3D texture coordinate per vertex"
366 << "allowed!" << std::endl;
367 return false;
368 }
369 }
370
371 // color per vertex
372 else if (keyWrd == "vc")
373 {
374 stream >> r; stream >> g; stream >> b;
375
376 if ( !stream.fail() ){
377 if ( userOptions.vertex_has_color() ) {
378 colors.push_back(OpenMesh::Vec3f(r,g,b));
379 fileOptions += Options::VertexColor;
380 }
381 }
382 }
383
384 // normal
385 else if (keyWrd == "vn")
386 {
387 stream >> x; stream >> y; stream >> z;
388
389 if ( !stream.fail() ) {
390 if (userOptions.vertex_has_normal() ){
391 normals.push_back(OpenMesh::Vec3f(x,y,z));
392 fileOptions += Options::VertexNormal;
393 }
394 }
395 }
396 }
397
398 return true;
399}
400
401//-----------------------------------------------------------------------------
402
403bool
405read(std::istream& _in, BaseImporter& _bi, Options& _opt)
406{
407 std::string line;
408 std::string keyWrd;
409
410 std::vector<Vec3f> normals;
411 std::vector<Vec3f> colors;
412 std::vector<Vec3f> texcoords3d;
413 std::vector<Vec2f> texcoords;
414 std::vector<VertexHandle> vertexHandles;
415
416 BaseImporter::VHandles vhandles;
417 std::vector<Vec3f> face_texcoords3d;
418 std::vector<Vec2f> face_texcoords;
419
420 std::string matname;
421
422 std::stringstream stream, lineData, tmp;
423
424
425 // Options supplied by the user
426 Options userOptions = _opt;
427
428 // Options collected via file parsing
429 Options fileOptions;
430
431 // pass 1: read vertices
432 if ( !read_vertices(_in, _bi, _opt,
433 normals, colors, texcoords3d, texcoords,
434 vertexHandles, fileOptions) ){
435 return false;
436 }
437
438 // reset stream for second pass
439 _in.clear();
440 _in.seekg(0, std::ios::beg);
441
442 int nCurrentPositions = 0,
443 nCurrentTexcoords = 0,
444 nCurrentNormals = 0;
445
446 // pass 2: read faces
447 while( _in && !_in.eof() )
448 {
449 std::getline(_in,line);
450 if ( _in.bad() ){
451 omerr() << " Warning! Could not read file properly!\n";
452 return false;
453 }
454
455 // Trim Both leading and trailing spaces
456 trimString(line);
457
458 // comment
459 if ( line.size() == 0 || line[0] == '#' || isspace(line[0]) ) {
460 continue;
461 }
462
463 stream.str(line);
464 stream.clear();
465
466 stream >> keyWrd;
467
468 // material file
469 if (keyWrd == "mtllib")
470 {
471 std::string matFile;
472
473 // Get the rest of the line, removing leading or trailing spaces
474 // This will define the filename of the texture
475 std::getline(stream,matFile);
476 trimString(matFile);
477
478 matFile = path_ + matFile;
479
480 //omlog() << "Load material file " << matFile << std::endl;
481
482 std::fstream matStream( matFile.c_str(), std::ios_base::in );
483
484 if ( matStream ){
485
486 if ( !read_material( matStream ) )
487 omerr() << " Warning! Could not read file properly!\n";
488 matStream.close();
489
490 }else
491 omerr() << " Warning! Material file '" << matFile << "' not found!\n";
492
493 //omlog() << " " << materials_.size() << " materials loaded.\n";
494
495 for ( MaterialList::iterator material = materials_.begin(); material != materials_.end(); ++material )
496 {
497 // Save the texture information in a property
498 if ( (*material).second.has_map_Kd() )
499 _bi.add_texture_information( (*material).second.map_Kd_index() , (*material).second.map_Kd() );
500 }
501
502 }
503
504 // usemtl
505 else if (keyWrd == "usemtl")
506 {
507 stream >> matname;
508 if (materials_.find(matname)==materials_.end())
509 {
510 omerr() << "Warning! Material '" << matname
511 << "' not defined in material file.\n";
512 matname="";
513 }
514 }
515
516 // track current number of parsed vertex attributes,
517 // to allow for OBJs negative indices
518 else if (keyWrd == "v")
519 {
520 ++nCurrentPositions;
521 }
522 else if (keyWrd == "vt")
523 {
524 ++nCurrentTexcoords;
525 }
526 else if (keyWrd == "vn")
527 {
528 ++nCurrentNormals;
529 }
530
531 // faces
532 else if (keyWrd == "f")
533 {
534 int component(0), nV(0);
535 int value;
536
537 vhandles.clear();
538 face_texcoords.clear();
539 face_texcoords3d.clear();
540
541 // read full line after detecting a face
542 std::string faceLine;
543 std::getline(stream,faceLine);
544 lineData.str( faceLine );
545 lineData.clear();
546
547 FaceHandle fh;
548 BaseImporter::VHandles faceVertices;
549
550 // work on the line until nothing left to read
551 while ( !lineData.eof() )
552 {
553 // read one block from the line ( vertex/texCoord/normal )
554 std::string vertex;
555 lineData >> vertex;
556
557 do{
558
559 //get the component (vertex/texCoord/normal)
560 size_t found=vertex.find("/");
561
562 // parts are seperated by '/' So if no '/' found its the last component
563 if( found != std::string::npos ){
564
565 // read the index value
566 tmp.str( vertex.substr(0,found) );
567 tmp.clear();
568
569 // If we get an empty string this property is undefined in the file
570 if ( vertex.substr(0,found).empty() ) {
571 // Switch to next field
572 vertex = vertex.substr(found+1);
573
574 // Now we are at the next component
575 ++component;
576
577 // Skip further processing of this component
578 continue;
579 }
580
581 // Read current value
582 tmp >> value;
583
584 // remove the read part from the string
585 vertex = vertex.substr(found+1);
586
587 } else {
588
589 // last component of the vertex, read it.
590 tmp.str( vertex );
591 tmp.clear();
592 tmp >> value;
593
594 // Clear vertex after finished reading the line
595 vertex="";
596
597 // Nothing to read here ( garbage at end of line )
598 if ( tmp.fail() ) {
599 continue;
600 }
601 }
602
603 // store the component ( each component is referenced by the index here! )
604 switch (component)
605 {
606 case 0: // vertex
607 if ( value < 0 ) {
608 // Calculation of index :
609 // -1 is the last vertex in the list
610 // As obj counts from 1 and not zero add +1
611 value = nCurrentPositions + value + 1;
612 }
613 // Obj counts from 1 and not zero .. array counts from zero therefore -1
614 vhandles.push_back(VertexHandle(value-1));
615 faceVertices.push_back(VertexHandle(value-1));
616 if (fileOptions.vertex_has_color()) {
617 if ((unsigned int)(value - 1) < colors.size()) {
618 _bi.set_color(vhandles.back(), colors[value - 1]);
619 }
620 else {
621 omerr() << "Error setting vertex color" << std::endl;
622 }
623 }
624 break;
625
626 case 1: // texture coord
627 if ( value < 0 ) {
628 // Calculation of index :
629 // -1 is the last vertex in the list
630 // As obj counts from 1 and not zero add +1
631 value = nCurrentTexcoords + value + 1;
632 }
633 assert(!vhandles.empty());
634
635
636 if ( fileOptions.vertex_has_texcoord() && userOptions.vertex_has_texcoord() ) {
637
638 if (!texcoords.empty() && (unsigned int) (value - 1) < texcoords.size()) {
639 // Obj counts from 1 and not zero .. array counts from zero therefore -1
640 _bi.set_texcoord(vhandles.back(), texcoords[value - 1]);
641 if(!texcoords3d.empty() && (unsigned int) (value -1) < texcoords3d.size())
642 _bi.set_texcoord(vhandles.back(), texcoords3d[value - 1]);
643 } else {
644 omerr() << "Error setting Texture coordinates" << std::endl;
645 }
646
647 }
648
649 if (fileOptions.face_has_texcoord() && userOptions.face_has_texcoord() ) {
650
651 if (!texcoords.empty() && (unsigned int) (value - 1) < texcoords.size()) {
652 face_texcoords.push_back( texcoords[value-1] );
653 if(!texcoords3d.empty() && (unsigned int) (value -1) < texcoords3d.size())
654 face_texcoords3d.push_back( texcoords3d[value-1] );
655 } else {
656 omerr() << "Error setting Texture coordinates" << std::endl;
657 }
658 }
659
660
661 break;
662
663 case 2: // normal
664 if ( value < 0 ) {
665 // Calculation of index :
666 // -1 is the last vertex in the list
667 // As obj counts from 1 and not zero add +1
668 value = nCurrentNormals + value + 1;
669 }
670
671 // Obj counts from 1 and not zero .. array counts from zero therefore -1
672 if (fileOptions.vertex_has_normal() ) {
673 assert(!vhandles.empty());
674 if ((unsigned int)(value - 1) < normals.size()) {
675 _bi.set_normal(vhandles.back(), normals[value - 1]);
676 }
677 else {
678 omerr() << "Error setting vertex normal" << std::endl;
679 }
680 }
681 break;
682 }
683
684 // Prepare for reading next component
685 ++component;
686
687 // Read until line does not contain any other info
688 } while ( !vertex.empty() );
689
690 component = 0;
691 nV++;
692
693 }
694
695 // note that add_face can possibly triangulate the faces, which is why we have to
696 // store the current number of faces first
697 size_t n_faces = _bi.n_faces();
698 remove_duplicated_vertices(faceVertices);
699
700 //A minimum of three vertices are required.
701 if (faceVertices.size() > 2)
702 fh = _bi.add_face(faceVertices);
703
704 if (!vhandles.empty() && fh.is_valid() )
705 {
706 _bi.add_face_texcoords(fh, vhandles[0], face_texcoords);
707 _bi.add_face_texcoords(fh, vhandles[0], face_texcoords3d);
708 }
709
710 if ( !matname.empty() )
711 {
712 std::vector<FaceHandle> newfaces;
713
714 for( size_t i=0; i < _bi.n_faces()-n_faces; ++i )
715 newfaces.push_back(FaceHandle(int(n_faces+i)));
716
717 Material& mat = materials_[matname];
718
719 if ( mat.has_Kd() ) {
720 Vec3uc fc = color_cast<Vec3uc, Vec3f>(mat.Kd());
721
722 if ( userOptions.face_has_color()) {
723
724 for (std::vector<FaceHandle>::iterator it = newfaces.begin(); it != newfaces.end(); ++it)
725 _bi.set_color(*it, fc);
726
727 fileOptions += Options::FaceColor;
728 }
729 }
730
731 // Set the texture index in the face index property
732 if ( mat.has_map_Kd() ) {
733
734 if (userOptions.face_has_texcoord()) {
735
736 for (std::vector<FaceHandle>::iterator it = newfaces.begin(); it != newfaces.end(); ++it)
737 _bi.set_face_texindex(*it, mat.map_Kd_index());
738
739 fileOptions += Options::FaceTexCoord;
740
741 }
742
743 } else {
744
745 // If we don't have the info, set it to no texture
746 if (userOptions.face_has_texcoord()) {
747
748 for (std::vector<FaceHandle>::iterator it = newfaces.begin(); it != newfaces.end(); ++it)
749 _bi.set_face_texindex(*it, 0);
750
751 }
752 }
753
754 } else {
755 std::vector<FaceHandle> newfaces;
756
757 for( size_t i=0; i < _bi.n_faces()-n_faces; ++i )
758 newfaces.push_back(FaceHandle(int(n_faces+i)));
759
760 // Set the texture index to zero as we don't have any information
761 if ( userOptions.face_has_texcoord() )
762 for (std::vector<FaceHandle>::iterator it = newfaces.begin(); it != newfaces.end(); ++it)
763 _bi.set_face_texindex(*it, 0);
764 }
765
766 }
767
768 }
769
770 // If we do not have any faces,
771 // assume this is a point cloud and read the normals and colors directly
772 if (_bi.n_faces() == 0)
773 {
774 int i = 0;
775 // add normal per vertex
776
777 if (normals.size() == _bi.n_vertices()) {
778 if ( fileOptions.vertex_has_normal() && userOptions.vertex_has_normal() ) {
779 for (std::vector<VertexHandle>::iterator it = vertexHandles.begin(); it != vertexHandles.end(); ++it, i++)
780 _bi.set_normal(*it, normals[i]);
781 }
782 }
783
784 // add color per vertex
785 i = 0;
786 if (colors.size() >= _bi.n_vertices())
787 if (fileOptions.vertex_has_color() && userOptions.vertex_has_color()) {
788 for (std::vector<VertexHandle>::iterator it = vertexHandles.begin(); it != vertexHandles.end(); ++it, i++)
789 _bi.set_color(*it, colors[i]);
790 }
791
792 }
793
794 // Return, what we actually read
795 _opt = fileOptions;
796
797 return true;
798}
799
800
801//=============================================================================
802} // namespace IO
803} // namespace OpenMesh
804//=============================================================================
bool is_valid() const
The handle is valid iff the index is not negative.
Definition: Handles.hh:72
Set options for reader/writer modules.
Definition: Options.hh:92
void clear(void)
Clear all bits.
Definition: Options.hh:143
@ FaceColor
Has (r) / store (w) face colors.
Definition: Options.hh:110
@ FaceTexCoord
Has (r) / store (w) face texture coordinates.
Definition: Options.hh:111
@ VertexNormal
Has (r) / store (w) vertex normals.
Definition: Options.hh:105
@ VertexTexCoord
Has (r) / store (w) texture coordinates.
Definition: Options.hh:107
@ VertexColor
Has (r) / store (w) vertex colors.
Definition: Options.hh:106
bool register_module(BaseReader *_bl)
Definition: IOManager.hh:217
bool read(const std::string &_filename, BaseImporter &_bi, Options &_opt) override
Definition: OBJReader.cc:124
static constexpr size_t size()
returns dimension of the vector
Definition: Vector11T.hh:107
_IOManager_ & IOManager()
Definition: IOManager.cc:72
Handle for a face entity.
Definition: Handles.hh:142
Handle for a vertex entity.
Definition: Handles.hh:121