1 /*
  2  * Max3DSLoader.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2015 Emmanuel PUYBARET / eTeks <info@eteks.com>
  5  *
  6  * This program is free software; you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation; either version 2 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * This program is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with this program; if not, write to the Free Software
 18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 19  */
 20 
 21 // Requires core.js
 22 //          gl-matrix-min.js
 23 //          jszip.min.js
 24 //          scene3d.js
 25 //          URLContent.js
 26 //          ModelLoader.js
 27 
 28 /**
 29  * Creates an instance of an 3DS loader.
 30  * @constructor
 31  * @extends ModelLoader
 32  * @author Emmanuel Puybaret
 33  */
 34 function Max3DSLoader() {
 35   ModelLoader.call(this, "3ds");
 36   
 37   if (Max3DSLoader.DEFAULT_APPEARANCE === null) {
 38     Max3DSLoader.DEFAULT_APPEARANCE = new Appearance3D("Default");
 39     Max3DSLoader.DEFAULT_APPEARANCE.setAmbientColor(vec3.fromValues(0.4, 0.4, 0.4));
 40     Max3DSLoader.DEFAULT_APPEARANCE.setDiffuseColor(vec3.fromValues(0.7102, 0.702, 0.6531));
 41     Max3DSLoader.DEFAULT_APPEARANCE.setSpecularColor(vec3.fromValues(0.3, 0.3, 0.3));
 42     Max3DSLoader.DEFAULT_APPEARANCE.setShininess(128.);
 43   }
 44 }
 45 Max3DSLoader.prototype = Object.create(ModelLoader.prototype);
 46 Max3DSLoader.prototype.constructor = Max3DSLoader;
 47 
 48 Max3DSLoader.NULL_CHUNK = 0x0000;
 49 Max3DSLoader.M3DMAGIC = 0x4D4D;   // 3DS file
 50 Max3DSLoader.MLIBMAGIC = 0x3DAA;  // MLI file
 51 Max3DSLoader.CMAGIC = 0xC23D;     // PRJ file
 52 Max3DSLoader.M3D_VERSION = 0x0002;
 53 
 54 Max3DSLoader.COLOR_FLOAT = 0x0010;
 55 Max3DSLoader.COLOR_24 = 0x0011;
 56 Max3DSLoader.LINEAR_COLOR_24 = 0x0012;
 57 Max3DSLoader.LINEAR_COLOR_FLOAT = 0x0013;
 58 Max3DSLoader.PERCENTAGE_INT = 0x0030;
 59 Max3DSLoader.PERCENTAGE_FLOAT = 0x0031;
 60 
 61 Max3DSLoader.EDITOR_DATA = 0x3D3D;
 62 Max3DSLoader.MESH_VERSION = 0x3D3E;
 63 Max3DSLoader.MASTER_SCALE = 0x0100;
 64 
 65 Max3DSLoader.MATERIAL_ENTRY = 0xAFFF;
 66 Max3DSLoader.MATERIAL_NAME = 0xA000;
 67 Max3DSLoader.MATERIAL_AMBIENT = 0xA010;
 68 Max3DSLoader.MATERIAL_DIFFUSE = 0xA020;
 69 Max3DSLoader.MATERIAL_SPECULAR = 0xA030;
 70 Max3DSLoader.MATERIAL_SHININESS = 0xA040;
 71 Max3DSLoader.MATERIAL_TRANSPARENCY = 0xA050;
 72 Max3DSLoader.MATERIAL_TWO_SIDED = 0xA081;
 73 Max3DSLoader.MATERIAL_TEXMAP = 0xA200;
 74 Max3DSLoader.MATERIAL_MAPNAME = 0xA300;
 75 
 76 Max3DSLoader.NAMED_OBJECT = 0x4000;
 77 Max3DSLoader.TRIANGLE_MESH_OBJECT = 0x4100;
 78 Max3DSLoader.POINT_ARRAY = 0x4110;
 79 Max3DSLoader.FACE_ARRAY = 0x4120;
 80 Max3DSLoader.MESH_MATERIAL_GROUP = 0x4130;
 81 Max3DSLoader.SMOOTHING_GROUP = 0x4150;
 82 Max3DSLoader.MESH_BOXMAP = 0x4190;
 83 Max3DSLoader.TEXTURE_COORDINATES = 0x4140;
 84 Max3DSLoader.MESH_MATRIX = 0x4160;
 85 Max3DSLoader.MESH_COLOR = 0x4165;
 86 
 87 Max3DSLoader.KEY_FRAMER_DATA = 0xB000;
 88 Max3DSLoader.OBJECT_NODE_TAG = 0xB002;
 89 Max3DSLoader.NODE_ID = 0xB030;
 90 Max3DSLoader.NODE_HIERARCHY = 0xB010;
 91 Max3DSLoader.PIVOT = 0xB013;
 92 Max3DSLoader.POSITION_TRACK_TAG = 0xB020;
 93 Max3DSLoader.ROTATION_TRACK_TAG = 0xB021;
 94 Max3DSLoader.SCALE_TRACK_TAG = 0xB022;
 95 
 96 Max3DSLoader.TRACK_KEY_USE_TENS      = 0x01;
 97 Max3DSLoader.TRACK_KEY_USE_CONT      = 0x02;
 98 Max3DSLoader.TRACK_KEY_USE_BIAS      = 0x04;
 99 Max3DSLoader.TRACK_KEY_USE_EASE_TO   = 0x08;
100 Max3DSLoader.TRACK_KEY_USE_EASE_FROM = 0x10;
101 
102 Max3DSLoader.DEFAULT_APPEARANCE = null;
103 
104 /**
105  * Creates a new scene from the parsed data and calls onmodelcreated asynchronously or 
106  * returns the created scene if onmodelcreated is null.
107  * @private
108  */
109 Max3DSLoader.prototype.createScene = function(meshes, meshesGroups, materials, root, masterScale, onmodelcreated, onprogression) {
110   var sceneRoot = new Group3D();
111   var transform = mat4.create();
112   mat4.fromXRotation(transform, -Math.PI / 2);
113   mat4.scale(transform, transform, vec3.fromValues(masterScale, masterScale, masterScale));
114   var mainGroup = new TransformGroup3D(transform);
115   sceneRoot.addChild(mainGroup);
116   // If key framer data contained a hierarchy, add it to main group
117   if (root.getChildren().length > 0) {
118     mainGroup.addChild(root);
119     mainGroup = root;
120   }
121   
122   // Create appearances from 3DS materials
123   var appearances = {};
124   for (var name in materials) {
125     var material = materials [name];
126     var appearance = new Appearance3D(name);
127     if (material.ambientColor !== null) {
128       appearance.setAmbientColor(material.ambientColor);
129     }
130     if (material.diffuseColor !== null) {
131       appearance.setDiffuseColor(material.diffuseColor);
132     }
133     if (material.shininess !== null) {
134       appearance.setShininess(material.shininess * 128 * 0.6);
135     }
136     if (material.specularColor !== null) {
137       if (material.shininess !== null) {
138         // Reduce specular color shininess effect
139         var modifiedSpecularColor = vec3.clone(material.specularColor);
140         vec3.scale(modifiedSpecularColor, modifiedSpecularColor, material.shininess);
141         appearance.setSpecularColor(modifiedSpecularColor);
142       } else {
143         appearance.setSpecularColor(material.specularColor);
144       }
145     }
146     
147     if (material.transparency !== null && material.transparency > 0) {
148       appearance.setTransparency(Math.min(1, material.transparency));
149     }
150      
151     if (material.texture) {
152       appearance.imageEntryName = material.texture;
153     }
154     appearances [name] = appearance;
155   }
156   
157   if (onmodelcreated === null) {
158     onprogression(ModelLoader.BUILDING_MODEL, "", 0);
159     for (var i = 0; i < meshes.length; i++) {
160       this.createShapes(meshes [i], meshesGroups, appearances, sceneRoot, mainGroup);
161     }
162     onprogression(ModelLoader.BUILDING_MODEL, "", 1);
163     return sceneRoot;
164   } else {
165     var meshesCount = meshes.length;
166     var builtGeometryCount = 0;
167     var loader = this;
168     var sceneBuilder = function() {
169         onprogression(ModelLoader.BUILDING_MODEL, "", meshesCount !== 0 ? builtGeometryCount / meshesCount : 0);
170         var start = Date.now();
171         while (meshes.length > 0) {
172           loader.createShapes(meshes [0], meshesGroups, appearances, sceneRoot, mainGroup);
173           builtGeometryCount++;
174           meshes.splice(0, 1);
175           if (builtGeometryCount < meshesCount 
176               && Date.now() - start > 10) {
177             // Continue shapes creation later
178             setTimeout(sceneBuilder, 0);
179             return;
180           }
181         }
182         // All shapes are created
183         setTimeout(
184             function() {
185               onprogression(ModelLoader.BUILDING_MODEL, "", 1);
186               onmodelcreated(sceneRoot);
187             }, 0);
188       };
189     sceneBuilder();
190   }
191 }
192 
193 /**
194  * Creates the 3D shapes matching the parsed mesh data, and adds them to <code>sceneRoot</code>.
195  * @private
196  */
197 Max3DSLoader.prototype.createShapes = function(mesh, meshesGroups, appearances, sceneRoot, mainGroup) {
198   var identity = mat4.create();
199   var faces = mesh.faces;
200   if (faces !== null && faces.length > 0) {
201     var vertices = mesh.vertices;
202     // Compute default normals
203     var sharedVertices = new Array(vertices.length);
204     for (var i = 0; i < sharedVertices.length; i++) {
205       sharedVertices [i] = null;
206     }
207     var defaultNormals = new Array(3 * faces.length);
208     var vector1 = vec3.create();
209     var vector2 = vec3.create();
210     for (var i = 0, k = 0; i < faces.length; i++) {
211       var face = faces [i];
212       var vertexIndices = face.vertexIndices;
213       for (var j = 0; j < 3; j++, k++) {
214         var vertexIndex = vertexIndices [j];
215         vec3.subtract(vector1, vertices [vertexIndices [j < 2 ? j + 1 : 0]], vertices [vertexIndex]);
216         vec3.subtract(vector2, vertices [vertexIndices [j > 0 ? j - 1 : 2]], vertices [vertexIndex]);
217         var normal = vec3.create();
218         vec3.cross(normal, vector1, vector2);
219         var length = vec3.length(normal);
220         if (length > 0) {
221           var weight = Math.atan2(length, vec3.dot(vector1, vector2));
222           vec3.scale(normal, normal, weight / length);
223         }
224         
225         // Add vertex index to the list of shared vertices 
226         var sharedVertex = new Max3DSLoader.Mesh3DSSharedVertex(i, normal);
227         sharedVertex.nextVertex = sharedVertices [vertexIndex];
228         sharedVertices [vertexIndex] = sharedVertex;
229         defaultNormals [k] = normal;
230       }
231     }
232     
233     // Adjust the normals of shared vertices belonging to no smoothing group 
234     // or to the same smoothing group
235     var normals = new Array(3 * faces.length);
236     for (var i = 0, k = 0; i < faces.length; i++) {
237       var face = faces [i];
238       var vertexIndices = face.vertexIndices;
239       var normalIndices = new Array(3);
240       for (var j = 0; j < 3; j++, k++) {
241         var vertexIndex = vertexIndices [j];
242         var normal = vec3.create();
243         if (face.smoothingGroup === null) {
244           for (var sharedVertex = sharedVertices [vertexIndex]; 
245                sharedVertex !== null; 
246                sharedVertex = sharedVertex.nextVertex) {
247             // Take into account only normals of shared vertex with a crease angle  
248             // smaller than PI / 2 (i.e. dot product > 0) 
249             if (faces [sharedVertex.faceIndex].smoothingGroup === null
250                 && (sharedVertex.normal === defaultNormals [k]
251                     || vec3.dot(sharedVertex.normal, defaultNormals [k]) > 0)) {
252               vec3.add(normal, normal, sharedVertex.normal);
253             }
254           }
255         } else {
256           var smoothingGroup = face.smoothingGroup;
257           for (var sharedVertex = sharedVertices [vertexIndex]; 
258                sharedVertex !== null; 
259                sharedVertex = sharedVertex.nextVertex) {
260             var sharedIndexFace = faces [sharedVertex.faceIndex];
261             if (sharedIndexFace.smoothingGroup !== null
262                 && (face.smoothingGroup & sharedIndexFace.smoothingGroup) !== 0) {
263               smoothingGroup |= sharedIndexFace.smoothingGroup;
264             }
265           }
266           for (var sharedVertex = sharedVertices [vertexIndex]; 
267               sharedVertex !== null; 
268               sharedVertex = sharedVertex.nextVertex) {
269             var sharedIndexFace = faces [sharedVertex.faceIndex];
270             if (sharedIndexFace.smoothingGroup !== null
271                 && (smoothingGroup & sharedIndexFace.smoothingGroup) !== 0) {
272               vec3.add(normal, normal, sharedVertex.normal);
273             }
274           }
275         }
276         
277         if (vec3.squaredLength(normal) !== 0) {
278           vec3.normalize(normal, normal);
279         } else {
280           // If smoothing leads to a null normal, use default normal
281           vec3.copy(normal, defaultNormals [k]);
282           if (vec3.squaredLength(normal) !== 0) {
283             vec3.normalize(normal, normal);
284           }
285         }
286         normals [k] = normal;
287         normalIndices [j] = k;
288       }
289       
290       face.normalIndices = normalIndices;
291     }
292     
293     // Sort faces to ensure they are cited material group by material group
294     faces.sort(function(face1, face2) {
295         var material1 = face1.material;
296         var material2 = face2.material;
297         if (material1 === null) {
298           if (material2 === null) {
299             return face1.index - face2.index;
300           } else {
301             return -1;
302           }
303         } else if (material2 === null) {
304           return 1;
305         } else {
306           return 0;
307         }
308       });
309     
310     // Seek the parent of this mesh
311     var parentGroup;
312     var meshGroups = meshesGroups [mesh.name];
313     if (meshGroups === undefined) {
314       parentGroup = mainGroup;
315     } else if (meshGroups.length === 1) {
316       parentGroup = meshGroups [0];
317     } else {
318       var sharedGroup = new SharedGroup3D(); 
319       for (var i = 0; i < meshGroups.length; i++) {
320         meshGroups [i].addChild(new Link3D(sharedGroup));
321       }
322       parentGroup = sharedGroup;
323     }
324     
325     // Apply mesh transform
326     var transform = mesh.transform;
327     if (transform !== null) {
328       if (!mat4.exactEquals(transform, identity)) {
329         var transformGroup = new TransformGroup3D(transform);
330         parentGroup.addChild(transformGroup);
331         parentGroup = transformGroup;
332       }
333     }
334 
335     var textureCoordinates = mesh.textureCoordinates;
336     var i = 0;
337     var shape = null;
338     var material = null;
339     while (i < faces.length) {
340       var firstFace = faces [i];
341       var firstMaterial = firstFace.material;
342       
343       // Search how many faces share the same characteristics
344       var max = i;
345       while (++max < faces.length) {
346         if (firstMaterial !== faces [max].material) {
347           break;
348         }
349       }
350       
351       // Create indices arrays for the faces with an index between i and max
352       var faceCount = max - i;
353       var coordinateIndices = new Array(faceCount * 3);
354       var normalIndices     = new Array(faceCount * 3);
355       for (var j = 0, k = 0; j < faceCount; j++) {
356         var face = faces [i + j];
357         var vertexIndices = face.vertexIndices;
358         var faceNormalIndices = face.normalIndices;
359         for (var l = 0; l < 3; l++, k++) {
360           coordinateIndices [k] = vertexIndices [l];
361           normalIndices [k]     = faceNormalIndices [l];
362         }
363       }
364       
365       // Generate geometry 
366       var geometryInfo = new GeometryInfo3D(GeometryInfo3D.TRIANGLE_ARRAY);
367       geometryInfo.setCoordinates(vertices);
368       geometryInfo.setCoordinateIndices(coordinateIndices);
369       geometryInfo.setNormals(normals);
370       geometryInfo.setNormalIndices(normalIndices);
371       if (textureCoordinates !== null) {
372         geometryInfo.setTextureCoordinates(textureCoordinates);
373         geometryInfo.setTextureCoordinateIndices(coordinateIndices);
374       }
375       geometryArray = geometryInfo.getIndexedGeometryArray();
376 
377       if (shape === null || material !== firstMaterial) {
378         material = firstMaterial;
379         var appearance = Max3DSLoader.DEFAULT_APPEARANCE;
380         if (firstMaterial !== null
381             && appearances [firstMaterial.name] !== undefined) {
382           appearance = appearances [firstMaterial.name];
383         }
384         appearance = appearance.clone();
385         if (firstMaterial !== null && firstMaterial.twoSided) {
386           appearance.setCullFace(Appearance3D.CULL_NONE);
387         }
388         shape = new Shape3D(geometryArray, appearance);   
389         parentGroup.addChild(shape);
390         shape.setName(mesh.name + (i === 0 ? "" : "_" + i));
391       } else {
392         shape.addGeometry(geometryArray);
393       }
394       i = max;
395     }
396   }
397 }
398 
399 /**
400  * Returns the content of the model stored in the given entry.
401  * @protected
402  */
403 Max3DSLoader.prototype.getModelContent = function(modelEntry) {
404   return modelEntry.asUint8Array();
405 }
406 
407 /**
408  * Parses the given 3DS content and calls onmodelloaded asynchronously or 
409  * returns the scene it describes if onmodelloaded is null.
410  * @protected
411  */
412 Max3DSLoader.prototype.parseEntryScene = function(max3dsContent, max3dsEntryName, zip, modelContext, onmodelloaded, onprogression) {
413   var meshes = [];       // Mesh3DS 
414   var meshesGroups = {}; // TransformGroup3D []
415   var materials = {};    // Material3DS
416   var masterScale;
417   var root = new TransformGroup3D();
418   
419   if (onmodelloaded === null) {
420     onprogression(ModelLoader.PARSING_MODEL, max3dsEntryName, 0);
421     masterScale = this.parse3DSStream(new Max3DSLoader.ChunksInputStream(max3dsContent), max3dsEntryName, zip, meshes, meshesGroups, materials, root);
422     onprogression(ModelLoader.PARSING_MODEL, max3dsEntryName, 1);
423     return this.createScene(meshes, meshesGroups, materials, root, masterScale, null, onprogression);
424   } else {
425     var loader = this;
426     masterScale = this.parse3DSStream(new Max3DSLoader.ChunksInputStream(max3dsContent), max3dsEntryName, zip, meshes, meshesGroups, materials, root);
427     var max3dsEntryParser = function() {
428         // Parsing is finished
429         setTimeout(
430             function() {
431               onprogression(ModelLoader.PARSING_MODEL, max3dsEntryName, 1);
432               loader.createScene(meshes, meshesGroups, materials, root, masterScale, 
433                   function(scene) { 
434                     onmodelloaded(scene); 
435                   }, 
436                   onprogression);
437             }, 0);
438       };
439     max3dsEntryParser();
440   }
441 }
442 
443 /**
444  * Returns the scene with data read from the given 3DS stream.
445  * @param {Max3DSLoader.ChunksInputStream} input
446  * @param {string} max3dsEntryName
447  * @param {JSZip}  zip 
448  * @param {Array.<Max3DSLoader.Mesh3DS>} meshes 
449  * @param {Object} meshesGroups
450  * @param {Object} materials
451  * @param {TransformGroup3D} root
452  * @return {number} master scale
453  * @private
454  */
455 Max3DSLoader.prototype.parse3DSStream = function(input, max3dsEntryName, zip, meshes, meshesGroups, materials, root) {
456   var masterScale = 1;
457   try {
458     var magicNumberRead = false; 
459     switch (input.readChunkHeader().id) {
460       case Max3DSLoader.M3DMAGIC :
461       case Max3DSLoader.MLIBMAGIC :
462       case Max3DSLoader.CMAGIC :
463         magicNumberRead = true; 
464         while (!input.isChunckEndReached()) {
465           switch (input.readChunkHeader().id) {
466             case Max3DSLoader.M3D_VERSION :
467               input.readLittleEndianUnsignedInt();
468               break;
469             case Max3DSLoader.EDITOR_DATA : 
470               this.parseEditorData(input, max3dsEntryName, zip, meshes, materials);
471               break;
472             case Max3DSLoader.KEY_FRAMER_DATA :
473               this.parseKeyFramerData(input, meshesGroups, root);
474               break;
475             default :
476               input.readUntilChunkEnd();
477               break;
478           }
479           input.releaseChunk();
480         } 
481         break;
482       case Max3DSLoader.EDITOR_DATA :
483         masterScale = this.parseEditorData(input, max3dsEntryName, zip, meshes, materials);
484         break;
485       default :
486         if (magicNumberRead) {
487           input.readUntilChunkEnd();
488         } else {
489           throw new IncorrectFormat3DException("Bad magic number");
490         }
491     }
492     input.releaseChunk();
493   } catch (ex) {
494     // In case of an error, clear already read data
495     meshes.length = 0;
496   }
497   
498   return masterScale;
499 }
500 
501 /**
502  * Parses 3DS data in the current chunk.
503  * @param {Max3DSLoader.ChunksInputStream} input
504  * @param {string} max3dsEntryName
505  * @param {JSZip}  zip 
506  * @param {Array.<Max3DSLoader.Mesh3DS>} meshes
507  * @param {Object} materials
508  * @return {number} master scale
509  * @private
510  */
511 Max3DSLoader.prototype.parseEditorData = function(input, max3dsEntryName, zip, meshes, materials) {
512   var masterScale = 1;
513   while (!input.isChunckEndReached()) {
514     switch (input.readChunkHeader().id) {
515       case Max3DSLoader.MESH_VERSION : 
516         input.readLittleEndianInt();
517         break;
518       case Max3DSLoader.MASTER_SCALE : 
519         masterScale = input.readLittleEndianFloat();
520         break;
521       case Max3DSLoader.NAMED_OBJECT : 
522         this.parseNamedObject(input, meshes, materials);
523         break;
524       case Max3DSLoader.MATERIAL_ENTRY : 
525         var material = this.parseMaterial(input, max3dsEntryName, zip);
526         materials [material.name] = material;
527         break;
528       default :
529         input.readUntilChunkEnd();
530         break;
531     }
532     input.releaseChunk();
533   } 
534   return masterScale;
535 }
536 
537 /**
538  * Parses named objects like mesh in the current chunk.
539  * @param {Max3DSLoader.ChunksInputStream} input
540  * @param {Array.<Max3DSLoader.Mesh3DS>} meshes 
541  * @param {Object} materials
542  * @private
543  */
544 Max3DSLoader.prototype.parseNamedObject = function(input, meshes, materials) {
545   var name = input.readString();
546   while (!input.isChunckEndReached()) {
547     switch (input.readChunkHeader().id) {
548       case Max3DSLoader.TRIANGLE_MESH_OBJECT : 
549         meshes.push(this.parseMeshData(input, name, materials));
550         break;
551       default :
552         input.readUntilChunkEnd();
553         break;
554     }
555     input.releaseChunk();
556   } 
557 }
558 
559 /**
560  * Returns the mesh read from the current chunk.  
561  * @param {Max3DSLoader.ChunksInputStream} input
562  * @param {string} name
563  * @param {Object} materials
564  * @private
565  */
566 Max3DSLoader.prototype.parseMeshData = function(input, name, materials) {
567   var vertices = null;
568   var textureCoordinates = null;
569   var transform = null;
570   var color = null;
571   var faces = null; 
572   while (!input.isChunckEndReached()) {
573     switch (input.readChunkHeader().id) {
574       case Max3DSLoader.MESH_MATRIX :
575         transform = this.parseMatrix(input);
576         // Returns null if not invertible 
577         transform = mat4.invert(transform, transform);
578         break;
579       case Max3DSLoader.MESH_COLOR : 
580         color = input.readUnsignedByte();
581         break;
582       case Max3DSLoader.POINT_ARRAY : 
583         vertices = new Array(input.readLittleEndianUnsignedShort());
584         for (var i = 0; i < vertices.length; i++) {
585           vertices [i] = vec3.fromValues(input.readLittleEndianFloat(), 
586               input.readLittleEndianFloat(), input.readLittleEndianFloat());
587         }
588         break;
589       case Max3DSLoader.FACE_ARRAY : 
590         faces = this.parseFacesData(input);
591         while (!input.isChunckEndReached()) {
592           switch (input.readChunkHeader().id) {
593             case Max3DSLoader.MESH_MATERIAL_GROUP : 
594               var materialName = input.readString();
595               var material = null;
596               if (materials !== null) {
597                 material = materials [materialName];
598               }
599               for (var i = 0, n = input.readLittleEndianUnsignedShort(); i < n; i++) {
600                 var index = input.readLittleEndianUnsignedShort();
601                 if (index < faces.length) {
602                   faces [index].material = material;
603                 }
604               }
605               break;
606             case Max3DSLoader.SMOOTHING_GROUP :
607               for (var i = 0; i < faces.length; i++) {
608                 faces [i].smoothingGroup = input.readLittleEndianUnsignedInt();
609               }
610               break;
611             case Max3DSLoader.MESH_BOXMAP :
612             default :
613               input.readUntilChunkEnd();
614               break;
615           }
616           input.releaseChunk();
617         } 
618         break;
619       case Max3DSLoader.TEXTURE_COORDINATES : 
620         textureCoordinates = new Array(input.readLittleEndianUnsignedShort());
621         for (var i = 0; i < textureCoordinates.length; i++) {
622           textureCoordinates [i] = 
623               vec2.fromValues(input.readLittleEndianFloat(), input.readLittleEndianFloat());
624         }
625         break;
626       default :
627         input.readUntilChunkEnd();
628         break;
629     }
630     input.releaseChunk();
631   } 
632   return new Max3DSLoader.Mesh3DS(name, vertices, textureCoordinates, faces, color, transform);
633 }
634 
635 /**
636  * Parses key framer data.
637  * @param {Max3DSLoader.ChunksInputStream} input
638  * @param {Object} meshesGroups
639  * @param {TransformGroup3D} root
640  * @private
641  */
642 Max3DSLoader.prototype.parseKeyFramerData = function(input, meshesGroups, root) {
643   var transformGroups = [];
644   var transformGroupNodeIds = [];
645   var currentTransformGroup = null;
646   while (!input.isChunckEndReached()) {
647     switch (input.readChunkHeader().id) {
648       case Max3DSLoader.OBJECT_NODE_TAG :
649         var meshGroup = true;
650         var pivot = null;
651         var position = null;
652         var rotationAngle = 0;
653         var rotationAxis = null;
654         var scale = null;
655         var nodeId = -1;
656         while (!input.isChunckEndReached()) {
657           switch (input.readChunkHeader().id) {
658             case Max3DSLoader.NODE_ID :
659               nodeId = input.readLittleEndianShort();
660               break;
661             case Max3DSLoader.NODE_HIERARCHY :
662               var meshName = input.readString();
663               meshGroup = "$$$DUMMY" != meshName;
664               input.readLittleEndianUnsignedShort(); 
665               input.readLittleEndianUnsignedShort();
666               var parentId = input.readLittleEndianShort();
667               var transformGroup = new TransformGroup3D();
668               if (parentId === -1) {
669                 root.addChild(transformGroup);
670               } else {
671                 var found = false;
672                 for (var i = 0; i < transformGroupNodeIds.length; i++) {
673                   if (parentId === transformGroupNodeIds [i]) {
674                     transformGroups [i].addChild(transformGroup);  
675                     found = true;
676                     break;
677                   }
678                 }
679                 if (!found) {
680                   throw new IncorrectFormat3DException("Inconsistent nodes hierarchy");
681                 }
682               }
683               transformGroupNodeIds.push(nodeId);
684               transformGroups.push(transformGroup);
685               if (meshGroup) {
686                 // Store group parent of mesh 
687                 var meshGroups = meshesGroups [meshName];
688                 if (meshGroups === undefined) {
689                   meshGroups = [];
690                   meshesGroups [meshName] = meshGroups;
691                 }
692                 meshGroups.push(transformGroup);
693               }
694               currentTransformGroup = transformGroup;
695               break;
696             case Max3DSLoader.PIVOT :
697               pivot = this.parseVector(input);
698               break;
699             case Max3DSLoader.POSITION_TRACK_TAG :
700               this.parseKeyFramerTrackStart(input);
701               position = this.parseVector(input);
702               // Ignore next frames
703               input.readUntilChunkEnd();
704               break;
705             case Max3DSLoader.ROTATION_TRACK_TAG :
706               this.parseKeyFramerTrackStart(input);
707               rotationAngle = input.readLittleEndianFloat();
708               rotationAxis = this.parseVector(input);
709               // Ignore next frames
710               input.readUntilChunkEnd();
711               break;
712             case Max3DSLoader.SCALE_TRACK_TAG :
713               this.parseKeyFramerTrackStart(input);
714               scale = this.parseVector(input);
715               // Ignore next frames
716               input.readUntilChunkEnd();
717               break;
718             default :
719               input.readUntilChunkEnd();
720               break;
721           } 
722           input.releaseChunk();
723         }
724         
725         // Prepare transformations
726         var transform = mat4.create();
727         if (position !== null) {
728           mat4.translate(transform, transform, position);
729         } 
730         if (rotationAxis !== null
731             && rotationAngle !== 0) {
732           var length = vec3.length(rotationAxis);
733           if (length > 0) {
734             var halfAngle = -rotationAngle / 2.;
735             var sin = Math.sin(halfAngle) / length;
736             var cos = Math.cos(halfAngle);
737             var rotationTransform = mat4.create();
738             mat4.fromQuat(rotationTransform, quat.fromValues(rotationAxis [0] * sin, rotationAxis [1] * sin, rotationAxis [2] * sin, cos));
739             mat4.mul(transform, transform, rotationTransform);
740           }
741         } 
742         if (scale !== null) {
743           mat4.scale(transform, transform, scale);
744         }
745         if (pivot !== null 
746             && meshGroup) {
747           vec3.negate(pivot, pivot);
748           mat4.translate(transform, transform, pivot);
749         }
750         currentTransformGroup.setTransform(transform);
751         break;
752       default :
753         input.readUntilChunkEnd();
754         break;
755     }
756     input.releaseChunk();
757   } 
758 }
759 
760 /**
761  * Parses the start of a key framer track.
762  * @param {Max3DSLoader.ChunksInputStream} input
763  * @private
764  */
765 Max3DSLoader.prototype.parseKeyFramerTrackStart = function(input) {
766   input.readLittleEndianUnsignedShort(); // Flags
767   input.readLittleEndianUnsignedInt();
768   input.readLittleEndianUnsignedInt();
769   input.readLittleEndianInt();           // Key frames count
770   input.readLittleEndianInt();           // Key frame index
771   var flags = input.readLittleEndianUnsignedShort(); 
772   if ((flags & Max3DSLoader.TRACK_KEY_USE_TENS) !== 0) {
773     input.readLittleEndianFloat();
774   }
775   if ((flags & Max3DSLoader.TRACK_KEY_USE_CONT) !== 0) {
776     input.readLittleEndianFloat();
777   }
778   if ((flags & Max3DSLoader.TRACK_KEY_USE_BIAS) !== 0) {
779     input.readLittleEndianFloat();
780   }
781   if ((flags & Max3DSLoader.TRACK_KEY_USE_EASE_TO) !== 0) {
782     input.readLittleEndianFloat();
783   }
784   if ((flags & Max3DSLoader.TRACK_KEY_USE_EASE_FROM) !== 0) {
785     input.readLittleEndianFloat();
786   }
787 }
788 
789 /**
790  * Returns the mesh faces read from the current chunk. 
791  * @return {Max3DSLoader.Face3DS} 
792  * @private
793  */
794 Max3DSLoader.prototype.parseFacesData = function(input) {
795   var faces = new Array(input.readLittleEndianUnsignedShort());
796   for (var i = 0; i < faces.length; i++) {
797     faces [i] = new Max3DSLoader.Face3DS(
798       i, 
799       input.readLittleEndianUnsignedShort(), 
800       input.readLittleEndianUnsignedShort(),
801       input.readLittleEndianUnsignedShort(),
802       input.readLittleEndianUnsignedShort());
803   }
804   return faces;
805 }
806 
807 /**
808  * Returns the 3DS material read from the current chunk.
809  * @param {Max3DSLoader.ChunksInputStream} input
810  * @param {string} max3dsEntryName
811  * @param {JSZip}  zip 
812  * @return {Max3DSLoader.Material3DS}  
813  * @private
814  */
815 Max3DSLoader.prototype.parseMaterial = function(input, max3dsEntryName, zip) {
816   var name = null;
817   var ambientColor = null;
818   var diffuseColor = null;
819   var specularColor = null;
820   var shininess = null;
821   var transparency = null;
822   var twoSided = false;
823   var texture = null;
824   while (!input.isChunckEndReached()) {
825     switch (input.readChunkHeader().id) {
826       case Max3DSLoader.MATERIAL_NAME : 
827         name = input.readString();
828         break;
829       case Max3DSLoader.MATERIAL_AMBIENT : 
830         ambientColor = this.parseColor(input); 
831         break;
832       case Max3DSLoader.MATERIAL_DIFFUSE : 
833         diffuseColor = this.parseColor(input);
834         break;
835       case Max3DSLoader.MATERIAL_SPECULAR : 
836         specularColor = this.parseColor(input);
837         break;
838       case Max3DSLoader.MATERIAL_SHININESS :
839         shininess = this.parsePercentage(input);
840         break;
841       case Max3DSLoader.MATERIAL_TRANSPARENCY :
842         // 0 = fully opaque to 1 = fully transparent
843         transparency = this.parsePercentage(input);
844         break;
845       case Max3DSLoader.MATERIAL_TWO_SIDED :
846         twoSided = true;
847         break;         
848       case Max3DSLoader.MATERIAL_TEXMAP :
849         texture = this.parseTextureMap(input, max3dsEntryName, zip);
850         break;
851       default :
852         input.readUntilChunkEnd();
853         break;
854     }
855     input.releaseChunk();
856   } 
857   return new Max3DSLoader.Material3DS(name, ambientColor, diffuseColor, specularColor, 
858       shininess, transparency, texture, twoSided);
859 }
860 
861 /**
862  * Returns the color read from the current chunk.  
863  * @param {Max3DSLoader.ChunksInputStream} input
864  * @return {vec3}
865  * @private
866  */
867 Max3DSLoader.prototype.parseColor = function(input) {
868   var linearColor = false;
869   var color = null;
870   var readColor;
871   while (!input.isChunckEndReached()) {
872     switch (input.readChunkHeader().id) {
873       case Max3DSLoader.LINEAR_COLOR_24 :
874         linearColor = true;
875         color = vec3.fromValues(input.readUnsignedByte() / 255., 
876             input.readUnsignedByte() / 255., input.readUnsignedByte() / 255.);
877         break;
878       case Max3DSLoader.COLOR_24 :
879         readColor = vec3.fromValues(input.readUnsignedByte() / 255., 
880             input.readUnsignedByte() / 255., input.readUnsignedByte() / 255.);
881         if (!linearColor) {
882           color = readColor;
883         }
884         break;
885       case Max3DSLoader.LINEAR_COLOR_FLOAT : 
886         linearColor = true;
887         color = vec3.fromValues(input.readLittleEndianFloat(), 
888             input.readLittleEndianFloat(), input.readLittleEndianFloat());
889         break;
890       case Max3DSLoader.COLOR_FLOAT :
891         readColor = vec3.fromValues(input.readLittleEndianFloat(), 
892             input.readLittleEndianFloat(), input.readLittleEndianFloat());
893         if (!linearColor) {
894           color = readColor;
895         }
896         break;
897       default :
898         input.readUntilChunkEnd();
899         break;
900     }
901     input.releaseChunk();
902   } 
903   if (color !== null) {
904     return color;
905   } else {
906     throw new IncorrectFormat3DException("Expected color value");
907   }
908 }
909 
910 /**
911  * Returns the percentage read from the current chunk.  
912  * @param {Max3DSLoader.ChunksInputStream} input
913  * @return {number}
914  * @private
915  */
916 Max3DSLoader.prototype.parsePercentage = function(input) {
917   var percentage = null;
918   while (!input.isChunckEndReached()) {
919     switch (input.readChunkHeader().id) {
920       case Max3DSLoader.PERCENTAGE_INT :
921         percentage = input.readLittleEndianShort() / 100.;
922         break;
923       case Max3DSLoader.PERCENTAGE_FLOAT :
924         percentage = input.readLittleEndianFloat();
925         break;
926       default :
927         input.readUntilChunkEnd();
928         break;
929     }
930     input.releaseChunk();
931   } 
932   if (percentage !== null) {
933     return percentage;
934   } else {
935     throw new IncorrectFormat3DException("Expected percentage value");
936   }
937 }
938 
939 /**
940  * Returns the texture entry name read from the current chunk.    
941  * @param {Max3DSLoader.ChunksInputStream} input
942  * @param {string} max3dsEntryName
943  * @param {JSZip}  zip 
944  * @return {string}
945  * @private
946  */
947 Max3DSLoader.prototype.parseTextureMap = function(input, max3dsEntryName, zip) {
948   var mapName = null;
949   while (!input.isChunckEndReached()) {
950     switch (input.readChunkHeader().id) {
951       case Max3DSLoader.MATERIAL_MAPNAME :
952         mapName = input.readString();
953         break;
954       case Max3DSLoader.PERCENTAGE_INT :
955       default :
956         input.readUntilChunkEnd();
957         break;
958     }
959     input.releaseChunk();
960   } 
961   
962   if (mapName !== null) {
963     var lastSlash = max3dsEntryName.lastIndexOf("/");
964     if (lastSlash >= 0) {
965       mapName = max3dsEntryName.substring(0, lastSlash + 1) + mapName;
966     }
967     var imageEntry = zip.file(mapName);
968     if (imageEntry !== null) {
969       return mapName;
970     } else {
971       // Test also if the texture file doesn't exist ignoring case
972       return this.getEntryNameIgnoreCase(zip, mapName);
973     }
974   }
975   return null;
976 }
977 
978 /**
979  * Returns the entry in a zip file equal to the given name ignoring case.
980  * @private
981  */
982 Max3DSLoader.prototype.getEntryNameIgnoreCase = function(zip, searchedEntryName) {
983   searchedEntryName = searchedEntryName.toUpperCase();
984   var entries = zip.file(/.*/);
985   for (var i = 0; i < entries.length; i++) {
986     if (entries [i].name.toUpperCase() == searchedEntryName) {
987       return entries [i].name;
988     } 
989   }
990   return null;
991 }
992 
993 /**
994  * Returns the matrix read from the current chunk.  
995  * @param {Max3DSLoader.ChunksInputStream} input
996  * @return {mat4}
997  * @private
998  */
999 Max3DSLoader.prototype.parseMatrix = function(input) {
1000   return mat4.fromValues(
1001       input.readLittleEndianFloat(), input.readLittleEndianFloat(), input.readLittleEndianFloat(), 0,
1002       input.readLittleEndianFloat(), input.readLittleEndianFloat(), input.readLittleEndianFloat(), 0,
1003       input.readLittleEndianFloat(), input.readLittleEndianFloat(), input.readLittleEndianFloat(), 0,
1004       input.readLittleEndianFloat(), input.readLittleEndianFloat(), input.readLittleEndianFloat(), 1);
1005 }
1006 
1007 /**
1008  * Returns the vector read from the current chunk.
1009  * @param {Max3DSLoader.ChunksInputStream} input
1010  * @return {vec3}
1011  * @private
1012  */
1013 Max3DSLoader.prototype.parseVector = function(input) {
1014   return vec3.fromValues(input.readLittleEndianFloat(), 
1015       input.readLittleEndianFloat(), input.readLittleEndianFloat());
1016 }
1017 
1018 /**
1019  * Creates a chunk with its ID and length.
1020  * @param {number} id
1021  * @param {number} length
1022  * @constructor
1023  * @private
1024  */
1025 Max3DSLoader.Chunk3DS = function(id, length) {
1026   if (length < 6) {
1027     throw new IncorrectFormat3DException("Invalid chunk " + id + " length " + length);
1028   }
1029   this.id = id;
1030   this.length = length;
1031   this.readLength = 6;
1032 }
1033   
1034 Max3DSLoader.Chunk3DS.prototype.incrementReadLength = function(readBytes) {
1035   this.readLength += readBytes;
1036 }
1037 
1038 Max3DSLoader.Chunk3DS.prototype.toString = function() {
1039   return this.id + " " + this.length;
1040 }
1041 
1042 /**
1043  * Creates an input stream storing chunks hierarchy and other data required during parsing.
1044  * @param {Uint8Array} input
1045  * @constructor
1046  * @private
1047  */
1048 Max3DSLoader.ChunksInputStream = function(input) {
1049   this.input = input;
1050   this.index = 0;
1051   this.stack = []; // Chunk3DS [] 
1052 }
1053 
1054 /**
1055  * Returns the next value in input stream or -1 if end is reached.
1056  * @private
1057  */
1058 Max3DSLoader.ChunksInputStream.prototype.read = function() {
1059   if (this.index >= this.input.length) {
1060     return -1;
1061   } else {
1062     return this.input [this.index++];
1063   }
1064 }
1065 
1066 /**
1067  * Reads the next chunk id and length, pushes it in the stack and returns it.
1068  * <code>null</code> will be returned if the end of the stream is reached.
1069  * @return {Max3DSLoader.Chunk3DS}
1070  * @private
1071  */
1072 Max3DSLoader.ChunksInputStream.prototype.readChunkHeader = function() {
1073   var chunkId = this.readLittleEndianUnsignedShort(false);
1074   var chunk = new Max3DSLoader.Chunk3DS(chunkId, this.readLittleEndianUnsignedInt(false));
1075   this.stack.push(chunk);
1076   return chunk;
1077 }
1078   
1079 /**
1080  * Pops the chunk at the top of stack and checks it was entirely read. 
1081  * @private
1082  */
1083 Max3DSLoader.ChunksInputStream.prototype.releaseChunk = function() {      
1084   var chunk = this.stack.pop();
1085   if (chunk.length !== chunk.readLength) {
1086     throw new IncorrectFormat3DException("Chunk " + chunk.id + " invalid length. " 
1087         + "Expected to read " + chunk.length + " bytes, but actually read " + chunk.readLength + " bytes");
1088   }
1089   if (this.stack.length !== 0) {
1090     this.stack [this.stack.length - 1].incrementReadLength(chunk.length);
1091   }      
1092 }
1093 
1094 /**
1095  * Returns <code>true</code> if the current chunk end was reached.
1096  * @return {boolean}
1097  * @private
1098  */
1099 Max3DSLoader.ChunksInputStream.prototype.isChunckEndReached = function() {
1100   var chunk = this.stack [this.stack.length - 1];
1101   return chunk.length === chunk.readLength;
1102 }
1103   
1104 /**
1105  * Reads the stream until the end of the current chunk.
1106  * @private
1107  */
1108 Max3DSLoader.ChunksInputStream.prototype.readUntilChunkEnd = function() {
1109   var chunk = this.stack [this.stack.length - 1];
1110   var remainingLength = chunk.length - chunk.readLength;
1111   for (var length = remainingLength; length > 0; length--) {
1112     if (this.read() < 0) {
1113       throw new IncorrectFormat3DException("Chunk " + chunk.id + " too short");
1114     }
1115   }
1116   chunk.incrementReadLength(remainingLength);
1117 }
1118   
1119 /**
1120  * Returns the unsigned byte read from this stream.
1121  * @private
1122  */
1123 Max3DSLoader.ChunksInputStream.prototype.readUnsignedByte = function() {
1124   var b = this.read();
1125   if (b === -1) {
1126     throw new IncorrectFormat3DException ("Unexpected EOF");
1127   } else {
1128     this.stack [this.stack.length - 1].incrementReadLength(1);
1129     return b;
1130   }
1131 }
1132   
1133 /**
1134  * Returns the unsigned short read from this stream.
1135  * @private
1136  */
1137 Max3DSLoader.ChunksInputStream.prototype.readLittleEndianUnsignedShort = function(incrementReadLength) {
1138   if (incrementReadLength === undefined) {
1139     incrementReadLength = true;
1140   }
1141   var b1 = this.read();
1142   if (b1 === -1) {
1143     throw new IncorrectFormat3DException ("Unexpected EOF");
1144   }
1145   var b2 = this.read();
1146   if (b2 === -1) {
1147     throw new IncorrectFormat3DException("Can't read short");
1148   }
1149   if (incrementReadLength) {
1150     this.stack [this.stack.length - 1].incrementReadLength(2);
1151   }
1152   return (b2 << 8) | b1;
1153 }
1154   
1155 /**
1156  * Returns the short read from this stream.
1157  * @private
1158  */
1159 Max3DSLoader.ChunksInputStream.prototype.readLittleEndianShort = function(incrementReadLength) {
1160   var s = this.readLittleEndianUnsignedShort(incrementReadLength);
1161   if (s & 0x8000) {
1162     s = (-1 & ~0x7FFF) | s; // Extend sign bit
1163   } 
1164   return s;
1165 }
1166 
1167 // Create buffers to convert 4 bytes to a float
1168 Max3DSLoader.ChunksInputStream.converter = new Int8Array(4);
1169 Max3DSLoader.ChunksInputStream.int32Converter = new Int32Array(Max3DSLoader.ChunksInputStream.converter.buffer, 0, 1);
1170 Max3DSLoader.ChunksInputStream.float32Converter = new Float32Array(Max3DSLoader.ChunksInputStream.converter.buffer, 0, 1);
1171 
1172 /**
1173  * Returns the float read from this stream.
1174  * @private
1175  */
1176 Max3DSLoader.ChunksInputStream.prototype.readLittleEndianFloat = function() {
1177   Max3DSLoader.ChunksInputStream.int32Converter [0] = this.readLittleEndianUnsignedInt(true);
1178   return Max3DSLoader.ChunksInputStream.float32Converter [0]; // Float.intBitsToFloat
1179 }
1180   
1181 /**
1182  * Returns the unsigned integer read from this stream.
1183  * @private
1184  */
1185 Max3DSLoader.ChunksInputStream.prototype.readLittleEndianUnsignedInt = function(incrementReadLength) {
1186   if (incrementReadLength === undefined) {
1187     incrementReadLength = true;
1188   }
1189   var b1 = this.read();
1190   if (b1 === -1) {
1191     throw new IncorrectFormat3DException ("Unexpected EOF");
1192   }
1193   var b2 = this.read();
1194   var b3 = this.read();
1195   var b4 = this.read();
1196   if (b2 === -1 || b3 === -1 || b4 === -1) {
1197     throw new IncorrectFormat3DException("Can't read int");
1198   }
1199   if (incrementReadLength) {
1200     this.stack [this.stack.length - 1].incrementReadLength(4);
1201   }
1202   return (b4 << 24) | (b3 << 16) | (b2 << 8) | b1;
1203 }
1204 
1205 /**
1206  * Returns the integer read from this stream.
1207  * @private
1208  */
1209 Max3DSLoader.ChunksInputStream.prototype.readLittleEndianInt = function(incrementReadLength) {
1210   var i = this.readLittleEndianUnsignedInt(incrementReadLength);
1211   if (i & 0x80000000) {
1212     i = (-1 & ~0x7FFFFFFF) | i; // Extend sign bit
1213   } 
1214   return i;
1215 }
1216 
1217 /**
1218  * Returns the string read from this stream.
1219  * @private
1220  */
1221 Max3DSLoader.ChunksInputStream.prototype.readString = function() {
1222   var string = "";
1223   var b;
1224   // Read characters until terminal 0
1225   while ((b = this.read()) !== -1 && b !== 0) {
1226     string += String.fromCharCode(b);
1227   }
1228   if (b === -1) {
1229     throw new IncorrectFormat3DException("Unexpected end of file");
1230   }
1231   this.stack [this.stack.length - 1].incrementReadLength(string.length + 1);
1232   return string; // Need to take "ISO-8859-1" encoding into account?
1233 }
1234 
1235 /**
1236  * Creates a 3DS mesh.
1237  * @param {string} name, 
1238  * @param {Point3f[]} vertices
1239  * @param {TexCoord2f[]} textureCoordinates
1240  * @param {Array.<Max3DSLoader.Face3DS>} faces
1241  * @param {number} color
1242  * @param {mat4} transform
1243  * @constructor
1244  * @private
1245  */
1246 Max3DSLoader.Mesh3DS = function(name, vertices, textureCoordinates, faces, color, transform) {
1247   this.name = name;
1248   this.vertices = vertices;
1249   this.textureCoordinates = textureCoordinates;
1250   this.faces = faces;
1251   this.color = color;
1252   this.transform = transform;
1253 }
1254 
1255 /**
1256  * Creates a 3DS face.
1257  * @param {number} index
1258  * @param {number} vertexAIndex
1259  * @param {number} vertexBIndex
1260  * @param {number} vertexCIndex
1261  * @param {number} flags
1262  * @constructor
1263  * @private
1264  */
1265 Max3DSLoader.Face3DS = function(index, vertexAIndex, vertexBIndex, vertexCIndex, flags) {
1266   this.index = index;
1267   this.vertexIndices = [vertexAIndex, vertexBIndex, vertexCIndex];
1268   this.normalIndices = null;  // number []
1269   this.material = null;       // Material3DS
1270   this.smoothingGroup = null; // number
1271 }
1272 
1273 /**
1274  * Creates a 3DS material.
1275  * @param {string}  name
1276  * @param {vec3}    ambientColor
1277  * @param {vec3}    diffuseColor
1278  * @param {vec3}    specularColor
1279  * @param {number}  shininess
1280  * @param {number}  transparency
1281  * @param {Image}   texture
1282  * @param {boolean} twoSided
1283  * @constructor
1284  * @private
1285  */
1286 Max3DSLoader.Material3DS = function(name, ambientColor, diffuseColor, specularColor,
1287                      shininess, transparency, texture, twoSided) {
1288   this.name = name;
1289   this.ambientColor = ambientColor;
1290   this.diffuseColor = diffuseColor;
1291   this.specularColor = specularColor;
1292   this.shininess = shininess;
1293   this.transparency = transparency;
1294   this.texture = texture;
1295   this.twoSided = twoSided;
1296 }
1297 
1298 /**
1299  * Creates a vertex shared between faces in a mesh.
1300  * @param {number} faceIndex
1301  * @param {vec3} normal
1302  * @constructor
1303  * @private
1304  */
1305 Max3DSLoader.Mesh3DSSharedVertex = function(faceIndex, normal) {
1306   this.faceIndex = faceIndex;
1307   this.normal = normal;
1308   this.nextVertex = null; // Mesh3DSSharedVertex 
1309 }
1310