10
votes

I am trying to load in a .obj-File and draw it with the help of glDrawElements.

Now, with glDrawArrays everything works perfectly, but it is - of course - inefficient.

The problem I have right now is, that an .obj-file uses multiple index-buffers (for each attribute) while OpenGL may only use one. So I need to map them accordingly.

There are a lot of pseudo-algorithms out there and I even found a C++ implementation. I do know quite a bit of C++ but strangely neither helped me with my implementation in Scala.

Let's see:

private def parseObj(path: String): Model =
{
    val objSource: List[String] = Source.fromFile(path).getLines.toList

    val positions: List[Vector3] = objSource.filter(_.startsWith("v ")).map(_.split(" ")).map(v => new Vector3(v(1).toFloat,v(2).toFloat,v(3).toFloat))//, 1.0f))
    val normals: List[Vector4] = objSource.filter(_.startsWith("vn ")).map(_.split(" ")).map(v => new Vector4(v(1)toFloat,v(2).toFloat, v(3).toFloat, 0.0f))
    val textureCoordinates: List[Vector2] = objSource.filter(_.startsWith("vt ")).map(_.split(" ")).map(v => new Vector2(v(1).toFloat, 1-v(2).toFloat)) // TODO 1-y because of blender
    val faces: List[(Int, Int, Int)] = objSource.filter(_.startsWith("f ")).map(_.split(" ")).flatten.filterNot(_ == "f").map(_.split("/")).map(a => ((a(0).toInt, a(1).toInt, a(2).toInt)))

    val vertices: List[Vertex] =  for(face <- faces) yield(new Vertex(positions(face._1-1), textureCoordinates(face._2-1)))

    val f: List[(Vector3, Vector2, Vector4)] = for(face <- faces) yield((positions(face._1-1), textureCoordinates(face._2-1), normals(face._3-1)))
    println(f.mkString("\n"))

    val indices: List[Int] = faces.map(f => f._1-1) // Wrong!

    new Model(vertices.toArray, indices.toArray)
}

The val indices: List[Int] was my first naive approach and of course is wrong. But let's start at the top:

I load in the file and go through it. (I assume you know how an .obj-file is made up)

I read in the vertices, texture-coordinates and normals. Then I come to the faces.

Now, each face in my example has 3 values v_x, t_y, n_z defining the vertexAtIndexX, textureCoordAtIndexY, normalAtIndexZ. So each of these define one Vertex while a triple of these (or one line in the file) defines a Face/Polygon/Triangle.

in val vertices: List[Vertex] = for(face <- faces) yield(new Vertex(positions(face._1-1), textureCoordinates(face._2-1))) I actually try to create Vertices (a case-class that currently only holds positions and texture-coordinates and neglects normals for now)

The real problem is this line:

val indices: List[Int] = faces.map(f => f._1-1) // Wrong!

To get the real indices I basically need to do this instead of

val vertices: List[Vertex] = for(face <- faces) yield(new Vertex(positions(face._1-1), textureCoordinates(face._2-1))) and val indices: List[Int] = faces.map(f => f._1-1) // Wrong!

Pseudo-Code:

Iterate over all faces
    Iterate over all vertices in a face
       Check if we already have that combination of(position, texturecoordinate, normal) in our newVertices

       if(true)
          indices.put(indexOfCurrentVertex)
       else
          create a new Vertex from the face
          store the new vertex in the vertex list
          indices.put(indexOfNewVertex)

Yet I'm totally stuck. I've tried different things, but can't come up with a nice and clean solution that actually works.

Things like:

val f: List[(Vector3, Vector2, Vector4)] = for(face <- faces) yield((positions(face._1-1), textureCoordinates(face._2-1), normals(face._3-1)))

and trying to f.distinct are not working, because there is nothing to distinct, all the entries there are unique, which totally makes sense if I look at the file and yet that's what the pseudo-code tells me to check.

Of course then I would need to fill the indices accordingly (preferably in a one-liner and with a lot of functional beauty)

But I should try to find duplicates, so... I'm kind of baffled. I guess I mix up the different "vertices" and "positions" too much, with all the referencing.

So, am I thinking wrong, or is the algorithm/thinking right and I just need to implement this in nice, clean (and actually working) Scala code?

Please, enlighten me!

As per comments, I made a little update:

var index: Int = 0
val map: mutable.HashMap[(Int, Int, Int), Int] = new mutable.HashMap[(Int, Int, Int), Int].empty

val combinedIndices: ListBuffer[Int] = new ListBuffer[Int]

for(face <- faces)
{
    val vID: Int = face._1-1
    val nID: Int = face._2-1
    val tID: Int = face._3-1

    var combinedIndex: Int = -1

    if(map.contains((vID, nID, tID)))
    {
        println("We have a duplicate, wow!")
        combinedIndex = map.get((vID, nID, tID)).get
    }
    else
    {
        combinedIndex = index
        map.put((vID, nID, tID), combinedIndex)
        index += 1
    }

    combinedIndices += combinedIndex
}

where faces still is:

val faces: List[(Int, Int, Int)] = objSource.filter(_.startsWith("f ")).map(_.split(" ")).flatten.filterNot(_ == "f").map(_.split("/")).map(a => ((a(0).toInt, a(1).toInt, a(2).toInt)))

Fun fact I'm still not understanding it obviously, because that way I never ever get a duplicate!

Meaning that combinedIndices at the end just holds the natural numbers like:

ListBuffer(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...)
1
I don't know Scala at all, so I can't tell if this is what you're already doing. But the key data structure you need is a map that uses a tuple with the indices of the position, texture coordinates and normal from the face record as the key, and the index of the OpenGL vertex as the value. Here is pseudo code in an older answer of mine, in case this is not one of the posts you already found in your searches: stackoverflow.com/questions/23349080/….Reto Koradi
@RetoKoradi: So for each face I need to create a Map-Entry with Key[positions(face._1-1), textureCoordinates(face._2-1), normals(face._2-1)] ) How do I then compute the index of the OpenGL vertex for the value?user4063815
You start at 0, and increment it each time you need a new vertex (i.e. the key was not already in the map). It should be fairly clear from the pseudo code I linked.Reto Koradi
And there lies the problem, with that approach (that I definitely misunderstand) I never get a duplicate, hence my indices are just the natural numbers (0,1,2,3,...) Please see my above edit.user4063815

1 Answers

1
votes

This is javascript (sorry not scala) but it's commented and shoul be fairly easy to convert.

// bow-tie
var objString = "v 0 0 0\nv 1 1 0\nv 1 -1 0\nv -1 1 0\nv -1 -1 0\n" +
    "vt 0 .5\nvt 1 1\nvt 1 0\n" +
    "vn 0 0 1\n" +
    "f 1/1/1 2/2/1 3/3/1\nf 1/1/1 4/2/1 5/3/1";
// output indices should be [0, 1, 2, 0, 3, 4]
// parse the file
var lines = objString.split("\n");
var data = lines.map(function(line) { return line.split(" "); });
var v = [];
var t = [];
var n = [];
var f = [];
var indexMap = new Map(); // HashMap<face:string, index:integer>
var nextIndex = 0;
var vertices = [];
var indices = [];
// fill vertex, texture and normal arrays
data.filter(function(d) { return d[0] == "v"; }).forEach(function(d) { v.push([parseFloat(d[1]), parseFloat(d[2]), parseFloat(d[3])]); });
data.filter(function(d) { return d[0] == "vt"; }).forEach(function(d) { t.push([parseFloat(d[1]), parseFloat(d[2])]); });
data.filter(function(d) { return d[0] == "vn"; }).forEach(function(d) { n.push([parseFloat(d[1]), parseFloat(d[2]), parseFloat(d[3])]); });
//
console.log("V", v.toString());
console.log("T", t.toString());
console.log("N", n.toString());
// create vertices and indices arrays by parsing faces
data.filter(function(d) { return d[0] == "f"; }).forEach(function(d) { 
    var f1 = d[1].split("/").map(function(d) { return parseInt(d)-1; });
    var f2 = d[2].split("/").map(function(d) { return parseInt(d)-1; });
    var f3 = d[3].split("/").map(function(d) { return parseInt(d)-1; });
    // 1
    if(indexMap.has(d[1].toString())) {
        indices.push(indexMap.get(d[1].toString()));
    } else {
        vertices = vertices.concat(v[f1[0]]).concat(t[f1[1]]).concat(n[f1[2]]);
        indexMap.set(d[1].toString(), nextIndex);
        indices.push(nextIndex++);
    }
    // 2
    if(indexMap.has(d[2].toString())) {
        indices.push(indexMap.get(d[2].toString()));
    } else {
        vertices = vertices.concat(v[f2[0]]).concat(t[f2[1]]).concat(n[f2[2]]);
        indexMap.set(d[2].toString(), nextIndex);
        indices.push(nextIndex++);
    }
    // 3
    if(indexMap.has(d[3].toString())) {
        indices.push(indexMap.get(d[3].toString()));
    } else {
        vertices = vertices.concat(v[f3[0]]).concat(t[f3[1]]).concat(n[f3[2]]);
        indexMap.set(d[3].toString(), nextIndex);
        indices.push(nextIndex++);
    }
});
//
console.log("Vertices", vertices.toString());
console.log("Indices", indices.toString());

The output

V 0,0,0,1,1,0,1,-1,0,-1,1,0,-1,-1,0
T 0,0.5,1,1,1,0
N 0,0,1
Vertices 0,0,0,0,0.5,0,0,1,1,1,0,1,1,0,0,1,1,-1,0,1,0,0,0,1,-1,1,0,1,1,0,0,1,-1,-1,0,1,0,0,0,1
Indices 0,1,2,0,3,4

The JSFiddle http://jsfiddle.net/8q7jLvsq/2

The only thing I'm doing ~differently is using the string hat represents one of the parts of a face as the key into my indexMap (ex: "25/32/5").

EDIT JSFiddle http://jsfiddle.net/8q7jLvsq/2/ this version combines repeated values for vertex, texture and normal. This optimizes OBJ files that repeat the same values values making every face unique.

// bow-tie
var objString = "v 0 0 0\nv 1 1 0\nv 1 -1 0\nv 0 0 0\nv -1 1 0\nv -1 -1 0\n" +
    "vt 0 .5\nvt 1 1\nvt 1 0\nvt 0 .5\nvt 1 1\nvt 1 0\n" +
    "vn 0 0 1\nvn 0 0 1\nvn 0 0 1\nvn 0 0 1\nvn 0 0 1\nvn 0 0 1\n" +
    "f 1/1/1 2/2/2 3/3/3\nf 4/4/4 5/5/5 6/6/6";
// output indices should be [0, 1, 2, 0, 3, 4]
// parse the file
var lines = objString.split("\n");
var data = lines.map(function(line) { return line.split(" "); });
var v = [];
var t = [];
var n = [];
var f = [];
var vIndexMap = new Map(); // map to earliest index in the list
var vtIndexMap = new Map();
var vnIndexMap = new Map();
var indexMap = new Map(); // HashMap<face:string, index:integer>
var nextIndex = 0;
var vertices = [];
var indices = [];
// fill vertex, texture and normal arrays
data.filter(function(d) { return d[0] == "v"; }).forEach(function(d, i) { 
    v[i] = [parseFloat(d[1]), parseFloat(d[2]), parseFloat(d[3])]; 
    var key = [d[1], d[2], d[3]].toString();
    if(!vIndexMap.has(key)) {
        vIndexMap.set(key, i);
    }
});
data.filter(function(d) { return d[0] == "vt"; }).forEach(function(d, i) { 
    t[i] = [parseFloat(d[1]), parseFloat(d[2])]; 
    var key = [d[1], d[2]].toString();
    if(!vtIndexMap.has(key)) {
        vtIndexMap.set(key, i);
    }
});
data.filter(function(d) { return d[0] == "vn"; }).forEach(function(d, i) { 
    n[i] = [parseFloat(d[1]), parseFloat(d[2]), parseFloat(d[3])]; 
    var key = [d[1], d[2], d[3]].toString();
    if(!vnIndexMap.has(key)) {
        vnIndexMap.set(key, i);
    }
});
//
console.log("V", v.toString());
console.log("T", t.toString());
console.log("N", n.toString());
// create vertices and indices arrays by parsing faces
data.filter(function(d) { return d[0] == "f"; }).forEach(function(d) { 
    var f1 = d[1].split("/").map(function(d, i) {
        var index = parseInt(d)-1;
        if(i == 0) index = vIndexMap.get(v[index].toString());
        else if(i == 1) index = vtIndexMap.get(t[index].toString());
        else if(i == 2) index = vnIndexMap.get(n[index].toString());
        return index;
    });
    var f2 = d[2].split("/").map(function(d, i) { 
        var index = parseInt(d)-1;
        if(i == 0) index = vIndexMap.get(v[index].toString());
        else if(i == 1) index = vtIndexMap.get(t[index].toString());
        else if(i == 2) index = vnIndexMap.get(n[index].toString());
        return index;
    });
    var f3 = d[3].split("/").map(function(d, i) { 
        var index = parseInt(d)-1;
        if(i == 0) index = vIndexMap.get(v[index].toString());
        else if(i == 1) index = vtIndexMap.get(t[index].toString());
        else if(i == 2) index = vnIndexMap.get(n[index].toString());
        return index; 
    });
    // 1
    if(indexMap.has(f1.toString())) {
        indices.push(indexMap.get(f1.toString()));
    } else {
        vertices = vertices.concat(v[f1[0]]).concat(t[f1[1]]).concat(n[f1[2]]);
        indexMap.set(f1.toString(), nextIndex);
        indices.push(nextIndex++);
    }
    // 2
    if(indexMap.has(f2.toString())) {
        indices.push(indexMap.get(f2.toString()));
    } else {
        vertices = vertices.concat(v[f2[0]]).concat(t[f2[1]]).concat(n[f2[2]]);
        indexMap.set(f2.toString(), nextIndex);
        indices.push(nextIndex++);
    }
    // 3
    if(indexMap.has(f3.toString())) {
        indices.push(indexMap.get(f3.toString()));
    } else {
        vertices = vertices.concat(v[f3[0]]).concat(t[f3[1]]).concat(n[f3[2]]);
        indexMap.set(f3.toString(), nextIndex);
        indices.push(nextIndex++);
    }
});
//
console.log("Vertices", vertices.toString());
console.log("Indices", indices.toString());