@blockv/threejs-to-v3d
Version:
Converts any format supported by ThreeJS to V3D.
554 lines (405 loc) • 20.3 kB
JavaScript
//
// 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;
}
}