import {Matrix3D, Vector3D, IAsset, URLLoaderDataFormat, URLRequest, ParserBase, ParserUtils, ResourceDependency, ByteArray} from "@awayjs/core"; import {BitmapImage2D, AttributesBuffer} from "@awayjs/stage"; import {MaterialUtils, IMaterial} from "@awayjs/renderer"; import {Graphics, Shape, TriangleElements} from "@awayjs/graphics"; import {DisplayObjectContainer, Sprite} from "@awayjs/scene"; import {MethodMaterial, MethodMaterialMode, ImageTexture2D} from "@awayjs/materials"; /** * Max3DSParser provides a parser for the 3ds data type. */ export class Max3DSParser extends ParserBase { private _byteData:ByteArray; private _textures:Object; private _materials:Object; private _unfinalized_objects:Object; private _cur_obj_end:number; private _cur_obj:ObjectVO; private _cur_mat_end:number; private _cur_mat:MaterialVO; private _useSmoothingGroups:boolean; /** * Creates a new Max3DSParser object. * * @param useSmoothingGroups Determines whether the parser looks for smoothing groups in the 3ds file or assumes uniform smoothing. Defaults to true. */ constructor(useSmoothingGroups:boolean = true) { super(URLLoaderDataFormat.ARRAY_BUFFER); this._useSmoothingGroups = useSmoothingGroups; } /** * Indicates whether or not a given file extension is supported by the parser. * @param extension The file extension of a potential file to be parsed. * @return Whether or not the given file type is supported. */ public static supportsType(extension:string):boolean { extension = extension.toLowerCase(); return extension == "3ds"; } /** * Tests whether a data block can be parsed by the parser. * @param data The data block to potentially be parsed. * @return Whether or not the given data is supported. */ public static supportsData(data:any):boolean { var ba:ByteArray; try{ //TODO!!!: crashes when trying to check against AudioData ba = ParserUtils.toByteArray(data); if (ba) { ba.position = 0; if (ba.readShort() == 0x4d4d) return true; } } catch(err){ return false; } return false; } /** * @inheritDoc */ public _iResolveDependency(resourceDependency:ResourceDependency):void { if (resourceDependency.assets.length == 1) { var asset:IAsset; asset = resourceDependency.assets[0]; if (asset.isAsset(BitmapImage2D)) { var tex:TextureVO; tex = this._textures[resourceDependency.id]; tex.texture = new ImageTexture2D( asset); } } } /** * @inheritDoc */ public _iResolveDependencyFailure(resourceDependency:ResourceDependency):void { // TODO: Implement } /** * @inheritDoc */ public _pProceedParsing():boolean { if (!this._byteData) { this._byteData = this._pGetByteData(); this._byteData.position = 0; //---------------------------------------------------------------------------- // LITTLE_ENDIAN - Default for ArrayBuffer / Not implemented in ByteArray //---------------------------------------------------------------------------- //this._byteData.endian = Endian.LITTLE_ENDIAN;// Should be default //---------------------------------------------------------------------------- this._textures = {}; this._materials = {}; this._unfinalized_objects = {}; } // TODO: With this construct, the loop will run no-op for as long // as there is time once file has finished reading. Consider a nice // way to stop loop when byte array is empty, without putting it in // the while-conditional, which will prevent finalizations from // happening after the last chunk. while (this._pHasTime()) { // If we are currently working on an object, and the most recent chunk was // the last one in that object, finalize the current object. if (this._cur_mat && this._byteData.position >= this._cur_mat_end) this.finalizeCurrentMaterial(); else if (this._cur_obj && this._byteData.position >= this._cur_obj_end) { // Can't finalize at this point, because we have to wait until the full // animation section has been parsed for any potential pivot definitions this._unfinalized_objects[this._cur_obj.name] = this._cur_obj; this._cur_obj_end = Number.MAX_VALUE /*uint*/; this._cur_obj = null; } if (this._byteData.getBytesAvailable() > 0) { var cid:number /*uint*/; var len:number /*uint*/; var end:number /*uint*/; cid = this._byteData.readUnsignedShort(); len = this._byteData.readUnsignedInt(); end = this._byteData.position + (len - 6); switch (cid) { case 0x4D4D: // MAIN3DS case 0x3D3D: // EDIT3DS case 0xB000: // KEYF3DS // This types are "container chunks" and contain only // sub-chunks (no data on their own.) This means that // there is nothing more to parse at this point, and // instead we should progress to the next chunk, which // will be the first sub-chunk of this one. continue; case 0xAFFF: // MATERIAL this._cur_mat_end = end; this._cur_mat = this.parseMaterial(); break; case 0x4000: // EDIT_OBJECT this._cur_obj_end = end; this._cur_obj = new ObjectVO(); this._cur_obj.name = this.readNulTermstring(); this._cur_obj.materials = new Array(); this._cur_obj.materialFaces = {}; break; case 0x4100: // OBJ_TRIMESH this._cur_obj.type = Sprite.assetType; break; case 0x4110: // TRI_VERTEXL this.parseVertexList(); break; case 0x4120: // TRI_FACELIST this.parseFaceList(); break; case 0x4140: // TRI_MAPPINGCOORDS this.parseUVList(); break; case 0x4130: // Face materials this.parseFaceMaterialList(); break; case 0x4160: // Transform this._cur_obj.transform = this.readTransform(); break; case 0xB002: // Object animation (including pivot) this.parseObjectAnimation(end); break; case 0x4150: // Smoothing groups this.parseSmoothingGroups(); break; default: // Skip this (unknown) chunk this._byteData.position += (len - 6); break; } // Pause parsing if there were any dependencies found during this // iteration (i.e. if there are any dependencies that need to be // retrieved at this time.) if (this.dependencies.length) { this._pPauseAndRetrieveDependencies(); break; } } } // More parsing is required if the entire byte array has not yet // been read, or if there is a currently non-finalized object in // the pipeline. if (this._byteData.getBytesAvailable() || this._cur_obj || this._cur_mat) { return ParserBase.MORE_TO_PARSE; } else { var name:string; // Finalize any remaining objects before ending. for (name in this._unfinalized_objects) { var obj:DisplayObjectContainer; obj = this.constructObject(this._unfinalized_objects[name]); if (obj) { //add to the content property ( this._pContent).addChild(obj); this._pFinalizeAsset(obj, name); } } return ParserBase.PARSING_DONE; } } public _pStartParsing(frameLimit:number):void { //create a content object for Loaders this._pContent = new DisplayObjectContainer(); super._pStartParsing(frameLimit); } private parseMaterial():MaterialVO { var mat:MaterialVO; mat = new MaterialVO(); while (this._byteData.position < this._cur_mat_end) { var cid:number /*uint*/; var len:number /*uint*/; var end:number /*uint*/; cid = this._byteData.readUnsignedShort(); len = this._byteData.readUnsignedInt(); end = this._byteData.position + (len - 6); switch (cid) { case 0xA000: // Material name mat.name = this.readNulTermstring(); break; case 0xA010: // Ambient color mat.ambientColor = this.readColor(); break; case 0xA020: // Diffuse color mat.diffuseColor = this.readColor(); break; case 0xA030: // Specular color mat.specularColor = this.readColor(); break; case 0xA081: // Two-sided, existence indicates "true" mat.twoSided = true; break; case 0xA200: // Main (color) texture mat.colorMap = this.parseTexture(end); break; case 0xA204: // Specular map mat.specularMap = this.parseTexture(end); break; default: this._byteData.position = end; break; } } return mat; } private parseTexture(end:number /*uint*/):TextureVO { var tex:TextureVO; tex = new TextureVO(); while (this._byteData.position < end) { var cid:number /*uint*/; var len:number /*uint*/; cid = this._byteData.readUnsignedShort(); len = this._byteData.readUnsignedInt(); switch (cid) { case 0xA300: tex.url = this.readNulTermstring(); break; default: // Skip this unknown texture sub-chunk this._byteData.position += (len - 6); break; } } this._textures[tex.url] = tex; this._pAddDependency(tex.url, new URLRequest(tex.url)); return tex; } private parseVertexList():void { var i:number /*uint*/; var len:number /*uint*/; var count:number /*uint*/; count = this._byteData.readUnsignedShort(); this._cur_obj.verts = new Array(count*3); i = 0; len = this._cur_obj.verts.length; while (i < len) { var x:number, y:number, z:number; x = this._byteData.readFloat(); y = this._byteData.readFloat(); z = this._byteData.readFloat(); this._cur_obj.verts[i++] = x; this._cur_obj.verts[i++] = z; this._cur_obj.verts[i++] = y; } } private parseFaceList():void { var i:number /*uint*/; var len:number /*uint*/; var count:number /*uint*/; count = this._byteData.readUnsignedShort(); this._cur_obj.indices = new Array(count*3) /*uint*/; i = 0; len = this._cur_obj.indices.length; while (i < len) { var i0:number /*uint*/, i1:number /*uint*/, i2:number /*uint*/; i0 = this._byteData.readUnsignedShort(); i1 = this._byteData.readUnsignedShort(); i2 = this._byteData.readUnsignedShort(); this._cur_obj.indices[i++] = i0; this._cur_obj.indices[i++] = i2; this._cur_obj.indices[i++] = i1; // Skip "face info", irrelevant in Away3D this._byteData.position += 2; } this._cur_obj.smoothingGroups = new Array(count) /*uint*/; } private parseSmoothingGroups():void { var len:number /*uint*/ = this._cur_obj.indices.length/3; var i:number /*uint*/ = 0; while (i < len) { this._cur_obj.smoothingGroups[i] = this._byteData.readUnsignedInt(); i++; } } private parseUVList():void { var i:number /*uint*/; var len:number /*uint*/; var count:number /*uint*/; count = this._byteData.readUnsignedShort(); this._cur_obj.uvs = new Array(count*2); i = 0; len = this._cur_obj.uvs.length; while (i < len) { this._cur_obj.uvs[i++] = this._byteData.readFloat(); this._cur_obj.uvs[i++] = 1.0 - this._byteData.readFloat(); } } private parseFaceMaterialList():void { var mat:string; var count:number /*uint*/; var i:number /*uint*/; var faces:Array /*uint*/; mat = this.readNulTermstring(); count = this._byteData.readUnsignedShort(); faces = new Array(count) /*uint*/; i = 0; while (i < faces.length) faces[i++] = this._byteData.readUnsignedShort(); this._cur_obj.materials.push(mat); this._cur_obj.materialFaces[mat] = faces; } private parseObjectAnimation(end:number):void { var vo:ObjectVO; var obj:DisplayObjectContainer; var pivot:Vector3D; var name:string; var hier:number /*uint*/; // Pivot defaults to origin pivot = new Vector3D; while (this._byteData.position < end) { var cid:number /*uint*/; var len:number /*uint*/; cid = this._byteData.readUnsignedShort(); len = this._byteData.readUnsignedInt(); switch (cid) { case 0xb010: // Name/hierarchy name = this.readNulTermstring(); this._byteData.position += 4; hier = this._byteData.readShort(); break; case 0xb013: // Pivot pivot.x = this._byteData.readFloat(); pivot.z = this._byteData.readFloat(); pivot.y = this._byteData.readFloat(); break; default: this._byteData.position += (len - 6); break; } } // If name is "$$$DUMMY" this is an empty object (e.g. a container) // and will be ignored in this version of the parser // TODO: Implement containers in 3DS parser. if (name != '$$$DUMMY' && this._unfinalized_objects.hasOwnProperty(name)) { vo = this._unfinalized_objects[name]; obj = this.constructObject(vo, pivot); if (obj) { //add to the content property ( this._pContent).addChild(obj); this._pFinalizeAsset(obj, vo.name); } delete this._unfinalized_objects[name]; } } private constructObject(obj:ObjectVO, pivot:Vector3D = null):DisplayObjectContainer { if (obj.type == Sprite.assetType) { var i:number /*uint*/; var sub:TriangleElements; var graphics:Graphics; var mat:IMaterial; var sprite:Sprite; var mtx:Matrix3D; var vertices:Array; var faces:Array; if (obj.materials.length > 1) console.log("The Away3D 3DS parser does not support multiple materials per sprite at this point."); // Ignore empty objects if (!obj.indices || obj.indices.length == 0) return null; vertices = new Array(obj.verts.length/3); faces = new Array(obj.indices.length/3); this.prepareData(vertices, faces, obj); if (this._useSmoothingGroups) this.applySmoothGroups(vertices, faces); obj.verts = new Array(vertices.length*3); for (i = 0; i < vertices.length; i++) { obj.verts[i*3] = vertices[i].x; obj.verts[i*3 + 1] = vertices[i].y; obj.verts[i*3 + 2] = vertices[i].z; } obj.indices = new Array(faces.length*3) /*uint*/; for (i = 0; i < faces.length; i++) { obj.indices[i*3] = faces[i].a; obj.indices[i*3 + 1] = faces[i].b; obj.indices[i*3 + 2] = faces[i].c; } if (obj.uvs) { // If the object had UVs to start with, use UVs generated by // smoothing group splitting algorithm. Otherwise those UVs // will be nonsense and should be skipped. obj.uvs = new Array(vertices.length*2); for (i = 0; i < vertices.length; i++) { obj.uvs[i*2] = vertices[i].u; obj.uvs[i*2 + 1] = vertices[i].v; } } if (obj.materials.length > 0) { var mname:string; mname = obj.materials[0]; mat = this._materials[mname].material; } // Build sprite and return it sprite = new Sprite(null, mat); sprite.transform.matrix3D = new Matrix3D(obj.transform); graphics = sprite.graphics; // Construct elements (potentially splitting buffers) // and add them to graphics. sub = new TriangleElements(new AttributesBuffer()); sub.setIndices(obj.indices); sub.setPositions(obj.verts); sub.setUVs(obj.uvs); graphics.addShape(new Shape(sub)); // Apply pivot translation to graphics if a pivot was // found while parsing the keyframe chunk earlier. if (pivot) { if (obj.transform) { // If a transform was found while parsing the // object chunk, use it to find the local pivot vector mtx = new Matrix3D(); mtx.copyRawDataFrom(obj.transform); mtx._rawData[12] = 0; mtx._rawData[13] = 0; mtx._rawData[14] = 0; pivot = mtx.transformVector(pivot); } pivot.scaleBy(-1); mtx = new Matrix3D(); mtx.appendTranslation(pivot.x, pivot.y, pivot.z); graphics.applyTransformation(mtx); } // Apply transformation to graphics if a transformation // was found while parsing the object chunk earlier. if (obj.transform) { mtx = new Matrix3D(); mtx.copyRawDataFrom(obj.transform); mtx.invert(); graphics.applyTransformation(mtx); } // Final transform applied to graphics. Finalize the graphics, // which will no longer be modified after this point. this._pFinalizeAsset(graphics, obj.name.concat('_graphics')); return sprite; } // If reached, unknown return null; } private prepareData(vertices:Array, faces:Array, obj:ObjectVO):void { // convert raw ObjectVO's data to structured VertexVO and FaceVO var i:number /*int*/; var j:number /*int*/; var k:number /*int*/; var len:number /*int*/ = obj.verts.length; for (i = 0, j = 0, k = 0; i < len;) { var v:VertexVO = new VertexVO; v.x = obj.verts[i++]; v.y = obj.verts[i++]; v.z = obj.verts[i++]; if (obj.uvs) { v.u = obj.uvs[j++]; v.v = obj.uvs[j++]; } vertices[k++] = v; } len = obj.indices.length; for (i = 0, k = 0; i < len;) { var f:FaceVO = new FaceVO(); f.a = obj.indices[i++]; f.b = obj.indices[i++]; f.c = obj.indices[i++]; f.smoothGroup = obj.smoothingGroups[k] || 0; faces[k++] = f; } } private applySmoothGroups(vertices:Array, faces:Array):void { // clone vertices according to following rule: // clone if vertex's in faces from groups 1+2 and 3 // don't clone if vertex's in faces from groups 1+2, 3 and 1+3 var i:number /*int*/; var j:number /*int*/; var k:number /*int*/; var l:number /*int*/; var len:number /*int*/; var numVerts:number /*uint*/ = vertices.length; var numFaces:number /*uint*/ = faces.length; // extract groups data for vertices var vGroups:Array> /*uint*/ = new Array>(numVerts) /*uint*/; for (i = 0; i < numVerts; i++) vGroups[i] = new Array() /*uint*/; for (i = 0; i < numFaces; i++) { var face:FaceVO = faces[i]; for (j = 0; j < 3; j++) { var groups:Array /*uint*/ = vGroups[(j == 0)? face.a : ((j == 1)? face.b : face.c)]; var group:number /*uint*/ = face.smoothGroup; for (k = groups.length - 1; k >= 0; k--) { if ((group & groups[k]) > 0) { group |= groups[k]; groups.splice(k, 1); k = groups.length - 1; } } groups.push(group); } } // clone vertices var vClones:Array> /*uint*/ = new Array>(numVerts) /*uint*/; for (i = 0; i < numVerts; i++) { if ((len = vGroups[i].length) < 1) continue; var clones:Array /*uint*/ = new Array(len) /*uint*/; vClones[i] = clones; clones[0] = i; var v0:VertexVO = vertices[i]; for (j = 1; j < len; j++) { var v1:VertexVO = new VertexVO; v1.x = v0.x; v1.y = v0.y; v1.z = v0.z; v1.u = v0.u; v1.v = v0.v; clones[j] = vertices.length; vertices.push(v1); } } numVerts = vertices.length; for (i = 0; i < numFaces; i++) { face = faces[i]; group = face.smoothGroup; for (j = 0; j < 3; j++) { k = (j == 0)? face.a : ((j == 1)? face.b : face.c); groups = vGroups[k]; len = groups.length; clones = vClones[k]; for (l = 0; l < len; l++) { if (((group == 0) && (groups[l] == 0)) || ((group & groups[l]) > 0)) { var index:number /*uint*/ = clones[l]; if (group == 0) { // vertex is unique if no smoothGroup found groups.splice(l, 1); clones.splice(l, 1); } if (j == 0) face.a = index; else if (j == 1) face.b = index; else face.c = index; l = len; } } } } } private finalizeCurrentMaterial():void { var mat:MethodMaterial; mat = new MethodMaterial(this._cur_mat.ambientColor); if (this._cur_mat.colorMap) mat.ambientMethod.texture = this._cur_mat.colorMap.texture || new ImageTexture2D(); mat.diffuseMethod.color = this._cur_mat.diffuseColor; mat.specularMethod.color = this._cur_mat.specularColor; if (this.materialMode >= 2) mat.mode = MethodMaterialMode.MULTI_PASS; mat.bothSides = this._cur_mat.twoSided; this._pFinalizeAsset(mat, this._cur_mat.name); this._materials[this._cur_mat.name] = this._cur_mat; this._cur_mat.material = mat; this._cur_mat = null; } private readNulTermstring():string { var chr:number /*int*/; var str:string = ""; while ((chr = this._byteData.readUnsignedByte()) > 0) str += String.fromCharCode(chr); return str; } private readTransform():Float32Array { var data:Float32Array = new Float32Array(16); // X axis data[0] = this._byteData.readFloat(); // X data[2] = this._byteData.readFloat(); // Z data[1] = this._byteData.readFloat(); // Y data[3] = 0; // Z axis data[8] = this._byteData.readFloat(); // X data[10] = this._byteData.readFloat(); // Z data[9] = this._byteData.readFloat(); // Y data[11] = 0; // Y Axis data[4] = this._byteData.readFloat(); // X data[6] = this._byteData.readFloat(); // Z data[5] = this._byteData.readFloat(); // Y data[7] = 0; // Translation data[12] = this._byteData.readFloat(); // X data[14] = this._byteData.readFloat(); // Z data[13] = this._byteData.readFloat(); // Y data[15] = 1; return data; } private readColor():number /*int*/ { var cid:number /*int*/; var len:number /*int*/; var r:number /*int*/, g:number /*int*/, b:number /*int*/; cid = this._byteData.readUnsignedShort(); len = this._byteData.readUnsignedInt(); switch (cid) { case 0x0010: // Floats r = this._byteData.readFloat()*255; g = this._byteData.readFloat()*255; b = this._byteData.readFloat()*255; break; case 0x0011: // 24-bit color r = this._byteData.readUnsignedByte(); g = this._byteData.readUnsignedByte(); b = this._byteData.readUnsignedByte(); break; default: this._byteData.position += (len - 6); break; } return (r << 16) | (g << 8) | b; } } /** * */ export class FaceVO { public a:number /*int*/; public b:number /*int*/; public c:number /*int*/; public smoothGroup:number /*int*/; } /** * */ export class MaterialVO { public name:string; public ambientColor:number /*int*/; public diffuseColor:number /*int*/; public specularColor:number /*int*/; public twoSided:boolean; public colorMap:TextureVO; public specularMap:TextureVO; public material:IMaterial; } /** * */ export class ObjectVO { public name:string; public type:string; public pivotX:number; public pivotY:number; public pivotZ:number; public transform:Float32Array; public verts:Array; public indices:Array /*int*/; public uvs:Array; public materialFaces:Object; public materials:Array; public smoothingGroups:Array /*int*/; } /** * */ export class TextureVO { public url:string; public texture:ImageTexture2D; } /** * */ export class VertexVO { public x:number; public y:number; public z:number; public u:number; public v:number; public normal:Vector3D; public tangent:Vector3D; }