webgl-obj-loader
Version:
A simple OBJ model loader to help facilitate the learning of WebGL
327 lines (262 loc) • 14.7 kB
Markdown
# webgl-obj-loader
[](https://travis-ci.org/frenchtoast747/webgl-obj-loader)
A simple script to help bring OBJ models to your WebGL world. I originally
wrote this script for my CS Graphics class so that we didn't have to only have
cubes and spheres for models in order to learn WebGL. At the time, the only
sort of model loader for WebGL was [Mr. Doob's ThreeJS](http://threejs.org/). And in order to use the
loaders you had to use the entire framework (or do some very serious hacking
and duct-taping in order get the model information). My main focus in creating
this loader was to easily allow importing models without having to have special
knowledge of a 3D graphics program (like Blender) while keeping it low-level
enough so that the focus was on learning WebGL rather than learning some
framework.
## Mesh(objStr)
The main Mesh class. The constructor will parse through the OBJ file data
and collect the vertex, vertex normal, texture, and face information. This
information can then be used later on when creating your VBOs. Look at the
`initMeshBuffers` source for an example of how to use the newly created Mesh
### Attributes:
* **vertices:** an array containing the vertex values that correspond to each unique face index. The array is flat in that each vertex's component is an element of the array. For example: with `verts = [1, -1, 1, ...]`, `verts[0] is x`, `verts[1] is y`, and `verts[2] is z`. Continuing on, `verts[3]` would be the beginning of the next vertex: its x component. This is in preparation for using `gl.ELEMENT_ARRAY_BUFFER` for the `gl.drawElements` call.
* Note that the `vertices` attribute is the [Geometric Vertex](https://en.wikipedia.org/wiki/Wavefront_.obj_file#Geometric_vertex) and denotes the position in 3D space.
* **vertexNormals:** an array containing the vertex normals that correspond to each unique face index. It is flat, just like `vertices`.
* **textures:** an array containing the `s` and `t` (or `u` and `v`) coordinates for this mesh's texture. It is flat just like `vertices` except it goes by groups of 2 instead of 3.
* **indices:** an array containing the indicies to be used in conjunction with the above three arrays in order to draw the triangles that make up faces. See below for more information on element indices.
#### Element Index
The `indices` attribute is a list of numbers that represent the indices of the above vertex groups. For example, the Nth index, `mesh.indices[N]`, may contain the value `38`. This points to the 39th (zero indexed) element. For Mesh classes, this points to a unique group vertex, normal, and texture values. However, the `vertices`, `normals`, and `textures` attributes are flattened lists of each attributes' components, e.g. the `vertices` list is a repeating pattern of [X, Y, Z, X, Y, Z, ...], so you cannot directly use the element index in order to look up the corresponding vertex position. That is to say `mesh.vertices[38]` does _not_ point to the 39th vertex's X component. The following diagram illustrates how the element index under the hood:

After describing the attribute data to WebGL via [vertexAttribPointer()](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer), what was once separate array elements in JS is now just one block of data on the graphics card. That block of data in its entirety is considered a single element.
To use the element index in order to index one of the attribute arrays in JS, you will have to mimic this "chunking" of data by taking into account the number of components in an attribute (e.g. a vertex has 3 components; x, y, and z). Have a look at the following code snippet to see how to correctly use the element index
in order to access an attribute for that index:
```javascript
// there are 3 components for a geometric vertex: X, Y, and Z
const NUM_COMPONENTS_FOR_VERTS = 3;
elementIdx = mesh.indices[SOME_IDX]; // e.g. 38
// in order to get the X component of the vertex component of element "38"
elementVertX = mesh.vertices[(elementIdx * NUM_COMPONENTS_FOR_VERTS) + 0]
// in order to get the Y component of the vertex component of element "38"
elementVertY = mesh.vertices[(elementIdx * NUM_COMPONENTS_FOR_VERTS) + 1]
// in order to get the Z component of the vertex component of element "38"
elementVertZ = mesh.vertices[(elementIdx * NUM_COMPONENTS_FOR_VERTS) + 2]
```
### Params:
* **objStr** a string representation of an OBJ file with newlines preserved.
A simple example:
In your `index.html` file:
```html
<html>
<head>
<script type="text/plain" id="my_cube.obj">
####
#
# OBJ File Generated by Blender
#
####
o my_cube.obj
v 1 1 1
v -1 1 1
v -1 -1 1
v 1 -1 1
v 1 1 -1
v -1 1 -1
v -1 -1 -1
v 1 -1 -1
vn 0 0 1
vn 1 0 0
vn -1 0 0
vn 0 0 -1
vn 0 1 0
vn 0 -1 0
f 1//1 2//1 3//1
f 3//1 4//1 1//1
f 5//2 1//2 4//2
f 4//2 8//2 5//2
f 2//3 6//3 7//3
f 7//3 3//3 2//3
f 7//4 8//4 5//4
f 5//4 6//4 7//4
f 5//5 6//5 2//5
f 2//5 1//5 5//5
f 8//6 4//6 3//6
f 3//6 7//6 8//6
</script>
</head>
</html>
```
And in your `app.js`:
```javascript
var gl = canvas.getContext('webgl');
var objStr = document.getElementById('my_cube.obj').innerHTML;
var mesh = new OBJ.Mesh(objStr);
// use the included helper function to initialize the VBOs
// if you don't want to use this function, have a look at its
// source to see how to use the Mesh instance.
OBJ.initMeshBuffers(gl, mesh);
// have a look at the initMeshBuffers docs for an exmample of how to
// render the model at this point
```
## Some helper functions
### downloadMeshes(nameAndURLs, completionCallback, meshes)
Takes in a JS Object of `mesh_name`, `'/url/to/OBJ/file'` pairs and a callback
function. Each OBJ file will be ajaxed in and automatically converted to
an OBJ.Mesh. When all files have successfully downloaded the callback
function provided will be called and passed in an object containing
the newly created meshes.
**Note:** In order to use this function as a way to download meshes, a
webserver of some sort must be used.
#### Params:
* **nameAndURLs:** an object where the key is the name of the mesh and the value is the url to that mesh's OBJ file
* **completionCallback:** should contain a function that will take one parameter: an object array where the keys will be the unique object name and the value will be a Mesh object
* **meshes:** In case other meshes are loaded separately or if a previously declared variable is desired to be used, pass in a (possibly empty) json object of the pattern: `{ 'mesh_name': OBJ.Mesh }`
A simple example:
```javascript
var app = {};
app.meshes = {};
var gl = document.getElementById('mycanvas').getContext('webgl');
function webGLStart(meshes){
app.meshes = meshes;
// initialize the VBOs
OBJ.initMeshBuffers(gl, app.meshes.suzanne);
OBJ.initMeshBuffers(gl, app.meshes.sphere);
... other cool stuff ...
// refer to the initMeshBuffers docs for an example of
// how to render the mesh to the screen after calling
// initMeshBuffers
}
window.onload = function(){
OBJ.downloadMeshes({
'suzanne': 'models/suzanne.obj', // located in the models folder on the server
'sphere': 'models/sphere.obj'
}, webGLStart);
}
```
### initMeshBuffers(gl, mesh)
Takes in the WebGL context and a Mesh, then creates and appends the buffers
to the mesh object as attributes.
#### Params:
* **gl** *WebGLRenderingContext* the `canvas.getContext('webgl')` context instance
* **mesh** *Mesh* a single `OBJ.Mesh` instance
The newly created mesh attributes are:
Attrbute | Description
:--- | ---
**normalBuffer** |contains the model's Vertex Normals
normalBuffer.itemSize |set to 3 items
normalBuffer.numItems |the total number of vertex normals
**textureBuffer** |contains the model's Texture Coordinates
textureBuffer.itemSize |set to 2 items (or 3 if W texture coord is enabled)
textureBuffer.numItems |the number of texture coordinates
**vertexBuffer** |contains the model's Vertex Position Coordinates (does not include w)
vertexBuffer.itemSize |set to 3 items
vertexBuffer.numItems |the total number of vertices
**indexBuffer** |contains the indices of the faces
indexBuffer.itemSize |is set to 1
indexBuffer.numItems |the total number of indices
A simple example (a lot of steps are missing, so don't copy and paste):
```javascript
var gl = canvas.getContext('webgl'),
var mesh = new OBJ.Mesh(obj_file_data);
// compile the shaders and create a shader program
var shaderProgram = gl.createProgram();
// compilation stuff here
...
// make sure you have vertex, vertex normal, and texture coordinate
// attributes located in your shaders and attach them to the shader program
shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
shaderProgram.vertexNormalAttribute = gl.getAttribLocation(shaderProgram, "aVertexNormal");
gl.enableVertexAttribArray(shaderProgram.vertexNormalAttribute);
shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord");
gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute);
// create and initialize the vertex, vertex normal, and texture coordinate buffers
// and save on to the mesh object
OBJ.initMeshBuffers(gl, mesh);
// now to render the mesh
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vertexBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, mesh.vertexBuffer.itemSize, gl.FLOAT, false, 0, 0);
// it's possible that the mesh doesn't contain
// any texture coordinates (e.g. suzanne.obj in the development branch).
// in this case, the texture vertexAttribArray will need to be disabled
// before the call to drawElements
if(!mesh.textures.length){
gl.disableVertexAttribArray(shaderProgram.textureCoordAttribute);
}
else{
// if the texture vertexAttribArray has been previously
// disabled, then it needs to be re-enabled
gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute);
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.textureBuffer);
gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, mesh.textureBuffer.itemSize, gl.FLOAT, false, 0, 0);
}
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.normalBuffer);
gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, mesh.normalBuffer.itemSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.mesh.indexBuffer);
gl.drawElements(gl.TRIANGLES, model.mesh.indexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
```
### deleteMeshBuffers(gl, mesh)
Deletes the mesh's buffers, which you would do when deleting an object from a
scene so that you don't leak video memory. Excessive buffer creation and
deletion leads to video memory fragmentation. Beware.
## Node.js
`npm install webgl-obj-loader`
```javascript
var fs = require('fs');
var OBJ = require('webgl-obj-loader');
var meshPath = './development/models/sphere.obj';
var opt = { encoding: 'utf8' };
fs.readFile(meshPath, opt, function (err, data){
if (err) return console.error(err);
var mesh = new OBJ.Mesh(data);
});
```
## Webpack Support
Thanks to [mentos1386](https://github.com/mentos1386) for the [webpack-obj-loader](https://github.com/mentos1386/webpack-obj-loader)!
## Demo
http://frenchtoast747.github.com/webgl-obj-loader/
This demo is the same thing inside of the gh-pages branch. Do a `git checkout gh-pages` inside of the webgl-obj-loader directory to see how the OBJ loader is used in a project.
## ChangeLog
**2.0.3**
* Add simple support for N-Gons (thanks [qtip](https://github.com/qtip)!)
* This uses a very elementary algorithm to triangulate N-gons, but should still produce a full mesh.
* Any help to create a better triangulation algorithm would be greatly appreciated! Please create a [pull request](https://github.com/frenchtoast747/webgl-obj-loader/pulls).
**2.0.0**
* Updated to TypeScript
* Breaking change: the Mesh option `indicesPerMaterial` has been removed in favor of always providing the indices per material.
* Instead of `mesh.indices` holding an array of arrays of numbers, `mesh.indicesPerMaterial` will now hold the indices where the top
level array index is the index of the material and the inner arrays are the indices for that material.
* Breaking change: the Layout class has changed from directly applying attributes to the Layout instance to creating an attributeMap
**1.1.0**
* Add Support for separating mesh indices by materials.
* Add calculation for tangents and bitangents
* Add runtime OBJ library version.
**1.0.1**
* Add support for 3D texture coordinates. By default the third texture
coordinate, w, is truncated. Support can be enabled by passing
`enableWTextureCoord: true` in the options parameter of the Mesh
class.
**1.0.0**
* Modularized all of the source files into ES6 modules.
* The Mesh, MaterialLibrary, and Material classes are now
actual ES6 classes.
* Added tests for each of the classes
* Found a bug in the Mesh class. Vertex normals would not appear
if the face declaration used the shorthand variant; e.g. `f 1/1`
* Provided Initial MTL file parsing support.
* Still requires Documentation. For now, have a look at the tests in the
test directory for examples of use.
* Use the new downloadModels() function in order to download the OBJ meshes
complete with their MTL files attached. If the MTL files reference images,
by default, those images will be downloaded and attached.
* The downloading functions now use the new `fetch()` API which utilizes
promises.
**0.1.1**
* Support for NodeJS.
**0.1.0**
* Dropped jQuery dependency: `downloadMeshes` no longer requires jQuery to ajax in the OBJ files.
* changed namespace to something a little shorter: `OBJ`
* Updated documentation
**0.0.3**
* Initial support for Quad models
**0.0.2**
* Texture Coordinates are now loaded into mesh.textures
**0.0.1**
* Vertex Normals are now loaded into mesh.vertexNormals
[](https://bitdeli.com/free "Bitdeli Badge")