frenchtoast747 / webgl-obj-loader

A simple OBJ model loader to help facilitate the learning of WebGL.
http://frenchtoast747.github.io/webgl-obj-loader/
MIT License
282 stars 59 forks source link
javascript learning-webgl model-loader mtl obj obj-loader webgl

webgl-obj-loader

Build Status

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. 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:

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:

obj loader element index description

After describing the attribute data to WebGL via 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:

// 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:

A simple example:

In your index.html file:

<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:

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:

A simple example:

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:

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):

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

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 for the 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

2.0.0

1.1.0

1.0.1

1.0.0

0.1.1

0.1.0

0.0.3

0.0.2

0.0.1

Bitdeli Badge