Esempio n. 1
0
void ObjMeshRender::renderGroupEdges(const char * groupName)
{
  //get the group
  string name(groupName);
  ObjMesh::Group group = mesh->getGroup(name);

  glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
  glEnable(GL_POLYGON_OFFSET_LINE);
  glPolygonOffset(1.0, 1.0);

  for( unsigned int iFace = 0; iFace < group.getNumFaces(); ++iFace )
  {
    ObjMesh::Face face = group.getFace(iFace);

    glBegin(GL_POLYGON);
    for( unsigned int iVertex = 0; iVertex < face.getNumVertices(); ++iVertex )
    {
      const ObjMesh::Vertex * vertexHandle = face.getVertexHandle(iVertex);
      Vec3d v = mesh->getPosition(*vertexHandle);
      glVertex3d(v[0], v[1], v[2]);
    }
    glEnd();
  }

  glDisable(GL_POLYGON_OFFSET_LINE);
}
ObjMesh * ObjMeshOrientable::GenerateOrientedMesh()
{
  ObjMesh * outputObjMesh = new ObjMesh(*objMesh);

  for(unsigned int i=0; i < outputObjMesh->getNumGroups(); i++) // over all groups
  {
    ObjMesh::Group * groupHandle = (ObjMesh::Group*) outputObjMesh->getGroupHandle(i);
    for (unsigned int j=0; j < groupHandle->getNumFaces(); j++) // over all faces
    {
      const ObjMesh::Face * face = groupHandle->getFaceHandle(j);
      if (face->getNumVertices() < 3)
      {
        printf("Warning: encountered a face with fewer than 3 vertices.\n");
        continue;
      }

      HalfEdge edge = edgeAtFace(i, j);

      // loop until edge is first edge
      unsigned int startPosition = face->getVertex(0).getPositionIndex();
      while (edgeStartVertex(edge).getPositionIndex() != startPosition)
        edge = edgeNext(edge);

      // reverse the face if not correctly oriented
      if (edgeEndVertex(edge).getPositionIndex() != face->getVertex(1).getPositionIndex())
        groupHandle->reverseFace(j);
    }
  }

  return outputObjMesh;
}
int main( int argc, char** argv )
{
  const int numFixedArguments = 3;
  if( argc < numFixedArguments )
  {
    std::cout << "Merges several obj meshes into one. This routine uses the \"objMesh\" class to load the meshes. It can also merge materials." << std::endl;
    std::cout << "Usage: " << argv[0] << " [list of obj files] [output file] [-m]" << std::endl;
    std::cout << "  -m : also output materials" << std::endl;
    std::cout << "  -d : (if -m) also remove duplicate materials" << std::endl;
    return 1;
  }

  bool outputMaterials = false; 
  bool removeDuplicatedMaterials = false; 
  char * objListFilename = argv[1];
  char * objMeshname = argv[2];

  opt_t opttable[] =
  {
    { (char*)"m", OPTBOOL, &outputMaterials },
    { (char*)"d", OPTBOOL, &removeDuplicatedMaterials },
    { NULL, 0, NULL }
  };
  
  argv += numFixedArguments-1;
  argc -= numFixedArguments-1;
  int optup = getopts(argc,argv,opttable);
  if (optup != argc)
  {
    printf("Error parsing options. Error at option %s.\n",argv[optup]);
    return 1;
  }

  ObjMesh outputObjMesh;

  char s[4096];
  FILE * fin;
  char fileMode[96] = "r";
  OpenFile_(objListFilename, &fin, fileMode);

  while(fgets(s,4096,fin) != 0)
  {
    if(s[strlen(s)-1] == '\n')
      s[strlen(s)-1] = '\0';

    printf("%s\n",s);

    ObjMesh * objMesh = new ObjMesh(s);

    // add vertices
    int numVerticesCurrent = outputObjMesh.getNumVertices();
    for(unsigned int i=0; i<objMesh->getNumVertices(); i++)
      outputObjMesh.addVertexPosition(objMesh->getPosition(i));

    // add normals
    int numNormalsCurrent = outputObjMesh.getNumNormals();
    for(unsigned int i=0; i<objMesh->getNumNormals(); i++)
      outputObjMesh.addVertexNormal(objMesh->getNormal(i));
   
    // add texture coordinates
    int numTextureCoordinatesCurrent = outputObjMesh.getNumTextureCoordinates();
    for(unsigned int i=0; i<objMesh->getNumTextureCoordinates(); i++)
      outputObjMesh.addTextureCoordinate(objMesh->getTextureCoordinate(i));

    // add materials
    int numMaterialsCurrent = outputObjMesh.getNumMaterials();
    for(unsigned int i=0; i<objMesh->getNumMaterials(); i++)
      outputObjMesh.addMaterial(objMesh->getMaterial(i));

    for(unsigned int i=0; i<objMesh->getNumGroups(); i++)
    {
      const ObjMesh::Group * group = objMesh->getGroupHandle(i);
      outputObjMesh.addGroup(group->getName());
      unsigned int newGroupID = outputObjMesh.getNumGroups() - 1;
      ObjMesh::Group * newGroup = (ObjMesh::Group*) outputObjMesh.getGroupHandle(newGroupID);
      newGroup->setMaterialIndex(numMaterialsCurrent + group->getMaterialIndex());

      // over all faces in the group of the current obj file
      for(unsigned int j=0; j<group->getNumFaces(); j++)
      {
        const ObjMesh::Face * face = group->getFaceHandle(j);
        for(unsigned int k=0; k<face->getNumVertices(); k++)
        {
          ObjMesh::Vertex * vertex = (ObjMesh::Vertex*) face->getVertexHandle(k);
          vertex->setPositionIndex(vertex->getPositionIndex() + numVerticesCurrent);
          if (vertex->hasNormalIndex())
            vertex->setNormalIndex(vertex->getNormalIndex() + numNormalsCurrent);
          if (vertex->hasTextureCoordinateIndex())
            vertex->setTextureCoordinateIndex(vertex->getTextureCoordinateIndex() + numTextureCoordinatesCurrent);
        }
        outputObjMesh.addFaceToGroup(*face,newGroupID);
      }
    }

    delete(objMesh);
  }
  
  fclose(fin);

  if (outputMaterials)
  {
    if (removeDuplicatedMaterials)
    {
      printf("Removing duplicated materials..."); fflush(NULL);
      int numNewMaterials = outputObjMesh.removeDuplicatedMaterials();
      printf(" retained %d materials.\n", numNewMaterials);
    }
    outputObjMesh.save(objMeshname, 1);
  }
  else
  {
    outputObjMesh.save(objMeshname);
  }

  return(0);
}
// returns the number of edges flipped
int ObjMeshOrientable::GenerateHalfEdgeDataStructure()
{
  std::cout << "Building the half edge data structure..." << std::endl;

  // Step 1: iterate over all faces
  // for each face, add all the edges onto the list of half-edges

  std::cout << "Step 1: Generating the list of half edges..." << std::endl;

  typedef std::vector<ObjMesh::Group> SGroup;

  int coutCounter = 0;

  for(unsigned int i = 0; i < objMesh->getNumGroups(); i++ )
  {
    const ObjMesh::Group * currentGroup = objMesh->getGroupHandle(i);

    std::cout << "  Processing obj group '" << currentGroup->getName() << std::endl;
    std::cout << "  Iterating through group faces..." << std::endl;

    for( unsigned int iFace = 0; iFace < currentGroup->getNumFaces(); ++iFace )
    {
      ObjMesh::Face face = currentGroup->getFace(iFace); // get face whose number is iFace

      if (coutCounter < 100)
      {
        std::cout << face.getNumVertices() ;
        coutCounter++;
      }
      if (coutCounter == 100)
      {
        cout << "...[and more]";
        coutCounter++;
      }

      unsigned int edgesSoFar = halfEdges_.size();

      for ( unsigned int iVertex = 0; iVertex < face.getNumVertices(); ++iVertex )
      {
        // create a half edge for each edge, store -1 for half-edge adjacent edge for now
        // index vertices starting from 0
        int nextEdge = edgesSoFar + ((iVertex + 1) % face.getNumVertices());
        HalfEdge halfEdge(edgesSoFar + iVertex, face.getVertex(iVertex).getPositionIndex(), face.getVertex((iVertex + 1) % face.getNumVertices()).getPositionIndex(),
		 	  iVertex, (iVertex + 1) % face.getNumVertices(), i, iFace, -1, nextEdge); 

        halfEdges_.push_back(halfEdge);
      }
    }
    std::cout << std::endl;
  }

/*  
  for (unsigned int i=0; i<halfEdges_.size(); i++)
  {
    cout << "Half edge "<< i << " :" << endl;
    cout << "  Opposite edge: " << halfEdges_[i].opposite() << endl; 
    cout << "  Next edge: " << halfEdges_[i].next() << endl; 
    cout << "  Group: " << halfEdges_[i].groupID() << endl; 
    cout << "  Face: " << halfEdges_[i].face() << endl; 
    cout << "  Start vertex: " << halfEdges_[i].startVertex() << endl; 
    cout << "  End vertex: " << halfEdges_[i].endVertex() << endl; 
    cout << "  Start vertex (local): " << halfEdges_[i].startV() << endl; 
    cout << "  End vertex (local): " << halfEdges_[i].endV() << endl; 
    cout << "  Is boundary: " << halfEdges_[i].isBoundary() << endl; 
  }
*/

  // Step 2: build correspondence among half-dges
  // for each half-edge, search for the opposite half-edge, if it exists

  std::cout << "Step 2: Building correspondence among half-edges..." << std::endl;
  std::cout << "Boundary edges: ";

  // insert all edges into a binary tree

  typedef std::multimap<std::pair< unsigned int, unsigned int > , unsigned int> BinaryTree;
  BinaryTree edges;

  for (unsigned int i=0; i < halfEdges_.size(); i++)
  {
    int vertex1 = halfEdges_[i].startVertex();
    int vertex2 = halfEdges_[i].endVertex();
   
    if (vertex1 == vertex2)
    {
      std::cout << "Error: encountered a degenerated edge with equal starting and ending vertex." << std::endl;
      std::cout << "  Group:" << halfEdges_[i].groupID() << "  Face #: " << halfEdges_[i].face() << "Vertex ID: " << vertex1 << std::endl;
      exit(1);
    }

    if (vertex1 > vertex2) // swap
    {
      int buffer = vertex1;
      vertex1 = vertex2;
      vertex2 = buffer;
    }

    std::pair<unsigned int, unsigned int> vertices(vertex1,vertex2);
    edges.insert(std::make_pair(vertices,i)); 
  }  
 
  // retrieve one by one and build correspondence
  for (unsigned int i=0; i < halfEdges_.size(); i++)
  {
    int vertex1 = halfEdges_[i].startVertex();
    int vertex2 = halfEdges_[i].endVertex();
   
    if (vertex1 > vertex2) // swap
    {
      int buffer = vertex1;
      vertex1 = vertex2;
      vertex2 = buffer;
    }

    std::pair<unsigned int, unsigned int> vertices(vertex1,vertex2);

    // search for the edge

    int hits = 0;
    int candidates = 0;
    BinaryTree::iterator pos;
    for (pos = edges.lower_bound(vertices); 
         pos != edges.upper_bound(vertices);
         ++pos)
    {
      candidates++;
      // check if we found ourselves
      if (pos->second != i) 
      { // not ourselves
        halfEdges_[i].setOpposite(pos->second);
        hits++;
      }
    }

    if (candidates >= 3) 
    {
      std::cout << "Error: encountered an edge that appears in more than two triangles. Geometry is non-manifold. Exiting." << std::endl;
      int faceNum = halfEdges_[i].face();
      std::cout << "  Group:" << halfEdges_[i].groupID() << std::endl << "  Face #: " << faceNum << std::endl;
      std::cout << "  Edge: " << vertex1 << " " << vertex2 << std::endl;
      std::cout << "  Vertices: " << objMesh->getPosition(vertex1) << " " << objMesh->getPosition(vertex2) << std::endl;
      exit(1);
    }

    if (hits == 0) // boundary edge
    {  
      //std::cout << "B"; 
      std::cout << "B(" << vertex1 << "," << vertex2 << ") "; 
      boundaryEdges_.push_back(i);
    }
  }  

  std::cout << " total: " << boundaryEdges_.size() << std::endl;
/*  
  for (unsigned int i=0; i<halfEdges_.size(); i++)
  {
    cout << "Half edge "<< i << " :" << endl;
    cout << "  Opposite edge: " << halfEdges_[i].opposite() << endl; 
    cout << "  Next edge: " << halfEdges_[i].next() << endl; 
    cout << "  Group: " << halfEdges_[i].groupID() << endl; 
    cout << "  Face: " << halfEdges_[i].face() << endl; 
    cout << "  Start vertex: " << halfEdges_[i].startVertex() << endl; 
    cout << "  End vertex: " << halfEdges_[i].endVertex() << endl; 
    cout << "  Start vertex (local): " << halfEdges_[i].startV() << endl; 
    cout << "  End vertex (local): " << halfEdges_[i].endV() << endl; 
    cout << "  Is boundary: " << halfEdges_[i].isBoundary() << endl; 
  }
*/
  // now, each half-edge knows its mirror edge, but orientations of faces might be inconsistent

  // orient all half-edges consistently
  std::cout << "Step 3: Attempting to orient the faces coherently..." << std::endl;

  // generate marks for all the edges
  std::vector<int> marks(halfEdges_.size(), 0);

  // initialize queue
  std::set<int> queue;

  connectedComponents = 0;
  int numOrientationFlips = 0;

  while(1) // breakable
  {
    // find the first un-marked edge and queue it
    unsigned int unmarkedEdge;
    for (unmarkedEdge = 0; unmarkedEdge < halfEdges_.size(); unmarkedEdge++)
    {
      if (marks[unmarkedEdge] == 0)
        break; // found an unmarked edge
    }

    if (unmarkedEdge == halfEdges_.size()) // no unmarked edge was found
    {
      break; // out of while; we are done
    }
    else
    {
      cout << "Found a new connected component. Seed half-edge is: " << unmarkedEdge << endl;
      connectedComponents++;
      queue.insert(unmarkedEdge);

      while(queue.size() > 0)
      {
        int edge = *(queue.begin());
        queue.erase(queue.begin());

        //std::cout << "Retrieved edge from queue: " << edge << " Queue size: " << queue.size() << std::endl;
        //cout << "The edge is boundary: " << halfEdges_[edge].isBoundary() << endl;

        //std::cout << "Marking all the edges on this face: ";
        // first, mark all the edges on this face as visited
        int loop = edge;
        do 
        {
          marks[loop] = 1;
          //std::cout << loop << " ";
          loop = halfEdges_[loop].next();
        }
        while (loop != edge);
        //std::cout << std::endl; 

        // check if edge is consistent with the opposite edge orientation
        // careful: edge might be on the boundary
        // find a non-boundary edge on the same face (if it exists)
        //std::cout << "Seeking for a non-boundary edge on this face...";
        loop = edge;
        int exitFlag = 0;

        while ((halfEdges_[loop].isBoundary()) && (exitFlag == 0))
        {
          //cout << loop << " ";
          loop = halfEdges_[loop].next();
          if (loop == edge) // all edges are boundary
            exitFlag = 1;
        }

        if (exitFlag == 1) // all edges are boundary; this is an isolated face
        {
          cout << "Encountered an isolated face." << endl;
          //cout << "none found." << endl;
          continue; // no need to queue anything or flip anything, this was an isolated face
          // also, this case can only happen during the first iteration of the while loop, which will also be the last one
        }

        edge = loop; // now, edge is a non-boundary halfedge

        //std::cout << "found non-boundary edge: " << edge << std::endl;
        //std::cout << "opposite edge is: " << halfEdges_[edge].opposite() << std::endl;

        bool orientationFlipNecessary = (marks[halfEdges_[edge].opposite()] == 1) && (halfEdges_[edge].startVertex() == (edgeOpposite(halfEdges_[edge])).startVertex());

        //std::cout << "Orientation flip necessary for this face: " << orientationFlipNecessary << std::endl;

        if (orientationFlipNecessary) 
        { 
          // flip all edges along this face
          //cout << "Orientation flip" << endl;
          numOrientationFlips++;
          loop = edge;
          int cache = 0;
          do
          {
            int nextO = halfEdges_[loop].next();
            halfEdges_[loop].setNext(cache);
            cache = loop;
            halfEdges_[loop].flipOrientation(); // flip orientation
            loop = nextO;
          }
          while (loop != edge);
          halfEdges_[loop].setNext(cache);

          int groupID = halfEdges_[loop].groupID();
          int faceID = halfEdges_[loop].face();

          ObjMesh::Group * currentGroup = (ObjMesh::Group*) objMesh->getGroupHandle(groupID);
          currentGroup->getFace(faceID).printVertices();
          currentGroup->reverseFace(faceID);
          currentGroup->getFace(faceID).printVertices();
        }

        // check if new orientation is consistent eveywhere along the face
        // if not, surface is not orientable
        // at the same time, queue the opposite edges if they are not marked already
        loop = edge;
        do
        {
          if (!halfEdges_[loop].isBoundary()) // skip boundary edges
          {
            // if opposite unmarked, queue the opposite edge 
            if (marks[halfEdges_[loop].opposite()] == 0)
            {
              queue.insert(halfEdges_[loop].opposite());
              //marks[halfEdges_[loop].opposite()] = 1; // JNB, 2008
              //std::cout << "visiting edge: " << loop << " pushing opposite: " << halfEdges_[loop].opposite() << std::endl;
            }
            else
            { 
              // opposite edge is marked as already visited
              // if orientation consistent, do nothing 
              // if orientation not consistent, surface is not orientable
        
              bool orientationConsistent = (halfEdges_[loop].startVertex() == (edgeOpposite(halfEdges_[loop])).endVertex()); 

              //std::cout << "visiting edge: " << loop << " opposite marked " << std::endl;

              if (!orientationConsistent)
              {
                std::cout << "Error: surface is non-orientable. Offending edge: [" << halfEdges_[loop].startVertex() << "," << halfEdges_[loop].endVertex() << "]" << std::endl;
                exit(1);
              }
            }
          }
          loop = halfEdges_[loop].next();
        }
        while (loop != edge);

      }
    }
  } // end of while  

  printf("Consistent orientation generated. Performed %d orientation flips.\n", numOrientationFlips);

  //PrintHalfEdges();

  // step 4: for every vertex, find a half-edge emanating out of it
  std::cout << "Step 4: For every vertex, caching a half-edge emanating out of it..." << std::endl;
  std::cout << "        For every face, caching a half-edge on it..." << std::endl;

  for (unsigned int i=0; i< objMesh->getNumVertices(); i++)
    edgesAtVertices_.push_back(-1); // value of -1 corresponds to no edge (i.e. isolated vertex)

  for (unsigned int i=0; i < halfEdges_.size(); i++)
  {
    //cout << i << " " << halfEdges_[i].startVertex() << " " << halfEdges_[i].endVertex() << endl;
    edgesAtVertices_[halfEdges_[i].endVertex()] = i;
  }

  // if vertex is on the boundary, rotate the edge until it is an incoming boundary edge
  // rotate edge until it is either on the boundary, or we come around to the same edge
  int numIsolatedVertices = 0;
  for (unsigned int i=0; i < objMesh->getNumVertices(); i++)
  {
    if (isIsolatedVertex(i))
    {
      numIsolatedVertices++;
      continue;
    }
    HalfEdge * loop = &edgeAtVertex(i);
    HalfEdge * start = loop; 
    do
    {
      if (loop->isBoundary())
      {
        // set the edge to the current edge
        edgesAtVertices_[i] = loop->position();
        break;
      }
      loop = &edgePrevious(edgeOpposite(*loop));
    }
    while (*loop != *start);
    // if we came around, no need to change edgeAtVertices[i]
  } 

  if (numIsolatedVertices > 0)
    printf("Warning: mesh has %d isolated vertices.\n", numIsolatedVertices);

  // build the cache for faces, first reset to -1
  for (unsigned int i=0; i < objMesh->getNumGroups(); i++)
  {
    const ObjMesh::Group * currentGroup = objMesh->getGroupHandle(i);
    std::vector<int> dataForThisGroup;
    dataForThisGroup.clear();
    for (unsigned int j=0; j < currentGroup->getNumFaces(); j++)
    { 
      dataForThisGroup.push_back(-1);
    }
    edgesAtFaces_.push_back(dataForThisGroup);
  }   
  for (unsigned int i=0; i < halfEdges_.size(); i++)
    edgesAtFaces_[halfEdges_[i].groupID()][halfEdges_[i].face()] = i;

  // sanity check: none of the face entries should be -1
  for (unsigned int i=0; i < objMesh->getNumGroups(); i++)
  {
    const ObjMesh::Group * currentGroup = objMesh->getGroupHandle(i);
    for (unsigned int j=0; j < currentGroup->getNumFaces(); j++)
      if (edgesAtFaces_[i][j] == -1)
        cout << "Warning: face on group " << i << "(" << currentGroup->getName() << "), position " << j << " has no edges." << endl;
  }

  determineIfSurfaceHasBoundary();

  // testing: previous edge capability
  /*
  cout << "Testing previous edge capability..." << endl;
  for (unsigned int i=0; i < halfEdges_.size(); i++)
  {
    cout << i << ": " << edgePrevious(halfEdges_[i]).position() << endl;
  }

  // testing: print out associated edges for every vertex
  for (unsigned int i=0; i < vertexPositions_.size(); i++)
  {
    cout << "Halfedge into vertex " << i << ": " << edgeAtVertex(i).position() << endl;
  }
 
  // testing: print out associated edges for every face
  for (unsigned int i=0; i < groups_.size(); i++)
    for (unsigned int j=0; j < groups_[i].getNumFaces(); j++)
    {
      cout << "Halfedge on face " << i << " " << j << ": " << edgeAtFace(i,j).position() << endl;
    }

  // testing: loop around every vertex
  for (unsigned int i=0; i < vertexPositions_.size(); i++)
  {
    cout << "Looping around vertex " << i << ":"; 
    int flag = 0;
    HalfEdge * start = &edgeAtVertex(i);
    HalfEdge * loop = start;
    do 
    {
      cout << loop->position() << " ";

      if (flag != 0) // boundary edge, exit the loop
      {
        cout << " B";
        break;
      }
      loop = &loopVertex(*loop,flag);
    }
    while (loop->position() != start->position());

    cout << endl;
  }
  */

  std::cout << "Half-edge datastructure constructed successfully." << std::endl;

  std::cout << "Statistics: " << std::endl;
  std::cout << "  Half-edges: " << halfEdges_.size() << std::endl;
  std::cout << "  Boundary half-edges: " << boundaryEdges_.size() << std::endl;
  std::cout << "  Connected components: " << connectedComponents << std::endl;
  
  return numOrientationFlips;
} 
Esempio n. 5
0
int ObjMeshClose::Close(ObjMesh * objMesh)
{
  ObjMeshOrientable * objMeshOrientable = NULL;
  try
  {
    int * numOrientationFlips = NULL; 
    int verbose=0;
    objMeshOrientable = new ObjMeshOrientable(objMesh, 1, numOrientationFlips, verbose);
  }
  catch (int)
  {
    printf("Mesh is non-orientable.\n");
    return 1;
  }

  int numBoundaryEdges = (int)objMeshOrientable->numBoundaryEdges();
  set<int> boundaryEdges;
  for(int i=0; i<numBoundaryEdges; i++)
    boundaryEdges.insert(objMeshOrientable->boundaryEdge(i));

  while (boundaryEdges.size() > 0)
  {
    int firstEdge = *(boundaryEdges.begin());

    vector<int> boundaryVertices;

    // iterate around the boundary loop
    int edge = firstEdge;
    do
    {
      //printf("edge %d ", edge);
      // push vertices into "boundaryVertices", and erase edges from boundaryEdges
      ObjMeshOrientable::HalfEdge halfEdge = objMeshOrientable->halfEdge(edge);
      boundaryVertices.push_back(halfEdge.startVertex());
      boundaryEdges.erase(edge);

      // find the next boundary edge
      ObjMeshOrientable::HalfEdge nextHalfEdge = halfEdge;
      do
      {
        nextHalfEdge = objMeshOrientable->edgeNext(nextHalfEdge);
        if (nextHalfEdge.isBoundary())
          break;
        nextHalfEdge = objMeshOrientable->edgeOpposite(nextHalfEdge);
      }
      while (nextHalfEdge != halfEdge);

      if (nextHalfEdge == halfEdge)
      {
        printf("Mesh is non-orientable (cannot detect next edge along the hole).\n");
        return 2;
      }
      
      edge = nextHalfEdge.position();
    }
    while (edge != firstEdge);
    //printf("\n");

    // compute centroid
    Vec3d avg(0,0,0);
    for(unsigned int i=0; i<boundaryVertices.size(); i++)
      avg += objMesh->getPosition(boundaryVertices[i]);
    avg /= boundaryVertices.size();

    int numVertices = objMesh->getNumVertices();
    objMesh->addVertexPosition(avg);

    // find group containing edge "firstEdge"
    ObjMeshOrientable::HalfEdge firstHalfEdge = objMeshOrientable->halfEdge(firstEdge);
    unsigned int group = firstHalfEdge.groupID();
    ObjMesh::Group * groupHandle = (ObjMesh::Group*) objMesh->getGroupHandle(group);

    // add triangles to close the face (a "tent")
    //printf("Adding triangles:\n");
    for(unsigned int i=0; i<boundaryVertices.size(); i++)
    {
      int vtxIndex[3] = { numVertices, boundaryVertices[(i+1) % boundaryVertices.size()], boundaryVertices[i] };
      //printf("[%d %d %d] ", vtxIndex[0], vtxIndex[1], vtxIndex[2]);
      
      // compute flat normal
      Vec3d normal = norm(cross(objMesh->getPosition(vtxIndex[1]) - objMesh->getPosition(vtxIndex[0]), objMesh->getPosition(vtxIndex[2]) - objMesh->getPosition(vtxIndex[0]) ));

      // add the normal
      int numNormals = objMesh->getNumNormals();
      objMesh->addVertexNormal(normal);

      // create and add new face
      ObjMesh::Vertex vertex0(vtxIndex[0]);
      vertex0.setNormalIndex(numNormals);
      ObjMesh::Vertex vertex1(vtxIndex[1]);
      vertex1.setNormalIndex(numNormals);
      ObjMesh::Vertex vertex2(vtxIndex[2]);
      vertex2.setNormalIndex(numNormals);
      ObjMesh::Face face(vertex0, vertex1, vertex2);
      groupHandle->addFace(face);
    }
    //printf("\n");
  }

  return 0;
}