├── .babelrc ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .npmignore ├── .prettierignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── development ├── app.js ├── gl-matrix.js ├── index.html └── models │ ├── die.blend │ ├── die.mtl │ ├── die.obj │ ├── face1.bmp │ ├── face2.bmp │ ├── face3.bmp │ ├── face4.bmp │ ├── face5.bmp │ ├── face6.bmp │ ├── suzanne.mtl │ └── suzanne.obj ├── dist ├── index.d.ts ├── layout.d.ts ├── material.d.ts ├── mesh.d.ts ├── utils.d.ts ├── webgl-obj-loader.js └── webgl-obj-loader.min.js ├── package.json ├── src ├── index.ts ├── layout.ts ├── material.ts ├── mesh.ts └── utils.ts ├── test ├── layout.ts ├── material.ts ├── mesh.ts ├── mocha.opts ├── tsconfig.json └── tshook.js ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js ├── webpack.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-3" 5 | ], 6 | "retainLines": true 7 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "google", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module" 6 | }, 7 | "env": { "es6": true }, 8 | }; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | package-lock.json 4 | .nyc_output/* 5 | coverage/* 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | development/* 2 | test/* 3 | 4 | .travis.yml 5 | webpack.* 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | test/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | - "12" -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Aaron Boman and aaronboman.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webgl-obj-loader 2 | [![Build Status](https://travis-ci.org/frenchtoast747/webgl-obj-loader.svg?branch=master)](https://travis-ci.org/frenchtoast747/webgl-obj-loader) 3 | 4 | A simple script to help bring OBJ models to your WebGL world. I originally 5 | wrote this script for my CS Graphics class so that we didn't have to only have 6 | cubes and spheres for models in order to learn WebGL. At the time, the only 7 | sort of model loader for WebGL was [Mr. Doob's ThreeJS](http://threejs.org/). And in order to use the 8 | loaders you had to use the entire framework (or do some very serious hacking 9 | and duct-taping in order get the model information). My main focus in creating 10 | this loader was to easily allow importing models without having to have special 11 | knowledge of a 3D graphics program (like Blender) while keeping it low-level 12 | enough so that the focus was on learning WebGL rather than learning some 13 | framework. 14 | 15 | ## Mesh(objStr) 16 | 17 | The main Mesh class. The constructor will parse through the OBJ file data 18 | and collect the vertex, vertex normal, texture, and face information. This 19 | information can then be used later on when creating your VBOs. Look at the 20 | `initMeshBuffers` source for an example of how to use the newly created Mesh 21 | 22 | ### Attributes: 23 | * **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. 24 | * 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. 25 | * **vertexNormals:** an array containing the vertex normals that correspond to each unique face index. It is flat, just like `vertices`. 26 | * **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. 27 | * **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. 28 | 29 | #### Element Index 30 | 31 | 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: 32 | 33 | ![obj loader element index description](https://user-images.githubusercontent.com/1094000/51001179-cf0fb680-14f4-11e9-8bd7-51d4807c3195.png) 34 | 35 | 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. 36 | 37 | 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 38 | in order to access an attribute for that index: 39 | 40 | ```javascript 41 | // there are 3 components for a geometric vertex: X, Y, and Z 42 | const NUM_COMPONENTS_FOR_VERTS = 3; 43 | elementIdx = mesh.indices[SOME_IDX]; // e.g. 38 44 | // in order to get the X component of the vertex component of element "38" 45 | elementVertX = mesh.vertices[(elementIdx * NUM_COMPONENTS_FOR_VERTS) + 0] 46 | // in order to get the Y component of the vertex component of element "38" 47 | elementVertY = mesh.vertices[(elementIdx * NUM_COMPONENTS_FOR_VERTS) + 1] 48 | // in order to get the Z component of the vertex component of element "38" 49 | elementVertZ = mesh.vertices[(elementIdx * NUM_COMPONENTS_FOR_VERTS) + 2] 50 | ``` 51 | 52 | ### Params: 53 | 54 | * **objStr** a string representation of an OBJ file with newlines preserved. 55 | 56 | A simple example: 57 | 58 | In your `index.html` file: 59 | ```html 60 | 61 | 62 | 96 | 97 | 98 | ``` 99 | And in your `app.js`: 100 | 101 | ```javascript 102 | var gl = canvas.getContext('webgl'); 103 | var objStr = document.getElementById('my_cube.obj').innerHTML; 104 | var mesh = new OBJ.Mesh(objStr); 105 | 106 | // use the included helper function to initialize the VBOs 107 | // if you don't want to use this function, have a look at its 108 | // source to see how to use the Mesh instance. 109 | OBJ.initMeshBuffers(gl, mesh); 110 | // have a look at the initMeshBuffers docs for an exmample of how to 111 | // render the model at this point 112 | 113 | ``` 114 | 115 | ## Some helper functions 116 | 117 | ### downloadMeshes(nameAndURLs, completionCallback, meshes) 118 | 119 | Takes in a JS Object of `mesh_name`, `'/url/to/OBJ/file'` pairs and a callback 120 | function. Each OBJ file will be ajaxed in and automatically converted to 121 | an OBJ.Mesh. When all files have successfully downloaded the callback 122 | function provided will be called and passed in an object containing 123 | the newly created meshes. 124 | 125 | **Note:** In order to use this function as a way to download meshes, a 126 | webserver of some sort must be used. 127 | 128 | #### Params: 129 | 130 | * **nameAndURLs:** an object where the key is the name of the mesh and the value is the url to that mesh's OBJ file 131 | 132 | * **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 133 | 134 | * **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 }` 135 | 136 | A simple example: 137 | 138 | 139 | ```javascript 140 | var app = {}; 141 | app.meshes = {}; 142 | 143 | var gl = document.getElementById('mycanvas').getContext('webgl'); 144 | 145 | function webGLStart(meshes){ 146 | app.meshes = meshes; 147 | // initialize the VBOs 148 | OBJ.initMeshBuffers(gl, app.meshes.suzanne); 149 | OBJ.initMeshBuffers(gl, app.meshes.sphere); 150 | ... other cool stuff ... 151 | // refer to the initMeshBuffers docs for an example of 152 | // how to render the mesh to the screen after calling 153 | // initMeshBuffers 154 | } 155 | 156 | window.onload = function(){ 157 | OBJ.downloadMeshes({ 158 | 'suzanne': 'models/suzanne.obj', // located in the models folder on the server 159 | 'sphere': 'models/sphere.obj' 160 | }, webGLStart); 161 | } 162 | ``` 163 | 164 | ### initMeshBuffers(gl, mesh) 165 | 166 | Takes in the WebGL context and a Mesh, then creates and appends the buffers 167 | to the mesh object as attributes. 168 | 169 | #### Params: 170 | 171 | * **gl** *WebGLRenderingContext* the `canvas.getContext('webgl')` context instance 172 | 173 | * **mesh** *Mesh* a single `OBJ.Mesh` instance 174 | 175 | The newly created mesh attributes are: 176 | 177 | Attrbute | Description 178 | :--- | --- 179 | **normalBuffer** |contains the model's Vertex Normals 180 | normalBuffer.itemSize |set to 3 items 181 | normalBuffer.numItems |the total number of vertex normals 182 | **textureBuffer** |contains the model's Texture Coordinates 183 | textureBuffer.itemSize |set to 2 items (or 3 if W texture coord is enabled) 184 | textureBuffer.numItems |the number of texture coordinates 185 | **vertexBuffer** |contains the model's Vertex Position Coordinates (does not include w) 186 | vertexBuffer.itemSize |set to 3 items 187 | vertexBuffer.numItems |the total number of vertices 188 | **indexBuffer** |contains the indices of the faces 189 | indexBuffer.itemSize |is set to 1 190 | indexBuffer.numItems |the total number of indices 191 | 192 | A simple example (a lot of steps are missing, so don't copy and paste): 193 | ```javascript 194 | var gl = canvas.getContext('webgl'), 195 | var mesh = new OBJ.Mesh(obj_file_data); 196 | // compile the shaders and create a shader program 197 | var shaderProgram = gl.createProgram(); 198 | // compilation stuff here 199 | ... 200 | // make sure you have vertex, vertex normal, and texture coordinate 201 | // attributes located in your shaders and attach them to the shader program 202 | shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); 203 | gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); 204 | 205 | shaderProgram.vertexNormalAttribute = gl.getAttribLocation(shaderProgram, "aVertexNormal"); 206 | gl.enableVertexAttribArray(shaderProgram.vertexNormalAttribute); 207 | 208 | shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord"); 209 | gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute); 210 | 211 | // create and initialize the vertex, vertex normal, and texture coordinate buffers 212 | // and save on to the mesh object 213 | OBJ.initMeshBuffers(gl, mesh); 214 | 215 | // now to render the mesh 216 | gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vertexBuffer); 217 | gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, mesh.vertexBuffer.itemSize, gl.FLOAT, false, 0, 0); 218 | 219 | // it's possible that the mesh doesn't contain 220 | // any texture coordinates (e.g. suzanne.obj in the development branch). 221 | // in this case, the texture vertexAttribArray will need to be disabled 222 | // before the call to drawElements 223 | if(!mesh.textures.length){ 224 | gl.disableVertexAttribArray(shaderProgram.textureCoordAttribute); 225 | } 226 | else{ 227 | // if the texture vertexAttribArray has been previously 228 | // disabled, then it needs to be re-enabled 229 | gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute); 230 | gl.bindBuffer(gl.ARRAY_BUFFER, mesh.textureBuffer); 231 | gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, mesh.textureBuffer.itemSize, gl.FLOAT, false, 0, 0); 232 | } 233 | 234 | gl.bindBuffer(gl.ARRAY_BUFFER, mesh.normalBuffer); 235 | gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, mesh.normalBuffer.itemSize, gl.FLOAT, false, 0, 0); 236 | 237 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.mesh.indexBuffer); 238 | gl.drawElements(gl.TRIANGLES, model.mesh.indexBuffer.numItems, gl.UNSIGNED_SHORT, 0); 239 | ``` 240 | 241 | ### deleteMeshBuffers(gl, mesh) 242 | Deletes the mesh's buffers, which you would do when deleting an object from a 243 | scene so that you don't leak video memory. Excessive buffer creation and 244 | deletion leads to video memory fragmentation. Beware. 245 | 246 | ## Node.js 247 | `npm install webgl-obj-loader` 248 | 249 | ```javascript 250 | var fs = require('fs'); 251 | var OBJ = require('webgl-obj-loader'); 252 | 253 | var meshPath = './development/models/sphere.obj'; 254 | var opt = { encoding: 'utf8' }; 255 | 256 | fs.readFile(meshPath, opt, function (err, data){ 257 | if (err) return console.error(err); 258 | var mesh = new OBJ.Mesh(data); 259 | }); 260 | ``` 261 | 262 | ## Webpack Support 263 | Thanks to [mentos1386](https://github.com/mentos1386) for the [webpack-obj-loader](https://github.com/mentos1386/webpack-obj-loader)! 264 | 265 | ## Demo 266 | http://frenchtoast747.github.com/webgl-obj-loader/ 267 | 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. 268 | 269 | ## ChangeLog 270 | **2.0.3** 271 | * Add simple support for N-Gons (thanks [qtip](https://github.com/qtip)!) 272 | * This uses a very elementary algorithm to triangulate N-gons, but should still produce a full mesh. 273 | * 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). 274 | 275 | **2.0.0** 276 | * Updated to TypeScript 277 | * Breaking change: the Mesh option `indicesPerMaterial` has been removed in favor of always providing the indices per material. 278 | * Instead of `mesh.indices` holding an array of arrays of numbers, `mesh.indicesPerMaterial` will now hold the indices where the top 279 | level array index is the index of the material and the inner arrays are the indices for that material. 280 | * Breaking change: the Layout class has changed from directly applying attributes to the Layout instance to creating an attributeMap 281 | 282 | **1.1.0** 283 | * Add Support for separating mesh indices by materials. 284 | * Add calculation for tangents and bitangents 285 | * Add runtime OBJ library version. 286 | 287 | **1.0.1** 288 | * Add support for 3D texture coordinates. By default the third texture 289 | coordinate, w, is truncated. Support can be enabled by passing 290 | `enableWTextureCoord: true` in the options parameter of the Mesh 291 | class. 292 | 293 | **1.0.0** 294 | * Modularized all of the source files into ES6 modules. 295 | * The Mesh, MaterialLibrary, and Material classes are now 296 | actual ES6 classes. 297 | * Added tests for each of the classes 298 | * Found a bug in the Mesh class. Vertex normals would not appear 299 | if the face declaration used the shorthand variant; e.g. `f 1/1` 300 | * Provided Initial MTL file parsing support. 301 | * Still requires Documentation. For now, have a look at the tests in the 302 | test directory for examples of use. 303 | * Use the new downloadModels() function in order to download the OBJ meshes 304 | complete with their MTL files attached. If the MTL files reference images, 305 | by default, those images will be downloaded and attached. 306 | * The downloading functions now use the new `fetch()` API which utilizes 307 | promises. 308 | 309 | **0.1.1** 310 | * Support for NodeJS. 311 | 312 | **0.1.0** 313 | * Dropped jQuery dependency: `downloadMeshes` no longer requires jQuery to ajax in the OBJ files. 314 | * changed namespace to something a little shorter: `OBJ` 315 | * Updated documentation 316 | 317 | **0.0.3** 318 | * Initial support for Quad models 319 | 320 | **0.0.2** 321 | * Texture Coordinates are now loaded into mesh.textures 322 | 323 | **0.0.1** 324 | * Vertex Normals are now loaded into mesh.vertexNormals 325 | 326 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/frenchtoast747/webgl-obj-loader/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 327 | -------------------------------------------------------------------------------- /development/app.js: -------------------------------------------------------------------------------- 1 | // WebGL context 2 | var gl = {}; 3 | // the canvas element 4 | var canvas = null; 5 | // main shader program 6 | var shaderProgram = null; 7 | // main app object 8 | var app = {}; 9 | app.meshes = {}; 10 | app.models = {}; 11 | app.mvMatrix = mat4.create(); 12 | app.mvMatrixStack = []; 13 | app.pMatrix = mat4.create(); 14 | 15 | window.requestAnimFrame = (function() { 16 | return ( 17 | window.requestAnimationFrame || 18 | window.webkitRequestAnimationFrame || 19 | window.mozRequestAnimationFrame || 20 | window.oRequestAnimationFrame || 21 | window.msRequestAnimationFrame || 22 | function(/* function FrameRequestCallback */ callback, /* DOMElement Element */ element) { 23 | return window.setTimeout(callback, 1000 / 60); 24 | } 25 | ); 26 | })(); 27 | 28 | function initWebGL(canvas) { 29 | try { 30 | // Try to grab the standard context. If it fails, fallback to experimental. 31 | gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); 32 | } catch (e) {} 33 | if (!gl) { 34 | alert("Unable to initialize WebGL. Your browser may not support it."); 35 | gl = null; 36 | } 37 | gl.viewportWidth = canvas.width; 38 | gl.viewportHeight = canvas.height; 39 | gl.viewport(0, 0, canvas.width, canvas.height); 40 | return gl; 41 | } 42 | 43 | function getShader(gl, id) { 44 | var shaderScript = document.getElementById(id); 45 | if (!shaderScript) { 46 | return null; 47 | } 48 | 49 | var str = ""; 50 | var k = shaderScript.firstChild; 51 | while (k) { 52 | if (k.nodeType == 3) { 53 | str += k.textContent; 54 | } 55 | k = k.nextSibling; 56 | } 57 | 58 | var shader; 59 | if (shaderScript.type == "x-shader/x-fragment") { 60 | shader = gl.createShader(gl.FRAGMENT_SHADER); 61 | } else if (shaderScript.type == "x-shader/x-vertex") { 62 | shader = gl.createShader(gl.VERTEX_SHADER); 63 | } else { 64 | return null; 65 | } 66 | 67 | gl.shaderSource(shader, str); 68 | gl.compileShader(shader); 69 | 70 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 71 | alert(gl.getShaderInfoLog(shader)); 72 | return null; 73 | } 74 | 75 | return shader; 76 | } 77 | 78 | function initShaders() { 79 | var fragmentShader = getShader(gl, "shader-fs"); 80 | var vertexShader = getShader(gl, "shader-vs"); 81 | 82 | shaderProgram = gl.createProgram(); 83 | gl.attachShader(shaderProgram, vertexShader); 84 | gl.attachShader(shaderProgram, fragmentShader); 85 | gl.linkProgram(shaderProgram); 86 | 87 | if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { 88 | alert("Could not initialise shaders"); 89 | } 90 | gl.useProgram(shaderProgram); 91 | 92 | const attrs = { 93 | aVertexPosition: OBJ.Layout.POSITION.key, 94 | aVertexNormal: OBJ.Layout.NORMAL.key, 95 | aTextureCoord: OBJ.Layout.UV.key, 96 | aDiffuse: OBJ.Layout.DIFFUSE.key, 97 | aSpecular: OBJ.Layout.SPECULAR.key, 98 | aSpecularExponent: OBJ.Layout.SPECULAR_EXPONENT.key 99 | }; 100 | 101 | shaderProgram.attrIndices = {}; 102 | for (const attrName in attrs) { 103 | if (!attrs.hasOwnProperty(attrName)) { 104 | continue; 105 | } 106 | shaderProgram.attrIndices[attrName] = gl.getAttribLocation(shaderProgram, attrName); 107 | if (shaderProgram.attrIndices[attrName] != -1) { 108 | gl.enableVertexAttribArray(shaderProgram.attrIndices[attrName]); 109 | } else { 110 | console.warn( 111 | 'Shader attribute "' + 112 | attrName + 113 | '" not found in shader. Is it undeclared or unused in the shader code?' 114 | ); 115 | } 116 | } 117 | 118 | shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix"); 119 | shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix"); 120 | shaderProgram.nMatrixUniform = gl.getUniformLocation(shaderProgram, "uNMatrix"); 121 | 122 | shaderProgram.applyAttributePointers = function(model) { 123 | const layout = model.mesh.vertexBuffer.layout; 124 | for (const attrName in attrs) { 125 | if (!attrs.hasOwnProperty(attrName) || shaderProgram.attrIndices[attrName] == -1) { 126 | continue; 127 | } 128 | const layoutKey = attrs[attrName]; 129 | if (shaderProgram.attrIndices[attrName] != -1) { 130 | const attr = layout.attributeMap[layoutKey]; 131 | gl.vertexAttribPointer( 132 | shaderProgram.attrIndices[attrName], 133 | attr.size, 134 | gl[attr.type], 135 | attr.normalized, 136 | attr.stride, 137 | attr.offset 138 | ); 139 | } 140 | } 141 | }; 142 | } 143 | 144 | function drawObject(model) { 145 | /* 146 | Takes in a model that points to a mesh and draws the object on the scene. 147 | Assumes that the setMatrixUniforms function exists 148 | as well as the shaderProgram has a uniform attribute called "samplerUniform" 149 | */ 150 | // gl.useProgram(shaderProgram); 151 | 152 | gl.bindBuffer(gl.ARRAY_BUFFER, model.mesh.vertexBuffer); 153 | shaderProgram.applyAttributePointers(model); 154 | 155 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.mesh.indexBuffer); 156 | setMatrixUniforms(); 157 | gl.drawElements(gl.TRIANGLES, model.mesh.indexBuffer.numItems, gl.UNSIGNED_SHORT, 0); 158 | } 159 | 160 | function mvPushMatrix() { 161 | var copy = mat4.create(); 162 | mat4.copy(copy, app.mvMatrix); 163 | app.mvMatrixStack.push(copy); 164 | } 165 | 166 | function mvPopMatrix() { 167 | if (app.mvMatrixStack.length === 0) { 168 | throw "Invalid popMatrix!"; 169 | } 170 | app.mvMatrix = app.mvMatrixStack.pop(); 171 | } 172 | 173 | function setMatrixUniforms() { 174 | gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, app.pMatrix); 175 | gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, app.mvMatrix); 176 | 177 | var normalMatrix = mat3.create(); 178 | mat3.normalFromMat4(normalMatrix, app.mvMatrix); 179 | gl.uniformMatrix3fv(shaderProgram.nMatrixUniform, false, normalMatrix); 180 | } 181 | 182 | function initBuffers() { 183 | var layout = new OBJ.Layout( 184 | OBJ.Layout.POSITION, 185 | OBJ.Layout.NORMAL, 186 | OBJ.Layout.DIFFUSE, 187 | OBJ.Layout.UV, 188 | OBJ.Layout.SPECULAR, 189 | OBJ.Layout.SPECULAR_EXPONENT 190 | ); 191 | 192 | // initialize the mesh's buffers 193 | for (var mesh in app.meshes) { 194 | // Create the vertex buffer for this mesh 195 | var vertexBuffer = gl.createBuffer(); 196 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); 197 | var vertexData = app.meshes[mesh].makeBufferData(layout); 198 | gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW); 199 | vertexBuffer.numItems = vertexData.numItems; 200 | vertexBuffer.layout = layout; 201 | app.meshes[mesh].vertexBuffer = vertexBuffer; 202 | 203 | // Create the index buffer for this mesh 204 | var indexBuffer = gl.createBuffer(); 205 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); 206 | var indexData = app.meshes[mesh].makeIndexBufferDataForMaterials(...Object.values(app.meshes[mesh].materialIndices)); 207 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW); 208 | indexBuffer.numItems = indexData.numItems; 209 | app.meshes[mesh].indexBuffer = indexBuffer; 210 | 211 | // this loops through the mesh names and creates new 212 | // model objects and setting their mesh to the current mesh 213 | app.models[mesh] = {}; 214 | app.models[mesh].mesh = app.meshes[mesh]; 215 | } 216 | } 217 | 218 | function animate() { 219 | app.timeNow = new Date().getTime(); 220 | app.elapsed = app.timeNow - app.lastTime; 221 | if (!app.time) { 222 | app.time = 0.0; 223 | } 224 | app.time += app.elapsed / 1000.0; 225 | if (app.lastTime !== 0) { 226 | // do animations 227 | } 228 | app.lastTime = app.timeNow; 229 | } 230 | 231 | function drawScene() { 232 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 233 | mat4.perspective(app.pMatrix, 45 * Math.PI / 180.0, gl.viewportWidth / gl.viewportHeight, 0.01, 1000.0); 234 | mat4.identity(app.mvMatrix); 235 | // move the camera 236 | mat4.translate(app.mvMatrix, app.mvMatrix, [0, 0, -5]); 237 | mat4.rotate(app.mvMatrix, app.mvMatrix, app.time * 0.25 * Math.PI, [0, 1, 0]); 238 | // set up the scene 239 | mvPushMatrix(); 240 | drawObject(app.models.suzanne); 241 | mvPopMatrix(); 242 | } 243 | 244 | function tick() { 245 | requestAnimFrame(tick); 246 | drawScene(); 247 | animate(); 248 | } 249 | 250 | function webGLStart(meshes) { 251 | app.meshes = meshes; 252 | canvas = document.getElementById("mycanvas"); 253 | gl = initWebGL(canvas); 254 | initShaders(); 255 | initBuffers(); 256 | gl.clearColor(0.5, 0.5, 0.5, 1.0); 257 | gl.enable(gl.DEPTH_TEST); 258 | 259 | tick(); 260 | // drawScene(); 261 | } 262 | 263 | window.onload = function() { 264 | // OBJ.downloadMeshes({ 265 | // 'suzanne': '/development/models/suzanne.obj' 266 | // }, webGLStart); 267 | let p = OBJ.downloadModels([ 268 | { 269 | name: "die", 270 | obj: "/development/models/die.obj", 271 | mtl: "/development/models/die.mtl" 272 | }, 273 | { 274 | obj: "/development/models/suzanne.obj", 275 | mtl: true 276 | } // , 277 | // { 278 | // obj: '/development/models/suzanne.obj' 279 | // } 280 | ]); 281 | 282 | p.then(models => { 283 | for ([name, mesh] of Object.entries(models)) { 284 | console.log("Name:", name); 285 | console.log("Mesh:", mesh); 286 | } 287 | webGLStart(models); 288 | }); 289 | }; 290 | -------------------------------------------------------------------------------- /development/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | webgl-obj-loader dev 5 | 6 | 28 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 | 67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /development/models/die.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frenchtoast747/webgl-obj-loader/8c03729f67f94c3b49ce15e5ca254cc9735934b7/development/models/die.blend -------------------------------------------------------------------------------- /development/models/die.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'die.blend' 2 | # Material Count: 6 3 | 4 | newmtl Face1 5 | Ns 96.078431 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.640000 0.640000 0.640000 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.000000 11 | d 1.000000 12 | illum 2 13 | map_Kd face1.bmp 14 | 15 | newmtl Face2 16 | Ns 96.078431 17 | Ka 1.000000 1.000000 1.000000 18 | Kd 0.640000 0.640000 0.640000 19 | Ks 0.500000 0.500000 0.500000 20 | Ke 0.000000 0.000000 0.000000 21 | Ni 1.000000 22 | d 1.000000 23 | illum 2 24 | map_Kd face2.bmp 25 | 26 | newmtl Face3 27 | Ns 96.078431 28 | Ka 1.000000 1.000000 1.000000 29 | Kd 0.640000 0.640000 0.640000 30 | Ks 0.500000 0.500000 0.500000 31 | Ke 0.000000 0.000000 0.000000 32 | Ni 1.000000 33 | d 1.000000 34 | illum 2 35 | map_Kd face3.bmp 36 | 37 | newmtl Face4 38 | Ns 96.078431 39 | Ka 1.000000 1.000000 1.000000 40 | Kd 0.640000 0.640000 0.640000 41 | Ks 0.500000 0.500000 0.500000 42 | Ke 0.000000 0.000000 0.000000 43 | Ni 1.000000 44 | d 1.000000 45 | illum 2 46 | map_Kd face4.bmp 47 | 48 | newmtl Face5 49 | Ns 96.078431 50 | Ka 1.000000 1.000000 1.000000 51 | Kd 0.640000 0.640000 0.640000 52 | Ks 0.500000 0.500000 0.500000 53 | Ke 0.000000 0.000000 0.000000 54 | Ni 1.000000 55 | d 1.000000 56 | illum 2 57 | map_Kd face5.bmp 58 | 59 | newmtl Face6 60 | Ns 96.078431 61 | Ka 1.000000 1.000000 1.000000 62 | Kd 0.640000 0.640000 0.640000 63 | Ks 0.500000 0.500000 0.500000 64 | Ke 0.000000 0.000000 0.000000 65 | Ni 1.000000 66 | d 1.000000 67 | illum 2 68 | map_Kd face6.bmp 69 | -------------------------------------------------------------------------------- /development/models/die.obj: -------------------------------------------------------------------------------- 1 | # Blender v2.76 (sub 0) OBJ File: 'die.blend' 2 | # www.blender.org 3 | mtllib die.mtl 4 | o Cube 5 | v 1.000000 -1.000000 -1.000000 6 | v 1.000000 -1.000000 1.000000 7 | v -1.000000 -1.000000 1.000000 8 | v -1.000000 -1.000000 -1.000000 9 | v 1.000000 1.000000 -0.999999 10 | v 0.999999 1.000000 1.000001 11 | v -1.000000 1.000000 1.000000 12 | v -1.000000 1.000000 -1.000000 13 | vt -0.002269 1.002269 14 | vt -0.002268 -0.002268 15 | vt 1.002269 -0.002269 16 | vt 1.002269 1.002268 17 | vt 1.000000 0.000000 18 | vt 1.000000 1.000000 19 | vt 0.000000 1.000000 20 | vt 0.000000 0.000000 21 | vn 1.000000 0.000000 0.000000 22 | vn 0.000000 1.000000 0.000000 23 | vn 0.000000 0.000000 -1.000000 24 | vn -0.000000 -0.000000 1.000000 25 | vn 0.000000 -1.000000 0.000000 26 | vn -1.000000 -0.000000 -0.000000 27 | usemtl Face1 28 | s off 29 | f 1/1/1 5/2/1 6/3/1 2/4/1 30 | usemtl Face2 31 | f 5/5/2 8/6/2 7/7/2 6/8/2 32 | usemtl Face3 33 | f 5/8/3 1/5/3 4/6/3 8/7/3 34 | usemtl Face4 35 | f 2/7/4 6/8/4 7/5/4 3/6/4 36 | usemtl Face5 37 | f 1/7/5 2/8/5 3/5/5 4/6/5 38 | usemtl Face6 39 | f 3/6/6 7/7/6 8/8/6 4/5/6 40 | -------------------------------------------------------------------------------- /development/models/face1.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frenchtoast747/webgl-obj-loader/8c03729f67f94c3b49ce15e5ca254cc9735934b7/development/models/face1.bmp -------------------------------------------------------------------------------- /development/models/face2.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frenchtoast747/webgl-obj-loader/8c03729f67f94c3b49ce15e5ca254cc9735934b7/development/models/face2.bmp -------------------------------------------------------------------------------- /development/models/face3.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frenchtoast747/webgl-obj-loader/8c03729f67f94c3b49ce15e5ca254cc9735934b7/development/models/face3.bmp -------------------------------------------------------------------------------- /development/models/face4.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frenchtoast747/webgl-obj-loader/8c03729f67f94c3b49ce15e5ca254cc9735934b7/development/models/face4.bmp -------------------------------------------------------------------------------- /development/models/face5.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frenchtoast747/webgl-obj-loader/8c03729f67f94c3b49ce15e5ca254cc9735934b7/development/models/face5.bmp -------------------------------------------------------------------------------- /development/models/face6.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frenchtoast747/webgl-obj-loader/8c03729f67f94c3b49ce15e5ca254cc9735934b7/development/models/face6.bmp -------------------------------------------------------------------------------- /development/models/suzanne.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'None' 2 | # Material Count: 2 3 | 4 | newmtl Gold 5 | Ns 10.0 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.000000 0.000000 0.000000 8 | Ks 1.000000 0.645042 0.125491 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.000000 11 | d 1.000000 12 | illum 2 13 | 14 | newmtl Plastic 15 | Ns 233.333333 16 | Ka 1.000000 1.000000 1.000000 17 | Kd 0.800000 0.000000 0.002118 18 | Ks 1.000000 1.000000 1.000000 19 | Ke 0.000000 0.000000 0.000000 20 | Ni 1.000000 21 | d 1.000000 22 | illum 2 23 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import Mesh, { MeshOptions, MaterialNameToIndex, IndexToMaterial, ArrayBufferWithItemSize, Uint16ArrayWithItemSize } from "./mesh"; 2 | import { Material, MaterialLibrary, Vec3, UVW, TextureMapData } from "./material"; 3 | import { Layout, TYPES, AttributeInfo, DuplicateAttributeException, Attribute } from "./layout"; 4 | import { downloadModels, downloadMeshes, initMeshBuffers, deleteMeshBuffers, DownloadModelsOptions, MeshMap, NameAndUrls, ExtendedGLBuffer, MeshWithBuffers } from "./utils"; 5 | declare const version = "2.0.3"; 6 | export declare const OBJ: { 7 | Attribute: typeof Attribute; 8 | DuplicateAttributeException: typeof DuplicateAttributeException; 9 | Layout: typeof Layout; 10 | Material: typeof Material; 11 | MaterialLibrary: typeof MaterialLibrary; 12 | Mesh: typeof Mesh; 13 | TYPES: typeof TYPES; 14 | downloadModels: typeof downloadModels; 15 | downloadMeshes: typeof downloadMeshes; 16 | initMeshBuffers: typeof initMeshBuffers; 17 | deleteMeshBuffers: typeof deleteMeshBuffers; 18 | version: string; 19 | }; 20 | /** 21 | * @namespace 22 | */ 23 | export { ArrayBufferWithItemSize, Attribute, AttributeInfo, DownloadModelsOptions, DuplicateAttributeException, ExtendedGLBuffer, IndexToMaterial, Layout, Material, MaterialLibrary, MaterialNameToIndex, Mesh, MeshMap, MeshOptions, MeshWithBuffers, NameAndUrls, TextureMapData, TYPES, Uint16ArrayWithItemSize, UVW, Vec3, downloadModels, downloadMeshes, initMeshBuffers, deleteMeshBuffers, version, }; 24 | -------------------------------------------------------------------------------- /dist/layout.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum TYPES { 2 | "BYTE" = "BYTE", 3 | "UNSIGNED_BYTE" = "UNSIGNED_BYTE", 4 | "SHORT" = "SHORT", 5 | "UNSIGNED_SHORT" = "UNSIGNED_SHORT", 6 | "FLOAT" = "FLOAT" 7 | } 8 | export interface AttributeInfo { 9 | attribute: Attribute; 10 | size: Attribute["size"]; 11 | type: Attribute["type"]; 12 | normalized: Attribute["normalized"]; 13 | offset: number; 14 | stride: number; 15 | } 16 | /** 17 | * An exception for when two or more of the same attributes are found in the 18 | * same layout. 19 | * @private 20 | */ 21 | export declare class DuplicateAttributeException extends Error { 22 | /** 23 | * Create a DuplicateAttributeException 24 | * @param {Attribute} attribute - The attribute that was found more than 25 | * once in the {@link Layout} 26 | */ 27 | constructor(attribute: Attribute); 28 | } 29 | /** 30 | * Represents how a vertex attribute should be packed into an buffer. 31 | * @private 32 | */ 33 | export declare class Attribute { 34 | key: string; 35 | size: number; 36 | type: TYPES; 37 | normalized: boolean; 38 | sizeOfType: number; 39 | sizeInBytes: number; 40 | /** 41 | * Create an attribute. Do not call this directly, use the predefined 42 | * constants. 43 | * @param {string} key - The name of this attribute as if it were a key in 44 | * an Object. Use the camel case version of the upper snake case 45 | * const name. 46 | * @param {number} size - The number of components per vertex attribute. 47 | * Must be 1, 2, 3, or 4. 48 | * @param {string} type - The data type of each component for this 49 | * attribute. Possible values:
50 | * "BYTE": signed 8-bit integer, with values in [-128, 127]
51 | * "SHORT": signed 16-bit integer, with values in 52 | * [-32768, 32767]
53 | * "UNSIGNED_BYTE": unsigned 8-bit integer, with values in 54 | * [0, 255]
55 | * "UNSIGNED_SHORT": unsigned 16-bit integer, with values in 56 | * [0, 65535]
57 | * "FLOAT": 32-bit floating point number 58 | * @param {boolean} normalized - Whether integer data values should be 59 | * normalized when being casted to a float.
60 | * If true, signed integers are normalized to [-1, 1].
61 | * If true, unsigned integers are normalized to [0, 1].
62 | * For type "FLOAT", this parameter has no effect. 63 | */ 64 | constructor(key: string, size: number, type: TYPES, normalized?: boolean); 65 | } 66 | /** 67 | * A class to represent the memory layout for a vertex attribute array. Used by 68 | * {@link Mesh}'s TBD(...) method to generate a packed array from mesh data. 69 | *

70 | * Layout can sort of be thought of as a C-style struct declaration. 71 | * {@link Mesh}'s TBD(...) method will use the {@link Layout} instance to 72 | * pack an array in the given attribute order. 73 | *

74 | * Layout also is very helpful when calling a WebGL context's 75 | * vertexAttribPointer method. If you've created a buffer using 76 | * a Layout instance, then the same Layout instance can be used to determine 77 | * the size, type, normalized, stride, and offset parameters for 78 | * vertexAttribPointer. 79 | *

80 | * For example: 81 | *


 82 |  *
 83 |  * const index = glctx.getAttribLocation(shaderProgram, "pos");
 84 |  * glctx.vertexAttribPointer(
 85 |  *   layout.position.size,
 86 |  *   glctx[layout.position.type],
 87 |  *   layout.position.normalized,
 88 |  *   layout.position.stride,
 89 |  *   layout.position.offset);
 90 |  * 
91 | * @see {@link Mesh} 92 | */ 93 | export declare class Layout { 94 | /** 95 | * Attribute layout to pack a vertex's x, y, & z as floats 96 | * 97 | * @see {@link Layout} 98 | */ 99 | static POSITION: Attribute; 100 | /** 101 | * Attribute layout to pack a vertex's normal's x, y, & z as floats 102 | * 103 | * @see {@link Layout} 104 | */ 105 | static NORMAL: Attribute; 106 | /** 107 | * Attribute layout to pack a vertex's normal's x, y, & z as floats. 108 | *

109 | * This value will be computed on-the-fly based on the texture coordinates. 110 | * If no texture coordinates are available, the generated value will default to 111 | * 0, 0, 0. 112 | * 113 | * @see {@link Layout} 114 | */ 115 | static TANGENT: Attribute; 116 | /** 117 | * Attribute layout to pack a vertex's normal's bitangent x, y, & z as floats. 118 | *

119 | * This value will be computed on-the-fly based on the texture coordinates. 120 | * If no texture coordinates are available, the generated value will default to 121 | * 0, 0, 0. 122 | * @see {@link Layout} 123 | */ 124 | static BITANGENT: Attribute; 125 | /** 126 | * Attribute layout to pack a vertex's texture coordinates' u & v as floats 127 | * 128 | * @see {@link Layout} 129 | */ 130 | static UV: Attribute; 131 | /** 132 | * Attribute layout to pack an unsigned short to be interpreted as a the index 133 | * into a {@link Mesh}'s materials list. 134 | *

135 | * The intention of this value is to send all of the {@link Mesh}'s materials 136 | * into multiple shader uniforms and then reference the current one by this 137 | * vertex attribute. 138 | *

139 | * example glsl code: 140 | * 141 | *


142 |      *  // this is bound using MATERIAL_INDEX
143 |      *  attribute int materialIndex;
144 |      *
145 |      *  struct Material {
146 |      *    vec3 diffuse;
147 |      *    vec3 specular;
148 |      *    vec3 specularExponent;
149 |      *  };
150 |      *
151 |      *  uniform Material materials[MAX_MATERIALS];
152 |      *
153 |      *  // ...
154 |      *
155 |      *  vec3 diffuse = materials[materialIndex];
156 |      *
157 |      * 
158 | * TODO: More description & test to make sure subscripting by attributes even 159 | * works for webgl 160 | * 161 | * @see {@link Layout} 162 | */ 163 | static MATERIAL_INDEX: Attribute; 164 | static MATERIAL_ENABLED: Attribute; 165 | static AMBIENT: Attribute; 166 | static DIFFUSE: Attribute; 167 | static SPECULAR: Attribute; 168 | static SPECULAR_EXPONENT: Attribute; 169 | static EMISSIVE: Attribute; 170 | static TRANSMISSION_FILTER: Attribute; 171 | static DISSOLVE: Attribute; 172 | static ILLUMINATION: Attribute; 173 | static REFRACTION_INDEX: Attribute; 174 | static SHARPNESS: Attribute; 175 | static MAP_DIFFUSE: Attribute; 176 | static MAP_AMBIENT: Attribute; 177 | static MAP_SPECULAR: Attribute; 178 | static MAP_SPECULAR_EXPONENT: Attribute; 179 | static MAP_DISSOLVE: Attribute; 180 | static ANTI_ALIASING: Attribute; 181 | static MAP_BUMP: Attribute; 182 | static MAP_DISPLACEMENT: Attribute; 183 | static MAP_DECAL: Attribute; 184 | static MAP_EMISSIVE: Attribute; 185 | stride: number; 186 | attributes: Attribute[]; 187 | attributeMap: { 188 | [idx: string]: AttributeInfo; 189 | }; 190 | /** 191 | * Create a Layout object. This constructor will throw if any duplicate 192 | * attributes are given. 193 | * @param {Array} ...attributes - An ordered list of attributes that 194 | * describe the desired memory layout for each vertex attribute. 195 | *

196 | * 197 | * @see {@link Mesh} 198 | */ 199 | constructor(...attributes: Attribute[]); 200 | } 201 | -------------------------------------------------------------------------------- /dist/material.d.ts: -------------------------------------------------------------------------------- 1 | export declare type Vec3 = [number, number, number]; 2 | export interface UVW { 3 | u: number; 4 | v: number; 5 | w: number; 6 | } 7 | export interface TextureMapData { 8 | colorCorrection: boolean; 9 | horizontalBlending: boolean; 10 | verticalBlending: boolean; 11 | boostMipMapSharpness: number; 12 | modifyTextureMap: { 13 | brightness: number; 14 | contrast: number; 15 | }; 16 | offset: UVW; 17 | scale: UVW; 18 | turbulence: UVW; 19 | clamp: boolean; 20 | textureResolution: number | null; 21 | bumpMultiplier: number; 22 | imfChan: string | null; 23 | filename: string; 24 | reflectionType?: string; 25 | texture?: HTMLImageElement; 26 | } 27 | /** 28 | * The Material class. 29 | */ 30 | export declare class Material { 31 | name: string; 32 | /** 33 | * Constructor 34 | * @param {String} name the unique name of the material 35 | */ 36 | ambient: Vec3; 37 | diffuse: Vec3; 38 | specular: Vec3; 39 | emissive: Vec3; 40 | transmissionFilter: Vec3; 41 | dissolve: number; 42 | specularExponent: number; 43 | transparency: number; 44 | illumination: number; 45 | refractionIndex: number; 46 | sharpness: number; 47 | mapDiffuse: TextureMapData; 48 | mapAmbient: TextureMapData; 49 | mapSpecular: TextureMapData; 50 | mapSpecularExponent: TextureMapData; 51 | mapDissolve: TextureMapData; 52 | antiAliasing: boolean; 53 | mapBump: TextureMapData; 54 | mapDisplacement: TextureMapData; 55 | mapDecal: TextureMapData; 56 | mapEmissive: TextureMapData; 57 | mapReflections: TextureMapData[]; 58 | constructor(name: string); 59 | } 60 | /** 61 | * https://en.wikipedia.org/wiki/Wavefront_.obj_file 62 | * http://paulbourke.net/dataformats/mtl/ 63 | */ 64 | export declare class MaterialLibrary { 65 | data: string; 66 | /** 67 | * Constructs the Material Parser 68 | * @param mtlData the MTL file contents 69 | */ 70 | currentMaterial: Material; 71 | materials: { 72 | [k: string]: Material; 73 | }; 74 | constructor(data: string); 75 | /** 76 | * Creates a new Material object and adds to the registry. 77 | * @param tokens the tokens associated with the directive 78 | */ 79 | parse_newmtl(tokens: string[]): void; 80 | /** 81 | * See the documenation for parse_Ka below for a better understanding. 82 | * 83 | * Given a list of possible color tokens, returns an array of R, G, and B 84 | * color values. 85 | * 86 | * @param tokens the tokens associated with the directive 87 | * @return {*} a 3 element array containing the R, G, and B values 88 | * of the color. 89 | */ 90 | parseColor(tokens: string[]): Vec3; 91 | /** 92 | * Parse the ambient reflectivity 93 | * 94 | * A Ka directive can take one of three forms: 95 | * - Ka r g b 96 | * - Ka spectral file.rfl 97 | * - Ka xyz x y z 98 | * These three forms are mutually exclusive in that only one 99 | * declaration can exist per material. It is considered a syntax 100 | * error otherwise. 101 | * 102 | * The "Ka" form specifies the ambient reflectivity using RGB values. 103 | * The "g" and "b" values are optional. If only the "r" value is 104 | * specified, then the "g" and "b" values are assigned the value of 105 | * "r". Values are normally in the range 0.0 to 1.0. Values outside 106 | * of this range increase or decrease the reflectivity accordingly. 107 | * 108 | * The "Ka spectral" form specifies the ambient reflectivity using a 109 | * spectral curve. "file.rfl" is the name of the ".rfl" file containing 110 | * the curve data. "factor" is an optional argument which is a multiplier 111 | * for the values in the .rfl file and defaults to 1.0 if not specified. 112 | * 113 | * The "Ka xyz" form specifies the ambient reflectivity using CIEXYZ values. 114 | * "x y z" are the values of the CIEXYZ color space. The "y" and "z" arguments 115 | * are optional and take on the value of the "x" component if only "x" is 116 | * specified. The "x y z" values are normally in the range of 0.0 to 1.0 and 117 | * increase or decrease ambient reflectivity accordingly outside of that 118 | * range. 119 | * 120 | * @param tokens the tokens associated with the directive 121 | */ 122 | parse_Ka(tokens: string[]): void; 123 | /** 124 | * Diffuse Reflectivity 125 | * 126 | * Similar to the Ka directive. Simply replace "Ka" with "Kd" and the rules 127 | * are the same 128 | * 129 | * @param tokens the tokens associated with the directive 130 | */ 131 | parse_Kd(tokens: string[]): void; 132 | /** 133 | * Spectral Reflectivity 134 | * 135 | * Similar to the Ka directive. Simply replace "Ks" with "Kd" and the rules 136 | * are the same 137 | * 138 | * @param tokens the tokens associated with the directive 139 | */ 140 | parse_Ks(tokens: string[]): void; 141 | /** 142 | * Emissive 143 | * 144 | * The amount and color of light emitted by the object. 145 | * 146 | * @param tokens the tokens associated with the directive 147 | */ 148 | parse_Ke(tokens: string[]): void; 149 | /** 150 | * Transmission Filter 151 | * 152 | * Any light passing through the object is filtered by the transmission 153 | * filter, which only allows specific colors to pass through. For example, Tf 154 | * 0 1 0 allows all of the green to pass through and filters out all of the 155 | * red and blue. 156 | * 157 | * Similar to the Ka directive. Simply replace "Ks" with "Tf" and the rules 158 | * are the same 159 | * 160 | * @param tokens the tokens associated with the directive 161 | */ 162 | parse_Tf(tokens: string[]): void; 163 | /** 164 | * Specifies the dissolve for the current material. 165 | * 166 | * Statement: d [-halo] `factor` 167 | * 168 | * Example: "d 0.5" 169 | * 170 | * The factor is the amount this material dissolves into the background. A 171 | * factor of 1.0 is fully opaque. This is the default when a new material is 172 | * created. A factor of 0.0 is fully dissolved (completely transparent). 173 | * 174 | * Unlike a real transparent material, the dissolve does not depend upon 175 | * material thickness nor does it have any spectral character. Dissolve works 176 | * on all illumination models. 177 | * 178 | * The dissolve statement allows for an optional "-halo" flag which indicates 179 | * that a dissolve is dependent on the surface orientation relative to the 180 | * viewer. For example, a sphere with the following dissolve, "d -halo 0.0", 181 | * will be fully dissolved at its center and will appear gradually more opaque 182 | * toward its edge. 183 | * 184 | * "factor" is the minimum amount of dissolve applied to the material. The 185 | * amount of dissolve will vary between 1.0 (fully opaque) and the specified 186 | * "factor". The formula is: 187 | * 188 | * dissolve = 1.0 - (N*v)(1.0-factor) 189 | * 190 | * @param tokens the tokens associated with the directive 191 | */ 192 | parse_d(tokens: string[]): void; 193 | /** 194 | * The "illum" statement specifies the illumination model to use in the 195 | * material. Illumination models are mathematical equations that represent 196 | * various material lighting and shading effects. 197 | * 198 | * The illumination number can be a number from 0 to 10. The following are 199 | * the list of illumination enumerations and their summaries: 200 | * 0. Color on and Ambient off 201 | * 1. Color on and Ambient on 202 | * 2. Highlight on 203 | * 3. Reflection on and Ray trace on 204 | * 4. Transparency: Glass on, Reflection: Ray trace on 205 | * 5. Reflection: Fresnel on and Ray trace on 206 | * 6. Transparency: Refraction on, Reflection: Fresnel off and Ray trace on 207 | * 7. Transparency: Refraction on, Reflection: Fresnel on and Ray trace on 208 | * 8. Reflection on and Ray trace off 209 | * 9. Transparency: Glass on, Reflection: Ray trace off 210 | * 10. Casts shadows onto invisible surfaces 211 | * 212 | * Example: "illum 2" to specify the "Highlight on" model 213 | * 214 | * @param tokens the tokens associated with the directive 215 | */ 216 | parse_illum(tokens: string[]): void; 217 | /** 218 | * Optical Density (AKA Index of Refraction) 219 | * 220 | * Statement: Ni `index` 221 | * 222 | * Example: Ni 1.0 223 | * 224 | * Specifies the optical density for the surface. `index` is the value 225 | * for the optical density. The values can range from 0.001 to 10. A value of 226 | * 1.0 means that light does not bend as it passes through an object. 227 | * Increasing the optical_density increases the amount of bending. Glass has 228 | * an index of refraction of about 1.5. Values of less than 1.0 produce 229 | * bizarre results and are not recommended 230 | * 231 | * @param tokens the tokens associated with the directive 232 | */ 233 | parse_Ni(tokens: string[]): void; 234 | /** 235 | * Specifies the specular exponent for the current material. This defines the 236 | * focus of the specular highlight. 237 | * 238 | * Statement: Ns `exponent` 239 | * 240 | * Example: "Ns 250" 241 | * 242 | * `exponent` is the value for the specular exponent. A high exponent results 243 | * in a tight, concentrated highlight. Ns Values normally range from 0 to 244 | * 1000. 245 | * 246 | * @param tokens the tokens associated with the directive 247 | */ 248 | parse_Ns(tokens: string[]): void; 249 | /** 250 | * Specifies the sharpness of the reflections from the local reflection map. 251 | * 252 | * Statement: sharpness `value` 253 | * 254 | * Example: "sharpness 100" 255 | * 256 | * If a material does not have a local reflection map defined in its material 257 | * defintions, sharpness will apply to the global reflection map defined in 258 | * PreView. 259 | * 260 | * `value` can be a number from 0 to 1000. The default is 60. A high value 261 | * results in a clear reflection of objects in the reflection map. 262 | * 263 | * Tip: sharpness values greater than 100 introduce aliasing effects in 264 | * flat surfaces that are viewed at a sharp angle. 265 | * 266 | * @param tokens the tokens associated with the directive 267 | */ 268 | parse_sharpness(tokens: string[]): void; 269 | /** 270 | * Parses the -cc flag and updates the options object with the values. 271 | * 272 | * @param values the values passed to the -cc flag 273 | * @param options the Object of all image options 274 | */ 275 | parse_cc(values: string[], options: TextureMapData): void; 276 | /** 277 | * Parses the -blendu flag and updates the options object with the values. 278 | * 279 | * @param values the values passed to the -blendu flag 280 | * @param options the Object of all image options 281 | */ 282 | parse_blendu(values: string[], options: TextureMapData): void; 283 | /** 284 | * Parses the -blendv flag and updates the options object with the values. 285 | * 286 | * @param values the values passed to the -blendv flag 287 | * @param options the Object of all image options 288 | */ 289 | parse_blendv(values: string[], options: TextureMapData): void; 290 | /** 291 | * Parses the -boost flag and updates the options object with the values. 292 | * 293 | * @param values the values passed to the -boost flag 294 | * @param options the Object of all image options 295 | */ 296 | parse_boost(values: string[], options: TextureMapData): void; 297 | /** 298 | * Parses the -mm flag and updates the options object with the values. 299 | * 300 | * @param values the values passed to the -mm flag 301 | * @param options the Object of all image options 302 | */ 303 | parse_mm(values: string[], options: TextureMapData): void; 304 | /** 305 | * Parses and sets the -o, -s, and -t u, v, and w values 306 | * 307 | * @param values the values passed to the -o, -s, -t flag 308 | * @param {Object} option the Object of either the -o, -s, -t option 309 | * @param {Integer} defaultValue the Object of all image options 310 | */ 311 | parse_ost(values: string[], option: UVW, defaultValue: number): void; 312 | /** 313 | * Parses the -o flag and updates the options object with the values. 314 | * 315 | * @param values the values passed to the -o flag 316 | * @param options the Object of all image options 317 | */ 318 | parse_o(values: string[], options: TextureMapData): void; 319 | /** 320 | * Parses the -s flag and updates the options object with the values. 321 | * 322 | * @param values the values passed to the -s flag 323 | * @param options the Object of all image options 324 | */ 325 | parse_s(values: string[], options: TextureMapData): void; 326 | /** 327 | * Parses the -t flag and updates the options object with the values. 328 | * 329 | * @param values the values passed to the -t flag 330 | * @param options the Object of all image options 331 | */ 332 | parse_t(values: string[], options: TextureMapData): void; 333 | /** 334 | * Parses the -texres flag and updates the options object with the values. 335 | * 336 | * @param values the values passed to the -texres flag 337 | * @param options the Object of all image options 338 | */ 339 | parse_texres(values: string[], options: TextureMapData): void; 340 | /** 341 | * Parses the -clamp flag and updates the options object with the values. 342 | * 343 | * @param values the values passed to the -clamp flag 344 | * @param options the Object of all image options 345 | */ 346 | parse_clamp(values: string[], options: TextureMapData): void; 347 | /** 348 | * Parses the -bm flag and updates the options object with the values. 349 | * 350 | * @param values the values passed to the -bm flag 351 | * @param options the Object of all image options 352 | */ 353 | parse_bm(values: string[], options: TextureMapData): void; 354 | /** 355 | * Parses the -imfchan flag and updates the options object with the values. 356 | * 357 | * @param values the values passed to the -imfchan flag 358 | * @param options the Object of all image options 359 | */ 360 | parse_imfchan(values: string[], options: TextureMapData): void; 361 | /** 362 | * This only exists for relection maps and denotes the type of reflection. 363 | * 364 | * @param values the values passed to the -type flag 365 | * @param options the Object of all image options 366 | */ 367 | parse_type(values: string[], options: TextureMapData): void; 368 | /** 369 | * Parses the texture's options and returns an options object with the info 370 | * 371 | * @param tokens all of the option tokens to pass to the texture 372 | * @return {Object} a complete object of objects to apply to the texture 373 | */ 374 | parseOptions(tokens: string[]): TextureMapData; 375 | /** 376 | * Parses the given texture map line. 377 | * 378 | * @param tokens all of the tokens representing the texture 379 | * @return a complete object of objects to apply to the texture 380 | */ 381 | parseMap(tokens: string[]): TextureMapData; 382 | /** 383 | * Parses the ambient map. 384 | * 385 | * @param tokens list of tokens for the map_Ka direcive 386 | */ 387 | parse_map_Ka(tokens: string[]): void; 388 | /** 389 | * Parses the diffuse map. 390 | * 391 | * @param tokens list of tokens for the map_Kd direcive 392 | */ 393 | parse_map_Kd(tokens: string[]): void; 394 | /** 395 | * Parses the specular map. 396 | * 397 | * @param tokens list of tokens for the map_Ks direcive 398 | */ 399 | parse_map_Ks(tokens: string[]): void; 400 | /** 401 | * Parses the emissive map. 402 | * 403 | * @param tokens list of tokens for the map_Ke direcive 404 | */ 405 | parse_map_Ke(tokens: string[]): void; 406 | /** 407 | * Parses the specular exponent map. 408 | * 409 | * @param tokens list of tokens for the map_Ns direcive 410 | */ 411 | parse_map_Ns(tokens: string[]): void; 412 | /** 413 | * Parses the dissolve map. 414 | * 415 | * @param tokens list of tokens for the map_d direcive 416 | */ 417 | parse_map_d(tokens: string[]): void; 418 | /** 419 | * Parses the anti-aliasing option. 420 | * 421 | * @param tokens list of tokens for the map_aat direcive 422 | */ 423 | parse_map_aat(tokens: string[]): void; 424 | /** 425 | * Parses the bump map. 426 | * 427 | * @param tokens list of tokens for the map_bump direcive 428 | */ 429 | parse_map_bump(tokens: string[]): void; 430 | /** 431 | * Parses the bump map. 432 | * 433 | * @param tokens list of tokens for the bump direcive 434 | */ 435 | parse_bump(tokens: string[]): void; 436 | /** 437 | * Parses the disp map. 438 | * 439 | * @param tokens list of tokens for the disp direcive 440 | */ 441 | parse_disp(tokens: string[]): void; 442 | /** 443 | * Parses the decal map. 444 | * 445 | * @param tokens list of tokens for the map_decal direcive 446 | */ 447 | parse_decal(tokens: string[]): void; 448 | /** 449 | * Parses the refl map. 450 | * 451 | * @param tokens list of tokens for the refl direcive 452 | */ 453 | parse_refl(tokens: string[]): void; 454 | /** 455 | * Parses the MTL file. 456 | * 457 | * Iterates line by line parsing each MTL directive. 458 | * 459 | * This function expects the first token in the line 460 | * to be a valid MTL directive. That token is then used 461 | * to try and run a method on this class. parse_[directive] 462 | * E.g., the `newmtl` directive would try to call the method 463 | * parse_newmtl. Each parsing function takes in the remaining 464 | * list of tokens and updates the currentMaterial class with 465 | * the attributes provided. 466 | */ 467 | parse(): void; 468 | } 469 | -------------------------------------------------------------------------------- /dist/mesh.d.ts: -------------------------------------------------------------------------------- 1 | import { Layout } from "./layout"; 2 | import { Material, MaterialLibrary } from "./material"; 3 | export interface MeshOptions { 4 | enableWTextureCoord?: boolean; 5 | calcTangentsAndBitangents?: boolean; 6 | materials?: { 7 | [key: string]: Material; 8 | }; 9 | } 10 | export interface MaterialNameToIndex { 11 | [k: string]: number; 12 | } 13 | export interface IndexToMaterial { 14 | [k: number]: Material; 15 | } 16 | export interface ArrayBufferWithItemSize extends ArrayBuffer { 17 | numItems?: number; 18 | } 19 | export interface Uint16ArrayWithItemSize extends Uint16Array { 20 | numItems?: number; 21 | } 22 | /** 23 | * The main Mesh class. The constructor will parse through the OBJ file data 24 | * and collect the vertex, vertex normal, texture, and face information. This 25 | * information can then be used later on when creating your VBOs. See 26 | * OBJ.initMeshBuffers for an example of how to use the newly created Mesh 27 | */ 28 | export default class Mesh { 29 | vertices: number[]; 30 | vertexNormals: number[]; 31 | textures: number[]; 32 | indices: number[]; 33 | name: string; 34 | vertexMaterialIndices: number[]; 35 | indicesPerMaterial: number[][]; 36 | materialNames: string[]; 37 | materialIndices: MaterialNameToIndex; 38 | materialsByIndex: IndexToMaterial; 39 | tangents: number[]; 40 | bitangents: number[]; 41 | textureStride: number; 42 | /** 43 | * Create a Mesh 44 | * @param {String} objectData - a string representation of an OBJ file with 45 | * newlines preserved. 46 | * @param {Object} options - a JS object containing valid options. See class 47 | * documentation for options. 48 | * @param {bool} options.enableWTextureCoord - Texture coordinates can have 49 | * an optional "w" coordinate after the u and v coordinates. This extra 50 | * value can be used in order to perform fancy transformations on the 51 | * textures themselves. Default is to truncate to only the u an v 52 | * coordinates. Passing true will provide a default value of 0 in the 53 | * event that any or all texture coordinates don't provide a w value. 54 | * Always use the textureStride attribute in order to determine the 55 | * stride length of the texture coordinates when rendering the element 56 | * array. 57 | * @param {bool} options.calcTangentsAndBitangents - Calculate the tangents 58 | * and bitangents when loading of the OBJ is completed. This adds two new 59 | * attributes to the Mesh instance: `tangents` and `bitangents`. 60 | */ 61 | constructor(objectData: string, options?: MeshOptions); 62 | /** 63 | * Calculates the tangents and bitangents of the mesh that forms an orthogonal basis together with the 64 | * normal in the direction of the texture coordinates. These are useful for setting up the TBN matrix 65 | * when distorting the normals through normal maps. 66 | * Method derived from: http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/ 67 | * 68 | * This method requires the normals and texture coordinates to be parsed and set up correctly. 69 | * Adds the tangents and bitangents as members of the class instance. 70 | */ 71 | calculateTangentsAndBitangents(): void; 72 | /** 73 | * @param layout - A {@link Layout} object that describes the 74 | * desired memory layout of the generated buffer 75 | * @return The packed array in the ... TODO 76 | */ 77 | makeBufferData(layout: Layout): ArrayBufferWithItemSize; 78 | makeIndexBufferData(): Uint16ArrayWithItemSize; 79 | makeIndexBufferDataForMaterials(...materialIndices: Array): Uint16ArrayWithItemSize; 80 | addMaterialLibrary(mtl: MaterialLibrary): void; 81 | } 82 | -------------------------------------------------------------------------------- /dist/utils.d.ts: -------------------------------------------------------------------------------- 1 | import Mesh from "./mesh"; 2 | export interface DownloadModelsOptions { 3 | obj: string; 4 | mtl?: boolean | string; 5 | downloadMtlTextures?: boolean; 6 | mtlTextureRoot?: string; 7 | name?: string; 8 | indicesPerMaterial?: boolean; 9 | calcTangentsAndBitangents?: boolean; 10 | } 11 | export declare type MeshMap = { 12 | [name: string]: Mesh; 13 | }; 14 | /** 15 | * Accepts a list of model request objects and returns a Promise that 16 | * resolves when all models have been downloaded and parsed. 17 | * 18 | * The list of model objects follow this interface: 19 | * { 20 | * obj: 'path/to/model.obj', 21 | * mtl: true | 'path/to/model.mtl', 22 | * downloadMtlTextures: true | false 23 | * mtlTextureRoot: '/models/suzanne/maps' 24 | * name: 'suzanne' 25 | * } 26 | * 27 | * The `obj` attribute is required and should be the path to the 28 | * model's .obj file relative to the current repo (absolute URLs are 29 | * suggested). 30 | * 31 | * The `mtl` attribute is optional and can either be a boolean or 32 | * a path to the model's .mtl file relative to the current URL. If 33 | * the value is `true`, then the path and basename given for the `obj` 34 | * attribute is used replacing the .obj suffix for .mtl 35 | * E.g.: {obj: 'models/foo.obj', mtl: true} would search for 'models/foo.mtl' 36 | * 37 | * The `name` attribute is optional and is a human friendly name to be 38 | * included with the parsed OBJ and MTL files. If not given, the base .obj 39 | * filename will be used. 40 | * 41 | * The `downloadMtlTextures` attribute is a flag for automatically downloading 42 | * any images found in the MTL file and attaching them to each Material 43 | * created from that file. For example, if material.mapDiffuse is set (there 44 | * was data in the MTL file), then material.mapDiffuse.texture will contain 45 | * the downloaded image. This option defaults to `true`. By default, the MTL's 46 | * URL will be used to determine the location of the images. 47 | * 48 | * The `mtlTextureRoot` attribute is optional and should point to the location 49 | * on the server that this MTL's texture files are located. The default is to 50 | * use the MTL file's location. 51 | * 52 | * @returns {Promise} the result of downloading the given list of models. The 53 | * promise will resolve with an object whose keys are the names of the models 54 | * and the value is its Mesh object. Each Mesh object will automatically 55 | * have its addMaterialLibrary() method called to set the given MTL data (if given). 56 | */ 57 | export declare function downloadModels(models: DownloadModelsOptions[]): Promise; 58 | export interface NameAndUrls { 59 | [meshName: string]: string; 60 | } 61 | /** 62 | * Takes in an object of `mesh_name`, `'/url/to/OBJ/file'` pairs and a callback 63 | * function. Each OBJ file will be ajaxed in and automatically converted to 64 | * an OBJ.Mesh. When all files have successfully downloaded the callback 65 | * function provided will be called and passed in an object containing 66 | * the newly created meshes. 67 | * 68 | * **Note:** In order to use this function as a way to download meshes, a 69 | * webserver of some sort must be used. 70 | * 71 | * @param {Object} nameAndAttrs an object where the key is the name of the mesh and the value is the url to that mesh's OBJ file 72 | * 73 | * @param {Function} 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 74 | * 75 | * @param {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: { '': OBJ.Mesh } 76 | * 77 | */ 78 | export declare function downloadMeshes(nameAndURLs: NameAndUrls, completionCallback: (meshes: MeshMap) => void, meshes: MeshMap): void; 79 | export interface ExtendedGLBuffer extends WebGLBuffer { 80 | itemSize: number; 81 | numItems: number; 82 | } 83 | export interface MeshWithBuffers extends Mesh { 84 | normalBuffer: ExtendedGLBuffer; 85 | textureBuffer: ExtendedGLBuffer; 86 | vertexBuffer: ExtendedGLBuffer; 87 | indexBuffer: ExtendedGLBuffer; 88 | } 89 | /** 90 | * Takes in the WebGL context and a Mesh, then creates and appends the buffers 91 | * to the mesh object as attributes. 92 | * 93 | * @param {WebGLRenderingContext} gl the `canvas.getContext('webgl')` context instance 94 | * @param {Mesh} mesh a single `OBJ.Mesh` instance 95 | * 96 | * The newly created mesh attributes are: 97 | * 98 | * Attrbute | Description 99 | * :--- | --- 100 | * **normalBuffer** |contains the model's Vertex Normals 101 | * normalBuffer.itemSize |set to 3 items 102 | * normalBuffer.numItems |the total number of vertex normals 103 | * | 104 | * **textureBuffer** |contains the model's Texture Coordinates 105 | * textureBuffer.itemSize |set to 2 items 106 | * textureBuffer.numItems |the number of texture coordinates 107 | * | 108 | * **vertexBuffer** |contains the model's Vertex Position Coordinates (does not include w) 109 | * vertexBuffer.itemSize |set to 3 items 110 | * vertexBuffer.numItems |the total number of vertices 111 | * | 112 | * **indexBuffer** |contains the indices of the faces 113 | * indexBuffer.itemSize |is set to 1 114 | * indexBuffer.numItems |the total number of indices 115 | * 116 | * A simple example (a lot of steps are missing, so don't copy and paste): 117 | * 118 | * const gl = canvas.getContext('webgl'), 119 | * mesh = OBJ.Mesh(obj_file_data); 120 | * // compile the shaders and create a shader program 121 | * const shaderProgram = gl.createProgram(); 122 | * // compilation stuff here 123 | * ... 124 | * // make sure you have vertex, vertex normal, and texture coordinate 125 | * // attributes located in your shaders and attach them to the shader program 126 | * shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); 127 | * gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); 128 | * 129 | * shaderProgram.vertexNormalAttribute = gl.getAttribLocation(shaderProgram, "aVertexNormal"); 130 | * gl.enableVertexAttribArray(shaderProgram.vertexNormalAttribute); 131 | * 132 | * shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord"); 133 | * gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute); 134 | * 135 | * // create and initialize the vertex, vertex normal, and texture coordinate buffers 136 | * // and save on to the mesh object 137 | * OBJ.initMeshBuffers(gl, mesh); 138 | * 139 | * // now to render the mesh 140 | * gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vertexBuffer); 141 | * gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, mesh.vertexBuffer.itemSize, gl.FLOAT, false, 0, 0); 142 | * // it's possible that the mesh doesn't contain 143 | * // any texture coordinates (e.g. suzanne.obj in the development branch). 144 | * // in this case, the texture vertexAttribArray will need to be disabled 145 | * // before the call to drawElements 146 | * if(!mesh.textures.length){ 147 | * gl.disableVertexAttribArray(shaderProgram.textureCoordAttribute); 148 | * } 149 | * else{ 150 | * // if the texture vertexAttribArray has been previously 151 | * // disabled, then it needs to be re-enabled 152 | * gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute); 153 | * gl.bindBuffer(gl.ARRAY_BUFFER, mesh.textureBuffer); 154 | * gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, mesh.textureBuffer.itemSize, gl.FLOAT, false, 0, 0); 155 | * } 156 | * 157 | * gl.bindBuffer(gl.ARRAY_BUFFER, mesh.normalBuffer); 158 | * gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, mesh.normalBuffer.itemSize, gl.FLOAT, false, 0, 0); 159 | * 160 | * gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.mesh.indexBuffer); 161 | * gl.drawElements(gl.TRIANGLES, model.mesh.indexBuffer.numItems, gl.UNSIGNED_SHORT, 0); 162 | */ 163 | export declare function initMeshBuffers(gl: WebGLRenderingContext, mesh: Mesh): MeshWithBuffers; 164 | export declare function deleteMeshBuffers(gl: WebGLRenderingContext, mesh: MeshWithBuffers): void; 165 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgl-obj-loader", 3 | "version": "2.0.8", 4 | "description": "A simple OBJ model loader to help facilitate the learning of WebGL", 5 | "main": "dist/webgl-obj-loader.min.js", 6 | "module": "dist/webgl-obj-loader.min.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "build": "webpack --config webpack.dev.js", 10 | "watch": "yarn build -- -w", 11 | "release": "yarn prettify && yarn test && webpack --config webpack.dev.js && webpack --config webpack.prod.js", 12 | "test": "mocha test", 13 | "coverage": "nyc npm test", 14 | "coverage:report": "nyc report -r html", 15 | "prettify": "prettier --write src/**/*.ts", 16 | "publish": "yarn release && npm publish" 17 | }, 18 | "prettier": { 19 | "printWidth": 120, 20 | "tabWidth": 4, 21 | "semi": true, 22 | "trailingComma": "all" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/frenchtoast747/webgl-obj-loader.git" 27 | }, 28 | "keywords": [ 29 | "obj", 30 | "mtl", 31 | "mesh", 32 | "load", 33 | "webgl", 34 | "parse", 35 | "vertex", 36 | "index", 37 | "normal", 38 | "texture" 39 | ], 40 | "author": "Aaron Boman", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/frenchtoast747/webgl-obj-loader/issues" 44 | }, 45 | "homepage": "https://github.com/frenchtoast747/webgl-obj-loader", 46 | "devDependencies": { 47 | "@types/chai": "4.2.4", 48 | "@types/mocha": "5.2.7", 49 | "awesome-typescript-loader": "5.2.1", 50 | "babel-core": "^6.26.0", 51 | "babel-loader": "^8.0.6", 52 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 53 | "babel-polyfill": "^6.26.0", 54 | "babel-preset-env": "^1.6.1", 55 | "babel-preset-stage-3": "^6.24.1", 56 | "chai": "^4.1.2", 57 | "mocha": "6.2.2", 58 | "nyc": "^14.1.1", 59 | "prettier": "1.18.2", 60 | "ts-node": "8.4.1", 61 | "typescript": "3.6.4", 62 | "webpack": "^4.41.2", 63 | "webpack-cli": "3.3.9", 64 | "webpack-merge": "^4.2.2" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Mesh, { 2 | MeshOptions, 3 | MaterialNameToIndex, 4 | IndexToMaterial, 5 | ArrayBufferWithItemSize, 6 | Uint16ArrayWithItemSize, 7 | } from "./mesh"; 8 | import { Material, MaterialLibrary, Vec3, UVW, TextureMapData } from "./material"; 9 | import { Layout, TYPES, AttributeInfo, DuplicateAttributeException, Attribute } from "./layout"; 10 | import { 11 | downloadModels, 12 | downloadMeshes, 13 | initMeshBuffers, 14 | deleteMeshBuffers, 15 | DownloadModelsOptions, 16 | MeshMap, 17 | NameAndUrls, 18 | ExtendedGLBuffer, 19 | MeshWithBuffers, 20 | } from "./utils"; 21 | 22 | const version = "2.0.3"; 23 | 24 | export const OBJ = { 25 | Attribute, 26 | DuplicateAttributeException, 27 | Layout, 28 | Material, 29 | MaterialLibrary, 30 | Mesh, 31 | TYPES, 32 | downloadModels, 33 | downloadMeshes, 34 | initMeshBuffers, 35 | deleteMeshBuffers, 36 | version, 37 | }; 38 | 39 | /** 40 | * @namespace 41 | */ 42 | export { 43 | ArrayBufferWithItemSize, 44 | Attribute, 45 | AttributeInfo, 46 | DownloadModelsOptions, 47 | DuplicateAttributeException, 48 | ExtendedGLBuffer, 49 | IndexToMaterial, 50 | Layout, 51 | Material, 52 | MaterialLibrary, 53 | MaterialNameToIndex, 54 | Mesh, 55 | MeshMap, 56 | MeshOptions, 57 | MeshWithBuffers, 58 | NameAndUrls, 59 | TextureMapData, 60 | TYPES, 61 | Uint16ArrayWithItemSize, 62 | UVW, 63 | Vec3, 64 | downloadModels, 65 | downloadMeshes, 66 | initMeshBuffers, 67 | deleteMeshBuffers, 68 | version, 69 | }; 70 | -------------------------------------------------------------------------------- /src/layout.ts: -------------------------------------------------------------------------------- 1 | export enum TYPES { 2 | "BYTE" = "BYTE", 3 | "UNSIGNED_BYTE" = "UNSIGNED_BYTE", 4 | "SHORT" = "SHORT", 5 | "UNSIGNED_SHORT" = "UNSIGNED_SHORT", 6 | "FLOAT" = "FLOAT", 7 | } 8 | 9 | export interface AttributeInfo { 10 | attribute: Attribute; 11 | size: Attribute["size"]; 12 | type: Attribute["type"]; 13 | normalized: Attribute["normalized"]; 14 | offset: number; 15 | stride: number; 16 | } 17 | 18 | /** 19 | * An exception for when two or more of the same attributes are found in the 20 | * same layout. 21 | * @private 22 | */ 23 | export class DuplicateAttributeException extends Error { 24 | /** 25 | * Create a DuplicateAttributeException 26 | * @param {Attribute} attribute - The attribute that was found more than 27 | * once in the {@link Layout} 28 | */ 29 | constructor(attribute: Attribute) { 30 | super(`found duplicate attribute: ${attribute.key}`); 31 | } 32 | } 33 | 34 | /** 35 | * Represents how a vertex attribute should be packed into an buffer. 36 | * @private 37 | */ 38 | export class Attribute { 39 | public sizeOfType: number; 40 | public sizeInBytes: number; 41 | /** 42 | * Create an attribute. Do not call this directly, use the predefined 43 | * constants. 44 | * @param {string} key - The name of this attribute as if it were a key in 45 | * an Object. Use the camel case version of the upper snake case 46 | * const name. 47 | * @param {number} size - The number of components per vertex attribute. 48 | * Must be 1, 2, 3, or 4. 49 | * @param {string} type - The data type of each component for this 50 | * attribute. Possible values:
51 | * "BYTE": signed 8-bit integer, with values in [-128, 127]
52 | * "SHORT": signed 16-bit integer, with values in 53 | * [-32768, 32767]
54 | * "UNSIGNED_BYTE": unsigned 8-bit integer, with values in 55 | * [0, 255]
56 | * "UNSIGNED_SHORT": unsigned 16-bit integer, with values in 57 | * [0, 65535]
58 | * "FLOAT": 32-bit floating point number 59 | * @param {boolean} normalized - Whether integer data values should be 60 | * normalized when being casted to a float.
61 | * If true, signed integers are normalized to [-1, 1].
62 | * If true, unsigned integers are normalized to [0, 1].
63 | * For type "FLOAT", this parameter has no effect. 64 | */ 65 | constructor(public key: string, public size: number, public type: TYPES, public normalized: boolean = false) { 66 | switch (type) { 67 | case "BYTE": 68 | case "UNSIGNED_BYTE": 69 | this.sizeOfType = 1; 70 | break; 71 | case "SHORT": 72 | case "UNSIGNED_SHORT": 73 | this.sizeOfType = 2; 74 | break; 75 | case "FLOAT": 76 | this.sizeOfType = 4; 77 | break; 78 | default: 79 | throw new Error(`Unknown gl type: ${type}`); 80 | } 81 | this.sizeInBytes = this.sizeOfType * size; 82 | } 83 | } 84 | 85 | /** 86 | * A class to represent the memory layout for a vertex attribute array. Used by 87 | * {@link Mesh}'s TBD(...) method to generate a packed array from mesh data. 88 | *

89 | * Layout can sort of be thought of as a C-style struct declaration. 90 | * {@link Mesh}'s TBD(...) method will use the {@link Layout} instance to 91 | * pack an array in the given attribute order. 92 | *

93 | * Layout also is very helpful when calling a WebGL context's 94 | * vertexAttribPointer method. If you've created a buffer using 95 | * a Layout instance, then the same Layout instance can be used to determine 96 | * the size, type, normalized, stride, and offset parameters for 97 | * vertexAttribPointer. 98 | *

99 | * For example: 100 | *


101 |  *
102 |  * const index = glctx.getAttribLocation(shaderProgram, "pos");
103 |  * glctx.vertexAttribPointer(
104 |  *   layout.position.size,
105 |  *   glctx[layout.position.type],
106 |  *   layout.position.normalized,
107 |  *   layout.position.stride,
108 |  *   layout.position.offset);
109 |  * 
110 | * @see {@link Mesh} 111 | */ 112 | export class Layout { 113 | // Geometry attributes 114 | /** 115 | * Attribute layout to pack a vertex's x, y, & z as floats 116 | * 117 | * @see {@link Layout} 118 | */ 119 | static POSITION = new Attribute("position", 3, TYPES.FLOAT); 120 | 121 | /** 122 | * Attribute layout to pack a vertex's normal's x, y, & z as floats 123 | * 124 | * @see {@link Layout} 125 | */ 126 | static NORMAL = new Attribute("normal", 3, TYPES.FLOAT); 127 | 128 | /** 129 | * Attribute layout to pack a vertex's normal's x, y, & z as floats. 130 | *

131 | * This value will be computed on-the-fly based on the texture coordinates. 132 | * If no texture coordinates are available, the generated value will default to 133 | * 0, 0, 0. 134 | * 135 | * @see {@link Layout} 136 | */ 137 | static TANGENT = new Attribute("tangent", 3, TYPES.FLOAT); 138 | 139 | /** 140 | * Attribute layout to pack a vertex's normal's bitangent x, y, & z as floats. 141 | *

142 | * This value will be computed on-the-fly based on the texture coordinates. 143 | * If no texture coordinates are available, the generated value will default to 144 | * 0, 0, 0. 145 | * @see {@link Layout} 146 | */ 147 | static BITANGENT = new Attribute("bitangent", 3, TYPES.FLOAT); 148 | 149 | /** 150 | * Attribute layout to pack a vertex's texture coordinates' u & v as floats 151 | * 152 | * @see {@link Layout} 153 | */ 154 | static UV = new Attribute("uv", 2, TYPES.FLOAT); 155 | 156 | // Material attributes 157 | 158 | /** 159 | * Attribute layout to pack an unsigned short to be interpreted as a the index 160 | * into a {@link Mesh}'s materials list. 161 | *

162 | * The intention of this value is to send all of the {@link Mesh}'s materials 163 | * into multiple shader uniforms and then reference the current one by this 164 | * vertex attribute. 165 | *

166 | * example glsl code: 167 | * 168 | *


169 |      *  // this is bound using MATERIAL_INDEX
170 |      *  attribute int materialIndex;
171 |      *
172 |      *  struct Material {
173 |      *    vec3 diffuse;
174 |      *    vec3 specular;
175 |      *    vec3 specularExponent;
176 |      *  };
177 |      *
178 |      *  uniform Material materials[MAX_MATERIALS];
179 |      *
180 |      *  // ...
181 |      *
182 |      *  vec3 diffuse = materials[materialIndex];
183 |      *
184 |      * 
185 | * TODO: More description & test to make sure subscripting by attributes even 186 | * works for webgl 187 | * 188 | * @see {@link Layout} 189 | */ 190 | static MATERIAL_INDEX = new Attribute("materialIndex", 1, TYPES.SHORT); 191 | static MATERIAL_ENABLED = new Attribute("materialEnabled", 1, TYPES.UNSIGNED_SHORT); 192 | static AMBIENT = new Attribute("ambient", 3, TYPES.FLOAT); 193 | static DIFFUSE = new Attribute("diffuse", 3, TYPES.FLOAT); 194 | static SPECULAR = new Attribute("specular", 3, TYPES.FLOAT); 195 | static SPECULAR_EXPONENT = new Attribute("specularExponent", 3, TYPES.FLOAT); 196 | static EMISSIVE = new Attribute("emissive", 3, TYPES.FLOAT); 197 | static TRANSMISSION_FILTER = new Attribute("transmissionFilter", 3, TYPES.FLOAT); 198 | static DISSOLVE = new Attribute("dissolve", 1, TYPES.FLOAT); 199 | static ILLUMINATION = new Attribute("illumination", 1, TYPES.UNSIGNED_SHORT); 200 | static REFRACTION_INDEX = new Attribute("refractionIndex", 1, TYPES.FLOAT); 201 | static SHARPNESS = new Attribute("sharpness", 1, TYPES.FLOAT); 202 | static MAP_DIFFUSE = new Attribute("mapDiffuse", 1, TYPES.SHORT); 203 | static MAP_AMBIENT = new Attribute("mapAmbient", 1, TYPES.SHORT); 204 | static MAP_SPECULAR = new Attribute("mapSpecular", 1, TYPES.SHORT); 205 | static MAP_SPECULAR_EXPONENT = new Attribute("mapSpecularExponent", 1, TYPES.SHORT); 206 | static MAP_DISSOLVE = new Attribute("mapDissolve", 1, TYPES.SHORT); 207 | static ANTI_ALIASING = new Attribute("antiAliasing", 1, TYPES.UNSIGNED_SHORT); 208 | static MAP_BUMP = new Attribute("mapBump", 1, TYPES.SHORT); 209 | static MAP_DISPLACEMENT = new Attribute("mapDisplacement", 1, TYPES.SHORT); 210 | static MAP_DECAL = new Attribute("mapDecal", 1, TYPES.SHORT); 211 | static MAP_EMISSIVE = new Attribute("mapEmissive", 1, TYPES.SHORT); 212 | 213 | public stride: number; 214 | public attributes: Attribute[]; 215 | public attributeMap: { [idx: string]: AttributeInfo }; 216 | /** 217 | * Create a Layout object. This constructor will throw if any duplicate 218 | * attributes are given. 219 | * @param {Array} ...attributes - An ordered list of attributes that 220 | * describe the desired memory layout for each vertex attribute. 221 | *

222 | * 223 | * @see {@link Mesh} 224 | */ 225 | constructor(...attributes: Attribute[]) { 226 | this.attributes = attributes; 227 | this.attributeMap = {}; 228 | let offset = 0; 229 | let maxStrideMultiple = 0; 230 | for (const attribute of attributes) { 231 | if (this.attributeMap[attribute.key]) { 232 | throw new DuplicateAttributeException(attribute); 233 | } 234 | // Add padding to satisfy WebGL's requirement that all 235 | // vertexAttribPointer calls have an offset that is a multiple of 236 | // the type size. 237 | if (offset % attribute.sizeOfType !== 0) { 238 | offset += attribute.sizeOfType - (offset % attribute.sizeOfType); 239 | console.warn("Layout requires padding before " + attribute.key + " attribute"); 240 | } 241 | this.attributeMap[attribute.key] = { 242 | attribute: attribute, 243 | size: attribute.size, 244 | type: attribute.type, 245 | normalized: attribute.normalized, 246 | offset: offset, 247 | } as AttributeInfo; 248 | offset += attribute.sizeInBytes; 249 | maxStrideMultiple = Math.max(maxStrideMultiple, attribute.sizeOfType); 250 | } 251 | // Add padding to the end to satisfy WebGL's requirement that all 252 | // vertexAttribPointer calls have a stride that is a multiple of the 253 | // type size. Because we're putting differently sized attributes into 254 | // the same buffer, it must be padded to a multiple of the largest 255 | // type size. 256 | if (offset % maxStrideMultiple !== 0) { 257 | offset += maxStrideMultiple - (offset % maxStrideMultiple); 258 | console.warn("Layout requires padding at the back"); 259 | } 260 | this.stride = offset; 261 | for (const attribute of attributes) { 262 | this.attributeMap[attribute.key].stride = this.stride; 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/material.ts: -------------------------------------------------------------------------------- 1 | export type Vec3 = [number, number, number]; 2 | 3 | export interface UVW { 4 | u: number; 5 | v: number; 6 | w: number; 7 | } 8 | 9 | export interface TextureMapData { 10 | colorCorrection: boolean; 11 | horizontalBlending: boolean; 12 | verticalBlending: boolean; 13 | boostMipMapSharpness: number; 14 | modifyTextureMap: { 15 | brightness: number; 16 | contrast: number; 17 | }; 18 | offset: UVW; 19 | scale: UVW; 20 | turbulence: UVW; 21 | clamp: boolean; 22 | textureResolution: number | null; 23 | bumpMultiplier: number; 24 | imfChan: string | null; 25 | filename: string; 26 | reflectionType?: string; 27 | texture?: HTMLImageElement; 28 | } 29 | 30 | /** 31 | * The Material class. 32 | */ 33 | export class Material { 34 | /** 35 | * Constructor 36 | * @param {String} name the unique name of the material 37 | */ 38 | // The values for the following attibutes 39 | // are an array of R, G, B normalized values. 40 | // Ka - Ambient Reflectivity 41 | ambient: Vec3 = [0, 0, 0]; 42 | // Kd - Defuse Reflectivity 43 | diffuse: Vec3 = [0, 0, 0]; 44 | // Ks 45 | specular: Vec3 = [0, 0, 0]; 46 | // Ke 47 | emissive: Vec3 = [0, 0, 0]; 48 | // Tf 49 | transmissionFilter: Vec3 = [0, 0, 0]; 50 | // d 51 | dissolve: number = 0; 52 | // valid range is between 0 and 1000 53 | specularExponent: number = 0; 54 | // either d or Tr; valid values are normalized 55 | transparency: number = 0; 56 | // illum - the enum of the illumination model to use 57 | illumination: number = 0; 58 | // Ni - Set to "normal" (air). 59 | refractionIndex: number = 1; 60 | // sharpness 61 | sharpness: number = 0; 62 | // map_Kd 63 | mapDiffuse: TextureMapData = emptyTextureOptions(); 64 | // map_Ka 65 | mapAmbient: TextureMapData = emptyTextureOptions(); 66 | // map_Ks 67 | mapSpecular: TextureMapData = emptyTextureOptions(); 68 | // map_Ns 69 | mapSpecularExponent: TextureMapData = emptyTextureOptions(); 70 | // map_d 71 | mapDissolve: TextureMapData = emptyTextureOptions(); 72 | // map_aat 73 | antiAliasing: boolean = false; 74 | // map_bump or bump 75 | mapBump: TextureMapData = emptyTextureOptions(); 76 | // disp 77 | mapDisplacement: TextureMapData = emptyTextureOptions(); 78 | // decal 79 | mapDecal: TextureMapData = emptyTextureOptions(); 80 | // map_Ke 81 | mapEmissive: TextureMapData = emptyTextureOptions(); 82 | // refl - when the reflection type is a cube, there will be multiple refl 83 | // statements for each side of the cube. If it's a spherical 84 | // reflection, there should only ever be one. 85 | mapReflections: TextureMapData[] = []; 86 | constructor(public name: string) {} 87 | } 88 | 89 | const SENTINEL_MATERIAL = new Material("sentinel"); 90 | 91 | /** 92 | * https://en.wikipedia.org/wiki/Wavefront_.obj_file 93 | * http://paulbourke.net/dataformats/mtl/ 94 | */ 95 | export class MaterialLibrary { 96 | /** 97 | * Constructs the Material Parser 98 | * @param mtlData the MTL file contents 99 | */ 100 | public currentMaterial: Material = SENTINEL_MATERIAL; 101 | public materials: { [k: string]: Material } = {}; 102 | 103 | constructor(public data: string) { 104 | this.parse(); 105 | } 106 | 107 | /* eslint-disable camelcase */ 108 | /* the function names here disobey camelCase conventions 109 | to make parsing/routing easier. see the parse function 110 | documentation for more information. */ 111 | 112 | /** 113 | * Creates a new Material object and adds to the registry. 114 | * @param tokens the tokens associated with the directive 115 | */ 116 | parse_newmtl(tokens: string[]) { 117 | const name = tokens[0]; 118 | // console.info('Parsing new Material:', name); 119 | 120 | this.currentMaterial = new Material(name); 121 | this.materials[name] = this.currentMaterial; 122 | } 123 | 124 | /** 125 | * See the documenation for parse_Ka below for a better understanding. 126 | * 127 | * Given a list of possible color tokens, returns an array of R, G, and B 128 | * color values. 129 | * 130 | * @param tokens the tokens associated with the directive 131 | * @return {*} a 3 element array containing the R, G, and B values 132 | * of the color. 133 | */ 134 | parseColor(tokens: string[]): Vec3 { 135 | if (tokens[0] == "spectral") { 136 | throw new Error( 137 | "The MTL parser does not support spectral curve files. You will " + 138 | "need to convert the MTL colors to either RGB or CIEXYZ.", 139 | ); 140 | } 141 | 142 | if (tokens[0] == "xyz") { 143 | throw new Error( 144 | "The MTL parser does not currently support XYZ colors. Either convert the " + 145 | "XYZ values to RGB or create an issue to add support for XYZ", 146 | ); 147 | } 148 | 149 | // from my understanding of the spec, RGB values at this point 150 | // will either be 3 floats or exactly 1 float, so that's the check 151 | // that i'm going to perform here 152 | if (tokens.length == 3) { 153 | const [x, y, z] = tokens; 154 | return [parseFloat(x), parseFloat(y), parseFloat(z)]; 155 | } 156 | 157 | // Since tokens at this point has a length of 3, we're going to assume 158 | // it's exactly 1, skipping the check for 2. 159 | const value = parseFloat(tokens[0]); 160 | // in this case, all values are equivalent 161 | return [value, value, value]; 162 | } 163 | 164 | /** 165 | * Parse the ambient reflectivity 166 | * 167 | * A Ka directive can take one of three forms: 168 | * - Ka r g b 169 | * - Ka spectral file.rfl 170 | * - Ka xyz x y z 171 | * These three forms are mutually exclusive in that only one 172 | * declaration can exist per material. It is considered a syntax 173 | * error otherwise. 174 | * 175 | * The "Ka" form specifies the ambient reflectivity using RGB values. 176 | * The "g" and "b" values are optional. If only the "r" value is 177 | * specified, then the "g" and "b" values are assigned the value of 178 | * "r". Values are normally in the range 0.0 to 1.0. Values outside 179 | * of this range increase or decrease the reflectivity accordingly. 180 | * 181 | * The "Ka spectral" form specifies the ambient reflectivity using a 182 | * spectral curve. "file.rfl" is the name of the ".rfl" file containing 183 | * the curve data. "factor" is an optional argument which is a multiplier 184 | * for the values in the .rfl file and defaults to 1.0 if not specified. 185 | * 186 | * The "Ka xyz" form specifies the ambient reflectivity using CIEXYZ values. 187 | * "x y z" are the values of the CIEXYZ color space. The "y" and "z" arguments 188 | * are optional and take on the value of the "x" component if only "x" is 189 | * specified. The "x y z" values are normally in the range of 0.0 to 1.0 and 190 | * increase or decrease ambient reflectivity accordingly outside of that 191 | * range. 192 | * 193 | * @param tokens the tokens associated with the directive 194 | */ 195 | parse_Ka(tokens: string[]) { 196 | this.currentMaterial.ambient = this.parseColor(tokens); 197 | } 198 | 199 | /** 200 | * Diffuse Reflectivity 201 | * 202 | * Similar to the Ka directive. Simply replace "Ka" with "Kd" and the rules 203 | * are the same 204 | * 205 | * @param tokens the tokens associated with the directive 206 | */ 207 | parse_Kd(tokens: string[]) { 208 | this.currentMaterial.diffuse = this.parseColor(tokens); 209 | } 210 | 211 | /** 212 | * Spectral Reflectivity 213 | * 214 | * Similar to the Ka directive. Simply replace "Ks" with "Kd" and the rules 215 | * are the same 216 | * 217 | * @param tokens the tokens associated with the directive 218 | */ 219 | parse_Ks(tokens: string[]) { 220 | this.currentMaterial.specular = this.parseColor(tokens); 221 | } 222 | 223 | /** 224 | * Emissive 225 | * 226 | * The amount and color of light emitted by the object. 227 | * 228 | * @param tokens the tokens associated with the directive 229 | */ 230 | parse_Ke(tokens: string[]) { 231 | this.currentMaterial.emissive = this.parseColor(tokens); 232 | } 233 | 234 | /** 235 | * Transmission Filter 236 | * 237 | * Any light passing through the object is filtered by the transmission 238 | * filter, which only allows specific colors to pass through. For example, Tf 239 | * 0 1 0 allows all of the green to pass through and filters out all of the 240 | * red and blue. 241 | * 242 | * Similar to the Ka directive. Simply replace "Ks" with "Tf" and the rules 243 | * are the same 244 | * 245 | * @param tokens the tokens associated with the directive 246 | */ 247 | parse_Tf(tokens: string[]) { 248 | this.currentMaterial.transmissionFilter = this.parseColor(tokens); 249 | } 250 | 251 | /** 252 | * Specifies the dissolve for the current material. 253 | * 254 | * Statement: d [-halo] `factor` 255 | * 256 | * Example: "d 0.5" 257 | * 258 | * The factor is the amount this material dissolves into the background. A 259 | * factor of 1.0 is fully opaque. This is the default when a new material is 260 | * created. A factor of 0.0 is fully dissolved (completely transparent). 261 | * 262 | * Unlike a real transparent material, the dissolve does not depend upon 263 | * material thickness nor does it have any spectral character. Dissolve works 264 | * on all illumination models. 265 | * 266 | * The dissolve statement allows for an optional "-halo" flag which indicates 267 | * that a dissolve is dependent on the surface orientation relative to the 268 | * viewer. For example, a sphere with the following dissolve, "d -halo 0.0", 269 | * will be fully dissolved at its center and will appear gradually more opaque 270 | * toward its edge. 271 | * 272 | * "factor" is the minimum amount of dissolve applied to the material. The 273 | * amount of dissolve will vary between 1.0 (fully opaque) and the specified 274 | * "factor". The formula is: 275 | * 276 | * dissolve = 1.0 - (N*v)(1.0-factor) 277 | * 278 | * @param tokens the tokens associated with the directive 279 | */ 280 | parse_d(tokens: string[]) { 281 | // this ignores the -halo option as I can't find any documentation on what 282 | // it's supposed to be. 283 | this.currentMaterial.dissolve = parseFloat(tokens.pop() || "0"); 284 | } 285 | 286 | /** 287 | * The "illum" statement specifies the illumination model to use in the 288 | * material. Illumination models are mathematical equations that represent 289 | * various material lighting and shading effects. 290 | * 291 | * The illumination number can be a number from 0 to 10. The following are 292 | * the list of illumination enumerations and their summaries: 293 | * 0. Color on and Ambient off 294 | * 1. Color on and Ambient on 295 | * 2. Highlight on 296 | * 3. Reflection on and Ray trace on 297 | * 4. Transparency: Glass on, Reflection: Ray trace on 298 | * 5. Reflection: Fresnel on and Ray trace on 299 | * 6. Transparency: Refraction on, Reflection: Fresnel off and Ray trace on 300 | * 7. Transparency: Refraction on, Reflection: Fresnel on and Ray trace on 301 | * 8. Reflection on and Ray trace off 302 | * 9. Transparency: Glass on, Reflection: Ray trace off 303 | * 10. Casts shadows onto invisible surfaces 304 | * 305 | * Example: "illum 2" to specify the "Highlight on" model 306 | * 307 | * @param tokens the tokens associated with the directive 308 | */ 309 | parse_illum(tokens: string[]) { 310 | this.currentMaterial.illumination = parseInt(tokens[0]); 311 | } 312 | 313 | /** 314 | * Optical Density (AKA Index of Refraction) 315 | * 316 | * Statement: Ni `index` 317 | * 318 | * Example: Ni 1.0 319 | * 320 | * Specifies the optical density for the surface. `index` is the value 321 | * for the optical density. The values can range from 0.001 to 10. A value of 322 | * 1.0 means that light does not bend as it passes through an object. 323 | * Increasing the optical_density increases the amount of bending. Glass has 324 | * an index of refraction of about 1.5. Values of less than 1.0 produce 325 | * bizarre results and are not recommended 326 | * 327 | * @param tokens the tokens associated with the directive 328 | */ 329 | parse_Ni(tokens: string[]) { 330 | this.currentMaterial.refractionIndex = parseFloat(tokens[0]); 331 | } 332 | 333 | /** 334 | * Specifies the specular exponent for the current material. This defines the 335 | * focus of the specular highlight. 336 | * 337 | * Statement: Ns `exponent` 338 | * 339 | * Example: "Ns 250" 340 | * 341 | * `exponent` is the value for the specular exponent. A high exponent results 342 | * in a tight, concentrated highlight. Ns Values normally range from 0 to 343 | * 1000. 344 | * 345 | * @param tokens the tokens associated with the directive 346 | */ 347 | parse_Ns(tokens: string[]) { 348 | this.currentMaterial.specularExponent = parseInt(tokens[0]); 349 | } 350 | 351 | /** 352 | * Specifies the sharpness of the reflections from the local reflection map. 353 | * 354 | * Statement: sharpness `value` 355 | * 356 | * Example: "sharpness 100" 357 | * 358 | * If a material does not have a local reflection map defined in its material 359 | * defintions, sharpness will apply to the global reflection map defined in 360 | * PreView. 361 | * 362 | * `value` can be a number from 0 to 1000. The default is 60. A high value 363 | * results in a clear reflection of objects in the reflection map. 364 | * 365 | * Tip: sharpness values greater than 100 introduce aliasing effects in 366 | * flat surfaces that are viewed at a sharp angle. 367 | * 368 | * @param tokens the tokens associated with the directive 369 | */ 370 | parse_sharpness(tokens: string[]) { 371 | this.currentMaterial.sharpness = parseInt(tokens[0]); 372 | } 373 | 374 | /** 375 | * Parses the -cc flag and updates the options object with the values. 376 | * 377 | * @param values the values passed to the -cc flag 378 | * @param options the Object of all image options 379 | */ 380 | parse_cc(values: string[], options: TextureMapData) { 381 | options.colorCorrection = values[0] == "on"; 382 | } 383 | 384 | /** 385 | * Parses the -blendu flag and updates the options object with the values. 386 | * 387 | * @param values the values passed to the -blendu flag 388 | * @param options the Object of all image options 389 | */ 390 | parse_blendu(values: string[], options: TextureMapData) { 391 | options.horizontalBlending = values[0] == "on"; 392 | } 393 | 394 | /** 395 | * Parses the -blendv flag and updates the options object with the values. 396 | * 397 | * @param values the values passed to the -blendv flag 398 | * @param options the Object of all image options 399 | */ 400 | parse_blendv(values: string[], options: TextureMapData) { 401 | options.verticalBlending = values[0] == "on"; 402 | } 403 | 404 | /** 405 | * Parses the -boost flag and updates the options object with the values. 406 | * 407 | * @param values the values passed to the -boost flag 408 | * @param options the Object of all image options 409 | */ 410 | parse_boost(values: string[], options: TextureMapData) { 411 | options.boostMipMapSharpness = parseFloat(values[0]); 412 | } 413 | 414 | /** 415 | * Parses the -mm flag and updates the options object with the values. 416 | * 417 | * @param values the values passed to the -mm flag 418 | * @param options the Object of all image options 419 | */ 420 | parse_mm(values: string[], options: TextureMapData) { 421 | options.modifyTextureMap.brightness = parseFloat(values[0]); 422 | options.modifyTextureMap.contrast = parseFloat(values[1]); 423 | } 424 | 425 | /** 426 | * Parses and sets the -o, -s, and -t u, v, and w values 427 | * 428 | * @param values the values passed to the -o, -s, -t flag 429 | * @param {Object} option the Object of either the -o, -s, -t option 430 | * @param {Integer} defaultValue the Object of all image options 431 | */ 432 | parse_ost(values: string[], option: UVW, defaultValue: number) { 433 | while (values.length < 3) { 434 | values.push(defaultValue.toString()); 435 | } 436 | 437 | option.u = parseFloat(values[0]); 438 | option.v = parseFloat(values[1]); 439 | option.w = parseFloat(values[2]); 440 | } 441 | 442 | /** 443 | * Parses the -o flag and updates the options object with the values. 444 | * 445 | * @param values the values passed to the -o flag 446 | * @param options the Object of all image options 447 | */ 448 | parse_o(values: string[], options: TextureMapData) { 449 | this.parse_ost(values, options.offset, 0); 450 | } 451 | 452 | /** 453 | * Parses the -s flag and updates the options object with the values. 454 | * 455 | * @param values the values passed to the -s flag 456 | * @param options the Object of all image options 457 | */ 458 | parse_s(values: string[], options: TextureMapData) { 459 | this.parse_ost(values, options.scale, 1); 460 | } 461 | 462 | /** 463 | * Parses the -t flag and updates the options object with the values. 464 | * 465 | * @param values the values passed to the -t flag 466 | * @param options the Object of all image options 467 | */ 468 | parse_t(values: string[], options: TextureMapData) { 469 | this.parse_ost(values, options.turbulence, 0); 470 | } 471 | 472 | /** 473 | * Parses the -texres flag and updates the options object with the values. 474 | * 475 | * @param values the values passed to the -texres flag 476 | * @param options the Object of all image options 477 | */ 478 | parse_texres(values: string[], options: TextureMapData) { 479 | options.textureResolution = parseFloat(values[0]); 480 | } 481 | 482 | /** 483 | * Parses the -clamp flag and updates the options object with the values. 484 | * 485 | * @param values the values passed to the -clamp flag 486 | * @param options the Object of all image options 487 | */ 488 | parse_clamp(values: string[], options: TextureMapData) { 489 | options.clamp = values[0] == "on"; 490 | } 491 | 492 | /** 493 | * Parses the -bm flag and updates the options object with the values. 494 | * 495 | * @param values the values passed to the -bm flag 496 | * @param options the Object of all image options 497 | */ 498 | parse_bm(values: string[], options: TextureMapData) { 499 | options.bumpMultiplier = parseFloat(values[0]); 500 | } 501 | 502 | /** 503 | * Parses the -imfchan flag and updates the options object with the values. 504 | * 505 | * @param values the values passed to the -imfchan flag 506 | * @param options the Object of all image options 507 | */ 508 | parse_imfchan(values: string[], options: TextureMapData) { 509 | options.imfChan = values[0]; 510 | } 511 | 512 | /** 513 | * This only exists for relection maps and denotes the type of reflection. 514 | * 515 | * @param values the values passed to the -type flag 516 | * @param options the Object of all image options 517 | */ 518 | parse_type(values: string[], options: TextureMapData) { 519 | options.reflectionType = values[0]; 520 | } 521 | 522 | /** 523 | * Parses the texture's options and returns an options object with the info 524 | * 525 | * @param tokens all of the option tokens to pass to the texture 526 | * @return {Object} a complete object of objects to apply to the texture 527 | */ 528 | parseOptions(tokens: string[]): TextureMapData { 529 | const options = emptyTextureOptions(); 530 | 531 | let option; 532 | let values; 533 | const optionsToValues: { [k: string]: string[] } = {}; 534 | 535 | tokens.reverse(); 536 | 537 | while (tokens.length) { 538 | // token is guaranteed to exists here, hence the explicit "as" 539 | const token = tokens.pop() as string; 540 | 541 | if (token.startsWith("-")) { 542 | option = token.substr(1); 543 | optionsToValues[option] = []; 544 | } else if (option) { 545 | optionsToValues[option].push(token); 546 | } 547 | } 548 | 549 | for (option in optionsToValues) { 550 | if (!optionsToValues.hasOwnProperty(option)) { 551 | continue; 552 | } 553 | values = optionsToValues[option]; 554 | const optionMethod = (this as any)[`parse_${option}`]; 555 | if (optionMethod) { 556 | optionMethod.bind(this)(values, options); 557 | } 558 | } 559 | 560 | return options; 561 | } 562 | 563 | /** 564 | * Parses the given texture map line. 565 | * 566 | * @param tokens all of the tokens representing the texture 567 | * @return a complete object of objects to apply to the texture 568 | */ 569 | parseMap(tokens: string[]): TextureMapData { 570 | // according to wikipedia: 571 | // (https://en.wikipedia.org/wiki/Wavefront_.obj_file#Vendor_specific_alterations) 572 | // there is at least one vendor that places the filename before the options 573 | // rather than after (which is to spec). All options start with a '-' 574 | // so if the first token doesn't start with a '-', we're going to assume 575 | // it's the name of the map file. 576 | let optionsString; 577 | let filename = ""; 578 | if (!tokens[0].startsWith("-")) { 579 | [filename, ...optionsString] = tokens; 580 | } else { 581 | filename = tokens.pop() as string; 582 | optionsString = tokens; 583 | } 584 | 585 | const options = this.parseOptions(optionsString); 586 | options.filename = filename.replace(/\\/g, "/"); 587 | 588 | return options; 589 | } 590 | 591 | /** 592 | * Parses the ambient map. 593 | * 594 | * @param tokens list of tokens for the map_Ka direcive 595 | */ 596 | parse_map_Ka(tokens: string[]) { 597 | this.currentMaterial.mapAmbient = this.parseMap(tokens); 598 | } 599 | 600 | /** 601 | * Parses the diffuse map. 602 | * 603 | * @param tokens list of tokens for the map_Kd direcive 604 | */ 605 | parse_map_Kd(tokens: string[]) { 606 | this.currentMaterial.mapDiffuse = this.parseMap(tokens); 607 | } 608 | 609 | /** 610 | * Parses the specular map. 611 | * 612 | * @param tokens list of tokens for the map_Ks direcive 613 | */ 614 | parse_map_Ks(tokens: string[]) { 615 | this.currentMaterial.mapSpecular = this.parseMap(tokens); 616 | } 617 | 618 | /** 619 | * Parses the emissive map. 620 | * 621 | * @param tokens list of tokens for the map_Ke direcive 622 | */ 623 | parse_map_Ke(tokens: string[]) { 624 | this.currentMaterial.mapEmissive = this.parseMap(tokens); 625 | } 626 | 627 | /** 628 | * Parses the specular exponent map. 629 | * 630 | * @param tokens list of tokens for the map_Ns direcive 631 | */ 632 | parse_map_Ns(tokens: string[]) { 633 | this.currentMaterial.mapSpecularExponent = this.parseMap(tokens); 634 | } 635 | 636 | /** 637 | * Parses the dissolve map. 638 | * 639 | * @param tokens list of tokens for the map_d direcive 640 | */ 641 | parse_map_d(tokens: string[]) { 642 | this.currentMaterial.mapDissolve = this.parseMap(tokens); 643 | } 644 | 645 | /** 646 | * Parses the anti-aliasing option. 647 | * 648 | * @param tokens list of tokens for the map_aat direcive 649 | */ 650 | parse_map_aat(tokens: string[]) { 651 | this.currentMaterial.antiAliasing = tokens[0] == "on"; 652 | } 653 | 654 | /** 655 | * Parses the bump map. 656 | * 657 | * @param tokens list of tokens for the map_bump direcive 658 | */ 659 | parse_map_bump(tokens: string[]) { 660 | this.currentMaterial.mapBump = this.parseMap(tokens); 661 | } 662 | 663 | /** 664 | * Parses the bump map. 665 | * 666 | * @param tokens list of tokens for the bump direcive 667 | */ 668 | parse_bump(tokens: string[]) { 669 | this.parse_map_bump(tokens); 670 | } 671 | 672 | /** 673 | * Parses the disp map. 674 | * 675 | * @param tokens list of tokens for the disp direcive 676 | */ 677 | parse_disp(tokens: string[]) { 678 | this.currentMaterial.mapDisplacement = this.parseMap(tokens); 679 | } 680 | 681 | /** 682 | * Parses the decal map. 683 | * 684 | * @param tokens list of tokens for the map_decal direcive 685 | */ 686 | parse_decal(tokens: string[]) { 687 | this.currentMaterial.mapDecal = this.parseMap(tokens); 688 | } 689 | 690 | /** 691 | * Parses the refl map. 692 | * 693 | * @param tokens list of tokens for the refl direcive 694 | */ 695 | parse_refl(tokens: string[]) { 696 | this.currentMaterial.mapReflections.push(this.parseMap(tokens)); 697 | } 698 | 699 | /** 700 | * Parses the MTL file. 701 | * 702 | * Iterates line by line parsing each MTL directive. 703 | * 704 | * This function expects the first token in the line 705 | * to be a valid MTL directive. That token is then used 706 | * to try and run a method on this class. parse_[directive] 707 | * E.g., the `newmtl` directive would try to call the method 708 | * parse_newmtl. Each parsing function takes in the remaining 709 | * list of tokens and updates the currentMaterial class with 710 | * the attributes provided. 711 | */ 712 | parse() { 713 | const lines = this.data.split(/\r?\n/); 714 | for (let line of lines) { 715 | line = line.trim(); 716 | if (!line || line.startsWith("#")) { 717 | continue; 718 | } 719 | 720 | const [directive, ...tokens] = line.split(/\s/); 721 | 722 | const parseMethod = (this as any)[`parse_${directive}`]; 723 | 724 | if (!parseMethod) { 725 | console.warn(`Don't know how to parse the directive: "${directive}"`); 726 | continue; 727 | } 728 | 729 | // console.log(`Parsing "${directive}" with tokens: ${tokens}`); 730 | parseMethod.bind(this)(tokens); 731 | } 732 | 733 | // some cleanup. These don't need to be exposed as public data. 734 | delete this.data; 735 | this.currentMaterial = SENTINEL_MATERIAL; 736 | } 737 | 738 | /* eslint-enable camelcase*/ 739 | } 740 | 741 | function emptyTextureOptions(): TextureMapData { 742 | return { 743 | colorCorrection: false, 744 | horizontalBlending: true, 745 | verticalBlending: true, 746 | boostMipMapSharpness: 0, 747 | modifyTextureMap: { 748 | brightness: 0, 749 | contrast: 1, 750 | }, 751 | offset: { u: 0, v: 0, w: 0 }, 752 | scale: { u: 1, v: 1, w: 1 }, 753 | turbulence: { u: 0, v: 0, w: 0 }, 754 | clamp: false, 755 | textureResolution: null, 756 | bumpMultiplier: 1, 757 | imfChan: null, 758 | filename: "", 759 | }; 760 | } 761 | -------------------------------------------------------------------------------- /src/mesh.ts: -------------------------------------------------------------------------------- 1 | import { Layout } from "./layout"; 2 | import { Material, MaterialLibrary } from "./material"; 3 | 4 | export interface MeshOptions { 5 | enableWTextureCoord?: boolean; 6 | calcTangentsAndBitangents?: boolean; 7 | materials?: { [key: string]: Material }; 8 | } 9 | 10 | interface UnpackedAttrs { 11 | verts: number[]; 12 | norms: number[]; 13 | textures: number[]; 14 | hashindices: { [k: string]: number }; 15 | indices: number[][]; 16 | materialIndices: number[]; 17 | index: number; 18 | } 19 | 20 | export interface MaterialNameToIndex { 21 | [k: string]: number; 22 | } 23 | 24 | export interface IndexToMaterial { 25 | [k: number]: Material; 26 | } 27 | 28 | export interface ArrayBufferWithItemSize extends ArrayBuffer { 29 | numItems?: number; 30 | } 31 | 32 | export interface Uint16ArrayWithItemSize extends Uint16Array { 33 | numItems?: number; 34 | } 35 | 36 | /** 37 | * The main Mesh class. The constructor will parse through the OBJ file data 38 | * and collect the vertex, vertex normal, texture, and face information. This 39 | * information can then be used later on when creating your VBOs. See 40 | * OBJ.initMeshBuffers for an example of how to use the newly created Mesh 41 | */ 42 | export default class Mesh { 43 | public vertices: number[]; 44 | public vertexNormals: number[]; 45 | public textures: number[]; 46 | public indices: number[]; 47 | public name: string = ""; 48 | public vertexMaterialIndices: number[]; 49 | public indicesPerMaterial: number[][] = []; 50 | public materialNames: string[]; 51 | public materialIndices: MaterialNameToIndex; 52 | public materialsByIndex: IndexToMaterial = {}; 53 | public tangents: number[] = []; 54 | public bitangents: number[] = []; 55 | public textureStride: number; 56 | 57 | /** 58 | * Create a Mesh 59 | * @param {String} objectData - a string representation of an OBJ file with 60 | * newlines preserved. 61 | * @param {Object} options - a JS object containing valid options. See class 62 | * documentation for options. 63 | * @param {bool} options.enableWTextureCoord - Texture coordinates can have 64 | * an optional "w" coordinate after the u and v coordinates. This extra 65 | * value can be used in order to perform fancy transformations on the 66 | * textures themselves. Default is to truncate to only the u an v 67 | * coordinates. Passing true will provide a default value of 0 in the 68 | * event that any or all texture coordinates don't provide a w value. 69 | * Always use the textureStride attribute in order to determine the 70 | * stride length of the texture coordinates when rendering the element 71 | * array. 72 | * @param {bool} options.calcTangentsAndBitangents - Calculate the tangents 73 | * and bitangents when loading of the OBJ is completed. This adds two new 74 | * attributes to the Mesh instance: `tangents` and `bitangents`. 75 | */ 76 | constructor(objectData: string, options?: MeshOptions) { 77 | options = options || {}; 78 | options.materials = options.materials || {}; 79 | options.enableWTextureCoord = !!options.enableWTextureCoord; 80 | 81 | // the list of unique vertex, normal, texture, attributes 82 | this.vertexNormals = []; 83 | this.textures = []; 84 | // the indicies to draw the faces 85 | this.indices = []; 86 | this.textureStride = options.enableWTextureCoord ? 3 : 2; 87 | 88 | /* 89 | The OBJ file format does a sort of compression when saving a model in a 90 | program like Blender. There are at least 3 sections (4 including textures) 91 | within the file. Each line in a section begins with the same string: 92 | * 'v': indicates vertex section 93 | * 'vn': indicates vertex normal section 94 | * 'f': indicates the faces section 95 | * 'vt': indicates vertex texture section (if textures were used on the model) 96 | Each of the above sections (except for the faces section) is a list/set of 97 | unique vertices. 98 | 99 | Each line of the faces section contains a list of 100 | (vertex, [texture], normal) groups. 101 | 102 | **Note:** The following documentation will use a capital "V" Vertex to 103 | denote the above (vertex, [texture], normal) groups whereas a lowercase 104 | "v" vertex is used to denote an X, Y, Z coordinate. 105 | 106 | Some examples: 107 | // the texture index is optional, both formats are possible for models 108 | // without a texture applied 109 | f 1/25 18/46 12/31 110 | f 1//25 18//46 12//31 111 | 112 | // A 3 vertex face with texture indices 113 | f 16/92/11 14/101/22 1/69/1 114 | 115 | // A 4 vertex face 116 | f 16/92/11 40/109/40 38/114/38 14/101/22 117 | 118 | The first two lines are examples of a 3 vertex face without a texture applied. 119 | The second is an example of a 3 vertex face with a texture applied. 120 | The third is an example of a 4 vertex face. Note: a face can contain N 121 | number of vertices. 122 | 123 | Each number that appears in one of the groups is a 1-based index 124 | corresponding to an item from the other sections (meaning that indexing 125 | starts at one and *not* zero). 126 | 127 | For example: 128 | `f 16/92/11` is saying to 129 | - take the 16th element from the [v] vertex array 130 | - take the 92nd element from the [vt] texture array 131 | - take the 11th element from the [vn] normal array 132 | and together they make a unique vertex. 133 | Using all 3+ unique Vertices from the face line will produce a polygon. 134 | 135 | Now, you could just go through the OBJ file and create a new vertex for 136 | each face line and WebGL will draw what appears to be the same model. 137 | However, vertices will be overlapped and duplicated all over the place. 138 | 139 | Consider a cube in 3D space centered about the origin and each side is 140 | 2 units long. The front face (with the positive Z-axis pointing towards 141 | you) would have a Top Right vertex (looking orthogonal to its normal) 142 | mapped at (1,1,1) The right face would have a Top Left vertex (looking 143 | orthogonal to its normal) at (1,1,1) and the top face would have a Bottom 144 | Right vertex (looking orthogonal to its normal) at (1,1,1). Each face 145 | has a vertex at the same coordinates, however, three distinct vertices 146 | will be drawn at the same spot. 147 | 148 | To solve the issue of duplicate Vertices (the `(vertex, [texture], normal)` 149 | groups), while iterating through the face lines, when a group is encountered 150 | the whole group string ('16/92/11') is checked to see if it exists in the 151 | packed.hashindices object, and if it doesn't, the indices it specifies 152 | are used to look up each attribute in the corresponding attribute arrays 153 | already created. The values are then copied to the corresponding unpacked 154 | array (flattened to play nice with WebGL's ELEMENT_ARRAY_BUFFER indexing), 155 | the group string is added to the hashindices set and the current unpacked 156 | index is used as this hashindices value so that the group of elements can 157 | be reused. The unpacked index is incremented. If the group string already 158 | exists in the hashindices object, its corresponding value is the index of 159 | that group and is appended to the unpacked indices array. 160 | */ 161 | const verts = []; 162 | const vertNormals = []; 163 | const textures = []; 164 | const materialNamesByIndex = []; 165 | const materialIndicesByName: MaterialNameToIndex = {}; 166 | // keep track of what material we've seen last 167 | let currentMaterialIndex = -1; 168 | let currentObjectByMaterialIndex = 0; 169 | // unpacking stuff 170 | const unpacked: UnpackedAttrs = { 171 | verts: [], 172 | norms: [], 173 | textures: [], 174 | hashindices: {}, 175 | indices: [[]], 176 | materialIndices: [], 177 | index: 0, 178 | }; 179 | 180 | const VERTEX_RE = /^v\s/; 181 | const NORMAL_RE = /^vn\s/; 182 | const TEXTURE_RE = /^vt\s/; 183 | const FACE_RE = /^f\s/; 184 | const WHITESPACE_RE = /\s+/; 185 | const USE_MATERIAL_RE = /^usemtl/; 186 | 187 | // array of lines separated by the newline 188 | const lines = objectData.split("\n"); 189 | 190 | for (let line of lines) { 191 | line = line.trim(); 192 | if (!line || line.startsWith("#")) { 193 | continue; 194 | } 195 | const elements = line.split(WHITESPACE_RE); 196 | elements.shift(); 197 | 198 | if (VERTEX_RE.test(line)) { 199 | // if this is a vertex 200 | verts.push(...elements); 201 | } else if (NORMAL_RE.test(line)) { 202 | // if this is a vertex normal 203 | vertNormals.push(...elements); 204 | } else if (TEXTURE_RE.test(line)) { 205 | let coords = elements; 206 | // by default, the loader will only look at the U and V 207 | // coordinates of the vt declaration. So, this truncates the 208 | // elements to only those 2 values. If W texture coordinate 209 | // support is enabled, then the texture coordinate is 210 | // expected to have three values in it. 211 | if (elements.length > 2 && !options.enableWTextureCoord) { 212 | coords = elements.slice(0, 2); 213 | } else if (elements.length === 2 && options.enableWTextureCoord) { 214 | // If for some reason W texture coordinate support is enabled 215 | // and only the U and V coordinates are given, then we supply 216 | // the default value of 0 so that the stride length is correct 217 | // when the textures are unpacked below. 218 | coords.push("0"); 219 | } 220 | textures.push(...coords); 221 | } else if (USE_MATERIAL_RE.test(line)) { 222 | const materialName = elements[0]; 223 | 224 | // check to see if we've ever seen it before 225 | if (!(materialName in materialIndicesByName)) { 226 | // new material we've never seen 227 | materialNamesByIndex.push(materialName); 228 | materialIndicesByName[materialName] = materialNamesByIndex.length - 1; 229 | // push new array into indices 230 | // already contains an array at index zero, don't add 231 | if (materialIndicesByName[materialName] > 0) { 232 | unpacked.indices.push([]); 233 | } 234 | } 235 | // keep track of the current material index 236 | currentMaterialIndex = materialIndicesByName[materialName]; 237 | // update current index array 238 | currentObjectByMaterialIndex = currentMaterialIndex; 239 | } else if (FACE_RE.test(line)) { 240 | // if this is a face 241 | /* 242 | split this face into an array of Vertex groups 243 | for example: 244 | f 16/92/11 14/101/22 1/69/1 245 | becomes: 246 | ['16/92/11', '14/101/22', '1/69/1']; 247 | */ 248 | 249 | const triangles = triangulate(elements); 250 | for (const triangle of triangles) { 251 | for (let j = 0, eleLen = triangle.length; j < eleLen; j++) { 252 | const hash = triangle[j] + "," + currentMaterialIndex; 253 | if (hash in unpacked.hashindices) { 254 | unpacked.indices[currentObjectByMaterialIndex].push(unpacked.hashindices[hash]); 255 | } else { 256 | /* 257 | Each element of the face line array is a Vertex which has its 258 | attributes delimited by a forward slash. This will separate 259 | each attribute into another array: 260 | '19/92/11' 261 | becomes: 262 | Vertex = ['19', '92', '11']; 263 | where 264 | Vertex[0] is the vertex index 265 | Vertex[1] is the texture index 266 | Vertex[2] is the normal index 267 | Think of faces having Vertices which are comprised of the 268 | attributes location (v), texture (vt), and normal (vn). 269 | */ 270 | const vertex = triangle[j].split("/"); 271 | // it's possible for faces to only specify the vertex 272 | // and the normal. In this case, vertex will only have 273 | // a length of 2 and not 3 and the normal will be the 274 | // second item in the list with an index of 1. 275 | const normalIndex = vertex.length - 1; 276 | /* 277 | The verts, textures, and vertNormals arrays each contain a 278 | flattend array of coordinates. 279 | 280 | Because it gets confusing by referring to Vertex and then 281 | vertex (both are different in my descriptions) I will explain 282 | what's going on using the vertexNormals array: 283 | 284 | vertex[2] will contain the one-based index of the vertexNormals 285 | section (vn). One is subtracted from this index number to play 286 | nice with javascript's zero-based array indexing. 287 | 288 | Because vertexNormal is a flattened array of x, y, z values, 289 | simple pointer arithmetic is used to skip to the start of the 290 | vertexNormal, then the offset is added to get the correct 291 | component: +0 is x, +1 is y, +2 is z. 292 | 293 | This same process is repeated for verts and textures. 294 | */ 295 | // Vertex position 296 | unpacked.verts.push(+verts[(+vertex[0] - 1) * 3 + 0]); 297 | unpacked.verts.push(+verts[(+vertex[0] - 1) * 3 + 1]); 298 | unpacked.verts.push(+verts[(+vertex[0] - 1) * 3 + 2]); 299 | // Vertex textures 300 | if (textures.length) { 301 | const stride = options.enableWTextureCoord ? 3 : 2; 302 | unpacked.textures.push(+textures[(+vertex[1] - 1) * stride + 0]); 303 | unpacked.textures.push(+textures[(+vertex[1] - 1) * stride + 1]); 304 | if (options.enableWTextureCoord) { 305 | unpacked.textures.push(+textures[(+vertex[1] - 1) * stride + 2]); 306 | } 307 | } 308 | // Vertex normals 309 | unpacked.norms.push(+vertNormals[(+vertex[normalIndex] - 1) * 3 + 0]); 310 | unpacked.norms.push(+vertNormals[(+vertex[normalIndex] - 1) * 3 + 1]); 311 | unpacked.norms.push(+vertNormals[(+vertex[normalIndex] - 1) * 3 + 2]); 312 | // Vertex material indices 313 | unpacked.materialIndices.push(currentMaterialIndex); 314 | // add the newly created Vertex to the list of indices 315 | unpacked.hashindices[hash] = unpacked.index; 316 | unpacked.indices[currentObjectByMaterialIndex].push(unpacked.hashindices[hash]); 317 | // increment the counter 318 | unpacked.index += 1; 319 | } 320 | } 321 | } 322 | } 323 | } 324 | this.vertices = unpacked.verts; 325 | this.vertexNormals = unpacked.norms; 326 | this.textures = unpacked.textures; 327 | this.vertexMaterialIndices = unpacked.materialIndices; 328 | this.indices = unpacked.indices[currentObjectByMaterialIndex]; 329 | this.indicesPerMaterial = unpacked.indices; 330 | 331 | this.materialNames = materialNamesByIndex; 332 | this.materialIndices = materialIndicesByName; 333 | this.materialsByIndex = {}; 334 | 335 | if (options.calcTangentsAndBitangents) { 336 | this.calculateTangentsAndBitangents(); 337 | } 338 | } 339 | 340 | /** 341 | * Calculates the tangents and bitangents of the mesh that forms an orthogonal basis together with the 342 | * normal in the direction of the texture coordinates. These are useful for setting up the TBN matrix 343 | * when distorting the normals through normal maps. 344 | * Method derived from: http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/ 345 | * 346 | * This method requires the normals and texture coordinates to be parsed and set up correctly. 347 | * Adds the tangents and bitangents as members of the class instance. 348 | */ 349 | calculateTangentsAndBitangents() { 350 | console.assert( 351 | !!( 352 | this.vertices && 353 | this.vertices.length && 354 | this.vertexNormals && 355 | this.vertexNormals.length && 356 | this.textures && 357 | this.textures.length 358 | ), 359 | "Missing attributes for calculating tangents and bitangents", 360 | ); 361 | 362 | const unpacked = { 363 | tangents: [...new Array(this.vertices.length)].map(_ => 0), 364 | bitangents: [...new Array(this.vertices.length)].map(_ => 0), 365 | }; 366 | 367 | // Loop through all faces in the whole mesh 368 | const indices = this.indices; 369 | const vertices = this.vertices; 370 | const normals = this.vertexNormals; 371 | const uvs = this.textures; 372 | 373 | for (let i = 0; i < indices.length; i += 3) { 374 | const i0 = indices[i + 0]; 375 | const i1 = indices[i + 1]; 376 | const i2 = indices[i + 2]; 377 | 378 | const x_v0 = vertices[i0 * 3 + 0]; 379 | const y_v0 = vertices[i0 * 3 + 1]; 380 | const z_v0 = vertices[i0 * 3 + 2]; 381 | 382 | const x_uv0 = uvs[i0 * 2 + 0]; 383 | const y_uv0 = uvs[i0 * 2 + 1]; 384 | 385 | const x_v1 = vertices[i1 * 3 + 0]; 386 | const y_v1 = vertices[i1 * 3 + 1]; 387 | const z_v1 = vertices[i1 * 3 + 2]; 388 | 389 | const x_uv1 = uvs[i1 * 2 + 0]; 390 | const y_uv1 = uvs[i1 * 2 + 1]; 391 | 392 | const x_v2 = vertices[i2 * 3 + 0]; 393 | const y_v2 = vertices[i2 * 3 + 1]; 394 | const z_v2 = vertices[i2 * 3 + 2]; 395 | 396 | const x_uv2 = uvs[i2 * 2 + 0]; 397 | const y_uv2 = uvs[i2 * 2 + 1]; 398 | 399 | const x_deltaPos1 = x_v1 - x_v0; 400 | const y_deltaPos1 = y_v1 - y_v0; 401 | const z_deltaPos1 = z_v1 - z_v0; 402 | 403 | const x_deltaPos2 = x_v2 - x_v0; 404 | const y_deltaPos2 = y_v2 - y_v0; 405 | const z_deltaPos2 = z_v2 - z_v0; 406 | 407 | const x_uvDeltaPos1 = x_uv1 - x_uv0; 408 | const y_uvDeltaPos1 = y_uv1 - y_uv0; 409 | 410 | const x_uvDeltaPos2 = x_uv2 - x_uv0; 411 | const y_uvDeltaPos2 = y_uv2 - y_uv0; 412 | 413 | const rInv = x_uvDeltaPos1 * y_uvDeltaPos2 - y_uvDeltaPos1 * x_uvDeltaPos2; 414 | const r = 1.0 / Math.abs(rInv < 0.0001 ? 1.0 : rInv); 415 | 416 | // Tangent 417 | const x_tangent = (x_deltaPos1 * y_uvDeltaPos2 - x_deltaPos2 * y_uvDeltaPos1) * r; 418 | const y_tangent = (y_deltaPos1 * y_uvDeltaPos2 - y_deltaPos2 * y_uvDeltaPos1) * r; 419 | const z_tangent = (z_deltaPos1 * y_uvDeltaPos2 - z_deltaPos2 * y_uvDeltaPos1) * r; 420 | 421 | // Bitangent 422 | const x_bitangent = (x_deltaPos2 * x_uvDeltaPos1 - x_deltaPos1 * x_uvDeltaPos2) * r; 423 | const y_bitangent = (y_deltaPos2 * x_uvDeltaPos1 - y_deltaPos1 * x_uvDeltaPos2) * r; 424 | const z_bitangent = (z_deltaPos2 * x_uvDeltaPos1 - z_deltaPos1 * x_uvDeltaPos2) * r; 425 | 426 | // Gram-Schmidt orthogonalize 427 | //t = glm::normalize(t - n * glm:: dot(n, t)); 428 | const x_n0 = normals[i0 * 3 + 0]; 429 | const y_n0 = normals[i0 * 3 + 1]; 430 | const z_n0 = normals[i0 * 3 + 2]; 431 | 432 | const x_n1 = normals[i1 * 3 + 0]; 433 | const y_n1 = normals[i1 * 3 + 1]; 434 | const z_n1 = normals[i1 * 3 + 2]; 435 | 436 | const x_n2 = normals[i2 * 3 + 0]; 437 | const y_n2 = normals[i2 * 3 + 1]; 438 | const z_n2 = normals[i2 * 3 + 2]; 439 | 440 | // Tangent 441 | const n0_dot_t = x_tangent * x_n0 + y_tangent * y_n0 + z_tangent * z_n0; 442 | const n1_dot_t = x_tangent * x_n1 + y_tangent * y_n1 + z_tangent * z_n1; 443 | const n2_dot_t = x_tangent * x_n2 + y_tangent * y_n2 + z_tangent * z_n2; 444 | 445 | const x_resTangent0 = x_tangent - x_n0 * n0_dot_t; 446 | const y_resTangent0 = y_tangent - y_n0 * n0_dot_t; 447 | const z_resTangent0 = z_tangent - z_n0 * n0_dot_t; 448 | 449 | const x_resTangent1 = x_tangent - x_n1 * n1_dot_t; 450 | const y_resTangent1 = y_tangent - y_n1 * n1_dot_t; 451 | const z_resTangent1 = z_tangent - z_n1 * n1_dot_t; 452 | 453 | const x_resTangent2 = x_tangent - x_n2 * n2_dot_t; 454 | const y_resTangent2 = y_tangent - y_n2 * n2_dot_t; 455 | const z_resTangent2 = z_tangent - z_n2 * n2_dot_t; 456 | 457 | const magTangent0 = Math.sqrt( 458 | x_resTangent0 * x_resTangent0 + y_resTangent0 * y_resTangent0 + z_resTangent0 * z_resTangent0, 459 | ); 460 | const magTangent1 = Math.sqrt( 461 | x_resTangent1 * x_resTangent1 + y_resTangent1 * y_resTangent1 + z_resTangent1 * z_resTangent1, 462 | ); 463 | const magTangent2 = Math.sqrt( 464 | x_resTangent2 * x_resTangent2 + y_resTangent2 * y_resTangent2 + z_resTangent2 * z_resTangent2, 465 | ); 466 | 467 | // Bitangent 468 | const n0_dot_bt = x_bitangent * x_n0 + y_bitangent * y_n0 + z_bitangent * z_n0; 469 | const n1_dot_bt = x_bitangent * x_n1 + y_bitangent * y_n1 + z_bitangent * z_n1; 470 | const n2_dot_bt = x_bitangent * x_n2 + y_bitangent * y_n2 + z_bitangent * z_n2; 471 | 472 | const x_resBitangent0 = x_bitangent - x_n0 * n0_dot_bt; 473 | const y_resBitangent0 = y_bitangent - y_n0 * n0_dot_bt; 474 | const z_resBitangent0 = z_bitangent - z_n0 * n0_dot_bt; 475 | 476 | const x_resBitangent1 = x_bitangent - x_n1 * n1_dot_bt; 477 | const y_resBitangent1 = y_bitangent - y_n1 * n1_dot_bt; 478 | const z_resBitangent1 = z_bitangent - z_n1 * n1_dot_bt; 479 | 480 | const x_resBitangent2 = x_bitangent - x_n2 * n2_dot_bt; 481 | const y_resBitangent2 = y_bitangent - y_n2 * n2_dot_bt; 482 | const z_resBitangent2 = z_bitangent - z_n2 * n2_dot_bt; 483 | 484 | const magBitangent0 = Math.sqrt( 485 | x_resBitangent0 * x_resBitangent0 + 486 | y_resBitangent0 * y_resBitangent0 + 487 | z_resBitangent0 * z_resBitangent0, 488 | ); 489 | const magBitangent1 = Math.sqrt( 490 | x_resBitangent1 * x_resBitangent1 + 491 | y_resBitangent1 * y_resBitangent1 + 492 | z_resBitangent1 * z_resBitangent1, 493 | ); 494 | const magBitangent2 = Math.sqrt( 495 | x_resBitangent2 * x_resBitangent2 + 496 | y_resBitangent2 * y_resBitangent2 + 497 | z_resBitangent2 * z_resBitangent2, 498 | ); 499 | 500 | unpacked.tangents[i0 * 3 + 0] += x_resTangent0 / magTangent0; 501 | unpacked.tangents[i0 * 3 + 1] += y_resTangent0 / magTangent0; 502 | unpacked.tangents[i0 * 3 + 2] += z_resTangent0 / magTangent0; 503 | 504 | unpacked.tangents[i1 * 3 + 0] += x_resTangent1 / magTangent1; 505 | unpacked.tangents[i1 * 3 + 1] += y_resTangent1 / magTangent1; 506 | unpacked.tangents[i1 * 3 + 2] += z_resTangent1 / magTangent1; 507 | 508 | unpacked.tangents[i2 * 3 + 0] += x_resTangent2 / magTangent2; 509 | unpacked.tangents[i2 * 3 + 1] += y_resTangent2 / magTangent2; 510 | unpacked.tangents[i2 * 3 + 2] += z_resTangent2 / magTangent2; 511 | 512 | unpacked.bitangents[i0 * 3 + 0] += x_resBitangent0 / magBitangent0; 513 | unpacked.bitangents[i0 * 3 + 1] += y_resBitangent0 / magBitangent0; 514 | unpacked.bitangents[i0 * 3 + 2] += z_resBitangent0 / magBitangent0; 515 | 516 | unpacked.bitangents[i1 * 3 + 0] += x_resBitangent1 / magBitangent1; 517 | unpacked.bitangents[i1 * 3 + 1] += y_resBitangent1 / magBitangent1; 518 | unpacked.bitangents[i1 * 3 + 2] += z_resBitangent1 / magBitangent1; 519 | 520 | unpacked.bitangents[i2 * 3 + 0] += x_resBitangent2 / magBitangent2; 521 | unpacked.bitangents[i2 * 3 + 1] += y_resBitangent2 / magBitangent2; 522 | unpacked.bitangents[i2 * 3 + 2] += z_resBitangent2 / magBitangent2; 523 | 524 | // TODO: check handedness 525 | } 526 | 527 | this.tangents = unpacked.tangents; 528 | this.bitangents = unpacked.bitangents; 529 | } 530 | 531 | /** 532 | * @param layout - A {@link Layout} object that describes the 533 | * desired memory layout of the generated buffer 534 | * @return The packed array in the ... TODO 535 | */ 536 | makeBufferData(layout: Layout): ArrayBufferWithItemSize { 537 | const numItems = this.vertices.length / 3; 538 | const buffer: ArrayBufferWithItemSize = new ArrayBuffer(layout.stride * numItems); 539 | buffer.numItems = numItems; 540 | const dataView = new DataView(buffer); 541 | for (let i = 0, vertexOffset = 0; i < numItems; i++) { 542 | vertexOffset = i * layout.stride; 543 | // copy in the vertex data in the order and format given by the 544 | // layout param 545 | for (const attribute of layout.attributes) { 546 | const offset = vertexOffset + layout.attributeMap[attribute.key].offset; 547 | switch (attribute.key) { 548 | case Layout.POSITION.key: 549 | dataView.setFloat32(offset, this.vertices[i * 3], true); 550 | dataView.setFloat32(offset + 4, this.vertices[i * 3 + 1], true); 551 | dataView.setFloat32(offset + 8, this.vertices[i * 3 + 2], true); 552 | break; 553 | case Layout.UV.key: 554 | dataView.setFloat32(offset, this.textures[i * 2], true); 555 | dataView.setFloat32(offset + 4, this.textures[i * 2 + 1], true); 556 | break; 557 | case Layout.NORMAL.key: 558 | dataView.setFloat32(offset, this.vertexNormals[i * 3], true); 559 | dataView.setFloat32(offset + 4, this.vertexNormals[i * 3 + 1], true); 560 | dataView.setFloat32(offset + 8, this.vertexNormals[i * 3 + 2], true); 561 | break; 562 | case Layout.MATERIAL_INDEX.key: 563 | dataView.setInt16(offset, this.vertexMaterialIndices[i], true); 564 | break; 565 | case Layout.AMBIENT.key: { 566 | const materialIndex = this.vertexMaterialIndices[i]; 567 | const material = this.materialsByIndex[materialIndex]; 568 | if (!material) { 569 | console.warn( 570 | 'Material "' + 571 | this.materialNames[materialIndex] + 572 | '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', 573 | ); 574 | break; 575 | } 576 | dataView.setFloat32(offset, material.ambient[0], true); 577 | dataView.setFloat32(offset + 4, material.ambient[1], true); 578 | dataView.setFloat32(offset + 8, material.ambient[2], true); 579 | break; 580 | } 581 | case Layout.DIFFUSE.key: { 582 | const materialIndex = this.vertexMaterialIndices[i]; 583 | const material = this.materialsByIndex[materialIndex]; 584 | if (!material) { 585 | console.warn( 586 | 'Material "' + 587 | this.materialNames[materialIndex] + 588 | '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', 589 | ); 590 | break; 591 | } 592 | dataView.setFloat32(offset, material.diffuse[0], true); 593 | dataView.setFloat32(offset + 4, material.diffuse[1], true); 594 | dataView.setFloat32(offset + 8, material.diffuse[2], true); 595 | break; 596 | } 597 | case Layout.SPECULAR.key: { 598 | const materialIndex = this.vertexMaterialIndices[i]; 599 | const material = this.materialsByIndex[materialIndex]; 600 | if (!material) { 601 | console.warn( 602 | 'Material "' + 603 | this.materialNames[materialIndex] + 604 | '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', 605 | ); 606 | break; 607 | } 608 | dataView.setFloat32(offset, material.specular[0], true); 609 | dataView.setFloat32(offset + 4, material.specular[1], true); 610 | dataView.setFloat32(offset + 8, material.specular[2], true); 611 | break; 612 | } 613 | case Layout.SPECULAR_EXPONENT.key: { 614 | const materialIndex = this.vertexMaterialIndices[i]; 615 | const material = this.materialsByIndex[materialIndex]; 616 | if (!material) { 617 | console.warn( 618 | 'Material "' + 619 | this.materialNames[materialIndex] + 620 | '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', 621 | ); 622 | break; 623 | } 624 | dataView.setFloat32(offset, material.specularExponent, true); 625 | break; 626 | } 627 | case Layout.EMISSIVE.key: { 628 | const materialIndex = this.vertexMaterialIndices[i]; 629 | const material = this.materialsByIndex[materialIndex]; 630 | if (!material) { 631 | console.warn( 632 | 'Material "' + 633 | this.materialNames[materialIndex] + 634 | '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', 635 | ); 636 | break; 637 | } 638 | dataView.setFloat32(offset, material.emissive[0], true); 639 | dataView.setFloat32(offset + 4, material.emissive[1], true); 640 | dataView.setFloat32(offset + 8, material.emissive[2], true); 641 | break; 642 | } 643 | case Layout.TRANSMISSION_FILTER.key: { 644 | const materialIndex = this.vertexMaterialIndices[i]; 645 | const material = this.materialsByIndex[materialIndex]; 646 | if (!material) { 647 | console.warn( 648 | 'Material "' + 649 | this.materialNames[materialIndex] + 650 | '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', 651 | ); 652 | break; 653 | } 654 | dataView.setFloat32(offset, material.transmissionFilter[0], true); 655 | dataView.setFloat32(offset + 4, material.transmissionFilter[1], true); 656 | dataView.setFloat32(offset + 8, material.transmissionFilter[2], true); 657 | break; 658 | } 659 | case Layout.DISSOLVE.key: { 660 | const materialIndex = this.vertexMaterialIndices[i]; 661 | const material = this.materialsByIndex[materialIndex]; 662 | if (!material) { 663 | console.warn( 664 | 'Material "' + 665 | this.materialNames[materialIndex] + 666 | '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', 667 | ); 668 | break; 669 | } 670 | dataView.setFloat32(offset, material.dissolve, true); 671 | break; 672 | } 673 | case Layout.ILLUMINATION.key: { 674 | const materialIndex = this.vertexMaterialIndices[i]; 675 | const material = this.materialsByIndex[materialIndex]; 676 | if (!material) { 677 | console.warn( 678 | 'Material "' + 679 | this.materialNames[materialIndex] + 680 | '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', 681 | ); 682 | break; 683 | } 684 | dataView.setInt16(offset, material.illumination, true); 685 | break; 686 | } 687 | case Layout.REFRACTION_INDEX.key: { 688 | const materialIndex = this.vertexMaterialIndices[i]; 689 | const material = this.materialsByIndex[materialIndex]; 690 | if (!material) { 691 | console.warn( 692 | 'Material "' + 693 | this.materialNames[materialIndex] + 694 | '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', 695 | ); 696 | break; 697 | } 698 | dataView.setFloat32(offset, material.refractionIndex, true); 699 | break; 700 | } 701 | case Layout.SHARPNESS.key: { 702 | const materialIndex = this.vertexMaterialIndices[i]; 703 | const material = this.materialsByIndex[materialIndex]; 704 | if (!material) { 705 | console.warn( 706 | 'Material "' + 707 | this.materialNames[materialIndex] + 708 | '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', 709 | ); 710 | break; 711 | } 712 | dataView.setFloat32(offset, material.sharpness, true); 713 | break; 714 | } 715 | case Layout.ANTI_ALIASING.key: { 716 | const materialIndex = this.vertexMaterialIndices[i]; 717 | const material = this.materialsByIndex[materialIndex]; 718 | if (!material) { 719 | console.warn( 720 | 'Material "' + 721 | this.materialNames[materialIndex] + 722 | '" not found in mesh. Did you forget to call addMaterialLibrary(...)?"', 723 | ); 724 | break; 725 | } 726 | dataView.setInt16(offset, material.antiAliasing ? 1 : 0, true); 727 | break; 728 | } 729 | } 730 | } 731 | } 732 | return buffer; 733 | } 734 | 735 | makeIndexBufferData(): Uint16ArrayWithItemSize { 736 | const buffer: Uint16ArrayWithItemSize = new Uint16Array(this.indices); 737 | buffer.numItems = this.indices.length; 738 | return buffer; 739 | } 740 | 741 | makeIndexBufferDataForMaterials(...materialIndices: Array): Uint16ArrayWithItemSize { 742 | const indices: number[] = new Array().concat( 743 | ...materialIndices.map(mtlIdx => this.indicesPerMaterial[mtlIdx]), 744 | ); 745 | const buffer: Uint16ArrayWithItemSize = new Uint16Array(indices); 746 | buffer.numItems = indices.length; 747 | return buffer; 748 | } 749 | 750 | addMaterialLibrary(mtl: MaterialLibrary) { 751 | for (const name in mtl.materials) { 752 | if (!(name in this.materialIndices)) { 753 | // This material is not referenced by the mesh 754 | continue; 755 | } 756 | 757 | const material = mtl.materials[name]; 758 | 759 | // Find the material index for this material 760 | const materialIndex = this.materialIndices[material.name]; 761 | 762 | // Put the material into the materialsByIndex object at the right 763 | // spot as determined when the obj file was parsed 764 | this.materialsByIndex[materialIndex] = material; 765 | } 766 | } 767 | } 768 | 769 | function* triangulate(elements: string[]) { 770 | if (elements.length <= 3) { 771 | yield elements; 772 | } else if (elements.length === 4) { 773 | yield [elements[0], elements[1], elements[2]]; 774 | yield [elements[2], elements[3], elements[0]]; 775 | } else { 776 | for (let i = 1; i < elements.length - 1; i++) { 777 | yield [elements[0], elements[i], elements[i + 1]]; 778 | } 779 | } 780 | } 781 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import Mesh from "./mesh"; 2 | import { MaterialLibrary, TextureMapData } from "./material"; 3 | 4 | function downloadMtlTextures(mtl: MaterialLibrary, root: string) { 5 | const mapAttributes = [ 6 | "mapDiffuse", 7 | "mapAmbient", 8 | "mapSpecular", 9 | "mapDissolve", 10 | "mapBump", 11 | "mapDisplacement", 12 | "mapDecal", 13 | "mapEmissive", 14 | ]; 15 | if (!root.endsWith("/")) { 16 | root += "/"; 17 | } 18 | const textures = []; 19 | 20 | for (const materialName in mtl.materials) { 21 | if (!mtl.materials.hasOwnProperty(materialName)) { 22 | continue; 23 | } 24 | const material = mtl.materials[materialName]; 25 | 26 | for (const attr of mapAttributes) { 27 | const mapData = (material as any)[attr] as TextureMapData; 28 | if (!mapData || !mapData.filename) { 29 | continue; 30 | } 31 | const url = root + mapData.filename; 32 | textures.push( 33 | fetch(url) 34 | .then(response => { 35 | if (!response.ok) { 36 | throw new Error(); 37 | } 38 | return response.blob(); 39 | }) 40 | .then(function(data) { 41 | const image = new Image(); 42 | image.src = URL.createObjectURL(data); 43 | mapData.texture = image; 44 | return new Promise(resolve => (image.onload = resolve)); 45 | }) 46 | .catch(() => { 47 | console.error(`Unable to download texture: ${url}`); 48 | }), 49 | ); 50 | } 51 | } 52 | 53 | return Promise.all(textures); 54 | } 55 | 56 | function getMtl(modelOptions: DownloadModelsOptions): string { 57 | if (!(typeof modelOptions.mtl === "string")) { 58 | return modelOptions.obj.replace(/\.obj$/, ".mtl"); 59 | } 60 | 61 | return modelOptions.mtl; 62 | } 63 | 64 | export interface DownloadModelsOptions { 65 | obj: string; 66 | mtl?: boolean | string; 67 | downloadMtlTextures?: boolean; 68 | mtlTextureRoot?: string; 69 | name?: string; 70 | indicesPerMaterial?: boolean; 71 | calcTangentsAndBitangents?: boolean; 72 | } 73 | 74 | type ModelPromises = [Promise, Promise, undefined | Promise]; 75 | export type MeshMap = { [name: string]: Mesh }; 76 | /** 77 | * Accepts a list of model request objects and returns a Promise that 78 | * resolves when all models have been downloaded and parsed. 79 | * 80 | * The list of model objects follow this interface: 81 | * { 82 | * obj: 'path/to/model.obj', 83 | * mtl: true | 'path/to/model.mtl', 84 | * downloadMtlTextures: true | false 85 | * mtlTextureRoot: '/models/suzanne/maps' 86 | * name: 'suzanne' 87 | * } 88 | * 89 | * The `obj` attribute is required and should be the path to the 90 | * model's .obj file relative to the current repo (absolute URLs are 91 | * suggested). 92 | * 93 | * The `mtl` attribute is optional and can either be a boolean or 94 | * a path to the model's .mtl file relative to the current URL. If 95 | * the value is `true`, then the path and basename given for the `obj` 96 | * attribute is used replacing the .obj suffix for .mtl 97 | * E.g.: {obj: 'models/foo.obj', mtl: true} would search for 'models/foo.mtl' 98 | * 99 | * The `name` attribute is optional and is a human friendly name to be 100 | * included with the parsed OBJ and MTL files. If not given, the base .obj 101 | * filename will be used. 102 | * 103 | * The `downloadMtlTextures` attribute is a flag for automatically downloading 104 | * any images found in the MTL file and attaching them to each Material 105 | * created from that file. For example, if material.mapDiffuse is set (there 106 | * was data in the MTL file), then material.mapDiffuse.texture will contain 107 | * the downloaded image. This option defaults to `true`. By default, the MTL's 108 | * URL will be used to determine the location of the images. 109 | * 110 | * The `mtlTextureRoot` attribute is optional and should point to the location 111 | * on the server that this MTL's texture files are located. The default is to 112 | * use the MTL file's location. 113 | * 114 | * @returns {Promise} the result of downloading the given list of models. The 115 | * promise will resolve with an object whose keys are the names of the models 116 | * and the value is its Mesh object. Each Mesh object will automatically 117 | * have its addMaterialLibrary() method called to set the given MTL data (if given). 118 | */ 119 | export function downloadModels(models: DownloadModelsOptions[]): Promise { 120 | const finished = []; 121 | 122 | for (const model of models) { 123 | if (!model.obj) { 124 | throw new Error( 125 | '"obj" attribute of model object not set. The .obj file is required to be set ' + 126 | "in order to use downloadModels()", 127 | ); 128 | } 129 | 130 | const options = { 131 | indicesPerMaterial: !!model.indicesPerMaterial, 132 | calcTangentsAndBitangents: !!model.calcTangentsAndBitangents, 133 | }; 134 | 135 | // if the name is not provided, dervive it from the given OBJ 136 | let name = model.name; 137 | if (!name) { 138 | const parts = model.obj.split("/"); 139 | name = parts[parts.length - 1].replace(".obj", ""); 140 | } 141 | const namePromise = Promise.resolve(name); 142 | 143 | const meshPromise = fetch(model.obj) 144 | .then(response => response.text()) 145 | .then(data => { 146 | return new Mesh(data, options); 147 | }); 148 | 149 | let mtlPromise; 150 | // Download MaterialLibrary file? 151 | if (model.mtl) { 152 | const mtl = getMtl(model); 153 | mtlPromise = fetch(mtl) 154 | .then(response => response.text()) 155 | .then( 156 | (data: string): Promise<[MaterialLibrary, any]> => { 157 | const material = new MaterialLibrary(data); 158 | if (model.downloadMtlTextures !== false) { 159 | let root = model.mtlTextureRoot; 160 | if (!root) { 161 | // get the directory of the MTL file as default 162 | root = mtl.substr(0, mtl.lastIndexOf("/")); 163 | } 164 | // downloadMtlTextures returns a Promise that 165 | // is resolved once all of the images it 166 | // contains are downloaded. These are then 167 | // attached to the map data objects 168 | return Promise.all([Promise.resolve(material), downloadMtlTextures(material, root)]); 169 | } 170 | return Promise.all([Promise.resolve(material), undefined]); 171 | }, 172 | ) 173 | .then((value: [MaterialLibrary, any]) => { 174 | return value[0]; 175 | }); 176 | } 177 | 178 | const parsed: ModelPromises = [namePromise, meshPromise, mtlPromise]; 179 | finished.push(Promise.all(parsed)); 180 | } 181 | 182 | return Promise.all(finished).then(ms => { 183 | // the "finished" promise is a list of name, Mesh instance, 184 | // and MaterialLibary instance. This unpacks and returns an 185 | // object mapping name to Mesh (Mesh points to MTL). 186 | const models: MeshMap = {}; 187 | 188 | for (const model of ms) { 189 | const [name, mesh, mtl] = model; 190 | mesh.name = name; 191 | if (mtl) { 192 | mesh.addMaterialLibrary(mtl); 193 | } 194 | models[name] = mesh; 195 | } 196 | 197 | return models; 198 | }); 199 | } 200 | 201 | export interface NameAndUrls { 202 | [meshName: string]: string; 203 | } 204 | 205 | /** 206 | * Takes in an object of `mesh_name`, `'/url/to/OBJ/file'` pairs and a callback 207 | * function. Each OBJ file will be ajaxed in and automatically converted to 208 | * an OBJ.Mesh. When all files have successfully downloaded the callback 209 | * function provided will be called and passed in an object containing 210 | * the newly created meshes. 211 | * 212 | * **Note:** In order to use this function as a way to download meshes, a 213 | * webserver of some sort must be used. 214 | * 215 | * @param {Object} nameAndAttrs an object where the key is the name of the mesh and the value is the url to that mesh's OBJ file 216 | * 217 | * @param {Function} 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 218 | * 219 | * @param {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: { '': OBJ.Mesh } 220 | * 221 | */ 222 | export function downloadMeshes( 223 | nameAndURLs: NameAndUrls, 224 | completionCallback: (meshes: MeshMap) => void, 225 | meshes: MeshMap, 226 | ) { 227 | if (meshes === undefined) { 228 | meshes = {}; 229 | } 230 | 231 | const completed: Promise<[string, Mesh]>[] = []; 232 | 233 | for (const mesh_name in nameAndURLs) { 234 | if (!nameAndURLs.hasOwnProperty(mesh_name)) { 235 | continue; 236 | } 237 | const url = nameAndURLs[mesh_name]; 238 | completed.push( 239 | fetch(url) 240 | .then(response => response.text()) 241 | .then(data => { 242 | return [mesh_name, new Mesh(data)] as [string, Mesh]; 243 | }), 244 | ); 245 | } 246 | 247 | Promise.all(completed).then(ms => { 248 | for (const [name, mesh] of ms) { 249 | meshes[name] = mesh; 250 | } 251 | 252 | return completionCallback(meshes); 253 | }); 254 | } 255 | 256 | export interface ExtendedGLBuffer extends WebGLBuffer { 257 | itemSize: number; 258 | numItems: number; 259 | } 260 | 261 | function _buildBuffer(gl: WebGLRenderingContext, type: GLenum, data: number[], itemSize: number): ExtendedGLBuffer { 262 | const buffer = gl.createBuffer() as ExtendedGLBuffer; 263 | const arrayView = type === gl.ARRAY_BUFFER ? Float32Array : Uint16Array; 264 | gl.bindBuffer(type, buffer); 265 | gl.bufferData(type, new arrayView(data), gl.STATIC_DRAW); 266 | buffer.itemSize = itemSize; 267 | buffer.numItems = data.length / itemSize; 268 | return buffer; 269 | } 270 | 271 | export interface MeshWithBuffers extends Mesh { 272 | normalBuffer: ExtendedGLBuffer; 273 | textureBuffer: ExtendedGLBuffer; 274 | vertexBuffer: ExtendedGLBuffer; 275 | indexBuffer: ExtendedGLBuffer; 276 | } 277 | 278 | /** 279 | * Takes in the WebGL context and a Mesh, then creates and appends the buffers 280 | * to the mesh object as attributes. 281 | * 282 | * @param {WebGLRenderingContext} gl the `canvas.getContext('webgl')` context instance 283 | * @param {Mesh} mesh a single `OBJ.Mesh` instance 284 | * 285 | * The newly created mesh attributes are: 286 | * 287 | * Attrbute | Description 288 | * :--- | --- 289 | * **normalBuffer** |contains the model's Vertex Normals 290 | * normalBuffer.itemSize |set to 3 items 291 | * normalBuffer.numItems |the total number of vertex normals 292 | * | 293 | * **textureBuffer** |contains the model's Texture Coordinates 294 | * textureBuffer.itemSize |set to 2 items 295 | * textureBuffer.numItems |the number of texture coordinates 296 | * | 297 | * **vertexBuffer** |contains the model's Vertex Position Coordinates (does not include w) 298 | * vertexBuffer.itemSize |set to 3 items 299 | * vertexBuffer.numItems |the total number of vertices 300 | * | 301 | * **indexBuffer** |contains the indices of the faces 302 | * indexBuffer.itemSize |is set to 1 303 | * indexBuffer.numItems |the total number of indices 304 | * 305 | * A simple example (a lot of steps are missing, so don't copy and paste): 306 | * 307 | * const gl = canvas.getContext('webgl'), 308 | * mesh = OBJ.Mesh(obj_file_data); 309 | * // compile the shaders and create a shader program 310 | * const shaderProgram = gl.createProgram(); 311 | * // compilation stuff here 312 | * ... 313 | * // make sure you have vertex, vertex normal, and texture coordinate 314 | * // attributes located in your shaders and attach them to the shader program 315 | * shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); 316 | * gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); 317 | * 318 | * shaderProgram.vertexNormalAttribute = gl.getAttribLocation(shaderProgram, "aVertexNormal"); 319 | * gl.enableVertexAttribArray(shaderProgram.vertexNormalAttribute); 320 | * 321 | * shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord"); 322 | * gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute); 323 | * 324 | * // create and initialize the vertex, vertex normal, and texture coordinate buffers 325 | * // and save on to the mesh object 326 | * OBJ.initMeshBuffers(gl, mesh); 327 | * 328 | * // now to render the mesh 329 | * gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vertexBuffer); 330 | * gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, mesh.vertexBuffer.itemSize, gl.FLOAT, false, 0, 0); 331 | * // it's possible that the mesh doesn't contain 332 | * // any texture coordinates (e.g. suzanne.obj in the development branch). 333 | * // in this case, the texture vertexAttribArray will need to be disabled 334 | * // before the call to drawElements 335 | * if(!mesh.textures.length){ 336 | * gl.disableVertexAttribArray(shaderProgram.textureCoordAttribute); 337 | * } 338 | * else{ 339 | * // if the texture vertexAttribArray has been previously 340 | * // disabled, then it needs to be re-enabled 341 | * gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute); 342 | * gl.bindBuffer(gl.ARRAY_BUFFER, mesh.textureBuffer); 343 | * gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, mesh.textureBuffer.itemSize, gl.FLOAT, false, 0, 0); 344 | * } 345 | * 346 | * gl.bindBuffer(gl.ARRAY_BUFFER, mesh.normalBuffer); 347 | * gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, mesh.normalBuffer.itemSize, gl.FLOAT, false, 0, 0); 348 | * 349 | * gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.mesh.indexBuffer); 350 | * gl.drawElements(gl.TRIANGLES, model.mesh.indexBuffer.numItems, gl.UNSIGNED_SHORT, 0); 351 | */ 352 | export function initMeshBuffers(gl: WebGLRenderingContext, mesh: Mesh): MeshWithBuffers { 353 | (mesh as MeshWithBuffers).normalBuffer = _buildBuffer(gl, gl.ARRAY_BUFFER, mesh.vertexNormals, 3); 354 | (mesh as MeshWithBuffers).textureBuffer = _buildBuffer(gl, gl.ARRAY_BUFFER, mesh.textures, mesh.textureStride); 355 | (mesh as MeshWithBuffers).vertexBuffer = _buildBuffer(gl, gl.ARRAY_BUFFER, mesh.vertices, 3); 356 | (mesh as MeshWithBuffers).indexBuffer = _buildBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, mesh.indices, 1); 357 | 358 | return mesh as MeshWithBuffers; 359 | } 360 | 361 | export function deleteMeshBuffers(gl: WebGLRenderingContext, mesh: MeshWithBuffers) { 362 | gl.deleteBuffer(mesh.normalBuffer); 363 | gl.deleteBuffer(mesh.textureBuffer); 364 | gl.deleteBuffer(mesh.vertexBuffer); 365 | gl.deleteBuffer(mesh.indexBuffer); 366 | } 367 | -------------------------------------------------------------------------------- /test/layout.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { Layout } from '../src/layout'; 4 | 5 | describe('Layout', function () { 6 | describe('#constructor()', function () { 7 | it('should compute stride', function () { 8 | let layout = new Layout( 9 | Layout.POSITION, 10 | Layout.UV); 11 | expect(layout).to.have.property('stride').that.equals(20); 12 | }); 13 | it('should set the offset correctly for each attribute', function () { 14 | let layout = new Layout( 15 | Layout.POSITION, 16 | Layout.UV, 17 | Layout.MATERIAL_INDEX); 18 | expect(layout.attributeMap).to.have.property('position') 19 | .that.has.property('offset').that.equals(0); 20 | expect(layout.attributeMap).to.have.property('uv') 21 | .that.has.property('offset').that.equals(12); 22 | expect(layout.attributeMap).to.have.property('materialIndex') 23 | .that.has.property('offset').that.equals(20); 24 | }); 25 | it('should set the stride correctly for each attribute', function () { 26 | let layout = new Layout( 27 | Layout.POSITION, 28 | Layout.UV, 29 | Layout.MATERIAL_INDEX); 30 | expect(layout.attributeMap).to.have.property('position') 31 | .that.has.property('stride').that.equals(24); 32 | expect(layout.attributeMap).to.have.property('uv') 33 | .that.has.property('stride').that.equals(24); 34 | expect(layout.attributeMap).to.have.property('materialIndex') 35 | .that.has.property('stride').that.equals(24); 36 | }); 37 | it('should throw when it detects duplicates', function () { 38 | expect(() => new Layout(Layout.UV, Layout.UV)).to.throw(); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/material.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { MaterialLibrary } from '../src/material'; 4 | 5 | 6 | describe('MaterialLibrary', function () { 7 | describe('#parse_newmtl()', function () { 8 | it('should update materials object to contain 3 materials', function () { 9 | let m = new MaterialLibrary( 10 | ` 11 | newmtl material_1 12 | newmtl material_2 13 | newmtl material_3 14 | ` 15 | ); 16 | 17 | expect(m.materials).to.have.all 18 | .keys('material_1', 'material_2', 'material_3'); 19 | }); 20 | }); 21 | 22 | describe('#parseColor', function () { 23 | it('should parse RGB colors correctly', function () { 24 | let m = new MaterialLibrary( 25 | ` 26 | newmtl my_material 27 | Ka 1.000000 1.000000 1.000000 28 | Kd 0.640000 0.640000 0.640000 29 | Ks 0.500000 0.500000 0.500000 30 | Ke 0.000000 0.000000 0.000000 31 | Tf 1.0000 1.0000 1.0000 32 | 33 | newmtl material2 34 | Ka 1.1 35 | ` 36 | ); 37 | 38 | let material = m.materials.my_material; 39 | 40 | expect(material.ambient).to.be.deep.equal([1.0, 1.0, 1.0]); 41 | expect(material.diffuse).to.be.deep.equal([0.64, 0.64, 0.64]); 42 | expect(material.specular).to.be.deep.equal([0.5, 0.5, 0.5]); 43 | expect(material.emissive).to.be.deep.equal([0, 0, 0]); 44 | expect(material.transmissionFilter).to.be.deep.equal([1, 1, 1]); 45 | 46 | expect(m.materials.material2.ambient).to.be.deep.equal([1.1, 1.1, 1.1]); 47 | }); 48 | }); 49 | 50 | describe('#parse_d', function () { 51 | it('should return the dissolve value as a number', function () { 52 | let m = new MaterialLibrary( 53 | ` 54 | newmtl my_material 55 | d 0.5 56 | ` 57 | ); 58 | let material = m.materials.my_material; 59 | 60 | expect(material.dissolve).to.be.equal(0.5); 61 | }); 62 | }); 63 | 64 | describe('#parse_illum', function () { 65 | it('should return the illum value as a number', function () { 66 | let m = new MaterialLibrary( 67 | ` 68 | newmtl my_material 69 | illum 2 70 | ` 71 | ); 72 | let material = m.materials.my_material; 73 | 74 | expect(material.illumination).to.be.equal(2); 75 | }); 76 | }); 77 | 78 | describe('#parse_Ni', function () { 79 | it('should return the refraction index value as a number', function () { 80 | let m = new MaterialLibrary( 81 | ` 82 | newmtl my_material 83 | Ni 2.5 84 | ` 85 | ); 86 | let material = m.materials.my_material; 87 | 88 | expect(material.refractionIndex).to.be.equal(2.5); 89 | }); 90 | }); 91 | 92 | describe('#parse_Ns', function () { 93 | it('should return the specular exponent value as a number', function () { 94 | let m = new MaterialLibrary( 95 | ` 96 | newmtl my_material 97 | Ns 250 98 | ` 99 | ); 100 | let material = m.materials.my_material; 101 | 102 | expect(material.specularExponent).to.be.equal(250); 103 | }); 104 | }); 105 | 106 | describe('#parse_sharpness', function () { 107 | it('should return the sharpess value as a number', function () { 108 | let m = new MaterialLibrary( 109 | ` 110 | newmtl my_material 111 | sharpness 100 112 | ` 113 | ); 114 | let material = m.materials.my_material; 115 | 116 | expect(material.sharpness).to.be.equal(100); 117 | }); 118 | }); 119 | 120 | describe('#parseMap', function () { 121 | it('should parse the filename and options associated with the directives', 122 | function () { 123 | let m = new MaterialLibrary( 124 | ` 125 | newmtl my_material 126 | map_Ka -cc on -blendu off -blendv off filename.jpg 127 | map_Kd -boost 123 -mm 1 2 filename.jpg 128 | map_Ks -o 1 other.jpg 129 | map_Ke -s 2 3 too.jpg 130 | map_Ns -t 4 5 6 lol.jpg 131 | refl -bm 42 -imfchan r -type sphere texture.jpg 132 | map_d -clamp on filename.jpg 133 | map_aat on 134 | # throwing in both so both functions get coverage 135 | map_bump -texres 1024 file.jpg 136 | bump -texres 1024 file.jpg 137 | disp filename.jpg -cc on 138 | decal file.jpg 139 | ` 140 | ); 141 | let material = m.materials.my_material; 142 | 143 | expect(material.mapAmbient.colorCorrection).to.be.true; 144 | expect(material.mapAmbient.horizontalBlending).to.be.false; 145 | expect(material.mapAmbient.verticalBlending).to.be.false; 146 | expect(material.mapAmbient.filename).to.be.equal('filename.jpg'); 147 | expect(material.mapDiffuse.boostMipMapSharpness) 148 | .to.be.equal(123); 149 | expect(material.mapDiffuse.modifyTextureMap.brightness) 150 | .to.be.equal(1); 151 | expect(material.mapDiffuse.modifyTextureMap.contrast) 152 | .to.be.equal(2); 153 | expect(material.mapSpecular.offset) 154 | .to.be.deep.equal({ u: 1, v: 0, w: 0 }); 155 | expect(material.mapEmissive.scale) 156 | .to.be.deep.equal({ u: 2, v: 3, w: 1 }); 157 | expect(material.mapSpecularExponent.turbulence) 158 | .to.be.deep.equal({ u: 4, v: 5, w: 6 }); 159 | expect(material.mapReflections[0].bumpMultiplier) 160 | .to.be.equal(42); 161 | expect(material.mapReflections[0].imfChan) 162 | .to.be.equal('r'); 163 | expect(material.mapReflections[0].reflectionType) 164 | .to.be.equal('sphere'); 165 | expect(material.antiAliasing).to.be.true; 166 | expect(material.mapBump.textureResolution).to.be.equal(1024); 167 | expect(material.mapDisplacement.filename) 168 | .to.be.equal('filename.jpg'); 169 | expect(material.mapDisplacement.colorCorrection).to.be.true; 170 | expect(material.mapDecal.filename).to.be.equal('file.jpg'); 171 | }); 172 | 173 | it('should parse the filename paths correctly and normalize the path separators', 174 | function () { 175 | let m = new MaterialLibrary( 176 | ` 177 | newmtl my_material 178 | map_Ka -cc on -blendu off -blendv off path\\to\\ambient.jpg 179 | map_Kd -boost 123 -mm 1 2 filename.jpg 180 | map_Ks -o 1 other.jpg 181 | map_Ke -s 2 3 too.jpg 182 | map_Ns -t 4 5 6 lol.jpg 183 | refl -bm 42 -imfchan r -type sphere texture.jpg 184 | map_d -clamp on filename.jpg 185 | map_aat on 186 | # throwing in both so both functions get coverage 187 | map_bump -texres 1024 path/to/file.jpg 188 | bump -texres 1024 file.jpg 189 | disp path\\to/displacement.jpg -cc on 190 | decal file.jpg 191 | ` 192 | ); 193 | let material = m.materials.my_material; 194 | 195 | expect(material.mapAmbient.filename).to.be.equal('path/to/ambient.jpg'); 196 | expect(material.mapDisplacement.filename).to.be.equal('path/to/displacement.jpg'); 197 | expect(material.mapDecal.filename).to.be.equal('file.jpg'); 198 | }); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /test/mesh.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import Mesh from '../src/mesh'; 4 | 5 | 6 | describe('Mesh', function () { 7 | describe('Test vertex parsing', function () { 8 | it('should contain the x, y, and z coordinates', function () { 9 | const m = new Mesh( 10 | ` 11 | v 0 0 0 12 | v 1.0 0 0 13 | v -1.0000 -1 0 14 | # a face is required in order 15 | # for vertices to be populated 16 | f 1 2 3 17 | ` 18 | ); 19 | expect(m.vertices).to.deep.equal([ 20 | 0, 0, 0, 21 | 1, 0, 0, 22 | -1, -1, 0 23 | ]); 24 | }); 25 | }); 26 | 27 | describe('Test vertex normal parsing', function () { 28 | it('should contain the x, y, and z vector components', function () { 29 | const m = new Mesh( 30 | ` 31 | # including a single vertex so that the face 32 | # parsing works successfully. 33 | v 0 0 0 34 | 35 | vn 1 1 1 36 | vn 0 0 0 37 | vn -1 0.1234 1.0 38 | f 1/1 1/2 1/3 39 | ` 40 | ); 41 | expect(m.vertexNormals).to.deep.equal([ 42 | 1, 1, 1, 43 | 0, 0, 0, 44 | -1, 0.1234, 1 45 | ]); 46 | }); 47 | }); 48 | 49 | describe('Test vertex texture parsing', function () { 50 | it('should contain the u and v texture coordinates', function () { 51 | const m = new Mesh( 52 | ` 53 | # including a single vertex and a single vertex normal so 54 | # that the face parsing works successfully. 55 | v 0 0 0 56 | vn 0 0 0 57 | 58 | vt 0 0.456 59 | vt 1.0 1 60 | vt -1.1 -1 61 | f 1/1/1 1/2/1 1/3/1 62 | ` 63 | ); 64 | expect(m.textures).to.deep.equal([ 65 | 0, 0.456, 66 | 1, 1, 67 | -1.1, -1 68 | ]); 69 | }); 70 | 71 | it('should contain the u, v, and w texture coordinates', function () { 72 | const m = new Mesh( 73 | ` 74 | # including a single vertex and a single vertex normal so 75 | # that the face parsing works successfully. 76 | v 0 0 0 77 | vn 0 0 0 78 | 79 | vt 0 0.456 0.5 80 | vt 1.0 1 0 81 | vt -1.1 -1 1.0 82 | f 1/1/1 1/2/1 1/3/1 83 | `, { 84 | enableWTextureCoord: true 85 | } 86 | ); 87 | expect(m.textures).to.deep.equal([ 88 | 0, 0.456, 0.5, 89 | 1, 1, 0, 90 | -1.1, -1, 1 91 | ]); 92 | }); 93 | }); 94 | 95 | describe('Test face parsing', function () { 96 | const data = ` 97 | v 0 0 0 98 | v 1 1 1 99 | v 2 2 2 100 | v 3 3 3 101 | 102 | vn 0 0 0 103 | vn 1 1 1 104 | vn 2 2 2 105 | vn 3 3 3 106 | 107 | vt 0.0 0.0 108 | vt 0.1 0.1 109 | vt 0.2 0.2 110 | vt 0.3 0.3 111 | ` 112 | it('should contain the unique vertex triplets and their indices', function () { 113 | let meshData = data + ` 114 | f 1/1/1 2/2/2 3/3/3 115 | f 2/2/2 3/3/3 4/4/4 116 | ` 117 | const m = new Mesh(meshData); 118 | expect(m.vertices).to.deep.equal([ 119 | 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 120 | ]); 121 | expect(m.vertexNormals).to.deep.equal([ 122 | 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 123 | ]); 124 | expect(m.textures).to.deep.equal([ 125 | 0.0, 0.0, 0.1, 0.1, 0.2, 0.2, 0.3, 0.3, 126 | ]); 127 | expect(m.indices).to.deep.equal([ 128 | 0, 1, 2, 1, 2, 3 129 | ]); 130 | }); 131 | 132 | it('should contain the proper index order when parsing quads', function () { 133 | let meshData = data + ` 134 | f 1/1/1 2/2/2 3/3/3 4/4/4 135 | ` 136 | const m = new Mesh(meshData); 137 | expect(m.indices).to.deep.equal([ 138 | 0, 1, 2, 2, 3, 0 139 | ]); 140 | }); 141 | 142 | it('should allow reuse of existing verts when parsing quads', function () { 143 | const data = ` 144 | v 0 0 0 145 | v 1 1 1 146 | v 2 2 2 147 | v 3 3 3 148 | v 4 4 4 149 | v 5 5 5 150 | 151 | vn 0 0 0 152 | vn 1 1 1 153 | vn 2 2 2 154 | vn 3 3 3 155 | vn 4 4 4 156 | vn 5 5 5 157 | 158 | vt 0.0 0.0 159 | vt 0.1 0.1 160 | vt 0.2 0.2 161 | vt 0.3 0.3 162 | vt 0.4 0.4 163 | vt 0.5 0.5 164 | 165 | f 1/1/1 2/2/2 3/3/3 4/4/4 166 | f 5/5/5 6/6/6 1/1/1 2/2/2 167 | ` 168 | const m = new Mesh(data); 169 | expect(m.indices).to.deep.equal([ 170 | 0, 1, 2, 2, 3, 0, 4, 5, 0, 0, 1, 4 171 | ]); 172 | expect(m.vertices).to.deep.equal([ 173 | 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5 174 | ]); 175 | }); 176 | 177 | it('should triangulate n-gons', function () { 178 | const data = ` 179 | v 0 0 0 180 | v 1 1 1 181 | v 2 2 2 182 | v 3 3 3 183 | v 4 4 4 184 | v 5 5 5 185 | v 6 6 6 186 | 187 | vn 0 0 0 188 | vn 1 1 1 189 | vn 2 2 2 190 | vn 3 3 3 191 | vn 4 4 4 192 | vn 5 5 5 193 | vn 6 6 6 194 | 195 | vt 0.0 0.0 196 | vt 0.1 0.1 197 | vt 0.2 0.2 198 | vt 0.3 0.3 199 | vt 0.4 0.4 200 | vt 0.5 0.5 201 | vt 0.6 0.6 202 | 203 | f 1/1/1 2/2/2 3/3/3 4/4/4 5/5/5 6/6/6 204 | ` 205 | const m = new Mesh(data); 206 | expect(m.indices).to.deep.equal([ 207 | 0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5 208 | ]); 209 | }); 210 | }); 211 | 212 | describe('Test tangents and bitangent calculation', function () { 213 | const data = ` 214 | v 0 0 0 215 | v 0 1 0 216 | v 1 0 0 217 | 218 | vn 0 0 1 219 | 220 | vt 0.0 0.0 221 | vt 0.1 0.1 222 | 223 | f 1/1/1 2/2/1 3/2/1 224 | ` 225 | 226 | const m = new Mesh(data); 227 | m.calculateTangentsAndBitangents(); 228 | const tangents = m.tangents; 229 | const bitangents = m.bitangents; 230 | const normals = m.vertexNormals; 231 | 232 | it('should contain tangents, bitangents and normals with the same length', function () { 233 | const normalsLength = normals.length; 234 | expect(tangents).to.have.length(normalsLength); 235 | expect(bitangents).to.have.length(normalsLength); 236 | }); 237 | 238 | it('should contain tangents orthogonal to normals', function () { 239 | let res = []; 240 | for (let i = 0; i < normals.length; i += 3) { 241 | const nx = normals[i + 0]; 242 | const ny = normals[i + 1]; 243 | const nz = normals[i + 2]; 244 | 245 | const tx = tangents[i + 0]; 246 | const ty = tangents[i + 1]; 247 | const tz = tangents[i + 2]; 248 | 249 | res.push(nx * tx + ny * ty + nz * tz); 250 | } 251 | res.forEach(i => expect(i).to.be.closeTo(0, 0.01)); 252 | }); 253 | 254 | it('should contain bitangents orthogonal to normals', function () { 255 | let res = []; 256 | for (let i = 0; i < normals.length; i += 3) { 257 | const nx = normals[i + 0]; 258 | const ny = normals[i + 1]; 259 | const nz = normals[i + 2]; 260 | 261 | const bx = bitangents[i + 0]; 262 | const by = bitangents[i + 1]; 263 | const bz = bitangents[i + 2]; 264 | 265 | res.push(nx * bx + ny * by + nz * bz); 266 | } 267 | res.forEach(i => expect(i).to.be.closeTo(0, 0.01)); 268 | }); 269 | }); 270 | }); 271 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require test/tshook.js 2 | test/**/*.ts -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "commonjs", 6 | "target": "es6", 7 | "strict": true, 8 | "lib": [ 9 | "es6", 10 | "dom", 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /test/tshook.js: -------------------------------------------------------------------------------- 1 | require("ts-node").register({ 2 | project: "test/tsconfig.json", 3 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es6", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "declaration": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | }, 15 | "include": [ 16 | "src/**/*.ts" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | "dist" 21 | ] 22 | } -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const OUTDIR = path.join(__dirname, "/dist"); 4 | 5 | module.exports = { 6 | mode: "none", 7 | entry: [path.join(__dirname, "/src/index.ts")], 8 | output: { 9 | path: OUTDIR, 10 | publicPath: "/", 11 | libraryTarget: "umd", 12 | umdNamedDefine: true, 13 | // it seems as though outputting `umd` should not dump `window`, 14 | // but it does... which breaks node.js projects. Explicitly 15 | // set the global object here. 16 | globalObject: "typeof self !== 'undefined' ? self : this", 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.tsx?$/, 22 | use: "awesome-typescript-loader", 23 | exclude: /node_modules/, 24 | }, 25 | ], 26 | }, 27 | resolve: { 28 | extensions: [".tsx", ".ts", ".js"], 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common'); 3 | 4 | 5 | module.exports = merge(common, { 6 | mode: 'development', 7 | devtool: 'inline-source-map', 8 | output: { 9 | filename: 'webgl-obj-loader.js', 10 | sourceMapFilename: 'webgl-obj-loader.js.map' 11 | } 12 | }); -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const common = require("./webpack.common"); 3 | const webpack = require("webpack"); 4 | 5 | module.exports = merge(common, { 6 | mode: "development", 7 | output: { 8 | filename: "webgl-obj-loader.min.js", 9 | sourceMapFilename: "webgl-obj-loader.min.js.map" 10 | }, 11 | optimization: { 12 | minimize: true 13 | }, 14 | plugins: [ 15 | new webpack.optimize.AggressiveMergingPlugin(), 16 | new webpack.DefinePlugin({ 17 | "process.env.NODE_ENV": JSON.stringify("production") 18 | }) 19 | ] 20 | }); 21 | --------------------------------------------------------------------------------