Day 17. Exploring OBJ format
This is a series of blog posts related to WebGL. New post will be available every day
Join mailing list to get new posts right to your inbox
Built with
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
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
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 🎉
That's it for today, see you tomorrow 👋
Join mailing list to get new posts right to your inbox
Built with
Top comments (0)