DEV Community

loading...

WebGL Month. Day 17. Exploring OBJ format

lesnitsky profile image Andrei Lesnitsky ・7 min read

Day 17. Exploring OBJ format

This is a series of blog posts related to WebGL. New post will be available every day

GitHub stars
Twitter Follow

Join mailing list to get new posts right to your inbox

Source code available here

Built with

Git Tutor Logo


Hey πŸ‘‹

Welcome to WebGL month.

Yesterday we've fixed our cube example, but vertices of this cube were defined right in our js code. This might get more complicated when rendering more complex objects.

Luckily 3D editors like Blender can export object definition in several formats.

Let's export a cube from blender

Blender export screenshot

Let's explore exported file

First two lines start with # which is just a comment

πŸ“„ assets/objects/cube.obj

+ # Blender v2.79 (sub 0) OBJ File: ''
+ # www.blender.org

mtllib line references the file of material of the object
We'll ignore this for now

πŸ“„ assets/objects/cube.obj

  # Blender v2.79 (sub 0) OBJ File: ''
  # www.blender.org
+ mtllib cube.mtl

o defines the name of the object

πŸ“„ assets/objects/cube.obj

  # Blender v2.79 (sub 0) OBJ File: ''
  # www.blender.org
  mtllib cube.mtl
+ o Cube

Lines with v define vertex positions

πŸ“„ assets/objects/cube.obj

  # www.blender.org
  mtllib cube.mtl
  o Cube
+ v 1.000000 -1.000000 -1.000000
+ v 1.000000 -1.000000 1.000000
+ v -1.000000 -1.000000 1.000000
+ v -1.000000 -1.000000 -1.000000
+ v 1.000000 1.000000 -0.999999
+ v 0.999999 1.000000 1.000001
+ v -1.000000 1.000000 1.000000
+ v -1.000000 1.000000 -1.000000

vn define vertex normals. In this case normals are perpendicular ot the cube facess

πŸ“„ assets/objects/cube.obj

  v 0.999999 1.000000 1.000001
  v -1.000000 1.000000 1.000000
  v -1.000000 1.000000 -1.000000
+ vn 0.0000 -1.0000 0.0000
+ vn 0.0000 1.0000 0.0000
+ vn 1.0000 0.0000 0.0000
+ vn -0.0000 -0.0000 1.0000
+ vn -1.0000 -0.0000 -0.0000
+ vn 0.0000 0.0000 -1.0000

usemtl tells which material to use for the elements (faces) following this line

πŸ“„ assets/objects/cube.obj

  vn -0.0000 -0.0000 1.0000
  vn -1.0000 -0.0000 -0.0000
  vn 0.0000 0.0000 -1.0000
+ usemtl Material

f lines define object faces referencing vertices and normals by indices

πŸ“„ assets/objects/cube.obj

  vn 0.0000 0.0000 -1.0000
  usemtl Material
  s off
+ f 1//1 2//1 3//1 4//1
+ f 5//2 8//2 7//2 6//2
+ f 1//3 5//3 6//3 2//3
+ f 2//4 6//4 7//4 3//4
+ f 3//5 7//5 8//5 4//5
+ f 5//6 1//6 4//6 8//6

So in this case the first face consists of vertices 1, 2, 3 and 4

Other thing to mention – our face consists of 4 vertices, but webgl can render only triangles. We can break this faces to triangles in JS or do this in Blender

Enter edit mode (Tab key), and hit Control + T (on macOS). That's it, cube faces are now triangulated

Triangulated cube

Now let's load .obj file with raw loader

πŸ“„ src/3d.js

  import fShaderSource from './shaders/3d.f.glsl';
  import { compileShader, setupShaderInput } from './gl-helpers';
  import { GLBuffer } from './GLBuffer';
+ import cubeObj from '../assets/objects/cube.obj';

  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');

πŸ“„ webpack.config.js

      module: {
          rules: [
              {
-                 test: /\.glsl$/,
+                 test: /\.(glsl|obj)$/,
                  use: 'raw-loader',
              },


and implement parser to get vertices and vertex indices

πŸ“„ src/3d.js


  import vShaderSource from './shaders/3d.v.glsl';
  import fShaderSource from './shaders/3d.f.glsl';
- import { compileShader, setupShaderInput } from './gl-helpers';
+ import { compileShader, setupShaderInput, parseObj } from './gl-helpers';
  import { GLBuffer } from './GLBuffer';
  import cubeObj from '../assets/objects/cube.obj';


  const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);

- const cubeVertices = new Float32Array([
-     // Front face
-     -1.0, -1.0, 1.0,
-     1.0, -1.0, 1.0,
-     1.0, 1.0, 1.0,
-     -1.0, 1.0, 1.0,
- 
-     // Back face
-     -1.0, -1.0, -1.0,
-     -1.0, 1.0, -1.0,
-     1.0, 1.0, -1.0,
-     1.0, -1.0, -1.0,
- 
-     // Top face
-     -1.0, 1.0, -1.0,
-     -1.0, 1.0, 1.0,
-     1.0, 1.0, 1.0,
-     1.0, 1.0, -1.0,
- 
-     // Bottom face
-     -1.0, -1.0, -1.0,
-     1.0, -1.0, -1.0,
-     1.0, -1.0, 1.0,
-     -1.0, -1.0, 1.0,
- 
-     // Right face
-     1.0, -1.0, -1.0,
-     1.0, 1.0, -1.0,
-     1.0, 1.0, 1.0,
-     1.0, -1.0, 1.0,
- 
-     // Left face
-     -1.0, -1.0, -1.0,
-     -1.0, -1.0, 1.0,
-     -1.0, 1.0, 1.0,
-     -1.0, 1.0, -1.0,
- ]);
- 
- const indices = new Uint8Array([
-     0, 1, 2, 0, 2, 3,       // front
-     4, 5, 6, 4, 6, 7,       // back
-     8, 9, 10, 8, 10, 11,    // top
-     12, 13, 14, 12, 14, 15, // bottom
-     16, 17, 18, 16, 18, 19, // right
-     20, 21, 22, 20, 22, 23, // left
- ]);
+ const { vertices, indices } = parseObj(cubeObj);

  const faceColors = [
      [1.0, 1.0, 1.0, 1.0], // Front face: white
      gl.uniform4fv(programInfo.uniformLocations[`colors[${index}]`], color);
  });

- const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, cubeVertices, gl.STATIC_DRAW);
+ const vertexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
  const colorsBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
  const indexBuffer = new GLBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);


πŸ“„ src/gl-helpers.js

          uniformLocations,
      }
  }
+ 
+ export function parseObj(objSource) {
+     const vertices = [];
+     const indices = [];
+ 
+     return { vertices, indices };
+ }

We can iterate over each line and search for those starting with v to get vertex coordinatess

πŸ“„ src/gl-helpers.js

      }
  }

+ export function parseVec(string, prefix) {
+     return string.replace(prefix, '').split(' ').map(Number);
+ }
+ 
  export function parseObj(objSource) {
      const vertices = [];
      const indices = [];

+     objSource.split('\n').forEach(line => {
+         if (line.startsWith('v ')) {
+             vertices.push(...parseVec(line, 'v '));
+         }
+     });
+ 
      return { vertices, indices };
  }

and do the same with faces

πŸ“„ src/gl-helpers.js

      return string.replace(prefix, '').split(' ').map(Number);
  }

+ export function parseFace(string) {
+     return string.replace('f ', '').split(' ').map(chunk => {
+         return chunk.split('/').map(Number);
+     })
+ }
+ 
  export function parseObj(objSource) {
      const vertices = [];
      const indices = [];
          if (line.startsWith('v ')) {
              vertices.push(...parseVec(line, 'v '));
          }
+ 
+         if (line.startsWith('f ')) {
+             indices.push(...parseFace(line).map(face => face[0]));
+         }
      });

      return { vertices, indices };

Let's also return typed arrays

πŸ“„ src/gl-helpers.js

          }
      });

-     return { vertices, indices };
+     return { 
+         vertices: new Float32Array(vertices), 
+         indices: new Uint8Array(indices),
+     };
  }

Ok, everything seem to work fine, but we have an error

glDrawElements: attempt to access out of range vertices in attribute 0

That's because indices in .obj file starts with 1, so we need to decrement each index

πŸ“„ src/gl-helpers.js

          }

          if (line.startsWith('f ')) {
-             indices.push(...parseFace(line).map(face => face[0]));
+             indices.push(...parseFace(line).map(face => face[0] - 1));
          }
      });


Let's also change the way we colorize our faces, just to make it possible to render any object with any amount of faces with random colors

πŸ“„ src/3d.js


  const colors = [];

- for (var j = 0; j < faceColors.length; ++j) {
-     colors.push(j, j, j, j);
+ for (var j = 0; j < indices.length / 3; ++j) {
+     const randomColorIndex = Math.floor(Math.random() * faceColors.length);
+     colors.push(randomColorIndex, randomColorIndex, randomColorIndex);
  }

  faceColors.forEach((color, index) => {

One more issue with existing code, is that we use gl.UNSIGNED_BYTE, so index buffer might only of a Uint8Array which fits numbers up to 255, so if object will have more than 255 vertices – it will be rendered incorrectly. Let's fix this

πŸ“„ src/3d.js


  gl.viewport(0, 0, canvas.width, canvas.height);

- gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
+ gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_SHORT, 0);

  function frame() {
      mat4.rotateY(modelMatrix, modelMatrix, Math.PI / 180);

      gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, modelMatrix);
-     gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
+     gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_SHORT, 0);

      requestAnimationFrame(frame);
  }

πŸ“„ src/gl-helpers.js


      return { 
          vertices: new Float32Array(vertices), 
-         indices: new Uint8Array(indices),
+         indices: new Uint16Array(indices),
      };
  }

Now let's render different object, for example monkey

πŸ“„ src/3d.js

  import fShaderSource from './shaders/3d.f.glsl';
  import { compileShader, setupShaderInput, parseObj } from './gl-helpers';
  import { GLBuffer } from './GLBuffer';
- import cubeObj from '../assets/objects/cube.obj';
+ import monkeyObj from '../assets/objects/monkey.obj';

  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');

  const programInfo = setupShaderInput(gl, program, vShaderSource, fShaderSource);

- const { vertices, indices } = parseObj(cubeObj);
+ const { vertices, indices } = parseObj(monkeyObj);

  const faceColors = [
      [1.0, 1.0, 1.0, 1.0], // Front face: white

  mat4.lookAt(
      viewMatrix,
-     [0, 7, -7],
+     [0, 0, -7],
      [0, 0, 0],
      [0, 1, 0],
  );

Cool! We now can render any objects exported from blender πŸŽ‰

Rotating monkey

That's it for today, see you tomorrow πŸ‘‹


GitHub stars
Twitter Follow

Join mailing list to get new posts right to your inbox

Source code available here

Built with

Git Tutor Logo

Discussion (0)

pic
Editor guide