From e54f3af37f0a47852c32c087c1238b79efaeaedf Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 27 Jul 2017 11:23:12 -0400 Subject: [PATCH] Async image reading/writing and other cleanup --- README.md | 12 +-- bin/obj2gltf.js | 12 +-- lib/createGltf.js | 150 +++++++++++++++++------------------- lib/gltfToGlb.js | 2 + lib/loadImage.js | 31 ++++++-- lib/loadObj.js | 4 +- lib/obj2gltf.js | 19 +++-- lib/writeUris.js | 133 ++++++++++++++++++++++---------- specs/lib/createGltfSpec.js | 8 +- 9 files changed, 217 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index e9045e3..7bb0e0f 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,12 @@ Using obj2gltf as a command-line tool: |`--metallicRoughness`|The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots.|No, default `false`| |`--specularGlossiness`|The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the `KHR_materials_pbrSpecularGlossiness` extension.|No, default `false`| |`--materialsCommon`|The glTF will be saved with the KHR_materials_common extension.|No, default `false`| -|`--metallicRoughnessOcclusionTexture`|Path to the metallic-roughness-occlusion texture used by the model, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. This may be used instead of setting texture paths in the .mtl file. The model will be saved with a pbrMetallicRoughness material. -|`--specularGlossinessTexture`|Path to the specular-glossiness texture used by the model, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. This may be used instead of setting texture paths in the .mtl file. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension. -|`--occlusionTexture`|Path to the occlusion texture used by the model. This may be used instead of setting texture paths in the .mtl file. Ignored if metallicRoughnessOcclusionTexture is also set. -|`--normalTexture`|Path to the normal texture used by the model. This may be used instead of setting texture paths in the .mtl file. -|`--baseColorTexture`|Path to the baseColor/diffuse texture used by the model. This may be used instead of setting texture paths in the .mtl file. -|`--emissiveTexture`|Path to the emissive texture used by the model. This may be used instead of setting texture paths in the .mtl file. +|`--metallicRoughnessOcclusionTexture`|Path to the metallic-roughness-occlusion texture used by the model, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. The model will be saved with a pbrMetallicRoughness material. +|`--specularGlossinessTexture`|Path to the specular-glossiness texture used by the model, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension. +|`--occlusionTexture`|Path to the occlusion texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. Ignored if metallicRoughnessOcclusionTexture is also set. +|`--normalTexture`|Path to the normal texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. +|`--baseColorTexture`|Path to the baseColor/diffuse texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. +|`--emissiveTexture`|Path to the emissive texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. ## Build Instructions diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index a8936a5..dab8dc8 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -91,32 +91,32 @@ var argv = yargs default: defaults.materialsCommon }, metallicRoughnessOcclusionTexture : { - describe: 'Path to the metallic-roughness-occlusion texture used by the model, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. This may be used instead of setting texture paths in the .mtl file. The model will be saved with a pbrMetallicRoughness material.', + describe: 'Path to the metallic-roughness-occlusion texture used by the model, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. The model will be saved with a pbrMetallicRoughness material.', type: 'string', normalize: true }, specularGlossinessTexture : { - describe: 'Path to the specular-glossiness texture used by the model, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. This may be used instead of setting texture paths in the .mtl file. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension.', + describe: 'Path to the specular-glossiness texture used by the model, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension.', type: 'string', normalize: true }, occlusionTexture : { - describe: 'Path to the occlusion texture used by the model. This may be used instead of setting texture paths in the .mtl file. Ignored if metallicRoughnessOcclusionTexture is also set.', + describe: 'Path to the occlusion texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. Ignored if metallicRoughnessOcclusionTexture is also set.', type: 'string', normalize: true }, normalTexture : { - describe: 'Path to the normal texture used by the model. This may be used instead of setting texture paths in the .mtl file.', + describe: 'Path to the normal texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material.', type: 'string', normalize: true }, baseColorTexture : { - describe: 'Path to the baseColor/diffuse texture used by the model. This may be used instead of setting texture paths in the .mtl file.', + describe: 'Path to the baseColor/diffuse texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material.', type: 'string', normalize: true }, emissiveTexture : { - describe: 'Path to the emissive texture used by the model. This may be used instead of setting texture paths in the .mtl file.', + describe: 'Path to the emissive texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material.', type: 'string', normalize: true } diff --git a/lib/createGltf.js b/lib/createGltf.js index 2cd0d92..e377156 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -1,7 +1,6 @@ 'use strict'; var Cesium = require('cesium'); var path = require('path'); -var PNG = require('pngjs').PNG; var getBufferPadded = require('./getBufferPadded'); var Material = require('./Material'); @@ -22,12 +21,12 @@ module.exports = createGltf; * @param {Boolean} options.specularGlossiness The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension. * @param {Boolean} options.materialsCommon The glTF will be saved with the KHR_materials_common extension. * @param {Object[]} options.overridingImages An array of images that override images in the .mtl file. - * @param {String} [options.metallicRoughnessOcclusionTexture] Path to the metallic-roughness-occlusion texture used by the model, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. This may be used instead of setting texture paths in the .mtl file. The model will be saved with a pbrMetallicRoughness material. - * @param {String} [options.specularGlossinessTexture] Path to the specular-glossiness texture used by the model, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. This may be used instead of setting texture paths in the .mtl file. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension. - * @param {String} [options.occlusionTexture] Path to the occlusion texture used by the model. This may be used instead of setting texture paths in the .mtl file. Ignored if metallicRoughnessOcclusionTexture is also set. - * @param {String} [options.normalTexture] Path to the normal texture used by the model. This may be used instead of setting texture paths in the .mtl file. - * @param {String} [options.baseColorTexture] Path to the baseColor/diffuse texture used by the model. This may be used instead of setting texture paths in the .mtl file. - * @param {String} [options.emissiveTexture] Path to the emissive texture used by the model. This may be used instead of setting texture paths in the .mtl file. + * @param {String} [options.metallicRoughnessOcclusionTexture] Path to the metallic-roughness-occlusion texture used by the model, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. The model will be saved with a pbrMetallicRoughness material. + * @param {String} [options.specularGlossinessTexture] Path to the specular-glossiness texture used by the model, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension. + * @param {String} [options.occlusionTexture] Path to the occlusion texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. Ignored if metallicRoughnessOcclusionTexture is also set. + * @param {String} [options.normalTexture] Path to the normal texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. + * @param {String} [options.baseColorTexture] Path to the baseColor/diffuse texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. + * @param {String} [options.emissiveTexture] Path to the emissive texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. * @param {Boolean} options.logger A callback function for handling logged messages. Defaults to console.log. * @returns {Object} A glTF asset. * @@ -177,7 +176,7 @@ function getImageName(image) { } function getTextureName(image) { - return getImageName(image); + return getImageName(image) + '_texture'; } function addTexture(gltf, image) { @@ -189,10 +188,7 @@ function addTexture(gltf, image) { gltf.images.push({ name : imageName, extras : { - _obj2gltf : { - source : image.source, - extension : image.extension - } + _obj2gltf : image } }); @@ -224,6 +220,7 @@ function getTexture(gltf, image) { if (!defined(textureIndex)) { textureIndex = addTexture(gltf, image); } + return { index : textureIndex }; @@ -247,9 +244,8 @@ function getEmissiveFactor(material) { return addColors(ambientColor, emissiveColor); } -function resizeChannel(sourcePixels, sourceWidth, sourceHeight, targetWidth, targetHeight) { +function resizeChannel(sourcePixels, sourceWidth, sourceHeight, targetPixels, targetWidth, targetHeight) { // Nearest neighbor sampling - var targetPixels = Buffer.alloc(targetWidth * targetHeight); var widthRatio = sourceWidth / targetWidth; var heightRatio = sourceHeight / targetHeight; @@ -266,24 +262,38 @@ function resizeChannel(sourcePixels, sourceWidth, sourceHeight, targetWidth, tar return targetPixels; } -function getImageChannel(image, index, targetWidth, targetHeight) { +var scratchResizeChannel; + +function getImageChannel(image, index, targetWidth, targetHeight, targetChannel) { var pixels = image.decoded; // RGBA - var width = image.width; - var height = image.height; - var pixelsLength = width * height; - var channel = Buffer.alloc(pixelsLength); - for (var i = 0; i < pixelsLength; ++i) { + var sourceWidth = image.width; + var sourceHeight = image.height; + var sourcePixelsLength = sourceWidth * sourceHeight; + var targetPixelsLength = targetWidth * targetHeight; + + // Allocate the scratchResizeChannel on demand if the texture needs to be resized + var sourceChannel = targetChannel; + if (sourcePixelsLength > targetPixelsLength) { + if (!defined(scratchResizeChannel) || (sourcePixelsLength > scratchResizeChannel.length)) { + scratchResizeChannel = Buffer.alloc(sourcePixelsLength); + } + sourceChannel = scratchResizeChannel; + } + + for (var i = 0; i < sourcePixelsLength; ++i) { var value = pixels.readUInt8(i * 4 + index); - channel.writeUInt8(value, i); + sourceChannel.writeUInt8(value, i); } - if (width !== targetWidth || height !== targetHeight) { - channel = resizeChannel(channel, width, height, targetWidth, targetHeight); + + if (sourcePixelsLength > targetPixelsLength) { + resizeChannel(sourceChannel, sourceWidth, sourceHeight, targetChannel, targetWidth, targetHeight); } - return channel; + + return targetChannel; } -function writeChannel(pixels, channel, index, width, height) { - var pixelsLength = width * height; +function writeChannel(pixels, channel, index) { + var pixelsLength = pixels.length / 4; for (var i = 0; i < pixelsLength; ++i) { var value = channel.readUInt8(i); pixels.writeUInt8(value, i * 4 + index); @@ -313,32 +323,6 @@ function getMinimumDimensions(images, options) { return [width, height]; } -function encodePng(pixels, width, height, inputChannels, outputChannels) { - var pngInput = { - data : pixels, - width : width, - height : height - }; - - // Constants defined by pngjs - var rgbColorType = 2; - var rgbaColorType = 6; - - var colorType = outputChannels === 4 ? rgbaColorType : rgbColorType; - var inputColorType = inputChannels === 4 ? rgbaColorType : rgbColorType; - var inputHasAlpha = inputChannels === 4; - - var pngOptions = { - width : width, - height : height, - colorType : colorType, - inputColorType : inputColorType, - inputHasAlpha : inputHasAlpha - }; - - return PNG.sync.write(pngInput, pngOptions); -} - function createMetallicRoughnessTexture(gltf, metallicImage, roughnessImage, occlusionImage, options) { var packMetallic = defined(metallicImage); var packRoughness = defined(roughnessImage); @@ -372,40 +356,41 @@ function createMetallicRoughnessTexture(gltf, metallicImage, roughnessImage, occ var height = dimensions[1]; var pixelsLength = width * height; var pixels = Buffer.alloc(pixelsLength * 4, 0xFF); // Initialize with 4 channels, unused channels will be white + var scratchChannel = Buffer.alloc(pixelsLength); if (packMetallic) { // Write into the B channel - var metallicChannel = getImageChannel(metallicImage, 0, width, height); - writeChannel(pixels, metallicChannel, 2, width, height); + var metallicChannel = getImageChannel(metallicImage, 0, width, height, scratchChannel); + writeChannel(pixels, metallicChannel, 2); } if (packRoughness) { // Write into the G channel - var roughnessChannel = getImageChannel(roughnessImage, 0, width, height); - writeChannel(pixels, roughnessChannel, 1, width, height); + var roughnessChannel = getImageChannel(roughnessImage, 0, width, height, scratchChannel); + writeChannel(pixels, roughnessChannel, 1); } if (packOcclusion) { // Write into the R channel - var occlusionChannel = getImageChannel(occlusionImage, 0, width, height); - writeChannel(pixels, occlusionChannel, 0, width, height); + var occlusionChannel = getImageChannel(occlusionImage, 0, width, height, scratchChannel); + writeChannel(pixels, occlusionChannel, 0); } var length = packedImages.length; var imageNames = new Array(length); for (var i = 0; i < length; ++i) { - var imagePath = packedImages[i].path; - imageNames[i] = path.basename(imagePath, path.extname(imagePath)); + imageNames[i] = getImageName(packedImages[i]); } var imageName = imageNames.join('_'); - var pngSource = encodePng(pixels, width, height, 4, 3); - var image = { transparent : false, - source : pngSource, + source : undefined, + extension : '.png', path : imageName, - extension : '.png' + decoded : pixels, + width : width, + height : height }; return getTexture(gltf, image); @@ -438,38 +423,39 @@ function createSpecularGlossinessTexture(gltf, specularImage, glossinessImage, o var height = dimensions[1]; var pixelsLength = width * height; var pixels = Buffer.alloc(pixelsLength * 4, 0xFF); // Initialize with 4 channels, unused channels will be white + var scratchChannel = Buffer.alloc(pixelsLength); if (packSpecular) { // Write into the R, G, B channels - var redChannel = getImageChannel(specularImage, 0, width, height); - var greenChannel = getImageChannel(specularImage, 1, width, height); - var blueChannel = getImageChannel(specularImage, 2, width, height); - writeChannel(pixels, redChannel, 0, width, height); - writeChannel(pixels, greenChannel, 1, width, height); - writeChannel(pixels, blueChannel, 2, width, height); + var redChannel = getImageChannel(specularImage, 0, width, height, scratchChannel); + var greenChannel = getImageChannel(specularImage, 1, width, height, scratchChannel); + var blueChannel = getImageChannel(specularImage, 2, width, height, scratchChannel); + writeChannel(pixels, redChannel, 0); + writeChannel(pixels, greenChannel, 1); + writeChannel(pixels, blueChannel, 2); } if (packGlossiness) { // Write into the A channel - var glossinessChannel = getImageChannel(glossinessImage, 0, width, height); - writeChannel(pixels, glossinessChannel, 3, width, height); + var glossinessChannel = getImageChannel(glossinessImage, 0, width, height, scratchChannel); + writeChannel(pixels, glossinessChannel, 3); } var length = packedImages.length; var imageNames = new Array(length); for (var i = 0; i < length; ++i) { - var imagePath = packedImages[i].path; - imageNames[i] = path.basename(imagePath, path.extname(imagePath)); + imageNames[i] = getImageName(packedImages[i]); } var imageName = imageNames.join('_'); - var pngSource = encodePng(pixels, width, height, 4, 4); - var image = { - transparent : false, - source : pngSource, + transparent : true, + source : undefined, + extension : '.png', path : imageName, - extension : '.png' + decoded : pixels, + width : width, + height : height }; return getTexture(gltf, image); @@ -628,12 +614,16 @@ function createMetallicRoughnessMaterial(gltf, images, material, options) { }; } +function luminance(color) { + return color[0] * 0.2125 + color[1] * 0.7154 + color[2] * 0.0721; +} + function convertTraditionalToMetallicRoughness(material) { // Translate the blinn-phong model to the pbr metallic-roughness model // Roughness factor is a combination of specular intensity and shininess // Metallic factor is 0.0 // This does not convert textures - var specularIntensity = material.specularColor[0]; + var specularIntensity = luminance(material.specularColor); var specularShininess = material.specularShininess; // Transform from 0-1000 range to 0-1 range. Then invert. diff --git a/lib/gltfToGlb.js b/lib/gltfToGlb.js index 350f787..e8d455b 100644 --- a/lib/gltfToGlb.js +++ b/lib/gltfToGlb.js @@ -9,6 +9,8 @@ module.exports = gltfToGlb; /** * Convert a glTF to binary glTF. * + * The glTF is expected to have all resources embedded as bufferViews and a single buffer whose content is stored in a data uri. + * * @param {Object} gltf A javascript object containing a glTF asset. * @returns {Promise} A promise that resolves to a buffer containing the binary glTF. * diff --git a/lib/loadImage.js b/lib/loadImage.js index 410a133..7ed720d 100644 --- a/lib/loadImage.js +++ b/lib/loadImage.js @@ -4,8 +4,10 @@ var fsExtra = require('fs-extra'); var jpeg = require('jpeg-js'); var path = require('path'); var PNG = require('pngjs').PNG; +var Promise = require('bluebird'); var defaultValue = Cesium.defaultValue; +var defined = Cesium.defined; module.exports = loadImage; @@ -75,6 +77,18 @@ function getChannels(colorType) { } } +function parsePng(data) { + return new Promise(function(resolve, reject) { + new PNG().parse(data, function(error, decodedResults) { + if (defined(error)) { + reject(error); + return; + } + resolve(decodedResults); + }); + }); +} + function getPngInfo(data, info, options) { // Color type is encoded in the 25th bit of the png var colorType = data[25]; @@ -84,13 +98,16 @@ function getPngInfo(data, info, options) { var decode = options.decode || checkTransparency; if (decode) { - var decodedResults = PNG.sync.read(data); - info.decoded = decodedResults.data; - info.width = decodedResults.width; - info.height = decodedResults.height; - if (checkTransparency) { - info.transparent = hasTransparency(info); - } + return parsePng(data) + .then(function(decodedResults) { + info.decoded = decodedResults.data; + info.width = decodedResults.width; + info.height = decodedResults.height; + if (checkTransparency) { + info.transparent = hasTransparency(info); + } + return info; + }); } return info; } diff --git a/lib/loadObj.js b/lib/loadObj.js index 2f6d2f5..b7d9441 100644 --- a/lib/loadObj.js +++ b/lib/loadObj.js @@ -368,9 +368,7 @@ function loadImages(imagesOptions, objPath, options) { logger('Could not read image file at ' + imagePath + '. Material will ignore this image.'); }); }, {concurrency : 10}) - .then(function() { - return images; - }); + .thenReturn(images); } function getImagesOptions(materials, options) { diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index 851e2b4..740317b 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -32,12 +32,12 @@ module.exports = obj2gltf; * @param {Boolean} [options.metallicRoughness=false] The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots. * @param {Boolean} [options.specularGlossiness=false] The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension. * @param {Boolean} [options.materialsCommon=false] The glTF will be saved with the KHR_materials_common extension. - * @param {String} [options.metallicRoughnessOcclusionTexture] Path to the metallic-roughness-occlusion texture used by the model, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. This may be used instead of setting texture paths in the .mtl file. The model will be saved with a pbrMetallicRoughness material. - * @param {String} [options.specularGlossinessTexture] Path to the specular-glossiness texture used by the model, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. This may be used instead of setting texture paths in the .mtl file. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension. - * @param {String} [options.occlusionTexture] Path to the occlusion texture used by the model. This may be used instead of setting texture paths in the .mtl file. Ignored if metallicRoughnessOcclusionTexture is also set. - * @param {String} [options.normalTexture] Path to the normal texture used by the model. This may be used instead of setting texture paths in the .mtl file. - * @param {String} [options.baseColorTexture] Path to the baseColor/diffuse texture used by the model. This may be used instead of setting texture paths in the .mtl file. - * @param {String} [options.emissiveTexture] Path to the emissive texture used by the model. This may be used instead of setting texture paths in the .mtl file. + * @param {String} [options.metallicRoughnessOcclusionTexture] Path to the metallic-roughness-occlusion texture used by the model, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. The model will be saved with a pbrMetallicRoughness material. + * @param {String} [options.specularGlossinessTexture] Path to the specular-glossiness texture used by the model, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension. + * @param {String} [options.occlusionTexture] Path to the occlusion texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. Ignored if metallicRoughnessOcclusionTexture is also set. + * @param {String} [options.normalTexture] Path to the normal texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. + * @param {String} [options.baseColorTexture] Path to the baseColor/diffuse texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. + * @param {String} [options.emissiveTexture] Path to the emissive texture used by the model. This may be used instead of setting texture paths in the .mtl file, and is intended for models that use one material. * @param {Logger} [options.logger] A callback function for handling logged messages. Defaults to console.log. * @return {Promise} A promise that resolves when the glTF file is saved. @@ -108,10 +108,6 @@ function obj2gltf(objPath, gltfPath, options) { gltfPath = path.join(path.dirname(gltfPath), modelName + extension); - var jsonOptions = { - spaces : 2 - }; - return loadOverridingImages(options) .then(function() { return loadObj(objPath, options); @@ -127,6 +123,9 @@ function obj2gltf(objPath, gltfPath, options) { var glb = gltfToGlb(gltf); return fsExtra.outputFile(gltfPath, glb); } + var jsonOptions = { + spaces : 2 + }; return fsExtra.outputJson(gltfPath, gltf, jsonOptions); }); } diff --git a/lib/writeUris.js b/lib/writeUris.js index a74b63f..ab171a7 100644 --- a/lib/writeUris.js +++ b/lib/writeUris.js @@ -3,6 +3,7 @@ var Cesium = require('cesium'); var fsExtra = require('fs-extra'); var mime = require('mime'); var path = require('path'); +var PNG = require('pngjs').PNG; var Promise = require('bluebird'); var getBufferPadded = require('./getBufferPadded'); @@ -24,50 +25,102 @@ module.exports = writeUris; * @private */ function writeUris(gltf, gltfPath, options) { - var separate = options.separate; - var separateTextures = options.separateTextures; - - var promises = []; - - var buffer = gltf.buffers[0]; - var bufferByteLength = buffer.extras._obj2gltf.source.length; - - var texturesByteLength = 0; - var images = gltf.images; - var imagesLength = images.length; - for (var i = 0; i < imagesLength; ++i) { - texturesByteLength += images[i].extras._obj2gltf.source.length; - } - - // 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 && !separate) { - return Promise.reject(new RuntimeError('Buffers and textures are too large to encode in the glTF. Use the --separate flag instead.')); - } - - var name = path.basename(gltfPath, path.extname(gltfPath)); - - if (separateTextures) { - promises.push(writeSeparateTextures(gltf, gltfPath)); - } else { - writeEmbeddedTextures(gltf); - } - - if (separate) { - promises.push(writeSeparateBuffer(gltf, gltfPath, name)); - } else { - writeEmbeddedBuffer(gltf); - } - - return Promise.all(promises) + return encodeImages(gltf) .then(function() { - deleteExtras(gltf); - cleanup(gltf); - return gltf; + var separate = options.separate; + var separateTextures = options.separateTextures; + + var buffer = gltf.buffers[0]; + var bufferByteLength = buffer.extras._obj2gltf.source.length; + + var texturesByteLength = 0; + var images = gltf.images; + var imagesLength = images.length; + for (var i = 0; i < imagesLength; ++i) { + texturesByteLength += images[i].extras._obj2gltf.source.length; + } + + // 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 && !separate) { + return Promise.reject(new RuntimeError('Buffers and textures are too large to encode in the glTF. Use the --separate flag instead.')); + } + + var name = path.basename(gltfPath, path.extname(gltfPath)); + + var promises = []; + if (separateTextures) { + promises.push(writeSeparateTextures(gltf, gltfPath)); + } else { + writeEmbeddedTextures(gltf); + } + + if (separate) { + promises.push(writeSeparateBuffer(gltf, gltfPath, name)); + } else { + writeEmbeddedBuffer(gltf); + } + + return Promise.all(promises) + .then(function() { + deleteExtras(gltf); + cleanup(gltf); + return gltf; + }); }); } +function encodePng(image) { + // Constants defined by pngjs + var rgbColorType = 2; + var rgbaColorType = 6; + + var png = new PNG({ + width : image.width, + height : image.height, + colorType : image.transparent ? rgbaColorType : rgbColorType, + inputColorType : rgbaColorType, + inputHasAlpha : true + }); + + png.data = image.decoded; + + return new Promise(function(resolve, reject) { + var chunks = []; + var stream = png.pack(); + stream.on('data', function(chunk) { + chunks.push(chunk); + }); + stream.on('end', function() { + resolve(Buffer.concat(chunks)); + }); + stream.on('error', reject); + }); +} + +function encodeImage(image) { + var imageExtras = image.extras._obj2gltf; + if (!defined(imageExtras.source) && defined(imageExtras.decoded) && imageExtras.extension === '.png') { + return encodePng(imageExtras) + .then(function(encoded) { + imageExtras.source = encoded; + }); + } +} + +function encodeImages(gltf) { + // Dynamically generated metallicRoughnessOcclusion and specularGlossiness + // textures need to be encoded to png's prior to being saved. + var encodePromises = []; + var images = gltf.images; + var length = images.length; + for (var i = 0; i < length; ++i) { + encodePromises.push(encodeImage(images[i])); + } + return Promise.all(encodePromises); +} + function deleteExtras(gltf) { var buffer = gltf.buffers[0]; delete buffer.extras; diff --git a/specs/lib/createGltfSpec.js b/specs/lib/createGltfSpec.js index 55d80ff..3fe2974 100644 --- a/specs/lib/createGltfSpec.js +++ b/specs/lib/createGltfSpec.js @@ -92,6 +92,8 @@ describe('createGltf', function() { var gltf = createGltf(boxObjData, defaultOptions); expect(gltf.materials.length).toBe(1); + expect(gltf.scene).toBe(0); + expect(gltf.scenes[0].nodes[0]).toBe(0); expect(gltf.nodes.length).toBe(1); expect(gltf.meshes.length).toBe(1); @@ -114,6 +116,8 @@ describe('createGltf', function() { var gltf = createGltf(groupObjData, defaultOptions); expect(gltf.materials.length).toBe(3); + expect(gltf.scene).toBe(0); + expect(gltf.scenes[0].nodes[0]).toBe(0); expect(gltf.nodes.length).toBe(4); expect(gltf.nodes[0].mesh).toBeUndefined(); expect(gltf.nodes[0].children.length).toBe(3); @@ -320,7 +324,7 @@ describe('createGltf', function() { }); }); - describe('specularGlosiness', function() { + describe('specularGlossiness', function() { it('sets default material values for specularGlossiness', function() { var options = clone(defaultOptions); options.specularGlossiness = true; @@ -440,7 +444,7 @@ describe('createGltf', function() { expect(kmc.values.doubleSided).toBe(false); expect(texture).toEqual({ - name : 'cesium', + name : 'cesium_texture', sampler : 0, source : 0 });