diff --git a/.npmignore b/.npmignore index 202de55..26f16d5 100644 --- a/.npmignore +++ b/.npmignore @@ -5,6 +5,7 @@ /specs /test /output +.editorconfig .jshintrc .npmignore .travis.yml diff --git a/CHANGES.md b/CHANGES.md index 326fa8c..160e98d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,16 @@ Change Log ========== +### 1.1.1 2017-04-25 + +* Fixed `CHANGES.md` formatting. + +### 1.1.0 2017-04-25 + +* Added ability to convert the up-axis of the obj model. [#68](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/68) +* Fixed issues with an extra .bin file being saved when using `--separate`. [#62](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/62) +* Fixed issue where an ambient color of `[1, 1, 1]` overly brightens the converted model. [#70](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/70) + ### 1.0.0 2017-04-13 * Breaking changes diff --git a/README.md b/README.md index 8d201ef..aa209af 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ Using obj2gltf as a command-line tool: |`--bypassPipeline`|Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above.|No, default `false`| |`--checkTransparency`|Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. By default textures are considered to be opaque.|No, default `false`| |`--secure`|Prevent the converter from reading image or mtl files outside of the input obj directory.|No, default `false`| +|`--inputUpAxis`|Up axis of the obj. Choices are 'X', 'Y', and 'Z'.|No, default `Y`| +|`--outputUpAxis`|Up axis of the converted glTF. Choices are 'X', 'Y', and 'Z'.|No, default `Y`| |`--packOcclusion`|Pack the occlusion texture in the red channel of metallic-roughness texture.|No, default `false`| |`--inputMetallicRoughness`|The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots.|No, default `false`| |`--inputSpecularGlossiness`|The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the `KHR_materials_pbrSpecularGlossiness` extension.|No, default `false`| diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index a50d2b0..24b6311 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -91,6 +91,18 @@ var argv = yargs type: 'boolean', default: defaults.secure }, + inputUpAxis : { + describe: 'Up axis of the obj.', + choices: ['X', 'Y', 'Z'], + type: 'string', + default: 'Y' + }, + outputUpAxis : { + describe: 'Up axis of the converted glTF.', + choices: ['X', 'Y', 'Z'], + type: 'string', + default: 'Y' + }, packOcclusion : { describe: 'Pack the occlusion texture in the red channel of metallic-roughness texture.', type: 'boolean', @@ -129,7 +141,11 @@ var options = { bypassPipeline : argv.bypassPipeline, checkTransparency : argv.checkTransparency, secure : argv.secure, - packOcclusion : argv.packOcclusion + inputUpAxis : argv.inputUpAxis, + outputUpAxis : argv.outputUpAxis, + packOcclusion : argv.packOcclusion, + inputMetallicRoughness : argv.inputMetallicRoughness, + inputSpecularGlossiness : argv.inputSpecularGlossiness }; console.time('Total'); diff --git a/lib/loadObj.js b/lib/loadObj.js index 13c6a8c..8991289 100644 --- a/lib/loadObj.js +++ b/lib/loadObj.js @@ -8,9 +8,12 @@ var loadImage = require('./loadImage'); var loadMtl = require('./loadMtl'); var readLines = require('./readLines'); +var Axis = Cesium.Axis; +var Cartesian3 = Cesium.Cartesian3; var ComponentDatatype = Cesium.ComponentDatatype; var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; +var Matrix4 = Cesium.Matrix4; var RuntimeError = Cesium.RuntimeError; module.exports = loadObj; @@ -46,6 +49,8 @@ var facePattern2 = /f( +(-?\d+)\/(-?\d+)\/?)( +(-?\d+)\/(-?\d+)\/?)( +(-?\d+)\/( var facePattern3 = /f( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))?/; // f vertex/uv/normal vertex/uv/normal vertex/uv/normal ... var facePattern4 = /f( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))?/; // f vertex//normal vertex//normal vertex//normal ... +var scratchCartesian = new Cartesian3(); + /** * Parse an obj file. * @@ -53,6 +58,8 @@ var facePattern4 = /f( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/( * @param {Object} options An object with the following properties: * @param {Boolean} options.checkTransparency Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. * @param {Boolean} options.secure Prevent the converter from reading image or mtl files outside of the input obj directory. + * @param {String} options.inputUpAxis Up axis of the obj. + * @param {String} options.outputUpAxis Up axis of the converted glTF. * @param {Boolean} options.logger A callback function for handling logged messages. Defaults to console.log. * @returns {Promise} A promise resolving to the obj data. * @exception {RuntimeError} The file does not have any geometry information in it. @@ -60,6 +67,8 @@ var facePattern4 = /f( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/( * @private */ function loadObj(objPath, options) { + var axisTransform = getAxisTransform(options.inputUpAxis, options.outputUpAxis); + // Global store of vertex attributes listed in the obj file var positions = new ArrayStorage(ComponentDatatype.FLOAT); var normals = new ArrayStorage(ComponentDatatype.FLOAT); @@ -223,13 +232,27 @@ function loadObj(objPath, options) { var paths = line.substring(7).trim().split(' '); mtlPaths = mtlPaths.concat(paths); } else if ((result = vertexPattern.exec(line)) !== null) { - positions.push(parseFloat(result[1])); - positions.push(parseFloat(result[2])); - positions.push(parseFloat(result[3])); + var position = scratchCartesian; + position.x = parseFloat(result[1]); + position.y = parseFloat(result[2]); + position.z = parseFloat(result[3]); + if (defined(axisTransform)) { + Matrix4.multiplyByPoint(axisTransform, position, position); + } + positions.push(position.x); + positions.push(position.y); + positions.push(position.z); } else if ((result = normalPattern.exec(line) ) !== null) { - normals.push(parseFloat(result[1])); - normals.push(parseFloat(result[2])); - normals.push(parseFloat(result[3])); + var normal = scratchCartesian; + normal.x = parseFloat(result[1]); + normal.y = parseFloat(result[2]); + normal.z = parseFloat(result[3]); + if (defined(axisTransform)) { + Matrix4.multiplyByPointAsVector(axisTransform, normal, normal); + } + normals.push(normal.x); + normals.push(normal.y); + normals.push(normal.z); } else if ((result = uvPattern.exec(line)) !== null) { uvs.push(parseFloat(result[1])); uvs.push(1.0 - parseFloat(result[2])); // Flip y so 0.0 is the bottom of the image @@ -471,3 +494,19 @@ function cleanNodes(nodes) { setDefaults(nodes); return nodes; } + +function getAxisTransform(inputUpAxis, outputUpAxis) { + if (inputUpAxis === 'X' && outputUpAxis === 'Y') { + return Axis.X_UP_TO_Y_UP; + } else if (inputUpAxis === 'X' && outputUpAxis === 'Z') { + return Axis.X_UP_TO_Z_UP; + } else if (inputUpAxis === 'Y' && outputUpAxis === 'X') { + return Axis.Y_UP_TO_X_UP; + } else if (inputUpAxis === 'Y' && outputUpAxis === 'Z') { + return Axis.Y_UP_TO_Z_UP; + } else if (inputUpAxis === 'Z' && outputUpAxis === 'X') { + return Axis.Z_UP_TO_X_UP; + } else if (inputUpAxis === 'Z' && outputUpAxis === 'Y') { + return Axis.Z_UP_TO_Y_UP; + } +} diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index f998f1a..1ed7fe4 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -2,18 +2,20 @@ var Cesium = require('cesium'); var fsExtra = require('fs-extra'); var GltfPipeline = require('gltf-pipeline').Pipeline; +var os = require('os'); var path = require('path'); var Promise = require('bluebird'); +var uuid = require('uuid'); var createGltf = require('./createGltf'); var loadObj = require('./loadObj'); var writeUris = require('./writeUris'); var fsExtraOutputJson = Promise.promisify(fsExtra.outputJson); -var fsExtraRemove = Promise.promisify(fsExtra.remove); var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; var DeveloperError = Cesium.DeveloperError; +var RuntimeError = Cesium.RuntimeError; module.exports = obj2gltf; @@ -35,6 +37,8 @@ module.exports = obj2gltf; * @param {Boolean} [options.bypassPipeline=false] Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above. * @param {Boolean} [options.checkTransparency=false] Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. * @param {Boolean} [options.secure=false] Prevent the converter from reading image or mtl files outside of the input obj directory. + * @param {String} [options.inputUpAxis='Y'] Up axis of the obj. Choices are 'X', 'Y', and 'Z'. + * @param {String} [options.outputUpAxis='Y'] Up axis of the converted glTF. Choices are 'X', 'Y', and 'Z'. * @param {Boolean} [options.packOcclusion=false] Pack the occlusion texture in the red channel of metallic-roughness texture. * @param {Boolean} [options.inputMetallicRoughness=false] The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots. * @param {Boolean} [options.inputSpecularGlossiness=false] The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension. @@ -56,6 +60,8 @@ function obj2gltf(objPath, gltfPath, options) { var bypassPipeline = defaultValue(options.bypassPipeline, defaults.bypassPipeline); var checkTransparency = defaultValue(options.checkTransparency, defaults.checkTransparency); var secure = defaultValue(options.secure, defaults.secure); + var inputUpAxis = defaultValue(options.inputUpAxis, defaults.inputUpAxis); + var outputUpAxis = defaultValue(options.outputUpAxis, defaults.outputUpAxis); var packOcclusion = defaultValue(options.packOcclusion, defaults.packOcclusion); var inputMetallicRoughness = defaultValue(options.inputMetallicRoughness, defaults.inputMetallicRoughness); var inputSpecularGlossiness = defaultValue(options.inputSpecularGlossiness, defaults.inputSpecularGlossiness); @@ -65,6 +71,8 @@ function obj2gltf(objPath, gltfPath, options) { options.separateTextures = separateTextures; options.checkTransparency = checkTransparency; options.secure = secure; + options.inputUpAxis = inputUpAxis; + options.outputUpAxis = outputUpAxis; options.packOcclusion = packOcclusion; options.inputMetallicRoughness = inputMetallicRoughness; options.inputSpecularGlossiness = inputSpecularGlossiness; @@ -79,14 +87,13 @@ function obj2gltf(objPath, gltfPath, options) { } var extension = path.extname(gltfPath).toLowerCase(); - var basePath = path.dirname(gltfPath); var modelName = path.basename(gltfPath, path.extname(gltfPath)); if (extension === '.glb') { binary = true; } if (binary && bypassPipeline) { - throw new DeveloperError('--bypassPipeline does not convert to binary glTF'); + return Promise.reject(new RuntimeError('--bypassPipeline does not convert to binary glTF')); } if (inputMetallicRoughness && inputSpecularGlossiness) { @@ -94,12 +101,13 @@ function obj2gltf(objPath, gltfPath, options) { } gltfPath = path.join(path.dirname(gltfPath), modelName + extension); + var resourcesDirectory = options.bypassPipeline ? path.dirname(gltfPath) : obj2gltf._getTempDirectory(); var aoOptions = ao ? {} : undefined; var pipelineOptions = { createDirectory : false, - basePath : basePath, + basePath : resourcesDirectory, binary : binary, embed : !separate, embedImage : !separateTextures, @@ -118,7 +126,7 @@ function obj2gltf(objPath, gltfPath, options) { return createGltf(objData, options); }) .then(function(gltf) { - return writeUris(gltf, gltfPath, options); + return writeUris(gltf, gltfPath, resourcesDirectory, options); }) .then(function(gltf) { if (bypassPipeline) { @@ -127,18 +135,17 @@ function obj2gltf(objPath, gltfPath, options) { return GltfPipeline.processJSONToDisk(gltf, gltfPath, pipelineOptions); } }) - .then(function() { - return cleanup(gltfPath, options); + .finally(function() { + return cleanup(resourcesDirectory, options); }); } -function cleanup(gltfPath, options) { - // gltf-pipeline also saves out a buffer so remove the one generated by obj2gltf +function cleanup(resourcesDirectory, options) { if (!options.bypassPipeline && options.separate) { - var bufferName = path.basename(gltfPath, path.extname(gltfPath)); - var bufferUri = bufferName + '.bin'; - var bufferPath = path.join(path.dirname(gltfPath), bufferUri); - return fsExtraRemove(bufferPath); + fsExtra.remove(resourcesDirectory, function () { + // Don't fail simply because we couldn't + // clean up the temporary files. + }); } } @@ -213,6 +220,18 @@ obj2gltf.defaults = { * @default false */ secure: false, + /** + * Gets or sets the up axis of the obj. + * @type String + * @default 'Y' + */ + inputUpAxis: 'Y', + /** + * Gets or sets the up axis of the converted glTF. + * @type String + * @default 'Y' + */ + outputUpAxis: 'Y', /** * Gets or sets whether to pack the occlusion texture in the red channel of the metallic-roughness texture. * @type Boolean @@ -247,6 +266,15 @@ obj2gltf.defaults = { */ obj2gltf._outputJson = fsExtraOutputJson; +/** + * Exposed for testing + * + * @private + */ +obj2gltf._getTempDirectory = function () { + return path.join(os.tmpdir(), uuid()); +}; + /** * A callback function that logs messages. * @callback Logger diff --git a/lib/writeUris.js b/lib/writeUris.js index 8bf4955..43dee14 100644 --- a/lib/writeUris.js +++ b/lib/writeUris.js @@ -16,6 +16,7 @@ module.exports = writeUris; * * @param {Object} gltf The glTF asset. * @param {String} gltfPath Path where the glTF will be saved. + * @param {String} resourcesDirectory Path where separate resources will be saved. * @param {Object} options An object with the following properties: * @param {Boolean} options.separate Writes out separate buffers. * @param {Boolean} options.separateTextures Write out separate textures only. @@ -23,7 +24,7 @@ module.exports = writeUris; * * @private */ -function writeUris(gltf, gltfPath, options) { +function writeUris(gltf, gltfPath, resourcesDirectory, options) { var separate = options.separate; var separateTextures = options.separateTextures; @@ -43,17 +44,19 @@ function writeUris(gltf, gltfPath, options) { var exceedsMaximum = (texturesByteLength + bufferByteLength > 201326580); if (exceedsMaximum && !separate) { - return Promise.reject(new RuntimeError('Buffers and textures are too large to encode in the glTF, saving as separate resources.')); + return Promise.reject(new RuntimeError('Buffers and textures are too large to encode in the glTF. Use the --separate flag instead.')); } + var name = path.basename(gltfPath, path.extname(gltfPath)); + if (separate) { - promises.push(writeSeparateBuffer(gltf, gltfPath)); + promises.push(writeSeparateBuffer(gltf, resourcesDirectory, name)); } else { writeEmbeddedBuffer(gltf); } if (separateTextures) { - promises.push(writeSeparateTextures(gltf, gltfPath)); + promises.push(writeSeparateTextures(gltf, resourcesDirectory)); } else { writeEmbeddedTextures(gltf); } @@ -95,23 +98,22 @@ function cleanup(gltf) { } } -function writeSeparateBuffer(gltf, gltfPath) { +function writeSeparateBuffer(gltf, resourcesDirectory, name) { var buffer = gltf.buffers[0]; var source = buffer.extras._obj2gltf.source; - var bufferName = path.basename(gltfPath, path.extname(gltfPath)); - var bufferUri = bufferName + '.bin'; + var bufferUri = name + '.bin'; buffer.uri = bufferUri; - var bufferPath = path.join(path.dirname(gltfPath), bufferUri); + var bufferPath = path.join(resourcesDirectory, bufferUri); return writeUris._outputFile(bufferPath, source); } -function writeSeparateTextures(gltf, gltfPath) { +function writeSeparateTextures(gltf, resourcesDirectory) { var images = gltf.images; return Promise.map(images, function(image) { var extras = image.extras._obj2gltf; var imageUri = image.name + extras.extension; image.uri = imageUri; - var imagePath = path.join(path.dirname(gltfPath), imageUri); + var imagePath = path.join(resourcesDirectory, imageUri); return writeUris._outputFile(imagePath, extras.source); }, {concurrency : 10}); } diff --git a/package.json b/package.json index 56f87bf..903d62a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obj2gltf", - "version": "1.0.0", + "version": "1.1.1", "description": "Convert OBJ model format to glTF", "license": "Apache-2.0", "contributors": [ @@ -34,6 +34,7 @@ "jpeg-js": "^0.2.0", "mime": "^1.3.4", "pngjs": "^3.2.0", + "uuid": "^3.0.1", "yargs": "^7.0.1" }, "devDependencies": { diff --git a/specs/data/box-rotated/box-rotated.mtl b/specs/data/box-rotated/box-rotated.mtl new file mode 100644 index 0000000..616ae6b --- /dev/null +++ b/specs/data/box-rotated/box-rotated.mtl @@ -0,0 +1,10 @@ +# Blender MTL File: 'axis.blend' +# Material Count: 1 + +newmtl None +Ns 0 +Ka 0.000000 0.000000 0.000000 +Kd 0.8 0.8 0.8 +Ks 0.8 0.8 0.8 +d 1 +illum 2 diff --git a/specs/data/box-rotated/box-rotated.obj b/specs/data/box-rotated/box-rotated.obj new file mode 100644 index 0000000..89d8c29 --- /dev/null +++ b/specs/data/box-rotated/box-rotated.obj @@ -0,0 +1,112 @@ +# Blender v2.78 (sub 0) OBJ File: 'axis.blend' +# www.blender.org +mtllib box-rotated.mtl +o Cube.002_Cube +v -1.707107 -0.292893 0.000000 +v -0.707107 0.707107 1.414214 +v -0.707107 0.707107 -1.414214 +v 0.292893 1.707107 0.000000 +v -0.292893 -1.707107 0.000000 +v 0.707107 -0.707107 1.414214 +v 0.707107 -0.707107 -1.414214 +v 1.707107 0.292893 0.000000 +vn -0.7071 0.7071 0.0000 +vn 0.5000 0.5000 -0.7071 +vn 0.7071 -0.7071 -0.0000 +vn -0.5000 -0.5000 0.7071 +vn -0.5000 -0.5000 -0.7071 +vn 0.5000 0.5000 0.7071 +usemtl None +s off +f 1//1 2//1 4//1 3//1 +f 3//2 4//2 8//2 7//2 +f 7//3 8//3 6//3 5//3 +f 5//4 6//4 2//4 1//4 +f 3//5 7//5 5//5 1//5 +f 8//6 4//6 2//6 6//6 +o Cube.001_Cube.002 +v -3.997752 -1.000000 3.997752 +v -3.997752 1.000000 3.997752 +v -3.997752 -1.000000 -3.997752 +v -3.997752 1.000000 -3.997752 +v 3.997752 -1.000000 3.997752 +v 3.997752 1.000000 3.997752 +v 3.997752 -1.000000 -3.997752 +v 3.997752 1.000000 -3.997752 +vn -1.0000 0.0000 0.0000 +vn 0.0000 0.0000 -1.0000 +vn 1.0000 0.0000 0.0000 +vn 0.0000 0.0000 1.0000 +vn 0.0000 -1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +usemtl None +s off +f 9//7 10//7 12//7 11//7 +f 11//8 12//8 16//8 15//8 +f 15//9 16//9 14//9 13//9 +f 13//10 14//10 10//10 9//10 +f 11//11 15//11 13//11 9//11 +f 16//12 12//12 10//12 14//12 +o Cube_Cube.001 +v -1.000000 4.235625 1.000000 +v -1.000000 6.235625 1.000000 +v -1.000000 4.235625 -1.000000 +v -1.000000 6.235625 -1.000000 +v 1.000000 4.235625 1.000000 +v 1.000000 6.235625 1.000000 +v 1.000000 4.235625 -1.000000 +v 1.000000 6.235625 -1.000000 +v 0.000000 9.029284 0.000000 +v 1.000000 4.663946 0.571679 +v 1.000000 5.807305 0.571679 +v 1.000000 4.663946 -0.571679 +v 1.000000 5.807305 -0.571679 +v 2.375958 4.663946 0.571679 +v 2.375958 5.807305 0.571679 +v 2.375958 4.663946 -0.571679 +v 2.375958 5.807305 -0.571679 +v -0.310735 4.924891 -1.000000 +v -0.310735 5.546360 -1.000000 +v 0.310735 4.924891 -1.000000 +v 0.310735 5.546360 -1.000000 +v 0.000000 5.235625 -1.538583 +vn -1.0000 0.0000 0.0000 +vn 0.0000 0.0000 -1.0000 +vn 1.0000 0.0000 0.0000 +vn 0.0000 0.0000 1.0000 +vn 0.0000 -1.0000 0.0000 +vn 0.0000 0.3370 -0.9415 +vn -0.9415 0.3370 0.0000 +vn 0.0000 0.3370 0.9415 +vn 0.9415 0.3370 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.8662 0.0000 -0.4997 +vn 0.0000 -0.8662 -0.4997 +vn 0.0000 0.8662 -0.4997 +vn -0.8662 0.0000 -0.4997 +usemtl None +s off +f 17//13 18//13 20//13 19//13 +f 23//14 19//14 34//14 36//14 +f 22//15 21//15 26//15 27//15 +f 21//16 22//16 18//16 17//16 +f 19//17 23//17 21//17 17//17 +f 24//18 20//18 25//18 +f 20//19 18//19 25//19 +f 18//20 22//20 25//20 +f 22//21 24//21 25//21 +f 29//22 27//22 31//22 33//22 +f 21//15 23//15 28//15 26//15 +f 24//15 22//15 27//15 29//15 +f 23//15 24//15 29//15 28//15 +f 32//15 33//15 31//15 30//15 +f 28//14 29//14 33//14 32//14 +f 27//16 26//16 30//16 31//16 +f 26//17 28//17 32//17 30//17 +f 37//23 36//23 38//23 +f 20//14 24//14 37//14 35//14 +f 19//14 20//14 35//14 34//14 +f 24//14 23//14 36//14 37//14 +f 36//24 34//24 38//24 +f 35//25 37//25 38//25 +f 34//26 35//26 38//26 diff --git a/specs/lib/createGltfSpec.js b/specs/lib/createGltfSpec.js index c75084c..22c8c77 100644 --- a/specs/lib/createGltfSpec.js +++ b/specs/lib/createGltfSpec.js @@ -70,7 +70,7 @@ describe('createGltf', function() { it('simple gltf', function(done) { var gltf = createGltf(boxObjData, defaultOptions); - expect(writeUris(gltf, boxGltfUrl, defaultOptions) + expect(writeUris(gltf, boxGltfUrl, path.dirname(boxGltfUrl), defaultOptions) .then(function() { expect(gltf).toEqual(boxGltf); }), done).toResolve(); @@ -79,7 +79,7 @@ describe('createGltf', function() { it('multiple nodes, meshes, and primitives', function(done) { var gltf = createGltf(groupObjData, defaultOptions); - expect(writeUris(gltf, groupGltfUrl, defaultOptions) + expect(writeUris(gltf, groupGltfUrl, path.dirname(groupGltfUrl), defaultOptions) .then(function() { expect(gltf).toEqual(groupGltf); expect(Object.keys(gltf.materials).length).toBe(3); @@ -359,4 +359,13 @@ describe('createGltf', function() { var positionAccessor = gltf.accessors[primitive.attributes.POSITION]; expect(positionAccessor.count).toBe(vertexCount); }); + + it('ambient of [1, 1, 1] is treated as [0, 0, 0]', function() { + boxObjData.materials.Material.ambientColor = [1.0, 1.0, 1.0, 1.0]; + + var gltf = createGltf(boxObjData); + var ambient = gltf.materials.Material.extensions.KHR_materials_common.values.ambient; + + expect(ambient).toEqual([0.0, 0.0, 0.0, 1.0]); + }); }); diff --git a/specs/lib/loadObjSpec.js b/specs/lib/loadObjSpec.js index 3375e64..ca7d2a7 100644 --- a/specs/lib/loadObjSpec.js +++ b/specs/lib/loadObjSpec.js @@ -5,10 +5,12 @@ var Promise = require('bluebird'); var loadObj = require('../../lib/loadObj'); var obj2gltf = require('../../lib/obj2gltf'); +var Cartesian3 = Cesium.Cartesian3; var clone = Cesium.clone; var RuntimeError = Cesium.RuntimeError; var objUrl = 'specs/data/box/box.obj'; +var objRotatedUrl = 'specs/data/box-rotated/box-rotated.obj'; var objNormalsUrl = 'specs/data/box-normals/box-normals.obj'; var objUvsUrl = 'specs/data/box-uvs/box-uvs.obj'; var objPositionsOnlyUrl = 'specs/data/box-positions-only/box-positions-only.obj'; @@ -62,6 +64,10 @@ function getImagePath(objPath, relativePath) { var defaultOptions = obj2gltf.defaults; describe('loadObj', function() { + beforeEach(function() { + spyOn(console, 'log'); + }); + it('loads obj with positions, normals, and uvs', function(done) { expect(loadObj(objUrl, defaultOptions) .then(function(data) { @@ -270,7 +276,6 @@ describe('loadObj', function() { }); it('loads obj with missing mtllib', function(done) { - spyOn(console, 'log'); expect(loadObj(objMissingMtllibUrl, defaultOptions) .then(function(data) { expect(data.materials).toEqual({}); @@ -288,8 +293,6 @@ describe('loadObj', function() { }); it('does not load resources outside of the obj directory when secure is true', function(done) { - spyOn(console, 'log'); - var options = clone(defaultOptions); options.secure = true; @@ -314,7 +317,6 @@ describe('loadObj', function() { }); it('loads obj with missing texture', function(done) { - spyOn(console, 'log'); expect(loadObj(objMissingTextureUrl, defaultOptions) .then(function(data) { var imagePath = getImagePath(objMissingTextureUrl, 'cesium.png'); @@ -341,7 +343,54 @@ describe('loadObj', function() { }), done).toResolve(); }); - it('does not process file with invalid contents', function(done) { + function getFirstPosition(data) { + var positions = data.nodes[0].meshes[0].positions; + return new Cartesian3(positions.get(0), positions.get(1), positions.get(2)); + } + + function getFirstNormal(data) { + var normals = data.nodes[0].meshes[0].normals; + return new Cartesian3(normals.get(0), normals.get(1), normals.get(2)); + } + + function checkAxisConversion(inputUpAxis, outputUpAxis, position, normal) { + var sameAxis = (inputUpAxis === outputUpAxis); + var options = clone(defaultOptions); + options.inputUpAxis = inputUpAxis; + options.outputUpAxis = outputUpAxis; + return loadObj(objRotatedUrl, options) + .then(function(data) { + var rotatedPosition = getFirstPosition(data); + var rotatedNormal = getFirstNormal(data); + if (sameAxis) { + expect(rotatedPosition).toEqual(position); + expect(rotatedNormal).toEqual(normal); + } else { + expect(rotatedPosition).not.toEqual(position); + expect(rotatedNormal).not.toEqual(normal); + } + }); + } + + it('performs up axis conversion', function(done) { + expect(loadObj(objRotatedUrl, defaultOptions) + .then(function(data) { + var position = getFirstPosition(data); + var normal = getFirstNormal(data); + + var axes = ['X', 'Y', 'Z']; + var axesLength = axes.length; + var promises = []; + for (var i = 0; i < axesLength; ++i) { + for (var j = 0; j < axesLength; ++j) { + promises.push(checkAxisConversion(axes[i], axes[j], position, normal)); + } + } + return Promise.all(promises); + }), done).toResolve(); + }); + + it('throws when file has invalid contents', function(done) { expect(loadObj(objInvalidContentsUrl, defaultOptions), done).toRejectWith(RuntimeError); }); diff --git a/specs/lib/obj2gltfSpec.js b/specs/lib/obj2gltfSpec.js index e85d1b7..22a9bdd 100644 --- a/specs/lib/obj2gltfSpec.js +++ b/specs/lib/obj2gltfSpec.js @@ -1,6 +1,9 @@ 'use strict'; +var fsExtra = require('fs-extra'); var GltfPipeline = require('gltf-pipeline').Pipeline; +var os = require('os'); var path = require('path'); +var Promise = require('bluebird'); var obj2gltf = require('../../lib/obj2gltf'); var writeUris = require('../../lib/writeUris'); @@ -10,28 +13,34 @@ var glbPath = 'specs/data/box-textured/box-textured.glb'; var objPathNonExistent = 'specs/data/non-existent.obj'; describe('obj2gltf', function() { + var tempDirectory; + + beforeAll(function() { + expect(obj2gltf._getTempDirectory()).toContain(os.tmpdir()); + tempDirectory = path.join(os.tmpdir(), 'testPath'); + spyOn(obj2gltf, '_getTempDirectory').and.returnValue(tempDirectory); + spyOn(obj2gltf, '_outputJson'); + spyOn(writeUris, '_outputFile'); + spyOn(fsExtra, 'remove'); + }); + + beforeEach(function() { + spyOn(GltfPipeline, 'processJSONToDisk').and.returnValue(Promise.resolve()); + }); + it('converts an obj to gltf', function(done) { - var spy = spyOn(GltfPipeline, 'processJSONToDisk'); expect(obj2gltf(objPath, gltfPath) .then(function() { - var args = spy.calls.first().args; + var args = GltfPipeline.processJSONToDisk.calls.first().args; var gltf = args[0]; var outputPath = args[1]; + var options = args[2]; expect(path.normalize(outputPath)).toEqual(path.normalize(gltfPath)); expect(gltf).toBeDefined(); expect(gltf.images.cesium).toBeDefined(); - }), done).toResolve(); - }); - - it('uses default gltf-pipeline options', function(done) { - var spy = spyOn(GltfPipeline, 'processJSONToDisk'); - expect(obj2gltf(objPath, gltfPath) - .then(function() { - var args = spy.calls.first().args; - var options = args[2]; expect(options).toEqual({ + basePath : tempDirectory, createDirectory : false, - basePath : path.dirname(objPath), binary : false, embed : true, embedImage : true, @@ -48,8 +57,6 @@ describe('obj2gltf', function() { }); it('sets options', function(done) { - var spy = spyOn(GltfPipeline, 'processJSONToDisk'); - spyOn(writeUris, '_outputFile'); var textureCompressionOptions = { format : 'dxt1', quality : 10 @@ -66,16 +73,18 @@ describe('obj2gltf', function() { textureCompressionOptions : textureCompressionOptions, checkTransparency : true, secure : true, + inputUpAxis : 'Z', + outputUpAxis : 'X', logger : obj2gltf.defaults.logger }; expect(obj2gltf(objPath, gltfPath, options) .then(function() { - var args = spy.calls.first().args; + var args = GltfPipeline.processJSONToDisk.calls.first().args; var options = args[2]; expect(options).toEqual({ + basePath : tempDirectory, createDirectory : false, - basePath : path.dirname(objPath), binary : true, embed : false, embedImage : false, @@ -93,18 +102,15 @@ describe('obj2gltf', function() { }); it('saves as binary if gltfPath has a .glb extension', function(done) { - var spy = spyOn(GltfPipeline, 'processJSONToDisk'); expect(obj2gltf(objPath, glbPath) .then(function() { - var args = spy.calls.first().args; + var args = GltfPipeline.processJSONToDisk.calls.first().args; var options = args[2]; expect(options.binary).toBe(true); }), done).toResolve(); }); it('bypassPipeline flag bypasses gltf-pipeline', function(done) { - spyOn(obj2gltf, '_outputJson'); - spyOn(GltfPipeline, 'processJSONToDisk'); var options = { bypassPipeline : true };