From cc8fee19c407e4b5a0fd43f1930e6c55a65b5c89 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 10 Apr 2017 17:57:56 -0400 Subject: [PATCH] Many updates --- README.md | 31 ++--- bin/obj2gltf.js | 65 +++++----- index.js | 4 +- lib/Material.js | 19 +++ lib/clone.js | 54 -------- lib/convert.js | 257 +++++++++++++++++++++++++-------------- lib/gltf.js | 19 ++- lib/image.js | 47 +++---- lib/mtl.js | 41 ++----- lib/obj.js | 109 ++++++----------- lib/writeUris.js | 44 +++---- specs/lib/convertSpec.js | 73 ++++------- specs/lib/gltfSpec.js | 183 ++++++++++++++-------------- specs/lib/imageSpec.js | 53 ++++---- specs/lib/mtlSpec.js | 13 +- specs/lib/objSpec.js | 67 +++++----- 16 files changed, 515 insertions(+), 564 deletions(-) create mode 100644 lib/Material.js delete mode 100644 lib/clone.js diff --git a/README.md b/README.md index eae15e4..0773c26 100644 --- a/README.md +++ b/README.md @@ -11,45 +11,40 @@ npm install --save obj2gltf Using obj2gltf as a library: ```javascript var obj2gltf = require('obj2gltf'); -var convert = obj2gltf.convert; var options = { separateTextures : true // Don't embed textures in the converted glTF } -convert('model.obj', 'model.gltf', options) +obj2gltf('model.obj', 'model.gltf', options) .then(function() { console.log('Converted model'); }); ``` Using obj2gltf as a command-line tool: -`node bin/obj2gltf.js model.obj` - -`node bin/obj2gltf.js model.obj model.gltf` +`node bin/obj2gltf.js -i model.obj` `node bin/obj2gltf.js -i model.obj -o model.gltf` -`node bin/obj2gltf.js -i model.obj -o model.gltf -s` - ## Usage ###Command line flags: |Flag|Description|Required| |----|-----------|--------| -|`-h`|Display help.|No| -|`-i`|Path to the obj file.| :white_check_mark: Yes| -|`-o`|Path of the converted glTF file.|No| -|`-b`|Save as binary glTF.|No, default `false`| -|`-s`|Writes out separate geometry data files, shader files, and textures instead of embedding them in the glTF file.|No, default `false`| -|`-t`|Write out separate textures only.|No, default `false`| -|`-c`|Quantize positions, compress texture coordinates, and oct-encode normals.|No, default `false`| -|`-z`|Use the optimization stages in the glTF pipeline.|No, default `false`| -|`-n`|Generate normals if they are missing.|No, default `false`| -|`--cesium`|Optimize the glTF for Cesium by using the sun as a default light source.|No, default `false`| +|`-h`, `--help`|Display help.|No| +|`-i`, `--input`|Path to the obj file.| :white_check_mark: Yes| +|`-o`, `--output`|Path of the converted glTF file.|No| +|`-b`, `--binary`|Save as binary glTF.|No, default `false`| +|`-s`, `--separate`|Writes out separate geometry data files, shader files, and textures instead of embedding them in the glTF file.|No, default `false`| +|`-t`, `--separateTextures`|Write out separate textures only.|No, default `false`| +|`-c`, `--compress`|Quantize positions, compress texture coordinates, and oct-encode normals.|No, default `false`| +|`-z`, `--optimize`|Use the optimization stages in the glTF pipeline.|No, default `false`| +|`-n`, `--generateNormals`|Generate normals if they are missing.|No, default `false`| +|`--optimizeForCesium`|Optimize the glTF for Cesium by using the sun as a default light source.|No, default `false`| |`--ao`|Apply ambient occlusion to the converted model.|No, default `false`| |`--kmc|Output glTF with the KHR_materials_common extension.|No, default `false`| |`--bypassPipeline`|Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above and will save the glTF with the KHR_materials_common extension.|No, default `false`| -|`--hasTransparency`|Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. By default textures with an alpha channel are considered to be transparent.|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`| ## Build Instructions diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index a10ff30..52e4c0e 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -5,11 +5,11 @@ var path = require('path'); var yargs = require('yargs'); var convert = require('../lib/convert'); -var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; +var defaults = convert.defaults; + var args = process.argv; -args = args.slice(2, args.length); var argv = yargs .usage('Usage: node $0 -i inputPath -o outputPath') @@ -21,7 +21,8 @@ var argv = yargs alias: 'i', describe: 'Path to the obj file.', type: 'string', - normalize: true + normalize: true, + demandOption: true }, output : { alias: 'o', @@ -33,77 +34,72 @@ var argv = yargs alias: 'b', describe: 'Save as binary glTF.', type: 'boolean', - default: false + default: defaults.binary }, separate : { alias: 's', describe: 'Write separate geometry data files, shader files, and textures instead of embedding them in the glTF.', type: 'boolean', - default: false + default: defaults.separate }, separateTextures : { alias: 't', describe: 'Write out separate textures only.', type: 'boolean', - default: false + default: defaults.separateTextures }, compress : { alias: 'c', describe: 'Quantize positions, compress texture coordinates, and oct-encode normals.', type: 'boolean', - default: false + default: defaults.compress }, optimize : { alias: 'z', - describe: 'Use the optimization stages in the glTF pipeline.', + describe: 'Optimize the glTF for size and runtime performance.', type: 'boolean', - default: false + default: defaults.optimize }, - cesium : { + optimizeForCesium : { describe: 'Optimize the glTF for Cesium by using the sun as a default light source.', type: 'boolean', - default: false + default: defaults.optimizeForCesium }, generateNormals : { alias: 'n', describe: 'Generate normals if they are missing.', type: 'boolean', - default: false + default: defaults.generateNormals }, ao : { describe: 'Apply ambient occlusion to the converted model.', type: 'boolean', - default: false + default: defaults.ao }, kmc : { describe: 'Output glTF with the KHR_materials_common extension.', type: 'boolean', - default: false + default: defaults.kmc }, bypassPipeline : { describe: 'Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above and will save the glTF with the KHR_materials_common extension.', type: 'boolean', - default: false + default: defaults.bypassPipeline }, - hasTransparency : { - describe: 'Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. By default textures with an alpha channel are considered to be transparent.', + checkTransparency : { + describe: '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.', type: 'boolean', - default: false + default: defaults.checkTransparency }, secure : { describe: 'Prevent the converter from reading image or mtl files outside of the input obj directory.', type: 'boolean', - default: false + default: defaults.secure } }).parse(args); -var objPath = defaultValue(argv.i, argv._[0]); -var gltfPath = defaultValue(argv.o, argv._[1]); - -if (!defined(objPath)) { - yargs.showHelp(); - return; -} +var objPath = argv.i; +var gltfPath = argv.o; if (!defined(gltfPath)) { var extension = argv.b ? '.glb' : '.gltf'; @@ -112,16 +108,17 @@ if (!defined(gltfPath)) { } var options = { - binary : argv.b, - separate : argv.s, - separateTextures : argv.t, - compress : argv.c, - optimize : argv.z, - generateNormals : argv.n, + binary : argv.binary, + separate : argv.separate, + separateTextures : argv.separateTextures, + compress : argv.compress, + optimize : argv.optimize, + optimizeForCesium : argv.optimizeForCesium, + generateNormals : argv.generateNormals, ao : argv.ao, - optimizeForCesium : argv.cesium, + kmc : argv.kmc, bypassPipeline : argv.bypassPipeline, - hasTransparency : argv.hasTransparency, + checkTransparency : argv.checkTransparency, secure : argv.secure }; diff --git a/index.js b/index.js index 326e68e..02ae37e 100644 --- a/index.js +++ b/index.js @@ -1,3 +1 @@ -module.exports = { - convert : require('./lib/convert') -}; +module.exports = require('./lib/convert'); diff --git a/lib/Material.js b/lib/Material.js new file mode 100644 index 0000000..60d284d --- /dev/null +++ b/lib/Material.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = Material; + +function Material() { + this.ambientColor = [0.0, 0.0, 0.0, 1.0]; // Ka + this.emissionColor = [0.0, 0.0, 0.0, 1.0]; // Ke + this.diffuseColor = [0.5, 0.5, 0.5, 1.0]; // Kd + this.specularColor = [0.0, 0.0, 0.0, 1.0]; // Ks + this.specularShininess = 0.0; // Ns + this.alpha = 1.0; // d / Tr + this.ambientTexture = undefined; // map_Ka + this.emissionTexture = undefined; // map_Ke + this.diffuseTexture = undefined; // map_Kd + this.specularTexture = undefined; // map_Ks + this.specularShininessMap = undefined; // map_Ns + this.normalMap = undefined; // map_Bump + this.alphaMap = undefined; // map_d +} diff --git a/lib/clone.js b/lib/clone.js deleted file mode 100644 index d93e059..0000000 --- a/lib/clone.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; -var Cesium = require('cesium'); -var ArrayStorage = require('./ArrayStorage'); - -var defaultValue = Cesium.defaultValue; - -module.exports = clone; - -/** - * Clones an object, returning a new object containing the same properties. - * Modified from Cesium.clone to support typed arrays, buffers, and the ArrayStorage class. - * - * @param {Object} object The object to clone. - * @param {Boolean} [deep=false] If true, all properties will be deep cloned recursively. - * @returns {Object} The cloned object. - * - * @private - */ -function clone(object, deep) { - if (object === null || typeof object !== 'object') { - return object; - } - - deep = defaultValue(deep, false); - - var isBuffer = Buffer.isBuffer(object); - var isTypedArray = Object.prototype.toString.call(object.buffer) === '[object ArrayBuffer]'; - var isArrayStorage = object instanceof ArrayStorage; - - var result; - if (isBuffer) { - result = Buffer.from(object); - return result; - } else if (isTypedArray) { - result = object.slice(); - return result; - } else if (isArrayStorage) { - result = new ArrayStorage(object.componentDatatype); - } else { - result = new object.constructor(); - } - - for (var propertyName in object) { - if (object.hasOwnProperty(propertyName)) { - var value = object[propertyName]; - if (deep) { - value = clone(value, deep); - } - result[propertyName] = value; - } - } - - return result; -} diff --git a/lib/convert.js b/lib/convert.js index 9303699..5e26300 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -16,16 +16,6 @@ var DeveloperError = Cesium.DeveloperError; module.exports = convert; -/** - * A callback function that logs messages. - * @callback Logger - * - * @param {String} message The message to log. - */ -var defaultLogger = function(message) { - console.log(message); -}; - /** * Converts an obj file to a glTF file. * @@ -36,107 +26,196 @@ var defaultLogger = function(message) { * @param {Boolean} [options.separate=false] Writes out separate geometry data files, shader files, and textures instead of embedding them in the glTF. * @param {Boolean} [options.separateTextures=false] Write out separate textures only. * @param {Boolean} [options.compress=false] Quantize positions, compress texture coordinates, and oct-encode normals. - * @param {Boolean} [options.optimize=false] Use the optimization stages in the glTF pipeline. + * @param {Boolean} [options.optimize=false] Optimize the glTF for size and runtime performance. * @param {Boolean} [options.optimizeForCesium=false] Optimize the glTF for Cesium by using the sun as a default light source. * @param {Boolean} [options.generateNormals=false] Generate normals if they are missing. * @param {Boolean} [options.ao=false] Apply ambient occlusion to the converted model. * @param {Boolean} [options.kmc=false] Output glTF with the KHR_materials_common extension. * @param {Boolean} [options.textureCompressionOptions] Options sent to the compressTextures stage of gltf-pipeline. * @param {Boolean} [options.bypassPipeline=false] Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above and will save the glTF with the KHR_materials_common extension. - * @param {Boolean} [options.hasTransparency=false] Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. + * @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 {Logger} [options.logger] A callback function for handling logged messages. Defaults to console.log. */ function convert(objPath, gltfPath, options) { - return new Promise(function(resolve, reject) { - options = defaultValue(options, {}); - var binary = defaultValue(options.binary, false); - var separate = defaultValue(options.separate, false); - var separateTextures = defaultValue(options.separateTextures, false) || separate; - var compress = defaultValue(options.compress, false); - var optimize = defaultValue(options.optimize, false); - var optimizeForCesium = defaultValue(options.optimizeForCesium, false); - var generateNormals = defaultValue(options.generateNormals, false); - var ao = defaultValue(options.ao, false); - var kmc = defaultValue(options.kmc, false); - var textureCompressionOptions = options.textureCompressionOptions; - var bypassPipeline = defaultValue(options.bypassPipeline, false); - var logger = defaultValue(options.logger, defaultLogger); - options.logger = logger; - options.hasTransparency = defaultValue(options.hasTransparency, false); - options.secure = defaultValue(options.secure, false); + var defaults = convert.defaults; - if (!defined(objPath)) { - throw new DeveloperError('objPath is required'); - } + options = defaultValue(options, {}); + var binary = defaultValue(options.binary, defaults.binary); + var separate = defaultValue(options.separate, defaults.separate); + var separateTextures = defaultValue(options.separateTextures, defaults.separateTextures) || separate; + var compress = defaultValue(options.compress, defaults.compress); + var optimize = defaultValue(options.optimize, defaults.optimize); + var optimizeForCesium = defaultValue(options.optimizeForCesium, defaults.optimizeForCesium); + var generateNormals = defaultValue(options.generateNormals, defaults.generateNormals); + var ao = defaultValue(options.ao, defaults.ao); + var kmc = defaultValue(options.kmc, defaults.kmc); + var textureCompressionOptions = options.textureCompressionOptions; + var bypassPipeline = defaultValue(options.bypassPipeline, defaults.bypassPipeline); + var checkTransparency = defaultValue(options.checkTransparency, defaults.checkTransparency); + var secure = defaultValue(options.secure, defaults.secure); + var logger = defaultValue(options.logger, defaults.logger); - if (!defined(gltfPath)) { - throw new DeveloperError('gltfPath is required'); - } + options.separate = separate; + options.separateTextures = separateTextures; + options.checkTransparency = checkTransparency; + options.secure = secure; + options.logger = logger; - var objExtension = path.extname(objPath).toLowerCase(); - if (objExtension !== '.obj') { - throw new DeveloperError('Invalid obj path "' + objPath + '"'); - } - var extension = path.extname(gltfPath).toLowerCase(); - if (extension !== '.gltf' && extension !== '.glb') { - throw new DeveloperError('Invalid gltf path "' + gltfPath + '"'); - } + if (!defined(objPath)) { + throw new DeveloperError('objPath is required'); + } - var basePath = path.dirname(gltfPath); - var modelName = path.basename(gltfPath, path.extname(gltfPath)); - if (extension === '.glb') { - binary = true; + if (!defined(gltfPath)) { + throw new DeveloperError('gltfPath is required'); + } + + 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'); + } + + gltfPath = path.join(path.dirname(gltfPath), modelName + extension); + + var aoOptions = ao ? {} : undefined; + var kmcOptions = kmc ? {} : undefined; + + var pipelineOptions = { + createDirectory : false, + basePath : basePath, + binary : binary, + embed : !separate, + embedImage : !separateTextures, + quantize : compress, + compressTextureCoordinates : compress, + encodeNormals : compress, + preserve : !optimize, + optimizeForCesium : optimizeForCesium, + smoothNormals : generateNormals, + aoOptions : aoOptions, + kmcOptions : kmcOptions, + textureCompressionOptions : textureCompressionOptions + }; + + return loadObj(objPath, options) + .then(function(objData) { + return createGltf(objData); + }) + .then(function(gltf) { + return writeUris(gltf, gltfPath, options); + }) + .then(function(gltf) { if (bypassPipeline) { - logger('--bypassPipeline does not convert to binary glTF, saving as .gltf'); - extension = '.gltf'; + return convert._outputJson(gltfPath, gltf); + } else { + return GltfPipeline.processJSONToDisk(gltf, gltfPath, pipelineOptions); } - } - gltfPath = path.join(path.dirname(gltfPath), modelName + extension); - - var aoOptions = ao ? {} : undefined; - var kmcOptions = kmc ? {} : undefined; - - var pipelineOptions = { - createDirectory : false, - basePath : basePath, - binary : binary, - embed : !separate, - embedImage : !separateTextures, - quantize : compress, - compressTextureCoordinates : compress, - encodeNormals : compress, - preserve : !optimize, - optimizeForCesium : optimizeForCesium, - smoothNormals : generateNormals, - aoOptions : aoOptions, - kmcOptions : kmcOptions, - textureCompressionOptions : textureCompressionOptions - }; - - return loadObj(objPath, options) - .then(function(objData) { - return createGltf(objData); - }) - .then(function(gltf) { - return writeUris(gltf, gltfPath, separate, separateTextures, logger); - }) - .then(function(gltf) { - if (bypassPipeline) { - return convert._outputJson(gltfPath, gltf); - } else { - return GltfPipeline.processJSONToDisk(gltf, gltfPath, pipelineOptions); - } - }) - .then(resolve) - .catch(reject); - }); + }); } +/** + * Default values that will be used when calling convert(options) unless specified in the options object. + */ +convert.defaults = { + /** + * Gets or sets whether the model will be saved as binary glTF. + * @type Boolean + * @default false + */ + binary: false, + /** + * Gets or sets whether to write out separate geometry/animation data files, + * shader files, and textures instead of embedding them in the glTF. + * @type Boolean + * @default false + */ + separate: false, + /** + * Gets or sets whether to write out separate textures only. + * @type Boolean + * @default false + */ + separateTextures: false, + /** + * Gets or sets whether to compress attribute data. This includes quantizing positions, compressing texture coordinates, and oct-encoding normals. + * @type Boolean + * @default false + */ + compress: false, + /** + * Gets or sets whether the model is optimized for size and runtime performance. + * @type Boolean + * @default false + */ + optimize: false, + /** + * Gets or sets whether the model is optimized for Cesium by using the sun as a default light source. + * @type Boolean + * @default false + */ + optimizeForCesium: false, + /** + * Gets or sets whether normals will be generated for the model if they are missing. + * @type Boolean + * @default false + */ + generateNormals: false, + /** + * Gets or sets whether the model will have ambient occlusion applied. + * @type Boolean + * @default false + */ + ao: false, + /** + * Gets or sets whether the model will be saved with the KHR_materials_common extension. + * @type Boolean + * @default false + */ + kmc: false, + /** + * Gets or sets whether the converter will bypass the gltf-pipeline for debugging purposes. + * @type Boolean + * @default false + */ + bypassPipeline: false, + /** + * Gets or sets whether the converter will do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. + * @type Boolean + * @default false + */ + checkTransparency: false, + /** + * Gets or sets whether the source model can reference paths outside of its directory. + * @type Boolean + * @default false + */ + secure: false, + /** + * @private + */ + logger: function(message) { + console.log(message); + } +}; + /** * Exposed for testing * * @private */ convert._outputJson = fsExtraOutputJson; + +/** + * A callback function that logs messages. + * @callback Logger + * + * @param {String} message The message to log. + */ + diff --git a/lib/gltf.js b/lib/gltf.js index 869276e..eb6aa7c 100644 --- a/lib/gltf.js +++ b/lib/gltf.js @@ -1,6 +1,7 @@ 'use strict'; var Cesium = require('cesium'); var path = require('path'); +var Material = require('./Material'); var defined = Cesium.defined; var defaultValue = Cesium.defaultValue; @@ -67,10 +68,10 @@ function createGltf(objData) { } function createMaterial(material, hasNormals) { - var ambient = defaultValue(defaultValue(getTextureId(material.ambientColorMap), material.ambientColor), [0, 0, 0, 1]); - var diffuse = defaultValue(defaultValue(getTextureId(material.diffuseColorMap), material.diffuseColor), [0.5, 0.5, 0.5, 1]); - var emission = defaultValue(defaultValue(getTextureId(material.emissionColorMap), material.emissionColor), [0, 0, 0, 1]); - var specular = defaultValue(defaultValue(getTextureId(material.specularColorMap), material.specularColor), [0, 0, 0, 1]); + var ambient = defaultValue(defaultValue(getTextureId(material.ambientTexture), material.ambientColor)); + var diffuse = defaultValue(defaultValue(getTextureId(material.diffuseTexture), material.diffuseColor)); + var emission = defaultValue(defaultValue(getTextureId(material.emissionTexture), material.emissionColor)); + var specular = defaultValue(defaultValue(getTextureId(material.specularTexture), material.specularColor)); var alpha = defaultValue(defaultValue(material.alpha), 1.0); var shininess = defaultValue(material.specularShininess, 0.0); var hasSpecular = (shininess > 0.0) && (specular[0] > 0.0 || specular[1] > 0.0 || specular[2] > 0.0); @@ -79,7 +80,7 @@ function createGltf(objData) { var transparency = 1.0; if (typeof diffuse === 'string') { transparency = alpha; - transparent = images[material.diffuseColorMap].transparent || (transparency < 1.0); + transparent = images[material.diffuseTexture].transparent || (transparency < 1.0); } else { diffuse[3] = alpha; transparent = diffuse[3] < 1.0; @@ -93,11 +94,6 @@ function createGltf(objData) { diffuse = [0, 0, 0, 1]; } - // It's not completely clear whether transparent and doubleSided belong under values or KHR_materials_common - // Put under both for now to handle both situations. - // https://github.com/KhronosGroup/glTF/tree/master/extensions/Khronos/KHR_materials_common - // https://github.com/KhronosGroup/glTF/issues/632 - var technique = hasNormals ? (hasSpecular ? 'PHONG' : 'LAMBERT') : 'CONSTANT'; return { extensions : { @@ -287,7 +283,8 @@ function createGltf(objData) { materialId = 'default'; } - var material = defaultValue(materials[materialId], {}); + var material = materials[materialId]; + material = defined(material) ? material : new Material(); var gltfMaterial = gltf.materials[materialId]; if (defined(gltfMaterial)) { // Check if this material has already been added but with incompatible shading diff --git a/lib/image.js b/lib/image.js index e5cbc45..8a3b686 100644 --- a/lib/image.js +++ b/lib/image.js @@ -7,7 +7,7 @@ var Promise = require('bluebird'); var fsExtraReadFile = Promise.promisify(fsExtra.readFile); -var defaultValue = Cesium.defaultValue; +var defined = Cesium.defined; var WebGLConstants = Cesium.WebGLConstants; module.exports = loadImage; @@ -16,19 +16,16 @@ module.exports = loadImage; * Load an image file and get information about it. * * @param {String} imagePath Path to the image file. - * @param {Object} [options] An object with the following properties: - * @param {Boolean} [options.hasTransparency=false] Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. + * @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. * @returns {Promise} A promise resolving to the image information, or undefined if the file doesn't exist. * * @private */ function loadImage(imagePath, options) { - options = defaultValue(options, defaultValue.EMPTY_OBJECT); - var hasTransparency = defaultValue(options.hasTransparency, false); - return fsExtraReadFile(imagePath) .then(function(data) { - var extension = path.extname(imagePath); + var extension = path.extname(imagePath).toLowerCase(); var info = { transparent : false, @@ -38,31 +35,35 @@ function loadImage(imagePath, options) { }; if (extension === '.png') { - // Color type is encoded in the 25th bit of the png - var colorType = data[25]; - var channels = getChannels(colorType); - info.format = getFormat(channels); - - if (channels === 4) { - info.transparent = true; - if (hasTransparency) { - return isTransparent(data) - .then(function(transparent) { - info.transparent = transparent; - return info; - }); - } - } + return getPngInfo(data, info, options); } return info; }); } +function getPngInfo(data, info, options) { + // Color type is encoded in the 25th bit of the png + var colorType = data[25]; + var channels = getChannels(colorType); + info.format = getFormat(channels); + + if (channels === 4) { + if (options.checkTransparency) { + return isTransparent(data) + .then(function(transparent) { + info.transparent = transparent; + return info; + }); + } + } + return info; +} + function isTransparent(data) { return new Promise(function(resolve, reject) { new PNG().parse(data, function(error, data) { - if (error) { + if (defined(error)) { reject(error); return; } diff --git a/lib/mtl.js b/lib/mtl.js index 4ced92e..6d3f506 100644 --- a/lib/mtl.js +++ b/lib/mtl.js @@ -1,30 +1,15 @@ 'use strict'; var path = require('path'); +var Material = require('./Material'); var readLines = require('./readLines'); module.exports = loadMtl; -function Material() { - this.ambientColor = undefined; // Ka - this.emissionColor = undefined; // Ke - this.diffuseColor = undefined; // Kd - this.specularColor = undefined; // Ks - this.specularShininess = undefined; // Ns - this.alpha = undefined; // d / Tr - this.ambientColorMap = undefined; // map_Ka - this.emissionColorMap = undefined; // map_Ke - this.diffuseColorMap = undefined; // map_Kd - this.specularColorMap = undefined; // map_Ks - this.specularShininessMap = undefined; // map_Ns - this.normalMap = undefined; // map_Bump - this.alphaMap = undefined; // map_d -} - /** * Parse an mtl file. * * @param {String} mtlPath Path to the mtl file. - * @returns {Promise} A promise resolving to the materials, or an empty object if the mtl file doesn't exist. + * @returns {Promise} A promise resolving to the materials. * * @private */ @@ -32,6 +17,7 @@ function loadMtl(mtlPath) { var material; var values; var value; + var mtlDirectory = path.dirname(mtlPath); var materials = {}; function parseLine(line) { @@ -82,19 +68,19 @@ function loadMtl(mtlPath) { value = line.substring(3).trim(); material.alpha = parseFloat(value); } else if (/^map_Ka /i.test(line)) { - material.ambientColorMap = getAbsolutePath(line.substring(7).trim(), mtlPath); + material.ambientTexture = path.resolve(mtlDirectory, line.substring(7).trim()); } else if (/^map_Ke /i.test(line)) { - material.emissionColorMap = getAbsolutePath(line.substring(7).trim(), mtlPath); + material.emissionTexture = path.resolve(mtlDirectory, line.substring(7).trim()); } else if (/^map_Kd /i.test(line)) { - material.diffuseColorMap = getAbsolutePath(line.substring(7).trim(), mtlPath); + material.diffuseTexture = path.resolve(mtlDirectory, line.substring(7).trim()); } else if (/^map_Ks /i.test(line)) { - material.specularColorMap = getAbsolutePath(line.substring(7).trim(), mtlPath); + material.specularTexture = path.resolve(mtlDirectory, line.substring(7).trim()); } else if (/^map_Ns /i.test(line)) { - material.specularShininessMap = getAbsolutePath(line.substring(7).trim(), mtlPath); + material.specularShininessMap = path.resolve(mtlDirectory, line.substring(7).trim()); } else if (/^map_Bump /i.test(line)) { - material.normalMap = getAbsolutePath(line.substring(9).trim(), mtlPath); + material.normalMap = path.resolve(mtlDirectory, line.substring(9).trim()); } else if (/^map_d /i.test(line)) { - material.alphaMap = getAbsolutePath(line.substring(6).trim(), mtlPath); + material.alphaMap = path.resolve(mtlDirectory, line.substring(6).trim()); } } @@ -103,10 +89,3 @@ function loadMtl(mtlPath) { return materials; }); } - -function getAbsolutePath(imagePath, mtlPath) { - if (!path.isAbsolute(imagePath)) { - imagePath = path.join(path.dirname(mtlPath), imagePath); - } - return imagePath; -} diff --git a/lib/obj.js b/lib/obj.js index 63ab31d..cfd1d7c 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -8,7 +8,6 @@ var loadImage = require('./image'); var loadMtl = require('./mtl'); var readLines = require('./readLines'); -var combine = Cesium.combine; var ComponentDatatype = Cesium.ComponentDatatype; var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; @@ -51,25 +50,16 @@ var facePattern4 = /f( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/( * Parse an obj file. * * @param {String} objPath Path to the obj file. - * @param {Object} [options] An object with the following properties: - * @param {Boolean} [options.hasTransparency=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 {Boolean} [options.logger] A callback function for handling logged messages. Defaults to console.log. + * @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 {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. * * @private */ function loadObj(objPath, options) { - // The defaults are set in convert as well, this just helps with testing loadObj individually - options = combine(options, { - hasTransparency : false, - secure : false, - logger : function(message) { - console.log(message); - } - }); - // Global store of vertex attributes listed in the obj file var positions = new ArrayStorage(ComponentDatatype.FLOAT); var normals = new ArrayStorage(ComponentDatatype.FLOAT); @@ -293,7 +283,7 @@ function loadObj(objPath, options) { function finishLoading(nodes, mtlPaths, objPath, options) { nodes = cleanNodes(nodes); if (nodes.length === 0) { - throw new RuntimeError(objPath + ' does not have any geometry data'); + return Promise.reject(new RuntimeError(objPath + ' does not have any geometry data')); } return loadMaterials(mtlPaths, objPath, options) .then(function(materials) { @@ -309,57 +299,50 @@ function finishLoading(nodes, mtlPaths, objPath, options) { }); } -function getAbsolutePath(mtlPath, objPath) { - if (!path.isAbsolute(mtlPath)) { - mtlPath = path.join(path.dirname(objPath), mtlPath); - } - return mtlPath; -} - function outsideDirectory(filePath, objPath) { return (path.relative(path.dirname(objPath), filePath).indexOf('..') === 0); } function loadMaterials(mtlPaths, objPath, options) { + var secure = options.secure; + var logger = options.logger; + var objDirectory = path.dirname(objPath); var materials = {}; return Promise.map(mtlPaths, function(mtlPath) { - mtlPath = getAbsolutePath(mtlPath, objPath); - if (options.secure && outsideDirectory(mtlPath, objPath)) { - options.logger('Could not read mtl file at ' + mtlPath + ' because it is outside of the obj directory and the secure flag is true. Using default material instead.'); + mtlPath = path.resolve(objDirectory, mtlPath); + if (secure && outsideDirectory(mtlPath, objPath)) { + logger('Could not read mtl file at ' + mtlPath + ' because it is outside of the obj directory and the secure flag is true. Using default material instead.'); return; } return loadMtl(mtlPath) .then(function(materialsInMtl) { - materials = combine(materials, materialsInMtl); + materials = Object.assign(materials, materialsInMtl); }) .catch(function() { - options.logger('Could not read mtl file at ' + mtlPath + '. Using default material instead.'); + logger('Could not read mtl file at ' + mtlPath + '. Using default material instead.'); }); - }).then(function() { - return materials; - }); + }, {concurrency : 10}) + .thenReturn(materials); } function loadImages(imagePaths, objPath, options) { + var secure = options.secure; + var logger = options.logger; var images = {}; return Promise.map(imagePaths, function(imagePath) { - if (options.secure && outsideDirectory(imagePath, objPath)) { - options.logger('Could not read image file at ' + imagePath + ' because it is outside of the obj directory and the secure flag is true. Material will ignore this image.'); + if (secure && outsideDirectory(imagePath, objPath)) { + logger('Could not read image file at ' + imagePath + ' because it is outside of the obj directory and the secure flag is true. Material will ignore this image.'); return; } return loadImage(imagePath, options) .then(function(image) { - if (defined(image)) { - images[imagePath] = image; - } + images[imagePath] = image; }) .catch(function() { - options.logger('Could not read image file at ' + imagePath + '. Material will ignore this image.'); - return undefined; + logger('Could not read image file at ' + imagePath + '. Material will ignore this image.'); }); - }).then(function() { - return images; - }); + }, {concurrency : 10}) + .thenReturn(images); } function getImagePaths(materials) { @@ -367,46 +350,32 @@ function getImagePaths(materials) { for (var name in materials) { if (materials.hasOwnProperty(name)) { var material = materials[name]; - if (defined(material.ambientColorMap)) { - imagePaths[material.ambientColorMap] = true; + if (defined(material.ambientTexture)) { + imagePaths[material.ambientTexture] = true; } - if (defined(material.diffuseColorMap)) { - imagePaths[material.diffuseColorMap] = true; + if (defined(material.diffuseTexture)) { + imagePaths[material.diffuseTexture] = true; } - if (defined(material.emissionColorMap)) { - imagePaths[material.emissionColorMap] = true; + if (defined(material.emissionTexture)) { + imagePaths[material.emissionTexture] = true; } - if (defined(material.specularColorMap)) { - imagePaths[material.specularColorMap] = true; + if (defined(material.specularTexture)) { + imagePaths[material.specularTexture] = true; } } } return Object.keys(imagePaths); } -function removeEmptyPrimitives(primitives) { - var final = []; - var primitivesLength = primitives.length; - for (var i = 0; i < primitivesLength; ++i) { - var primitive = primitives[i]; - if (primitive.indices.length > 0) { - final.push(primitive); - } - } - return final; -} - function removeEmptyMeshes(meshes) { - var final = []; - var meshesLength = meshes.length; - for (var i = 0; i < meshesLength; ++i) { - var mesh = meshes[i]; - mesh.primitives = removeEmptyPrimitives(mesh.primitives); - if ((mesh.primitives.length > 0) && (mesh.positions.length > 0)) { - final.push(mesh); - } - } - return final; + return meshes.filter(function(mesh) { + // Remove empty primitives + mesh.primitives = mesh.primitives.filter(function(primitive) { + return primitive.indices.length > 0; + }); + // Valid meshes must have at least one primitive and contain positions + return (mesh.primitives.length > 0) && (mesh.positions.length > 0); + }); } function meshesHaveNames(meshes) { diff --git a/lib/writeUris.js b/lib/writeUris.js index af88ec7..53e23c3 100644 --- a/lib/writeUris.js +++ b/lib/writeUris.js @@ -1,4 +1,5 @@ 'use strict'; +var Cesium = require('cesium'); var fsExtra = require('fs-extra'); var mime = require('mime'); var path = require('path'); @@ -6,6 +7,8 @@ var Promise = require('bluebird'); var fsExtraOutputFile = Promise.promisify(fsExtra.outputFile); +var RuntimeError = Cesium.RuntimeError; + module.exports = writeUris; /** @@ -13,14 +16,17 @@ module.exports = writeUris; * * @param {Object} gltf The glTF asset. * @param {String} gltfPath Path where the glTF will be saved. - * @param {Boolean} separateBuffers Writes out separate buffers. - * @param {Boolean} separateTextures Writes out separate textures. - * @param {Logger} logger A callback function for handling logged messages. Defaults to console.log. + * @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. * @returns {Promise} A promise that resolves to the glTF asset. * * @private */ -function writeUris(gltf, gltfPath, separateBuffers, separateTextures, logger) { +function writeUris(gltf, gltfPath, options) { + var separate = options.separate; + var separateTextures = options.separateTextures; + var promises = []; var buffer = gltf.buffers[Object.keys(gltf.buffers)[0]]; @@ -37,17 +43,17 @@ function writeUris(gltf, gltfPath, separateBuffers, separateTextures, logger) { // Buffers larger than ~192MB cannot be base64 encoded due to a NodeJS limitation. Source: https://github.com/nodejs/node/issues/4266 var exceedsMaximum = (texturesByteLength + bufferByteLength > 201326580); - if (exceedsMaximum) { - logger('Buffers and textures are too large to encode in the glTF, saving as separate resources.'); + if (exceedsMaximum && !separate) { + return Promise.reject(new RuntimeError('Buffers and textures are too large to encode in the glTF, saving as separate resources.')); } - if (separateBuffers || exceedsMaximum) { + if (separate) { promises.push(writeSeparateBuffer(gltf, gltfPath)); } else { writeEmbeddedBuffer(gltf); } - if (separateTextures || exceedsMaximum) { + if (separateTextures) { promises.push(writeSeparateTextures(gltf, gltfPath)); } else { writeEmbeddedTextures(gltf); @@ -84,19 +90,15 @@ function writeSeparateBuffer(gltf, gltfPath) { } function writeSeparateTextures(gltf, gltfPath) { - var promises = []; var images = gltf.images; - for (var id in images) { - if (images.hasOwnProperty(id)) { - var image = images[id]; - var extras = image.extras._obj2gltf; - var imageUri = image.name + extras.extension; - image.uri = imageUri; - var imagePath = path.join(path.dirname(gltfPath), imageUri); - promises.push(writeUris._outputFile(imagePath, extras.source)); - } - } - return Promise.all(promises); + return Promise.map(Object.keys(images), function(id) { + var image = images[id]; + var extras = image.extras._obj2gltf; + var imageUri = image.name + extras.extension; + image.uri = imageUri; + var imagePath = path.join(path.dirname(gltfPath), imageUri); + return writeUris._outputFile(imagePath, extras.source); + }, {concurrency : 10}); } function writeEmbeddedBuffer(gltf) { @@ -106,7 +108,6 @@ function writeEmbeddedBuffer(gltf) { } function writeEmbeddedTextures(gltf) { - var promises = []; var images = gltf.images; for (var id in images) { if (images.hasOwnProperty(id)) { @@ -115,7 +116,6 @@ function writeEmbeddedTextures(gltf) { image.uri = 'data:' + mime.lookup(extras.extension) + ';base64,' + extras.source.toString('base64'); } } - return Promise.all(promises); } /** diff --git a/specs/lib/convertSpec.js b/specs/lib/convertSpec.js index 35fd0eb..2d0476e 100644 --- a/specs/lib/convertSpec.js +++ b/specs/lib/convertSpec.js @@ -1,20 +1,13 @@ 'use strict'; -var Cesium = require('cesium'); var GltfPipeline = require('gltf-pipeline').Pipeline; var path = require('path'); var convert = require('../../lib/convert'); var writeUris = require('../../lib/writeUris'); -var DeveloperError = Cesium.DeveloperError; - var objPath = 'specs/data/box-textured/box-textured.obj'; var gltfPath = 'specs/data/box-textured/box-textured.gltf'; var glbPath = 'specs/data/box-textured/box-textured.glb'; -var objPathInvalid = 'invalid/'; -var gltfPathInvalid = 'invalid/model.invalid'; var objPathNonExistent = 'specs/data/non-existent.obj'; -var gltfPathNonExistent = 'specs/data/non-existent.gltf'; - var objExternalResourcesPath = 'specs/data/box-external-resources/box-external-resources.obj'; describe('convert', function() { @@ -57,8 +50,8 @@ describe('convert', function() { }); it('sets options', function(done) { - var spy1 = spyOn(GltfPipeline, 'processJSONToDisk'); - var spy2 = spyOn(writeUris, '_outputFile'); + var spy = spyOn(GltfPipeline, 'processJSONToDisk'); + spyOn(writeUris, '_outputFile'); var textureCompressionOptions = { format : 'dxt1', quality : 10 @@ -69,16 +62,19 @@ describe('convert', function() { separateTextures : true, compress : true, optimize : true, + optimizeForCesium : true, generateNormals : true, ao : true, kmc : true, - optimizeForCesium : true, - textureCompressionOptions : textureCompressionOptions + textureCompressionOptions : textureCompressionOptions, + checkTransparency : true, + secure : true, + logger : convert.defaults.logger }; expect(convert(objPath, gltfPath, options) .then(function() { - var args = spy1.calls.first().args; + var args = spy.calls.first().args; var options = args[2]; expect(options).toEqual({ createDirectory : false, @@ -96,7 +92,7 @@ describe('convert', function() { textureCompressionOptions : textureCompressionOptions, preserve : false }); - expect(spy2.calls.count()).toBe(2); // Saves out .png and .bin + expect(writeUris._outputFile.calls.count()).toBe(2); // Saves out .png and .bin }), done).toResolve(); }); @@ -111,50 +107,31 @@ describe('convert', function() { }); it('bypassPipeline flag bypasses gltf-pipeline', function(done) { - var spy1 = spyOn(convert, '_outputJson'); - var spy2 = spyOn(GltfPipeline, 'processJSONToDisk'); + spyOn(convert, '_outputJson'); + spyOn(GltfPipeline, 'processJSONToDisk'); var options = { bypassPipeline : true }; expect(convert(objPath, gltfPath, options) .then(function() { - expect(spy1.calls.count()).toBe(1); - expect(spy2.calls.count()).toBe(0); + expect(convert._outputJson).toHaveBeenCalled(); + expect(GltfPipeline.processJSONToDisk).not.toHaveBeenCalled(); }), done).toResolve(); }); - it('uses a custom logger', function(done) { - var spy = spyOn(GltfPipeline, 'processJSONToDisk'); - var logCount = 0; - var options = { - secure : true, // Needs to be set to trigger messages - logger : function() { - logCount++; - } - }; - expect(convert(objExternalResourcesPath, gltfPath, options) - .then(function() { - expect(logCount).toEqual(2); - }), done).toResolve(); - }); - - it('rejects if objPath is undefined', function(done) { - expect(convert(undefined, gltfPath), done).toRejectWith(DeveloperError); - }); - - it('rejects if gltfPath is undefined', function(done) { - expect(convert(objPath, undefined), done).toRejectWith(DeveloperError); - }); - - it('rejects if obj path is invalid', function(done) { - expect(convert(objPathInvalid, gltfPath), done).toRejectWith(DeveloperError); - }); - - it('rejects if gltf path is invalid', function(done) { - expect(convert(objPath, gltfPathInvalid), done).toRejectWith(DeveloperError); - }); - it('rejects if obj path does not exist', function(done) { expect(convert(objPathNonExistent, gltfPath), done).toRejectWith(Error); }); + + it('throws if objPath is undefined', function() { + expect(function() { + convert(undefined, gltfPath); + }).toThrowDeveloperError(); + }); + + it('rejects if gltfPath is undefined', function() { + expect(function() { + convert(objPath, undefined); + }).toThrowDeveloperError(); + }); }); diff --git a/specs/lib/gltfSpec.js b/specs/lib/gltfSpec.js index 297918e..b139ada 100644 --- a/specs/lib/gltfSpec.js +++ b/specs/lib/gltfSpec.js @@ -3,12 +3,14 @@ var Cesium = require('cesium'); var fsExtra = require('fs-extra'); var path = require('path'); var Promise = require('bluebird'); -var clone = require('../../lib/clone.js'); -var createGltf = require('../../lib/gltf.js'); -var loadImage = require('../../lib/image.js'); -var loadObj = require('../../lib/obj.js'); -var writeUris = require('../../lib/writeUris.js'); +var convert = require('../../lib/convert'); +var createGltf = require('../../lib/gltf'); +var loadImage = require('../../lib/image'); +var loadObj = require('../../lib/obj'); +var Material = require('../../lib/Material'); +var writeUris = require('../../lib/writeUris'); +var clone = Cesium.clone; var WebGLConstants = Cesium.WebGLConstants; var fsExtraReadJson = Promise.promisify(fsExtra.readJson); @@ -20,21 +22,30 @@ var groupGltfUrl = 'specs/data/box-objects-groups-materials/box-objects-groups-m var diffuseTextureUrl = 'specs/data/box-textured/cesium.png'; var transparentDiffuseTextureUrl = 'specs/data/box-complex-material/diffuse.png'; +var defaultOptions = convert.defaults; +var checkTransparencyOptions = clone(defaultOptions); +checkTransparencyOptions.checkTransparency = true; + describe('gltf', function() { var boxObjData; + var duplicateBoxObjData; var groupObjData; var boxGltf; var groupGltf; var diffuseTexture; var transparentDiffuseTexture; - beforeAll(function(done) { + beforeEach(function(done) { return Promise.all([ - loadObj(boxObjUrl) + loadObj(boxObjUrl, defaultOptions) .then(function(data) { boxObjData = data; }), - loadObj(groupObjUrl) + loadObj(boxObjUrl, defaultOptions) + .then(function(data) { + duplicateBoxObjData = data; + }), + loadObj(groupObjUrl, defaultOptions) .then(function(data) { groupObjData = data; }), @@ -46,11 +57,11 @@ describe('gltf', function() { .then(function(gltf) { groupGltf = gltf; }), - loadImage(diffuseTextureUrl) + loadImage(diffuseTextureUrl, defaultOptions) .then(function(image) { diffuseTexture = image; }), - loadImage(transparentDiffuseTextureUrl) + loadImage(transparentDiffuseTextureUrl, checkTransparencyOptions) .then(function(image) { transparentDiffuseTexture = image; }) @@ -58,19 +69,17 @@ describe('gltf', function() { }); it('simple gltf', function(done) { - var objData = clone(boxObjData, true); - var gltf = createGltf(objData); - expect(writeUris(gltf, boxGltfUrl, false, false) + var gltf = createGltf(boxObjData); + expect(writeUris(gltf, boxGltfUrl, defaultOptions) .then(function() { expect(gltf).toEqual(boxGltf); }), done).toResolve(); }); it('multiple nodes, meshes, and primitives', function(done) { - var objData = clone(groupObjData, true); - var gltf = createGltf(objData); + var gltf = createGltf(groupObjData); - expect(writeUris(gltf, groupGltfUrl, false, false) + expect(writeUris(gltf, groupGltfUrl, defaultOptions) .then(function() { expect(gltf).toEqual(groupGltf); expect(Object.keys(gltf.materials).length).toBe(3); @@ -88,10 +97,9 @@ describe('gltf', function() { }); it('sets default material values', function() { - var objData = clone(boxObjData, true); - objData.materials.Material = {}; + boxObjData.materials.Material = new Material(); - var gltf = createGltf(objData); + var gltf = createGltf(boxObjData); var material = gltf.materials.Material; var kmc = material.extensions.KHR_materials_common; var values = kmc.values; @@ -105,13 +113,12 @@ describe('gltf', function() { }); it('sets material for diffuse texture', function() { - var objData = clone(boxObjData, true); - objData.materials.Material = { - diffuseColorMap : diffuseTextureUrl - }; - objData.images[diffuseTextureUrl] = diffuseTexture; + var material = new Material(); + material.diffuseTexture = diffuseTextureUrl; + boxObjData.materials.Material = material; + boxObjData.images[diffuseTextureUrl] = diffuseTexture; - var gltf = createGltf(objData); + var gltf = createGltf(boxObjData); var kmc = gltf.materials.Material.extensions.KHR_materials_common; var texture = gltf.textures.texture_cesium; var image = gltf.images.cesium; @@ -145,12 +152,11 @@ describe('gltf', function() { }); it('sets material for alpha less than 1', function() { - var objData = clone(boxObjData, true); - objData.materials.Material = { - alpha : 0.4 - }; + var material = new Material(); + material.alpha = 0.4; + boxObjData.materials.Material = material; - var gltf = createGltf(objData); + var gltf = createGltf(boxObjData); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 0.4]); @@ -160,14 +166,14 @@ describe('gltf', function() { }); it('sets material for diffuse texture and alpha less than 1', function() { - var objData = clone(boxObjData, true); - objData.materials.Material = { - diffuseColorMap : diffuseTextureUrl, - alpha : 0.4 - }; - objData.images[diffuseTextureUrl] = diffuseTexture; + var material = new Material(); + material.diffuseTexture = diffuseTextureUrl; + material.alpha = 0.4; + boxObjData.materials.Material = material; - var gltf = createGltf(objData); + boxObjData.images[diffuseTextureUrl] = diffuseTexture; + + var gltf = createGltf(boxObjData); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.values.diffuse).toEqual('texture_cesium'); @@ -177,13 +183,13 @@ describe('gltf', function() { }); it('sets material for transparent diffuse texture', function() { - var objData = clone(boxObjData, true); - objData.materials.Material = { - diffuseColorMap : transparentDiffuseTextureUrl - }; - objData.images[transparentDiffuseTextureUrl] = transparentDiffuseTexture; + var material = new Material(); + material.diffuseTexture = transparentDiffuseTextureUrl; + boxObjData.materials.Material = material; - var gltf = createGltf(objData); + boxObjData.images[transparentDiffuseTextureUrl] = transparentDiffuseTexture; + + var gltf = createGltf(boxObjData); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.values.diffuse).toBe('texture_diffuse'); @@ -193,13 +199,12 @@ describe('gltf', function() { }); it('sets material for specular', function() { - var objData = clone(boxObjData, true); - objData.materials.Material = { - specularColor : [0.1, 0.1, 0.2, 1], - specularShininess : 0.1 - }; + var material = new Material(); + material.specularColor = [0.1, 0.1, 0.2, 1]; + material.specularShininess = 0.1; + boxObjData.materials.Material = material; - var gltf = createGltf(objData); + var gltf = createGltf(boxObjData); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.technique).toBe('PHONG'); @@ -208,14 +213,15 @@ describe('gltf', function() { }); it('sets constant material when there are no normals', function() { - var objData = clone(boxObjData, true); - objData.nodes[0].meshes[0].normals.length = 0; - objData.materials.Material = { - diffuseColorMap : diffuseTextureUrl - }; - objData.images[diffuseTextureUrl] = diffuseTexture; + boxObjData.nodes[0].meshes[0].normals.length = 0; - var gltf = createGltf(objData); + var material = new Material(); + material.diffuseTexture = diffuseTextureUrl; + boxObjData.materials.Material = material; + + boxObjData.images[diffuseTextureUrl] = diffuseTexture; + + var gltf = createGltf(boxObjData); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.technique).toBe('CONSTANT'); @@ -223,70 +229,66 @@ describe('gltf', function() { }); it('sets default material when texture is missing', function() { - var objData = clone(boxObjData, true); - objData.materials.Material = { - diffuseColorMap : diffuseTextureUrl - }; + var material = new Material(); + material.diffuseTexture = diffuseTextureUrl; + boxObjData.materials.Material = material; - var gltf = createGltf(objData); + var gltf = createGltf(boxObjData); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]); }); it('uses default material (1)', function() { - var objData = clone(boxObjData, true); - objData.nodes[0].meshes[0].primitives[0].material = undefined; + boxObjData.nodes[0].meshes[0].primitives[0].material = undefined; // Creates a material called "default" - var gltf = createGltf(objData); + var gltf = createGltf(boxObjData); expect(gltf.materials.default).toBeDefined(); var kmc = gltf.materials.default.extensions.KHR_materials_common; expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]); }); it('uses default material (2)', function() { - var objData = clone(boxObjData, true); - objData.materials = {}; + boxObjData.materials = {}; // Uses the original name of the material - var gltf = createGltf(objData); + var gltf = createGltf(boxObjData); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]); }); - it('handles material used with and without normals', function() { + it('handles material used with and without normals (1)', function() { // Two meshes - one with normals, and one without - var objData = clone(boxObjData, true); - objData.nodes.push(clone(objData.nodes[0], true)); - objData.nodes[1].meshes[0].normals.length = 0; + boxObjData.nodes.push(duplicateBoxObjData.nodes[0]); + boxObjData.nodes[1].meshes[0].normals.length = 0; - var gltf = createGltf(objData); + var gltf = createGltf(boxObjData); var kmc1 = gltf.materials.Material.extensions.KHR_materials_common; var kmc2 = gltf.materials.Material_constant.extensions.KHR_materials_common; expect(kmc1.technique).toBe('PHONG'); expect(kmc2.technique).toBe('CONSTANT'); + }); + it('handles material used with and without normals (2)', function() { // Now test in a different order - objData = clone(boxObjData, true); - objData.nodes.push(clone(objData.nodes[0], true)); - objData.nodes[0].meshes[0].normals.length = 0; + boxObjData.nodes.push(duplicateBoxObjData.nodes[0]); + boxObjData.nodes[0].meshes[0].normals.length = 0; - gltf = createGltf(objData); - kmc1 = gltf.materials.Material.extensions.KHR_materials_common; - kmc2 = gltf.materials.Material_shaded.extensions.KHR_materials_common; + var gltf = createGltf(boxObjData); + var kmc1 = gltf.materials.Material.extensions.KHR_materials_common; + var kmc2 = gltf.materials.Material_shaded.extensions.KHR_materials_common; expect(kmc1.technique).toBe('CONSTANT'); expect(kmc2.technique).toBe('PHONG'); }); it('runs without normals', function() { - var objData = clone(boxObjData, true); - objData.nodes[0].meshes[0].normals.length = 0; + boxObjData.nodes[0].meshes[0].normals.length = 0; - var gltf = createGltf(objData); + var gltf = createGltf(boxObjData); var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes; expect(attributes.POSITION).toBeDefined(); expect(attributes.NORMAL).toBeUndefined(); @@ -294,10 +296,9 @@ describe('gltf', function() { }); it('runs without uvs', function() { - var objData = clone(boxObjData, true); - objData.nodes[0].meshes[0].uvs.length = 0; + boxObjData.nodes[0].meshes[0].uvs.length = 0; - var gltf = createGltf(objData); + var gltf = createGltf(boxObjData); var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes; expect(attributes.POSITION).toBeDefined(); expect(attributes.NORMAL).toBeDefined(); @@ -305,11 +306,10 @@ describe('gltf', function() { }); it('runs without uvs and normals', function() { - var objData = clone(boxObjData, true); - objData.nodes[0].meshes[0].normals.length = 0; - objData.nodes[0].meshes[0].uvs.length = 0; + boxObjData.nodes[0].meshes[0].normals.length = 0; + boxObjData.nodes[0].meshes[0].uvs.length = 0; - var gltf = createGltf(objData); + var gltf = createGltf(boxObjData); var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes; expect(attributes.POSITION).toBeDefined(); expect(attributes.NORMAL).toBeUndefined(); @@ -344,13 +344,12 @@ describe('gltf', function() { } it('detects need to use uint32 indices', function() { - var objData = clone(boxObjData, true); - expandObjData(objData, 2731); // Right above 65536 limit - var mesh = objData.nodes[0].meshes[0]; + expandObjData(boxObjData, 2731); // Right above 65536 limit + var mesh = boxObjData.nodes[0].meshes[0]; var indicesLength = mesh.primitives[0].indices.length; var vertexCount = mesh.positions.length / 3; - var gltf = createGltf(objData); + var gltf = createGltf(boxObjData); var primitive = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0]; var indicesAccessor = gltf.accessors[primitive.indices]; expect(indicesAccessor.count).toBe(indicesLength); diff --git a/specs/lib/imageSpec.js b/specs/lib/imageSpec.js index e1eff09..452a8c0 100644 --- a/specs/lib/imageSpec.js +++ b/specs/lib/imageSpec.js @@ -1,8 +1,9 @@ 'use strict'; var Cesium = require('cesium'); -var path = require('path'); -var loadImage = require('../../lib/image.js'); +var convert = require('../../lib/convert'); +var loadImage = require('../../lib/image'); +var clone = Cesium.clone; var WebGLConstants = Cesium.WebGLConstants; var pngImage = 'specs/data/box-complex-material/shininess.png'; @@ -11,12 +12,12 @@ var jpegImage = 'specs/data/box-complex-material/specular.jpeg'; var gifImage = 'specs/data/box-complex-material/ambient.gif'; var grayscaleImage = 'specs/data/box-complex-material/alpha.png'; var transparentImage = 'specs/data/box-complex-material/diffuse.png'; -var opaqueAlphaImage = 'specs/data/box-complex-material/bump.png'; -var invalidImage = 'invalid.png'; + +var defaultOptions = convert.defaults; describe('image', function() { it('loads png image', function(done) { - expect(loadImage(pngImage) + expect(loadImage(pngImage, defaultOptions) .then(function(info) { expect(info.transparent).toBe(false); expect(info.format).toBe(WebGLConstants.RGB); @@ -26,7 +27,7 @@ describe('image', function() { }); it('loads jpg image', function(done) { - expect(loadImage(jpgImage) + expect(loadImage(jpgImage, defaultOptions) .then(function(info) { expect(info.transparent).toBe(false); expect(info.format).toBe(WebGLConstants.RGB); @@ -36,7 +37,7 @@ describe('image', function() { }); it('loads jpeg image', function(done) { - expect(loadImage(jpegImage) + expect(loadImage(jpegImage, defaultOptions) .then(function(info) { expect(info.transparent).toBe(false); expect(info.format).toBe(WebGLConstants.RGB); @@ -46,7 +47,7 @@ describe('image', function() { }); it('loads gif image', function(done) { - expect(loadImage(gifImage) + expect(loadImage(gifImage, defaultOptions) .then(function(info) { expect(info.transparent).toBe(false); expect(info.format).toBe(WebGLConstants.RGB); @@ -56,7 +57,7 @@ describe('image', function() { }); it('loads grayscale image', function(done) { - expect(loadImage(grayscaleImage) + expect(loadImage(grayscaleImage, defaultOptions) .then(function(info) { expect(info.transparent).toBe(false); expect(info.format).toBe(WebGLConstants.ALPHA); @@ -65,30 +66,20 @@ describe('image', function() { }), done).toResolve(); }); - it('loads transparent image', function(done) { - expect(loadImage(transparentImage) - .then(function(info) { - expect(info.transparent).toBe(true); - expect(info.format).toBe(WebGLConstants.RGBA); - expect(info.source).toBeDefined(); - expect(info.extension).toBe('.png'); - }), done).toResolve(); - }); - - it('loads image with fully opaque alpha channel', function(done) { - expect(loadImage(opaqueAlphaImage) - .then(function(info) { - expect(info.transparent).toBe(true); - }), done).toResolve(); - }); - - it('loads image with fully opaque alpha channel with hasTransparency flag', function(done) { - var options = { - hasTransparency : true - }; - expect(loadImage(opaqueAlphaImage, options) + it('loads image with alpha channel', function(done) { + expect(loadImage(transparentImage, defaultOptions) .then(function(info) { expect(info.transparent).toBe(false); }), done).toResolve(); }); + + it('loads image with checkTransparency flag', function(done) { + var options = clone(defaultOptions); + options.checkTransparency = true; + + expect(loadImage(transparentImage, options) + .then(function(info) { + expect(info.transparent).toBe(true); + }), done).toResolve(); + }); }); diff --git a/specs/lib/mtlSpec.js b/specs/lib/mtlSpec.js index 0be943c..3673d7c 100644 --- a/specs/lib/mtlSpec.js +++ b/specs/lib/mtlSpec.js @@ -1,13 +1,12 @@ 'use strict'; var path = require('path'); -var loadMtl = require('../../lib/mtl.js'); +var loadMtl = require('../../lib/mtl'); var complexMaterialUrl = 'specs/data/box-complex-material/box-complex-material.mtl'; var multipleMaterialsUrl = 'specs/data/box-multiple-materials/box-multiple-materials.mtl'; -var invalidMaterialUrl = 'invalid.mtl'; function getImagePath(objPath, relativePath) { - return path.normalize(path.join(path.dirname(objPath), relativePath)); + return path.resolve(path.dirname(objPath), relativePath); } describe('mtl', function() { @@ -22,10 +21,10 @@ describe('mtl', function() { expect(material.specularColor).toEqual([0.5, 0.5, 0.5, 1.0]); expect(material.specularShininess).toEqual(96.078431); expect(material.alpha).toEqual(0.9); - expect(material.ambientColorMap).toEqual(getImagePath(complexMaterialUrl, 'ambient.gif')); - expect(material.emissionColorMap).toEqual(getImagePath(complexMaterialUrl, 'emission.jpg')); - expect(material.diffuseColorMap).toEqual(getImagePath(complexMaterialUrl, 'diffuse.png')); - expect(material.specularColorMap).toEqual(getImagePath(complexMaterialUrl, 'specular.jpeg')); + expect(material.ambientTexture).toEqual(getImagePath(complexMaterialUrl, 'ambient.gif')); + expect(material.emissionTexture).toEqual(getImagePath(complexMaterialUrl, 'emission.jpg')); + expect(material.diffuseTexture).toEqual(getImagePath(complexMaterialUrl, 'diffuse.png')); + expect(material.specularTexture).toEqual(getImagePath(complexMaterialUrl, 'specular.jpeg')); expect(material.specularShininessMap).toEqual(getImagePath(complexMaterialUrl, 'shininess.png')); expect(material.normalMap).toEqual(getImagePath(complexMaterialUrl, 'bump.png')); expect(material.alphaMap).toEqual(getImagePath(complexMaterialUrl, 'alpha.png')); diff --git a/specs/lib/objSpec.js b/specs/lib/objSpec.js index c1aeac3..e8ac642 100644 --- a/specs/lib/objSpec.js +++ b/specs/lib/objSpec.js @@ -2,8 +2,10 @@ var Cesium = require('cesium'); var path = require('path'); var Promise = require('bluebird'); -var loadObj = require('../../lib/obj.js'); +var convert = require('../../lib/convert'); +var loadObj = require('../../lib/obj'); +var clone = Cesium.clone; var RuntimeError = Cesium.RuntimeError; var objUrl = 'specs/data/box/box.obj'; @@ -54,12 +56,14 @@ function getPrimitives(data) { } function getImagePath(objPath, relativePath) { - return path.normalize(path.join(path.dirname(objPath), relativePath)); + return path.resolve(path.dirname(objPath), relativePath); } +var defaultOptions = convert.defaults; + describe('obj', function() { it('loads obj with positions, normals, and uvs', function(done) { - expect(loadObj(objUrl) + expect(loadObj(objUrl, defaultOptions) .then(function(data) { var images = data.images; var materials = data.materials; @@ -88,7 +92,7 @@ describe('obj', function() { }); it('loads obj with normals', function(done) { - expect(loadObj(objNormalsUrl) + expect(loadObj(objNormalsUrl, defaultOptions) .then(function(data) { var mesh = getMeshes(data)[0]; expect(mesh.positions.length / 3).toBe(24); @@ -98,7 +102,7 @@ describe('obj', function() { }); it('loads obj with uvs', function(done) { - expect(loadObj(objUvsUrl) + expect(loadObj(objUvsUrl, defaultOptions) .then(function(data) { var mesh = getMeshes(data)[0]; expect(mesh.positions.length / 3).toBe(20); @@ -109,8 +113,8 @@ describe('obj', function() { it('loads obj with negative indices', function(done) { expect(Promise.all([ - loadObj(objPositionsOnlyUrl), - loadObj(objNegativeIndicesUrl) + loadObj(objPositionsOnlyUrl, defaultOptions), + loadObj(objNegativeIndicesUrl, defaultOptions) ]) .then(function(results) { var positionsReference = getMeshes(results[0])[0].positions.toFloatBuffer(); @@ -120,7 +124,7 @@ describe('obj', function() { }); it('loads obj with triangle faces', function(done) { - expect(loadObj(objTrianglesUrl) + expect(loadObj(objTrianglesUrl, defaultOptions) .then(function(data) { var mesh = getMeshes(data)[0]; var primitive = getPrimitives(data)[0]; @@ -130,7 +134,7 @@ describe('obj', function() { }); it('loads obj with objects', function(done) { - expect(loadObj(objObjectsUrl) + expect(loadObj(objObjectsUrl, defaultOptions) .then(function(data) { var nodes = data.nodes; expect(nodes.length).toBe(3); @@ -147,7 +151,7 @@ describe('obj', function() { }); it('loads obj with groups', function(done) { - expect(loadObj(objGroupsUrl) + expect(loadObj(objGroupsUrl, defaultOptions) .then(function(data) { var nodes = data.nodes; expect(nodes.length).toBe(3); @@ -164,7 +168,7 @@ describe('obj', function() { }); it('loads obj with objects and groups', function(done) { - expect(loadObj(objObjectsGroupsUrl) + expect(loadObj(objObjectsGroupsUrl, defaultOptions) .then(function(data) { var nodes = data.nodes; expect(nodes.length).toBe(3); @@ -187,7 +191,7 @@ describe('obj', function() { }); it('loads obj with usemtl only', function(done) { - expect(loadObj(objUsemtlUrl) + expect(loadObj(objUsemtlUrl, defaultOptions) .then(function(data) { var nodes = data.nodes; expect(nodes.length).toBe(1); @@ -206,7 +210,7 @@ describe('obj', function() { }); it('loads obj with no materials', function(done) { - expect(loadObj(objNoMaterialsUrl) + expect(loadObj(objNoMaterialsUrl, defaultOptions) .then(function(data) { var nodes = data.nodes; expect(nodes.length).toBe(1); @@ -219,7 +223,7 @@ describe('obj', function() { it('loads obj with multiple materials', function(done) { // The usemtl markers are interleaved, but should condense to just three primitives - expect(loadObj(objMultipleMaterialsUrl) + expect(loadObj(objMultipleMaterialsUrl, defaultOptions) .then(function(data) { var nodes = data.nodes; expect(nodes.length).toBe(1); @@ -239,7 +243,7 @@ describe('obj', function() { it('loads obj uncleaned', function(done) { // Obj with extraneous o, g, and usemtl lines // Also tests handling of o and g lines with the same names - expect(loadObj(objUncleanedUrl) + expect(loadObj(objUncleanedUrl, defaultOptions) .then(function(data) { var nodes = data.nodes; var meshes = getMeshes(data); @@ -255,7 +259,7 @@ describe('obj', function() { }); it('loads obj with multiple mtllibs', function(done) { - expect(loadObj(objMtllibUrl) + expect(loadObj(objMtllibUrl, defaultOptions) .then(function(data) { var materials = data.materials; expect(Object.keys(materials).length).toBe(3); @@ -267,7 +271,7 @@ describe('obj', function() { it('loads obj with missing mtllib', function(done) { spyOn(console, 'log'); - expect(loadObj(objMissingMtllibUrl) + expect(loadObj(objMissingMtllibUrl, defaultOptions) .then(function(data) { expect(data.materials).toEqual({}); expect(console.log.calls.argsFor(0)[0].indexOf('Could not read mtl file') >= 0).toBe(true); @@ -275,19 +279,20 @@ describe('obj', function() { }); it('loads resources outside of the obj directory', function(done) { - expect(loadObj(objExternalResourcesUrl) + expect(loadObj(objExternalResourcesUrl, defaultOptions) .then(function(data) { var imagePath = getImagePath(objTexturedUrl, 'cesium.png'); expect(data.images[imagePath]).toBeDefined(); - expect(data.materials.MaterialTextured.diffuseColorMap).toEqual(imagePath); + expect(data.materials.MaterialTextured.diffuseTexture).toEqual(imagePath); }), done).toResolve(); }); it('does not load resources outside of the obj directory when secure is true', function(done) { spyOn(console, 'log'); - var options = { - secure : true - }; + + var options = clone(defaultOptions); + options.secure = true; + expect(loadObj(objExternalResourcesUrl, options) .then(function(data) { var imagePath = getImagePath(objMissingTextureUrl, 'cesium.png'); @@ -300,36 +305,36 @@ describe('obj', function() { }); it('loads obj with texture', function(done) { - expect(loadObj(objTexturedUrl) + expect(loadObj(objTexturedUrl, defaultOptions) .then(function(data) { var imagePath = getImagePath(objTexturedUrl, 'cesium.png'); expect(data.images[imagePath]).toBeDefined(); - expect(data.materials.Material.diffuseColorMap).toEqual(imagePath); + expect(data.materials.Material.diffuseTexture).toEqual(imagePath); }), done).toResolve(); }); it('loads obj with missing texture', function(done) { spyOn(console, 'log'); - expect(loadObj(objMissingTextureUrl) + expect(loadObj(objMissingTextureUrl, defaultOptions) .then(function(data) { var imagePath = getImagePath(objMissingTextureUrl, 'cesium.png'); expect(data.images[imagePath]).toBeUndefined(); - expect(data.materials.Material.diffuseColorMap).toEqual(imagePath); + expect(data.materials.Material.diffuseTexture).toEqual(imagePath); expect(console.log.calls.argsFor(0)[0].indexOf('Could not read image file') >= 0).toBe(true); }), done).toResolve(); }); it('loads obj with subdirectories', function(done) { - expect(loadObj(objSubdirectoriesUrl) + expect(loadObj(objSubdirectoriesUrl, defaultOptions) .then(function(data) { var imagePath = getImagePath(objSubdirectoriesUrl, path.join('materials', 'images', 'cesium.png')); expect(data.images[imagePath]).toBeDefined(); - expect(data.materials.Material.diffuseColorMap).toEqual(imagePath); + expect(data.materials.Material.diffuseTexture).toEqual(imagePath); }), done).toResolve(); }); it('loads obj with complex material', function(done) { - expect(loadObj(objComplexMaterialUrl) + expect(loadObj(objComplexMaterialUrl, defaultOptions) .then(function(data) { var images = data.images; expect(Object.keys(images).length).toBe(4); // Only ambient, diffuse, emission, and specular maps are supported by the converter @@ -337,10 +342,10 @@ describe('obj', function() { }); it('does not process file with invalid contents', function(done) { - expect(loadObj(objInvalidContentsUrl), done).toRejectWith(RuntimeError); + expect(loadObj(objInvalidContentsUrl, defaultOptions), done).toRejectWith(RuntimeError); }); it('throw when reading invalid file', function(done) { - expect(loadObj(objInvalidUrl), done).toRejectWith(Error); + expect(loadObj(objInvalidUrl, defaultOptions), done).toRejectWith(Error); }); });