UNPKG

@blockv/threejs-to-v3d

Version:

Converts any format supported by ThreeJS to V3D.

554 lines (405 loc) 20.3 kB
// // OBJ to V3D file format converter // Load modules var FSFile = require("./data/FSFile"); var BrowserFile = require("./data/BrowserFile"); var V3DBuilder = require("./V3D/V3DBuilder"); // var triangulate = require("triangulate"); var NodeTextureLoader = require("./NodeTextureLoader"); var THREE = require("three"); // Apply FBX loader to ThreeJS require("./FBXLoader")(THREE, new NodeTextureLoader()); module.exports = class FBXConverter { /** Converts an FBX file (or file array including textures) and returns a promise of the V3D data in an ArrayBuffer. */ static convert(files, onprogress) { var converter = new FBXConverter(files); converter.onprogress = onprogress; return converter.convert(); } constructor(inputFiles) { // Properties this.status = "Idle"; this.files = []; this.issues = []; this.result = null; this.waitingPromises = []; this.onprogress = null; this.v3d = null; // Setup V3D file this.builder = new V3DBuilder(); this.builder.fileProperties["source.exporter"] = "ThreeJS to V3D converter"; // Add files if (inputFiles) for (var file of inputFiles || [inputFiles]) this.addFile(file); } /* Begin the conversion */ convert() { // Find .fbx file var objFile = null; for (var file of this.files) { if (file.path.toLowerCase().lastIndexOf(".fbx") == file.path.length-4) { if (objFile) this.addIssue("warning", "invalid_filelist", "Please only provide one .fbx file. " + objFile.path + " will be ignored."); objFile = file; } } // Fail if not found if (!objFile) return Promise.reject(new Error("No .fbx file in file list!")); // Set title this.builder.fileProperties.title = objFile.path; this.builder.fileProperties.title = this.replaceText(this.builder.fileProperties.title, "\\", "/"); var idx = this.builder.fileProperties.title.lastIndexOf("/"); if (idx != -1) this.builder.fileProperties.title = this.builder.fileProperties.title.substring(idx+1); // Load FBX file this.updateStatus("Loading " + objFile.path); return objFile.loadBytes().then(d => this.parseFBX(d)) // Prepare V3D blocks .then(e => this.updateStatus("Generating V3D blocks")) .then(e => this.buildScene()) // Wait for promises .then(e => this.updateStatus("Loading associated files...")) .then(e => this.waitForAllPromises()) // Output file data .then(e => this.updateStatus("Building file...")) .then(e => this.result = this.builder.build()) } /** Returns a Promise that finishes when all pending promises are resolved */ waitForAllPromises() { // Done if no more promises if (this.waitingPromises.length == 0) return true; var currentPromise = this.waitingPromises[0]; return currentPromise.then(e => { // Remove completed promise for (var i = 0 ; i < this.waitingPromises.length ; i++) if (this.waitingPromises[i] === currentPromise) this.waitingPromises.splice(i--, 1); // Wait again return this.waitForAllPromises(); }); } /** Add an input file to the processor */ addFile(file) { // Check file type if (BrowserFile.isSupported(file)) { // Add file this.files.push(new BrowserFile(file)); } else if (FSFile.isSupported(file)) { // Add file this.files.push(new FSFile(file)); } else { // Invalid file // var file = new DataFile(file); // file.error = new Error("Unknown object type. " + file); // this.files.push(); this.addIssue("warning", "unknown_file_object", "Unable to load file data for " + file + ". Please pass in a File object in the browser, or a file path string in Node."); } } /** @private Adds an issue */ addIssue(type, code, description) { // Check if an issue with this code already exists for (var issue of this.issues) if (issue.code == code) return; // Add it this.issues.push({ type: type, code: code, description: description }); } /** @private Gets the file with the specified path suffix */ getFile(name) { // Check if string if (typeof name != "string") return null // Convert all \ to / name = this.replaceText(name.toLowerCase(), "\\", "/"); // Remove path modifiers if (name.indexOf("/") == 0) name = name.substring(1).trim(); if (name.indexOf("./") == 0) name = name.substring(2).trim(); if (name.indexOf("../") == 0) name = name.substring(3).trim(); // Go through files for (var file of this.files) { // Replace \ with / var fname = this.replaceText(file.path.toLowerCase(), "\\", "/"); // Check it var idx = fname.lastIndexOf(name); if (idx != -1 && idx == fname.length - name.length) return file; } // Still not found, try again with all path components stripped, or die if it's already stripped var idx = name.lastIndexOf("/"); if (idx == -1) return; // Try again name = name.substring(idx+1); return this.getFile(name); } /** @private Update the status of the conversion */ updateStatus(text) { // Check if passed a block if (text.data && text.properties) text = "Generated " + text.properties.type + " block : " + text.properties.name; // Update properties this.status = text; // Call handler if (this.onprogress) this.onprogress(text); } /** @private Parse the ArrayBuffer of an FBX file */ parseFBX(bfr) { // Set status this.updateStatus("Parsing FBX data"); // Create loader var loader = new THREE.FBXLoader(); this.scene = loader.parse(bfr); } /** @private Removes all ocurrences of a string from another string */ replaceText(haystack, needle, replaceWith) { // Check args if (!haystack) haystack = ""; if (!needle) return haystack; if (!replaceWith) replaceWith = ""; // Replace it var idx = 0; while ((idx = haystack.indexOf(needle)) != -1) haystack = haystack.substring(0, idx) + replaceWith + haystack.substring(idx + needle.length); // Done return haystack; } /** @private Build the v3d file from the three.js scene */ buildScene() { // Sanity check: We should have a valid scene at this point if (!this.scene) throw new Error("We didn't load a scene! This is most likely a bug with the threejs-to-v3d exporter, please log a bug report."); // Create scene block var sceneBlock = this.builder.newBlock(); sceneBlock.properties.type = "scene"; sceneBlock.properties.name = this.scene.name || "Scene"; this.updateStatus("Generated scene block : " + sceneBlock.properties.name); // Output children entities for (var child of this.scene.children) this.buildSceneNode(child, sceneBlock); } /** @private Exports a scene graph node */ buildSceneNode(node, parentBlock) { // Create block var block = parentBlock.newChildBlock(); block.properties.type = "scene.group"; block.properties.name = node.name || node.type || "Group"; // Apply transforms block.properties.translation = [node.position.x, node.position.y, node.position.z]; block.properties.scale = [node.scale.x, node.scale.y, node.scale.z]; block.properties["rotation.quaternion"] = [node.quaternion.x, node.quaternion.y, node.quaternion.z, node.quaternion.w]; // Check type if (node.type == "Group" || node.type == "Object3D") { // No more settings, this is just a container node } else if (node.type == "Mesh") { // Mesh node block.properties.type = "scene.mesh"; // Convert BufferGeometry's to normal Geometry objects, which are easier to work with var geometry = node.geometry; if (node.geometry instanceof THREE.BufferGeometry) { geometry = new THREE.Geometry(); geometry.fromBufferGeometry(node.geometry); } // Calculate face normals geometry.computeFaceNormals(); // Create face index block var faceIndexBlock = this.builder.newBlock(); block.properties.faces = faceIndexBlock.id; faceIndexBlock.properties.type = "data.face-indexes"; for (var face of geometry.faces) { faceIndexBlock.data.addUInt32(face.a); faceIndexBlock.data.addUInt32(face.b); faceIndexBlock.data.addUInt32(face.c); } // Create vertex data block var vertexDataBlock = this.builder.newBlock(); block.properties.vertexData = vertexDataBlock.id; vertexDataBlock.properties.type = "data.vertices"; for (var vert of geometry.vertices) { vertexDataBlock.data.addFloat32(vert.x); vertexDataBlock.data.addFloat32(vert.y); vertexDataBlock.data.addFloat32(vert.z); } // Create vertex normals data block var normalsDataBlock = this.builder.newBlock(); block.properties.normals = normalsDataBlock.id; normalsDataBlock.properties.type = "data.normals"; for (var face of geometry.faces) { // Check if using vertex normals if (face.vertexNormals.length == 3) { // Add normals for (var i = 0 ; i < 3 ; i++) { normalsDataBlock.data.addFloat32(face.vertexNormals[i].x); normalsDataBlock.data.addFloat32(face.vertexNormals[i].y); normalsDataBlock.data.addFloat32(face.vertexNormals[i].z); } } else { // Add face normal to all vertices for (var i = 0 ; i < 3 ; i++) { normalsDataBlock.data.addFloat32(face.normal.x); normalsDataBlock.data.addFloat32(face.normal.y); normalsDataBlock.data.addFloat32(face.normal.z); } } } // Check if got face vertex UVs if (geometry.faceVertexUvs && geometry.faceVertexUvs[0] && geometry.faceVertexUvs[0].length > 0) { // Create UV block var uvBlock = this.builder.newBlock(); block.properties.uvs = uvBlock.id; uvBlock.properties.type = "data.uvs"; // Check length if (geometry.faceVertexUvs[0].length == geometry.faces.length) { // Correctly using FACE vertex UVs for (var i = 0 ; i < geometry.faces.length ; i++) { for (var x = 0 ; x < 3 ; x++) { uvBlock.data.addFloat32(geometry.faceVertexUvs[0][i][x].x); uvBlock.data.addFloat32(geometry.faceVertexUvs[0][i][x].y); } } // } else if (geometry.faceVertexUvs[0].length == geometry.faces.length) { // // // Incorrectly using face UVs - instead of face VERTEX UVs // this.addIssue("warning", "invalid_uv_attachment", "UVs are attached to faces, instead of face vertices. They will be copied over."); // for (var f = 0 ; f < geometry.faces.length ; f++) { // for (var i = 0 ; i < 3 ; i++) { // uvBlock.data.addFloat32(geometry.faceVertexUvs[0][f].x); // uvBlock.data.addFloat32(geometry.faceVertexUvs[0][f].y); // } // } // // } else if (geometry.faceVertexUvs[0].length == geometry.vertices.length) { // // // Incorrectly using vertex UVs - instead of FACE vertex UVs // this.addIssue("warning", "invalid_uv_attachment", "UVs are attached to vertex coordinates, instead of FACE vertices. They will be copied over."); // for (var face of geometry.faces) { // for (var vertexId of [face.a, face.b, face.c]) { // uvBlock.data.addFloat32(geometry.faceVertexUvs[0][vertexId] && geometry.faceVertexUvs[0][vertexId].x || 0); // uvBlock.data.addFloat32(geometry.faceVertexUvs[0][vertexId] && geometry.faceVertexUvs[0][vertexId].y || 0); // } // } } else { // Invalid UV count! This file is messed up... this.addIssue("warning", "invalid_uv_count_" + block.id, "Invalid number of UVs, found " + geometry.faceVertexUvs[0].length + " but there should have been " + (geometry.faces.length * 3)); // Remove UV block block.properties.uvs = undefined; } } // Check if got a material if (node.material) { // We don't support multiple materials if (node.material.length) { this.addIssue("warning", "multimaterial", "This object contains multiple materials per object, we don't support that yet.") node.material = node.material[0] } // Check if using vertex colors if (node.material.vertexColors != THREE.NoColors && (geometry.faces[0].vertexColors && geometry.faces[0].vertexColors.length == 3 || geometry.faces[0].color)) { // Create face vertex color block var vertexColorBlock = this.builder.newBlock(); block.properties.faceVertexColors = vertexColorBlock.id; vertexColorBlock.properties.type = "data.face-vertex-colors"; // Add each face vertex color for (var face of geometry.faces) { // Check if using vertex colors or face color if (face.vertexColors && face.vertexColors.length == 3) { // Using face vertex colors for (var i = 0 ; i < 3 ; i++) { vertexColorBlock.data.addFloat32(face.vertexColors[i].r); vertexColorBlock.data.addFloat32(face.vertexColors[i].g); vertexColorBlock.data.addFloat32(face.vertexColors[i].b); } } else if (face.color) { // Using face color, repeat for each vertex for (var i = 0 ; i < 3 ; i++) { vertexColorBlock.data.addFloat32(face.color.r); vertexColorBlock.data.addFloat32(face.color.g); vertexColorBlock.data.addFloat32(face.color.b); } } else { // Couldn't read color! this.addIssue("warning", "missing_face_vertex_color", "Unable to read some face vertex colors."); vertexColorBlock.data.addFloat32(0); vertexColorBlock.data.addFloat32(0); vertexColorBlock.data.addFloat32(0); } } } // Create material block TODO: Optimize/cache blocks in case materials are shared between Meshes? var matBlock = this.builder.newBlock(); block.properties.material = matBlock.id; matBlock.properties.type = "data.material"; matBlock.properties.name = node.material.name || "Material"; matBlock.properties["diffuse.color"] = [ 0.5, 0.5, 0.5 ]; matBlock.properties.textures = []; this.updateStatus(matBlock); // Add material color if (node.material.color) matBlock.properties["diffuse.color"] = [ node.material.color.r, node.material.color.g, node.material.color.b ]; // Add material lighting if (node.material instanceof THREE.MeshBasicMaterial) matBlock.properties.lightingMode = "none"; else if (node.material instanceof THREE.MeshPhongMaterial) matBlock.properties.lightingMode = "phong"; else matBlock.properties.lightingMode = "lambert"; // Check for texture if (node.material.map) matBlock.properties.textures.push(this.createTextureBlock(node.material.map, "diffuse").id); } } else { // Unknown block type this.addIssue("warning", "unknown_object_" + node.type, "Unknown object type " + node.type + ", it was ignored."); } // Notify this.updateStatus(block); // Generate children for (var child of node.children) this.buildSceneNode(child, block); } /** @private Creates a texture block, or reuses the existing one if needed */ createTextureBlock(tex, type) { // Check if exists if (tex.v3dBlock) { // Make sure type is the same if (type != tex.v3dBlock.properties["texture.type"]) this.addIssue("warning", "texture_reuse_different_type", "A texture block was reused, but a different type was specified."); return tex.v3dBlock; } // Create block tex.v3dBlock = this.builder.newBlock(); tex.v3dBlock.properties.type = "data.texture"; tex.v3dBlock.properties.name = tex.name || tex.path || "Texture"; tex.v3dBlock.properties.mimetype = tex.v3dBlock.properties.name.indexOf(".png") == -1 ? "image/jpeg" : "image/png"; tex.v3dBlock.properties["texture.type"] = type; tex.v3dBlock.properties.wrapS = true//tex.wrapS != THREE.ClampToEdgeWrapping; tex.v3dBlock.properties.wrapT = true//tex.wrapT != THREE.ClampToEdgeWrapping; // Check if embedded data if (tex.arrayBuffer) { // Use existing data tex.v3dBlock.data.addArrayBuffer(tex.arrayBuffer); tex.v3dBlock.properties.mimetype = tex.mimetype || tex.v3dBlock.properties.mimetype; this.updateStatus("Added embedded texture : " + tex.v3dBlock.properties.name); } else if (tex.path) { // Find file var file = this.getFile(tex.path); if (!file) { this.addIssue("warning", "missing_file_" + tex.path, "Unable to load texture from " + tex.path); return tex.v3dBlock; } // Load file data into block this.waitingPromises.push(file.loadBytes().then(data => tex.v3dBlock.data.addArrayBuffer(data))); this.updateStatus("Added texture from file : " + file.path); } else { // This block is a failure this.addIssue("warning", "texture_no_data", "Unable to find data for texture " + (tex.name || tex.uuid || tex.FBX_ID)) } // Done return tex.v3dBlock; } }