Many updates

This commit is contained in:
Sean Lilley 2017-04-10 17:57:56 -04:00
parent 28d081e0ae
commit cc8fee19c4
16 changed files with 515 additions and 564 deletions

View File

@ -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

View File

@ -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
};

View File

@ -1,3 +1 @@
module.exports = {
convert : require('./lib/convert')
};
module.exports = require('./lib/convert');

19
lib/Material.js Normal file
View File

@ -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
}

View File

@ -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;
}

View File

@ -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,35 +26,42 @@ 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) {
var defaults = convert.defaults;
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 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, false);
var logger = defaultValue(options.logger, defaultLogger);
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);
options.separate = separate;
options.separateTextures = separateTextures;
options.checkTransparency = checkTransparency;
options.secure = secure;
options.logger = logger;
options.hasTransparency = defaultValue(options.hasTransparency, false);
options.secure = defaultValue(options.secure, false);
if (!defined(objPath)) {
throw new DeveloperError('objPath is required');
@ -74,25 +71,17 @@ function convert(objPath, gltfPath, options) {
throw new DeveloperError('gltfPath is required');
}
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 + '"');
}
var basePath = path.dirname(gltfPath);
var modelName = path.basename(gltfPath, path.extname(gltfPath));
if (extension === '.glb') {
binary = true;
if (bypassPipeline) {
logger('--bypassPipeline does not convert to binary glTF, saving as .gltf');
extension = '.gltf';
}
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;
@ -120,7 +109,7 @@ function convert(objPath, gltfPath, options) {
return createGltf(objData);
})
.then(function(gltf) {
return writeUris(gltf, gltfPath, separate, separateTextures, logger);
return writeUris(gltf, gltfPath, options);
})
.then(function(gltf) {
if (bypassPipeline) {
@ -128,15 +117,105 @@ function convert(objPath, gltfPath, options) {
} 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.
*/

View File

@ -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

View File

@ -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,14 +35,21 @@ function loadImage(imagePath, options) {
};
if (extension === '.png') {
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) {
info.transparent = true;
if (hasTransparency) {
if (options.checkTransparency) {
return isTransparent(data)
.then(function(transparent) {
info.transparent = transparent;
@ -53,16 +57,13 @@ function loadImage(imagePath, options) {
});
}
}
}
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;
}

View File

@ -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;
}

View File

@ -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.');
});
}).then(function() {
return materials;
logger('Could not read mtl file at ' + mtlPath + '. Using default material instead.');
});
}, {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;
}
})
.catch(function() {
options.logger('Could not read image file at ' + imagePath + '. Material will ignore this image.');
return undefined;
});
}).then(function() {
return images;
logger('Could not read image file at ' + imagePath + '. Material will ignore this image.');
});
}, {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) {

View File

@ -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)) {
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);
promises.push(writeUris._outputFile(imagePath, extras.source));
}
}
return Promise.all(promises);
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);
}
/**

View File

@ -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();
});
});

View File

@ -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);

View File

@ -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();
});
});

View File

@ -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'));

View File

@ -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);
});
});