Map/Fold the multiple .OBJ-index-buffers to OpenGL's 1 index buffer

600 Views Asked by At

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

There are 1 best solutions below

6
On

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());