Obj file (*.obj) is a geometry definition format that was developed by Wavefront Technologies. Since then, the format became open and has been widely adopted by other 3D visualization packages and even games [1].

Why is it so popular? I think it is because of its simplicity. The format doesn’t hold any information about animations – therefore, it is approachable even by beginners.

Right from a getgo, I would like to point out that my take on the parsing of it is very far from being complete. I am using only geometry representation. The material system of the format is a bit outdated and inflexible. I also steer clear from any advanced concepts, like negative indices, smoothing groups, etc. – that makes the code hard to read and the models hard to reuse. If you are interested in a detailed explanation, there are great resources out there [2].

Let us jump into the implementation!

Physics-Based Lantern

In this post

The first inefficiency

My implementation is not just incomplete – it is not very efficient as well 🙂 But let us see the code first.

To avoid lengthy function declarations, I have an internal representation of an intermediate result. This class contains all of the parsed data for the mesh:

common_gl/com.blaster.assets.MeshLib::Intermediate

private class Intermediate {
    val aabb = aabb()
    val positionList = mutableListOf<Float>()
    val texCoordList = mutableListOf<Float>()
    val normalList = mutableListOf<Float>()
    val positions = mutableListOf<Float>()
    val texCoords = mutableListOf<Float>()
    val normals = mutableListOf<Float>()
    val indicesList = mutableListOf<Int>()
}

The first problem with my approach is that I do not know how many vertices/indices file contains – therefore I cannot preallocate buffers beforehand and have to copy information from lists:

common_gl/com.blaster.assets.MeshLib::loadMesh

fun loadMesh(meshFilename: String): Pair<GlMesh, aabb> {

Storage for everything

    val result = Intermediate()

Reading file line by line

    val inputStream = assetStream.openAsset(meshFilename)
    val bufferedReader = BufferedReader(InputStreamReader(inputStream, Charset.defaultCharset()))
    bufferedReader.use {
        var line = bufferedReader.readLine()
        while (line != null) {
            parseLine(line, result)
            line = bufferedReader.readLine()
        }
    }

Copying to appropriate buffers and sending to GPU memory

    val positionBuff = toByteBufferFloat(result.positions)
    val texCoordBuff = toByteBufferFloat(result.texCoords)
    val normalBuff = toByteBufferFloat(result.normals)
    val indicesBuff = toByteBufferInt(result.indicesList)
    val mesh = GlMesh(
        listOf(
            GlAttribute.ATTRIBUTE_POSITION to GlBuffer(backend.GL_ARRAY_BUFFER, positionBuff),
            GlAttribute.ATTRIBUTE_TEXCOORD to GlBuffer(backend.GL_ARRAY_BUFFER, texCoordBuff),
            GlAttribute.ATTRIBUTE_NORMAL to GlBuffer(backend.GL_ARRAY_BUFFER, normalBuff)
        ),
        GlBuffer(backend.GL_ELEMENT_ARRAY_BUFFER, indicesBuff), result.indicesList.size
    )
    return mesh to result.aabb
}

The second inefficiency

The second problem with my approach is that I am relying heavily on RegEx, even for simple things:

common_gl/com.blaster.assets.MeshLib::slashRegex

private val slashRegex = "/".toRegex()

While being more precise, Regular Expressions will sure take its sweet time while parsing. Here I am extracting information about positions, for example:

common_gl/com.blaster.assets.MeshLib::parsePosition

private fun parsePosition(line: String, result: Intermediate) {
    val split = line.split(whitespaceRegex)
    result.positionList.add(split[1].toFloat())
    result.positionList.add(split[2].toFloat())
    result.positionList.add(split[3].toFloat())
}

Let us see how I am deciding how to parse each line.

The only efficiency

Probably the only thing which is done right, is that I am switching between branches by characters. Characters will be unique for each case: v – vertex, vt – texture coordinate, vn – normal, f – face, and so on:

common_gl/com.blaster.assets.MeshLib::parseLine

private fun parseLine(line: String, result: Intermediate) {
    if (line.isEmpty()) {
        return
    }
    when (line[0]) {
        'v' -> parseVertexAttribute(line, result)
        'f' -> parsePolygon(line, result)
    }
}

common_gl/com.blaster.assets.MeshLib::parseVertexAttribute

private fun parseVertexAttribute(line: String, result: Intermediate) {
    when (line[1]) {
        ' ' -> parsePosition(line, result)
        't' -> parseTexCoordinate(line, result)
        'n' -> parseNormal(line, result)
        else -> fail("Unknown vertex attribute! $line")
    }
}

The third inefficiency

As I already mentioned, there are flaws in my parser, but this one is big 🙂

When I am traversing the file for the first time, I accumulate indexed vertex attributes into the lists. After the parsing is done, I am constructing each vertex from the information in listings. There is quite a lot of copying here.

common_gl/com.blaster.assets.MeshLib::parsePolygon

private fun parsePolygon(line: String, result: Intermediate) {

Splitting the line: ind1/ind2/ind3/…

    val split = line.split(whitespaceRegex)
    val verticesCnt = split.size - 1
    val indices = ArrayList<Int>()
    var nextIndex = result.positions.size / 3

Adding each vertex

    for (vertex in 0 until verticesCnt) {
        addVertex(split[vertex + 1], result)
        indices.add(nextIndex)
        nextIndex++
    }

Adding each triangle for the face

    val triangleCnt = verticesCnt - 2
    for (triangle in 0 until triangleCnt) {
        addTriangle(indices[0], indices[triangle + 1], indices[triangle + 2], result.indicesList)
    }
}

common_gl/com.blaster.assets.MeshLib::addVertex

private fun addVertex(vertex: String, result: Intermediate) {
    val vertSplit = vertex.split(slashRegex)
    val vertIndex = vertSplit[0].toInt() - 1
    val vx = result.positionList[vertIndex * 3 + 0]
    val vy = result.positionList[vertIndex * 3 + 1]
    val vz = result.positionList[vertIndex * 3 + 2]
    result.positions.add(vx)
    result.positions.add(vy)
    result.positions.add(vz)
    updateAabb(result.aabb, vx, vy, vz)

In case if we do not have texture coordinates, just using 1,1

    if (result.texCoordList.isNotEmpty()) {
        val texIndex = vertSplit[1].toInt()  - 1
        result.texCoords.add(result.texCoordList[texIndex  * 2 + 0])
        result.texCoords.add(result.texCoordList[texIndex  * 2 + 1])
    } else {
        result.texCoords.add(1f)
        result.texCoords.add(1f)
    }

If we do not have normals, using up direction

    if (result.normalList.isNotEmpty()) {
        val normIndex = vertSplit[2].toInt() - 1
        result.normals.add(result.normalList[normIndex * 3 + 0])
        result.normals.add(result.normalList[normIndex * 3 + 1])
        result.normals.add(result.normalList[normIndex * 3 + 2])
    } else {
        result.normals.add(0f)
        result.normals.add(1f)
        result.normals.add(0f)
    }
}

Adding indices for triangle is straightforward:

common_gl/com.blaster.assets.MeshLib::addTriangle

private fun addTriangle(ind0: Int, ind1: Int, ind2: Int, indicesList: MutableList<Int>) {
    indicesList.add(ind0)
    indicesList.add(ind1)
    indicesList.add(ind2)
}

Small bonus

While parsing the mesh, it is a good time to construct the Axis Aligned Bounding Box (AABB) for it. AABB can be handy: for example, you can calculate the position of the camera at the start of the app so that the whole object will fit in viewing frustum. But the more convenient usage is, of course, for scene partitioning.

common_gl/com.blaster.assets.MeshLib::updateAabb

private fun updateAabb(aabb: aabb, vx: Float, vy: Float, vz: Float) {
    if (vx < aabb.minX) {
        aabb.minX = vx
    } else if (vx > aabb.maxX) {
        aabb.maxX = vx
    }
    if (vy < aabb.minY) {
        aabb.minY = vy
    } else if (vy > aabb.maxY) {
        aabb.maxY = vy
    }
    if (vz < aabb.minZ) {
        aabb.minZ = vz
    } else if (vz > aabb.maxZ) {
        aabb.maxZ = vz
    }
}

Free stuff!

While working on the code for Blaster, I found out that there are quite a lot of places where you can look for free models. Here are my top 3 choices:

There are even rigged and PBR ready models available for noncommercial use. It will not be enough to help Bethesda to fix Fallout, but it is a great way to find resources for a personal hobby.

Anyway, in this post, I wanted to overview the parsing of meshes for Blaster. It is not elaborate, and I am okay with that for now. I think it is more important to leave yourself room for improvement instead of trying to do a production-ready solution from the start.

I hope you liked the material and I will be back soon with more!

References


Leave a Reply

Your email address will not be published. Required fields are marked *