1 /* 2 * Max3DSLoader.js 3 * 4 * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <info@sweethome3d.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