I almost always stick to 2d graphics, and when I do use 3d graphics, it’s procedural, so I’ve never had to deal with 3d file formats. I decided to learn the OBJ file format this week. OBJ is a text file that only contains geometry, but not animation or skinning or cameras or textures or materials. I think that’s the level at which I want to work right now; maybe later I’ll learn FBX.
This page is some notes I took for myself while learning how to deal with this format.
1 Reading OBJ files#
I used some CC0-licensed models from Quaternius. I looked at the OBJ file to see which features Blender used.
o objectname v x y z # vertex vn x y z # normal usemtl materialname # material named in *.mtl file s off # ?? f i//k … # faces, index into vertices, texture (missing), normals
There can be multiple named objects in the OBJ file. The faces are not limited to being triangles.
Reading this thread and others it seems like OBJ files have a separate index for the vertex position and the vertex normal, and since WebGL wants me to have the same index for these two, I need to construct a new index for each unique pair of (position, normal) indices in the
f structure. Or maybe I just forget about indexed drawing for now. It’s an optimization for later.
The obj-parser library doesn’t support faces with more than 4 vertices, or materials, and it doesn’t support all the materials listed here. There’s a command line tool obj2sc that looks like it will triangulate convex polygon faces. I don’t know if the model I’m working with has convex faces but I tried this tool first:
yarn add obj2sc node_modules/obj2sc/obj2sc.js \ <assets/CargoTrain_Wagon.obj \ >test.json
It’s convenient but I got some glitches. I don’t know what the cause is though — the tool or the model? (Spoiler: the tool)
The tool it ignores the
o lines and the
usemtl lines, and I need those two if I’m going to link this up with the MTL file, which has color and lighting information. So it would be a waste of time to figure out the glitches when I am not even going to continue using the tool.
I looked at the MTL files, and I think most of the fields aren’t really used for these models:
newmtl materialname Ns v # specular power, always 96.078431 Ka r g b # ambient light, always 1 1 1 Kd r g b # diffuse light, actual color of the model Ks r g b # specular light, always 1/2 1/2 1/2 Ke r g b # emissive light, always 0 0 0 Ni v # optical density for refraction, always 1.0 d a # alpha, always 1.0 in these models illum v # illumination model, always 2
So the only thing that really differs is the color. Some OBJ files have vertex colors directly but the ones I’m using do not. In these, the color is in the MTL file.
I ended up writing my own OBJ + MTL file parser that reads all the parts I actually need, and combined the positions, normals, and colors into a non-indexed drawing buffer. I had hoped to use an existing library but I didn’t find any obvious choices right away, and the file format looked easy enough that I decided to do it myself.
2 Polygons to triangles#
The OBJ file format has polygons, not necessarily convex. I had originally written quick & dirty convex polygon to triangle code, but it had some issues.
I’m pretty sure these train models have non-convex polygon faces. There’s enough data in the models that I don’t want to try checking. But I could use either the earcut library (@mourner, who writes libraries I love using) or libtess (Eric Veach! I know him).
Earcut conveniently returns indices instead of x,y like it used to. But it only works in 2D. Even though it says it takes points in more dimensions, it ignores them. So then I looked at libtess, but it doesn’t have much documentation, and as far as I can tell, it doesn’t return the indices. :-(
So I went back to earcut, with a workaround: I pass it x,y, then y,z, then z,x, and take the first non-empty output. Even that gave me some glitches.
I looked into why I got those glitches, and it turns out the first non-empty output isn’t enough. I ran it for all three: x,y, then y,z, then z,x, and take the output with the most triangles. Does this make a difference? It turns out yes, three of the models end up with different length output. This is all because I’m using a 2d triangulation library for 3d data, but I’m glad I found a workaround that fixed the problem.
I almost gave up during this process, telling myself I should never work with 3d models. Of course I should’ve used Three.js, which handles all of this for me, but I thought I’d learn the OBJ file format better if I had to go through it myself.
I tweaked lighting and colors but I couldn’t get things to match. Their grays are lighter than mine; their yellows are darker than mine; their blues are greener than mine. I’m pretty sure their rendering is using ambient occlusion, and I don’t have that implemented. I’m not sure I want to implement it right now, even though it doesn’t look too complicated (see this)
I think I will read a paper to learn about ambient occlusion, but I won’t implement anything. I’m done with this.
The train models are CC0.
LowPoly Models by @Quaternius Consider supporting me on Patreon, even $1 helps me a lot! https://www.patreon.com/quaternius ------------------------------------------------------- License: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication https://creativecommons.org/publicdomain/zero/1.0/
Open source libraries:
These two tutorial pages were helpful for me to understand a bit more about the OBJ file format and how to read it:
- WebGL Fundamentals’s page about OBJ files
- Loading and displaying a 3D mesh with regl
- Paul Bourke’s page
- Comparison of 3d file formats - gltf, fbx, alembic, usd, usdz, collada, obj, ply, stl
I usually go to the Paul Bourke pages but in this case I found Wikipedia to be more helpful.
My source code is in obj-file-format.js.