UNPKG

three-stdlib

Version:

stand-alone library of threejs examples

745 lines (744 loc) 22.3 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const LWO2Parser = require("./LWO2Parser.cjs"); const LWO3Parser = require("./LWO3Parser.cjs"); class IFFParser { constructor() { this.debugger = new Debugger(); } parse(buffer) { this.reader = new DataViewReader(buffer); this.tree = { materials: {}, layers: [], tags: [], textures: [] }; this.currentLayer = this.tree; this.currentForm = this.tree; this.parseTopForm(); if (this.tree.format === void 0) return; if (this.tree.format === "LWO2") { this.parser = new LWO2Parser.LWO2Parser(this); while (!this.reader.endOfFile()) this.parser.parseBlock(); } else if (this.tree.format === "LWO3") { this.parser = new LWO3Parser.LWO3Parser(this); while (!this.reader.endOfFile()) this.parser.parseBlock(); } this.debugger.offset = this.reader.offset; this.debugger.closeForms(); return this.tree; } parseTopForm() { this.debugger.offset = this.reader.offset; var topForm = this.reader.getIDTag(); if (topForm !== "FORM") { console.warn("LWOLoader: Top-level FORM missing."); return; } var length = this.reader.getUint32(); this.debugger.dataOffset = this.reader.offset; this.debugger.length = length; var type = this.reader.getIDTag(); if (type === "LWO2") { this.tree.format = type; } else if (type === "LWO3") { this.tree.format = type; } this.debugger.node = 0; this.debugger.nodeID = type; this.debugger.log(); return; } /// // FORM PARSING METHODS /// // Forms are organisational and can contain any number of sub chunks and sub forms // FORM ::= 'FORM'[ID4], length[U4], type[ID4], ( chunk[CHUNK] | form[FORM] ) * } parseForm(length) { var type = this.reader.getIDTag(); switch (type) { case "ISEQ": case "ANIM": case "STCC": case "VPVL": case "VPRM": case "NROT": case "WRPW": case "WRPH": case "FUNC": case "FALL": case "OPAC": case "GRAD": case "ENVS": case "VMOP": case "VMBG": case "OMAX": case "STEX": case "CKBG": case "CKEY": case "VMLA": case "VMLB": this.debugger.skipped = true; this.skipForm(length); break; case "META": case "NNDS": case "NODS": case "NDTA": case "ADAT": case "AOVS": case "BLOK": case "IBGC": case "IOPC": case "IIMG": case "TXTR": this.debugger.length = 4; this.debugger.skipped = true; break; case "IFAL": case "ISCL": case "IPOS": case "IROT": case "IBMP": case "IUTD": case "IVTD": this.parseTextureNodeAttribute(type); break; case "ENVL": this.parseEnvelope(length); break; case "CLIP": if (this.tree.format === "LWO2") { this.parseForm(length); } else { this.parseClip(length); } break; case "STIL": this.parseImage(); break; case "XREF": this.reader.skip(8); this.currentForm.referenceTexture = { index: this.reader.getUint32(), refName: this.reader.getString() // internal unique ref }; break; case "IMST": this.parseImageStateForm(length); break; case "SURF": this.parseSurfaceForm(length); break; case "VALU": this.parseValueForm(length); break; case "NTAG": this.parseSubNode(length); break; case "ATTR": case "SATR": this.setupForm("attributes", length); break; case "NCON": this.parseConnections(length); break; case "SSHA": this.parentForm = this.currentForm; this.currentForm = this.currentSurface; this.setupForm("surfaceShader", length); break; case "SSHD": this.setupForm("surfaceShaderData", length); break; case "ENTR": this.parseEntryForm(length); break; case "IMAP": this.parseImageMap(length); break; case "TAMP": this.parseXVAL("amplitude", length); break; case "TMAP": this.setupForm("textureMap", length); break; case "CNTR": this.parseXVAL3("center", length); break; case "SIZE": this.parseXVAL3("scale", length); break; case "ROTA": this.parseXVAL3("rotation", length); break; default: this.parseUnknownForm(type, length); } this.debugger.node = 0; this.debugger.nodeID = type; this.debugger.log(); } setupForm(type, length) { if (!this.currentForm) this.currentForm = this.currentNode; this.currentFormEnd = this.reader.offset + length; this.parentForm = this.currentForm; if (!this.currentForm[type]) { this.currentForm[type] = {}; this.currentForm = this.currentForm[type]; } else { console.warn("LWOLoader: form already exists on parent: ", type, this.currentForm); this.currentForm = this.currentForm[type]; } } skipForm(length) { this.reader.skip(length - 4); } parseUnknownForm(type, length) { console.warn("LWOLoader: unknown FORM encountered: " + type, length); printBuffer(this.reader.dv.buffer, this.reader.offset, length - 4); this.reader.skip(length - 4); } parseSurfaceForm(length) { this.reader.skip(8); var name = this.reader.getString(); var surface = { attributes: {}, // LWO2 style non-node attributes will go here connections: {}, name, inputName: name, nodes: {}, source: this.reader.getString() }; this.tree.materials[name] = surface; this.currentSurface = surface; this.parentForm = this.tree.materials; this.currentForm = surface; this.currentFormEnd = this.reader.offset + length; } parseSurfaceLwo2(length) { var name = this.reader.getString(); var surface = { attributes: {}, // LWO2 style non-node attributes will go here connections: {}, name, nodes: {}, source: this.reader.getString() }; this.tree.materials[name] = surface; this.currentSurface = surface; this.parentForm = this.tree.materials; this.currentForm = surface; this.currentFormEnd = this.reader.offset + length; } parseSubNode(length) { this.reader.skip(8); var name = this.reader.getString(); var node = { name }; this.currentForm = node; this.currentNode = node; this.currentFormEnd = this.reader.offset + length; } // collect attributes from all nodes at the top level of a surface parseConnections(length) { this.currentFormEnd = this.reader.offset + length; this.parentForm = this.currentForm; this.currentForm = this.currentSurface.connections; } // surface node attribute data, e.g. specular, roughness etc parseEntryForm(length) { this.reader.skip(8); var name = this.reader.getString(); this.currentForm = this.currentNode.attributes; this.setupForm(name, length); } // parse values from material - doesn't match up to other LWO3 data types // sub form of entry form parseValueForm() { this.reader.skip(8); var valueType = this.reader.getString(); if (valueType === "double") { this.currentForm.value = this.reader.getUint64(); } else if (valueType === "int") { this.currentForm.value = this.reader.getUint32(); } else if (valueType === "vparam") { this.reader.skip(24); this.currentForm.value = this.reader.getFloat64(); } else if (valueType === "vparam3") { this.reader.skip(24); this.currentForm.value = this.reader.getFloat64Array(3); } } // holds various data about texture node image state // Data other thanmipMapLevel unknown parseImageStateForm() { this.reader.skip(8); this.currentForm.mipMapLevel = this.reader.getFloat32(); } // LWO2 style image data node OR LWO3 textures defined at top level in editor (not as SURF node) parseImageMap(length) { this.currentFormEnd = this.reader.offset + length; this.parentForm = this.currentForm; if (!this.currentForm.maps) this.currentForm.maps = []; var map = {}; this.currentForm.maps.push(map); this.currentForm = map; this.reader.skip(10); } parseTextureNodeAttribute(type) { this.reader.skip(28); this.reader.skip(20); switch (type) { case "ISCL": this.currentNode.scale = this.reader.getFloat32Array(3); break; case "IPOS": this.currentNode.position = this.reader.getFloat32Array(3); break; case "IROT": this.currentNode.rotation = this.reader.getFloat32Array(3); break; case "IFAL": this.currentNode.falloff = this.reader.getFloat32Array(3); break; case "IBMP": this.currentNode.amplitude = this.reader.getFloat32(); break; case "IUTD": this.currentNode.uTiles = this.reader.getFloat32(); break; case "IVTD": this.currentNode.vTiles = this.reader.getFloat32(); break; } this.reader.skip(2); } // ENVL forms are currently ignored parseEnvelope(length) { this.reader.skip(length - 4); } /// // CHUNK PARSING METHODS /// // clips can either be defined inside a surface node, or at the top // level and they have a different format in each case parseClip(length) { var tag = this.reader.getIDTag(); if (tag === "FORM") { this.reader.skip(16); this.currentNode.fileName = this.reader.getString(); return; } this.reader.setOffset(this.reader.offset - 4); this.currentFormEnd = this.reader.offset + length; this.parentForm = this.currentForm; this.reader.skip(8); var texture = { index: this.reader.getUint32() }; this.tree.textures.push(texture); this.currentForm = texture; } parseClipLwo2(length) { var texture = { index: this.reader.getUint32(), fileName: "" }; while (true) { var tag = this.reader.getIDTag(); var n_length = this.reader.getUint16(); if (tag === "STIL") { texture.fileName = this.reader.getString(); break; } if (n_length >= length) { break; } } this.tree.textures.push(texture); this.currentForm = texture; } parseImage() { this.reader.skip(8); this.currentForm.fileName = this.reader.getString(); } parseXVAL(type, length) { var endOffset = this.reader.offset + length - 4; this.reader.skip(8); this.currentForm[type] = this.reader.getFloat32(); this.reader.setOffset(endOffset); } parseXVAL3(type, length) { var endOffset = this.reader.offset + length - 4; this.reader.skip(8); this.currentForm[type] = { x: this.reader.getFloat32(), y: this.reader.getFloat32(), z: this.reader.getFloat32() }; this.reader.setOffset(endOffset); } // Tags associated with an object // OTAG { type[ID4], tag-string[S0] } parseObjectTag() { if (!this.tree.objectTags) this.tree.objectTags = {}; this.tree.objectTags[this.reader.getIDTag()] = { tagString: this.reader.getString() }; } // Signals the start of a new layer. All the data chunks which follow will be included in this layer until another layer chunk is encountered. // LAYR: number[U2], flags[U2], pivot[VEC12], name[S0], parent[U2] parseLayer(length) { var layer = { number: this.reader.getUint16(), flags: this.reader.getUint16(), // If the least significant bit of flags is set, the layer is hidden. pivot: this.reader.getFloat32Array(3), // Note: this seems to be superflous, as the geometry is translated when pivot is present name: this.reader.getString() }; this.tree.layers.push(layer); this.currentLayer = layer; var parsedLength = 16 + stringOffset(this.currentLayer.name); this.currentLayer.parent = parsedLength < length ? this.reader.getUint16() : -1; } // VEC12 * ( F4 + F4 + F4 ) array of x,y,z vectors // Converting from left to right handed coordinate system: // x -> -x and switch material FrontSide -> BackSide parsePoints(length) { this.currentPoints = []; for (var i = 0; i < length / 4; i += 3) { this.currentPoints.push(this.reader.getFloat32(), this.reader.getFloat32(), -this.reader.getFloat32()); } } // parse VMAP or VMAD // Associates a set of floating-point vectors with a set of points. // VMAP: { type[ID4], dimension[U2], name[S0], ( vert[VX], value[F4] # dimension ) * } // VMAD Associates a set of floating-point vectors with the vertices of specific polygons. // Similar to VMAP UVs, but associates with polygon vertices rather than points // to solve to problem of UV seams: VMAD chunks are paired with VMAPs of the same name, // if they exist. The vector values in the VMAD will then replace those in the // corresponding VMAP, but only for calculations involving the specified polygons. // VMAD { type[ID4], dimension[U2], name[S0], ( vert[VX], poly[VX], value[F4] # dimension ) * } parseVertexMapping(length, discontinuous) { var finalOffset = this.reader.offset + length; var channelName = this.reader.getString(); if (this.reader.offset === finalOffset) { this.currentForm.UVChannel = channelName; return; } this.reader.setOffset(this.reader.offset - stringOffset(channelName)); var type = this.reader.getIDTag(); this.reader.getUint16(); var name = this.reader.getString(); var remainingLength = length - 6 - stringOffset(name); switch (type) { case "TXUV": this.parseUVMapping(name, finalOffset, discontinuous); break; case "MORF": case "SPOT": this.parseMorphTargets(name, finalOffset, type); break; case "APSL": case "NORM": case "WGHT": case "MNVW": case "PICK": case "RGB ": case "RGBA": this.reader.skip(remainingLength); break; default: console.warn("LWOLoader: unknown vertex map type: " + type); this.reader.skip(remainingLength); } } parseUVMapping(name, finalOffset, discontinuous) { var uvIndices = []; var polyIndices = []; var uvs = []; while (this.reader.offset < finalOffset) { uvIndices.push(this.reader.getVariableLengthIndex()); if (discontinuous) polyIndices.push(this.reader.getVariableLengthIndex()); uvs.push(this.reader.getFloat32(), this.reader.getFloat32()); } if (discontinuous) { if (!this.currentLayer.discontinuousUVs) this.currentLayer.discontinuousUVs = {}; this.currentLayer.discontinuousUVs[name] = { uvIndices, polyIndices, uvs }; } else { if (!this.currentLayer.uvs) this.currentLayer.uvs = {}; this.currentLayer.uvs[name] = { uvIndices, uvs }; } } parseMorphTargets(name, finalOffset, type) { var indices = []; var points = []; type = type === "MORF" ? "relative" : "absolute"; while (this.reader.offset < finalOffset) { indices.push(this.reader.getVariableLengthIndex()); points.push(this.reader.getFloat32(), this.reader.getFloat32(), -this.reader.getFloat32()); } if (!this.currentLayer.morphTargets) this.currentLayer.morphTargets = {}; this.currentLayer.morphTargets[name] = { indices, points, type }; } // A list of polygons for the current layer. // POLS { type[ID4], ( numvert+flags[U2], vert[VX] # numvert ) * } parsePolygonList(length) { var finalOffset = this.reader.offset + length; var type = this.reader.getIDTag(); var indices = []; var polygonDimensions = []; while (this.reader.offset < finalOffset) { var numverts = this.reader.getUint16(); numverts = numverts & 1023; polygonDimensions.push(numverts); for (var j = 0; j < numverts; j++) indices.push(this.reader.getVariableLengthIndex()); } var geometryData = { type, vertexIndices: indices, polygonDimensions, points: this.currentPoints }; if (polygonDimensions[0] === 1) geometryData.type = "points"; else if (polygonDimensions[0] === 2) geometryData.type = "lines"; this.currentLayer.geometry = geometryData; } // Lists the tag strings that can be associated with polygons by the PTAG chunk. // TAGS { tag-string[S0] * } parseTagStrings(length) { this.tree.tags = this.reader.getStringArray(length); } // Associates tags of a given type with polygons in the most recent POLS chunk. // PTAG { type[ID4], ( poly[VX], tag[U2] ) * } parsePolygonTagMapping(length) { var finalOffset = this.reader.offset + length; var type = this.reader.getIDTag(); if (type === "SURF") this.parseMaterialIndices(finalOffset); else { this.reader.skip(length - 4); } } parseMaterialIndices(finalOffset) { this.currentLayer.geometry.materialIndices = []; while (this.reader.offset < finalOffset) { var polygonIndex = this.reader.getVariableLengthIndex(); var materialIndex = this.reader.getUint16(); this.currentLayer.geometry.materialIndices.push(polygonIndex, materialIndex); } } parseUnknownCHUNK(blockID, length) { console.warn("LWOLoader: unknown chunk type: " + blockID + " length: " + length); var data = this.reader.getString(length); this.currentForm[blockID] = data; } } class DataViewReader { constructor(buffer) { this.dv = new DataView(buffer); this.offset = 0; this._textDecoder = new TextDecoder(); this._bytes = new Uint8Array(buffer); } size() { return this.dv.buffer.byteLength; } setOffset(offset) { if (offset > 0 && offset < this.dv.buffer.byteLength) { this.offset = offset; } else { console.error("LWOLoader: invalid buffer offset"); } } endOfFile() { if (this.offset >= this.size()) return true; return false; } skip(length) { this.offset += length; } getUint8() { var value = this.dv.getUint8(this.offset); this.offset += 1; return value; } getUint16() { var value = this.dv.getUint16(this.offset); this.offset += 2; return value; } getInt32() { var value = this.dv.getInt32(this.offset, false); this.offset += 4; return value; } getUint32() { var value = this.dv.getUint32(this.offset, false); this.offset += 4; return value; } getUint64() { var low, high; high = this.getUint32(); low = this.getUint32(); return high * 4294967296 + low; } getFloat32() { var value = this.dv.getFloat32(this.offset, false); this.offset += 4; return value; } getFloat32Array(size) { var a = []; for (var i = 0; i < size; i++) { a.push(this.getFloat32()); } return a; } getFloat64() { var value = this.dv.getFloat64(this.offset, this.littleEndian); this.offset += 8; return value; } getFloat64Array(size) { var a = []; for (var i = 0; i < size; i++) { a.push(this.getFloat64()); } return a; } // get variable-length index data type // VX ::= index[U2] | (index + 0xFF000000)[U4] // If the index value is less than 65,280 (0xFF00),then VX === U2 // otherwise VX === U4 with bits 24-31 set // When reading an index, if the first byte encountered is 255 (0xFF), then // the four-byte form is being used and the first byte should be discarded or masked out. getVariableLengthIndex() { var firstByte = this.getUint8(); if (firstByte === 255) { return this.getUint8() * 65536 + this.getUint8() * 256 + this.getUint8(); } return firstByte * 256 + this.getUint8(); } // An ID tag is a sequence of 4 bytes containing 7-bit ASCII values getIDTag() { return this.getString(4); } getString(size) { if (size === 0) return; const start = this.offset; let result; let length; if (size) { length = size; result = this._textDecoder.decode(new Uint8Array(this.dv.buffer, start, size)); } else { length = this._bytes.indexOf(0, start) - start; result = this._textDecoder.decode(new Uint8Array(this.dv.buffer, start, length)); length++; length += length % 2; } this.skip(length); return result; } getStringArray(size) { var a = this.getString(size); a = a.split("\0"); return a.filter(Boolean); } } class Debugger { constructor() { this.active = false; this.depth = 0; this.formList = []; } enable() { this.active = true; } log() { if (!this.active) return; var nodeType; switch (this.node) { case 0: nodeType = "FORM"; break; case 1: nodeType = "CHK"; break; case 2: nodeType = "S-CHK"; break; } console.log( "| ".repeat(this.depth) + nodeType, this.nodeID, `( ${this.offset} ) -> ( ${this.dataOffset + this.length} )`, this.node == 0 ? " {" : "", this.skipped ? "SKIPPED" : "", this.node == 0 && this.skipped ? "}" : "" ); if (this.node == 0 && !this.skipped) { this.depth += 1; this.formList.push(this.dataOffset + this.length); } this.skipped = false; } closeForms() { if (!this.active) return; for (var i = this.formList.length - 1; i >= 0; i--) { if (this.offset >= this.formList[i]) { this.depth -= 1; console.log("| ".repeat(this.depth) + "}"); this.formList.splice(-1, 1); } } } } function isEven(num) { return num % 2; } function stringOffset(string) { return string.length + 1 + (isEven(string.length + 1) ? 1 : 0); } function printBuffer(buffer, from, to) { console.log(new TextDecoder().decode(new Uint8Array(buffer, from, to))); } exports.IFFParser = IFFParser; //# sourceMappingURL=IFFParser.cjs.map