From c9ad66fcdb25ed57a6fb0b27291cfdb36d8d0147 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 3 May 2017 17:59:24 -0400 Subject: [PATCH] Add metallicRoughness and specularGlosiness output --- README.md | 3 + bin/obj2gltf.js | 20 +- lib/Material.js | 1 + lib/createGltf.js | 509 ++++++++++++++++++++++++++++++++-------------- lib/loadImage.js | 1 + lib/loadMtl.js | 5 +- lib/loadObj.js | 87 ++++---- lib/obj2gltf.js | 33 ++- lib/writeUris.js | 13 ++ package.json | 2 +- 10 files changed, 469 insertions(+), 205 deletions(-) diff --git a/README.md b/README.md index 18eff2d..8d201ef 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,9 @@ Using obj2gltf as a command-line tool: |`--bypassPipeline`|Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above.|No, default `false`| |`--checkTransparency`|Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. By default textures are considered to be opaque.|No, default `false`| |`--secure`|Prevent the converter from reading image or mtl files outside of the input obj directory.|No, default `false`| +|`--packOcclusion`|Pack the occlusion texture in the red channel of metallic-roughness texture.|No, default `false`| +|`--inputMetallicRoughness`|The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots.|No, default `false`| +|`--inputSpecularGlossiness`|The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the `KHR_materials_pbrSpecularGlossiness` extension.|No, default `false`| ## Build Instructions diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index cf7204f..a50d2b0 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -90,6 +90,21 @@ var argv = yargs describe: 'Prevent the converter from reading image or mtl files outside of the input obj directory.', type: 'boolean', default: defaults.secure + }, + packOcclusion : { + describe: 'Pack the occlusion texture in the red channel of metallic-roughness texture.', + type: 'boolean', + default: defaults.packOcclusion + }, + inputMetallicRoughness : { + describe: '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.', + type: 'boolean', + default : defaults.metallicRoughness + }, + inputSpecularGlossiness : { + describe: '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.', + type: 'boolean', + default : defaults.specularGlossiness } }).parse(args); @@ -113,7 +128,8 @@ var options = { ao : argv.ao, bypassPipeline : argv.bypassPipeline, checkTransparency : argv.checkTransparency, - secure : argv.secure + secure : argv.secure, + packOcclusion : argv.packOcclusion }; console.time('Total'); @@ -123,5 +139,5 @@ obj2gltf(objPath, gltfPath, options) console.timeEnd('Total'); }) .catch(function(error) { - console.log(error.message); + console.log(error); }); diff --git a/lib/Material.js b/lib/Material.js index 6a074ac..f5caa1d 100644 --- a/lib/Material.js +++ b/lib/Material.js @@ -3,6 +3,7 @@ module.exports = Material; function Material() { + this.name = ''; this.ambientColor = [0.0, 0.0, 0.0, 1.0]; // Ka this.emissiveColor = [0.0, 0.0, 0.0, 1.0]; // Ke this.diffuseColor = [0.5, 0.5, 0.5, 1.0]; // Kd diff --git a/lib/createGltf.js b/lib/createGltf.js index c16d4a3..fa01216 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -4,8 +4,8 @@ var path = require('path'); var PNG = require('pngjs').PNG; var Material = require('./Material'); +var CesiumMath = Cesium.Math; var defined = Cesium.defined; -var defaultValue = Cesium.defaultValue; var WebGLConstants = Cesium.WebGLConstants; module.exports = createGltf; @@ -15,6 +15,9 @@ module.exports = createGltf; * * @param {Object} objData Output of obj.js, containing an array of nodes containing geometry information, materials, and images. * @param {Object} options An object with the following properties: + * @param {Boolean} [options.packOcclusion=false] Pack the occlusion texture in the red channel of metallic-roughness texture. + * @param {Boolean} [options.inputMetallicRoughness=false] The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots. + * @param {Boolean} [options.inputSpecularGlossiness=false] The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension. * @param {Boolean} options.logger A callback function for handling logged messages. Defaults to console.log. * @returns {Object} A glTF asset. * @@ -81,7 +84,7 @@ function createGltf(objData, options) { } } - if (Object.keys(gltf.images).length > 0) { + if (gltf.images.length > 0) { gltf.samplers.push({ magFilter : WebGLConstants.LINEAR, minFilter : WebGLConstants.LINEAR, @@ -137,23 +140,27 @@ function addBuffers(gltf, bufferState) { } function getImage(images, imagePath) { - if (!defined(imagePath) || !defined(images[imagePath])) { - return undefined; + var imagesLength = images.length; + for (var i = 0; i < imagesLength; ++i) { + var image = images[i]; + if (image.path === imagePath) { + return image; + } } - return images[imagePath]; + return undefined; } -function getImageName(imagePath) { - return path.basename(imagePath, path.extname(imagePath)); +function getImageName(image) { + return path.basename(image.path, image.extension); } -function getTextureName(imagePath) { - return getImageName(imagePath); +function getTextureName(image) { + return getImageName(image); } -function addTexture(gltf, image, imagePath) { - var imageName = getImageName(imagePath); - var textureName = getTextureName(imagePath); +function addTexture(gltf, image) { + var imageName = getImageName(image); + var textureName = getTextureName(image); var imageIndex = gltf.images.length; var textureIndex = gltf.textures.length; @@ -176,34 +183,28 @@ function addTexture(gltf, image, imagePath) { return textureIndex; } -function getTextureIndex(gltf, imagePath) { - var name = getTextureName(imagePath); +function getTexture(gltf, image) { + if (!defined(image)) { + return undefined; + } + + var textureIndex; + var name = getTextureName(image); var textures = gltf.textures; var length = textures.length; for (var i = 0; i < length; ++i) { if (textures[i].name === name) { - return i; + textureIndex = i; + break; } } -} -function getTexture(gltf, images, imagePath) { - var image = getImage(images, imagePath); - if (!defined(image)) { - return undefined; - } - var textureIndex = getTextureIndex(gltf, imagePath); if (!defined(textureIndex)) { - textureIndex = addTexture(gltf, image, imagePath); + textureIndex = addTexture(gltf, image); } return textureIndex; } -function luminance(color) { - var value = 0.2125 * color[0] + 0.7154 * color[1] + 0.0721 * color[2]; - return Math.min(value, 1.0); // Clamp just to handle edge cases -} - function addColors(left, right) { var red = Math.min(left[0] + right[0], 1.0); var green = Math.min(left[1] + right[1], 1.0); @@ -211,6 +212,17 @@ function addColors(left, right) { return [red, green, blue]; } +function getEmissiveFactor(material) { + // If ambient color is [1, 1, 1] assume it is a multiplier and instead change to [0, 0, 0] + // Then add the ambient color to the emissive color to get the emissive factor. + var ambientColor = material.ambientColor; + var emissiveColor = material.emissiveColor; + if (ambientColor[0] === 1.0 && ambientColor[1] === 1.0 && ambientColor[2] === 1.0) { + ambientColor = [0.0, 0.0, 0.0, 1.0]; + } + return addColors(ambientColor, emissiveColor); +} + function resizeChannel(sourcePixels, sourceWidth, sourceHeight, targetWidth, targetHeight) { // Nearest neighbor sampling var targetPixels = Buffer.alloc(targetWidth * targetHeight); @@ -230,25 +242,20 @@ function resizeChannel(sourcePixels, sourceWidth, sourceHeight, targetWidth, tar return targetPixels; } -var scratchColor = new Array(3); - -function getGrayscaleChannel(image, targetWidth, targetHeight) { +function getImageChannel(image, index, targetWidth, targetHeight) { var pixels = image.decoded; // RGBA var width = image.width; var height = image.height; var pixelsLength = width * height; - var grayPixels = Buffer.alloc(pixelsLength); + var channel = Buffer.alloc(pixelsLength); for (var i = 0; i < pixelsLength; ++i) { - scratchColor[0] = pixels.readUInt8(i * 4); - scratchColor[1] = pixels.readUInt8(i * 4 + 1); - scratchColor[2] = pixels.readUInt8(i * 4 + 2); - var value = luminance(scratchColor) * 255; - grayPixels.writeUInt8(value, i); + var value = pixels.readUInt8(i * 4 + index); + channel.writeUInt8(value, i); } if (width !== targetWidth || height !== targetHeight) { - grayPixels = resizeChannel(grayPixels, width, height, targetWidth, targetHeight); + channel = resizeChannel(channel, width, height, targetWidth, targetHeight); } - return grayPixels; + return channel; } function writeChannel(pixels, channel, index, width, height) { @@ -259,204 +266,392 @@ function writeChannel(pixels, channel, index, width, height) { } } -function createMetallicRoughnessTexture(gltf, materialName, metallicImage, roughnessImage, options) { - if (!defined(metallicImage) && !defined(roughnessImage)) { - return undefined; +function getMinimumDimensions(images, options) { + var i; + var image; + var width = Number.POSITIVE_INFINITY; + var height = Number.POSITIVE_INFINITY; + + var length = images.length; + for (i = 0; i < length; ++i) { + image = images[i]; + if (defined(image)) { + width = Math.min(image.width, width); + height = Math.min(image.height, height); + } } - if (defined(metallicImage) && !defined(metallicImage.decoded)) { - options.logger('Could not get decoded image data for ' + metallicImage + '. The material will be created without a metallicRoughness texture.'); - return undefined; + for (i = 0; i < length; ++i) { + image = images[i]; + if (defined(image)) { + if (image.width !== width || image.height !== height) { + options.logger('Image ' + image.path + ' will be scaled from ' + image.width + 'x' + image.height + ' to ' + width + 'x' + height + '.'); + } + } } - if (defined(roughnessImage) && !defined(roughnessImage.decoded)) { - options.logger('Could not get decoded image data for ' + roughnessImage + '. The material will be created without a metallicRoughness texture.'); - return undefined; - } - - var width; - var height; - - if (defined(metallicImage) && defined(roughnessImage)) { - width = Math.min(metallicImage.width, roughnessImage.width); - height = Math.min(metallicImage.height, roughnessImage.height); - } else if (defined(metallicImage)) { - width = metallicImage.width; - height = metallicImage.height; - } else if (defined(roughnessImage)) { - width = roughnessImage.width; - height = roughnessImage.height; - } - - var pixelsLength = width * height; - var pixels = Buffer.alloc(pixelsLength * 4, 0xFF); // Initialize with 4 channels, unused channels will be white - - if (defined(metallicImage)) { - // Write into the B channel - var metallicChannel = getGrayscaleChannel(metallicImage, width, height); - writeChannel(pixels, metallicChannel, 2, width, height); - } - - if (defined(roughnessImage)) { - // Write into the G channel - var roughnessChannel = getGrayscaleChannel(roughnessImage, width, height); - writeChannel(pixels, roughnessChannel, 1, width, height); - } + 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 = 4; + + var colorType = outputChannels === 4 ? rgbaColorType : rgbColorType; + var inputColorType = inputChannels === 4 ? rgbaColorType : rgbColorType; + var inputHasAlpha = inputChannels === 4; + var pngOptions = { width : width, height : height, - colorType : 2, // RGB - inputHasAlpha : true + colorType : colorType, + inputColorType : inputColorType, + inputHasAlpha : inputHasAlpha }; - var encoded = PNG.sync.write(pngInput, pngOptions); + return PNG.sync.write(pngInput, pngOptions); +} + +function createMetallicRoughnessTexture(gltf, materialName, metallicImage, roughnessImage, occlusionImage, options) { + var packMetallic = defined(metallicImage); + var packRoughness = defined(roughnessImage); + var packOcclusion = defined(occlusionImage) && options.packOcclusion; + + if (!packMetallic && !packRoughness) { + return undefined; + } + + if (packMetallic && !defined(metallicImage.decoded)) { + options.logger('Could not get decoded image data for ' + metallicImage.path + '. The material will be created without a metallicRoughness texture.'); + return undefined; + } + + if (packRoughness && !defined(roughnessImage.decoded)) { + options.logger('Could not get decoded image data for ' + roughnessImage.path + '. The material will be created without a metallicRoughness texture.'); + return undefined; + } + + if (packOcclusion && !defined(occlusionImage.decoded)) { + options.logger('Could not get decoded image data for ' + occlusionImage.path + '. The occlusion texture will not be packed in the metallicRoughness texture.'); + return undefined; + } + + var dimensions = getMinimumDimensions([metallicImage, roughnessImage, occlusionImage], options); + var width = dimensions[0]; + var height = dimensions[1]; + var pixelsLength = width * height; + var pixels = Buffer.alloc(pixelsLength * 4, 0xFF); // Initialize with 4 channels, unused channels will be white + + if (packMetallic) { + // Write into the B channel + var metallicChannel = getImageChannel(metallicImage, 0, width, height); + writeChannel(pixels, metallicChannel, 2, width, height); + } + + if (packRoughness) { + // Write into the G channel + var roughnessChannel = getImageChannel(roughnessImage, 0, width, height); + writeChannel(pixels, roughnessChannel, 1, width, height); + } + + if (packOcclusion) { + // Write into the R channel + var occlusionChannel = getImageChannel(occlusionImage, 0, width, height); + writeChannel(pixels, occlusionChannel, 0, width, height); + } + + var imageName = materialName + '-' + 'MetallicRoughness'; + if (packOcclusion) { + imageName += 'Occlusion'; + } + + var pngSource = encodePng(pixels, width, height, 4, 3); var image = { transparent : false, - source : encoded, + source : pngSource, + path : imageName, extension : '.png' }; - var imageName = materialName + '-' + 'MetallicRoughness'; - return addTexture(gltf, image, imageName); + return addTexture(gltf, image); } -function addMaterial(gltf, images, material, name, hasNormals, options) { - // Translate the traditional diffuse/specular material to pbr metallic roughness. - // Specular intensity is extracted from the specular color and treated as the metallic factor. - // Specular shininess is typically an exponent from 0 to 1000, and is converted to a 0-1 range as the roughness factor. - var ambientTexture = getTexture(gltf, images, material.ambientTexture); - var emissiveTexture = getTexture(gltf, images, material.emissiveTexture); - var baseColorTexture = getTexture(gltf, images, material.diffuseTexture); - var normalTexture = getTexture(gltf, images, material.normalTexture); +function createSpecularGlossinessTexture(gltf, materialName, specularImage, glossinessImage, options) { + var packSpecular = defined(specularImage); + var packGlossiness = defined(glossinessImage); - // Emissive and ambient represent roughly the same concept, so chose whichever is defined. - emissiveTexture = defaultValue(emissiveTexture, ambientTexture); + if (!packSpecular && !packGlossiness) { + return undefined; + } + if (packSpecular && !defined(specularImage.decoded)) { + options.logger('Could not get decoded image data for ' + specularImage.path + '. The material will be created without a specularGlossiness texture.'); + return undefined; + } + + if (packGlossiness && !defined(glossinessImage.decoded)) { + options.logger('Could not get decoded image data for ' + glossinessImage.path + '. The material will be created without a specularGlossiness texture.'); + return undefined; + } + + var dimensions = getMinimumDimensions([specularImage, glossinessImage], options); + var width = dimensions[0]; + var height = dimensions[1]; + var pixelsLength = width * height; + var pixels = Buffer.alloc(pixelsLength * 4, 0xFF); // Initialize with 4 channels, unused channels will be white + + 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); + } + + if (packGlossiness) { + // Write into the A channel + var glossinessChannel = getImageChannel(glossinessImage, 0, width, height); + writeChannel(pixels, glossinessChannel, 3, width, height); + } + + var imageName = materialName + '-' + 'SpecularGlossiness'; + + var pngSource = encodePng(pixels, width, height, 4, 4); + + var image = { + transparent : false, + source : pngSource, + path : imageName, + extension : '.png' + }; + + return addTexture(gltf, image); +} + +function createSpecularGlossinessMaterial(gltf, images, material, options) { + var materialName = material.name; + + var emissiveImage = getImage(images, material.emissiveTexture); + var normalImage = getImage(images, material.normalTexture); + var occlusionImage = getImage(images, material.ambientTexture); + var diffuseImage = getImage(images, material.diffuseTexture); + var specularImage = getImage(images, material.specularTexture); + var glossinessImage = getImage(images, material.specularShininessTexture); + + var emissiveTexture = getTexture(gltf, emissiveImage); + var normalTexture = getTexture(gltf, normalImage); + var occlusionTexture = getTexture(gltf, occlusionImage); + var diffuseTexture = getTexture(gltf, diffuseImage); + var specularGlossinessTexture = createSpecularGlossinessTexture(gltf, materialName, specularImage, glossinessImage, options); + + var emissiveFactor = getEmissiveFactor(material); + var diffuseFactor = material.diffuseColor; + var specularFactor = material.specularColor; + var glossinessFactor = material.specularShininess; + + if (defined(emissiveTexture)) { + emissiveFactor = [1.0, 1.0, 1.0]; + } + + if (defined(diffuseTexture)) { + diffuseFactor = [1.0, 1.0, 1.0, 1.0]; + } + + if (defined(specularImage)) { + specularFactor = 1.0; + } + + if (defined(glossinessImage)) { + glossinessFactor = 1.0; + } + + var alpha = material.alpha; + diffuseFactor[3] = alpha; + + var transparent = alpha < 1.0; + if (defined(diffuseImage)) { + transparent |= diffuseImage.transparent; + } + + var doubleSided = transparent; + var alphaMode = transparent ? 'BLEND' : 'OPAQUE'; + + var gltfMaterial = { + name : materialName, + extensions : { + KHR_materials_pbrSpecularGlossiness: { + diffuseTexture : diffuseTexture, + specularGlossinessTexture : specularGlossinessTexture, + diffuseFactor : diffuseFactor, + specularFactor : specularFactor, + glossinessFactor : glossinessFactor + } + }, + emissiveTexture : emissiveTexture, + normalTexture : normalTexture, + occlusionTexture : occlusionTexture, + emissiveFactor : emissiveFactor, + alphaMode : alphaMode, + doubleSided : doubleSided + }; + + return gltfMaterial; +} + +function createMetallicRoughnessMaterial(gltf, images, material, options) { + var materialName = material.name; + + var emissiveImage = getImage(images, material.emissiveTexture); + var normalImage = getImage(images, material.normalTexture); + var occlusionImage = getImage(images, material.ambientTexture); + var baseColorImage = getImage(images, material.diffuseTexture); var metallicImage = getImage(images, material.specularTexture); var roughnessImage = getImage(images, material.specularShininessTexture); - var metallicRoughnessTexture = createMetallicRoughnessTexture(gltf, name, metallicImage, roughnessImage, options); - var baseColorFactor = [1.0, 1.0, 1.0, 1.0]; - var metallicFactor = 1.0; - var roughnessFactor = 1.0; - var emissiveFactor = [1.0, 1.0, 1.0]; + var emissiveTexture = getTexture(gltf, emissiveImage); + var normalTexture = getTexture(gltf, normalImage); + var baseColorTexture = getTexture(gltf, baseColorImage); + var metallicRoughnessTexture = createMetallicRoughnessTexture(gltf, materialName, metallicImage, roughnessImage, occlusionImage, options); - if (!defined(baseColorTexture)) { - baseColorFactor = material.diffuseColor; + var packOcclusion = defined(occlusionImage) || options.packOcclusion; + var occlusionTexture = packOcclusion ? metallicRoughnessTexture : getTexture(gltf, occlusionImage); + + var emissiveFactor = getEmissiveFactor(material); + var baseColorFactor = material.diffuseColor; + var metallicFactor = material.specularColor[0]; + var roughnessFactor = material.specularShininess; + + if (defined(emissiveTexture)) { + emissiveFactor = [1.0, 1.0, 1.0]; } - if (!defined(metallicImage)) { - metallicFactor = luminance(material.specularColor); + if (defined(baseColorTexture)) { + baseColorFactor = [1.0, 1.0, 1.0, 1.0]; } - if (!defined(roughnessImage)) { - var specularShininess = material.specularShininess; - if (specularShininess > 1.0) { - specularShininess /= 1000.0; - } - roughnessFactor = specularShininess; + if (defined(metallicImage)) { + metallicFactor = 1.0; } - if (!defined(emissiveTexture)) { - // If ambient color is [1, 1, 1] assume it is a multiplier and instead change to [0, 0, 0] - var ambientColor = material.ambientColor; - if (ambientColor[0] === 1.0 && ambientColor[1] === 1.0 && ambientColor[2] === 1.0) { - ambientColor = [0.0, 0.0, 0.0, 1.0]; - } - emissiveFactor = addColors(material.emissiveColor, ambientColor); + if (defined(roughnessImage)) { + roughnessFactor = 1.0; } var alpha = material.alpha; baseColorFactor[3] = alpha; var transparent = alpha < 1.0; - if (defined(material.diffuseTexture)) { - transparent |= images[material.diffuseTexture].transparent; + if (defined(baseColorImage)) { + transparent |= baseColorImage.transparent; } var doubleSided = transparent; var alphaMode = transparent ? 'BLEND' : 'OPAQUE'; - if (!hasNormals) { - // TODO : what is the lighting like for models that don't have normals? Can pbrMetallicRoughness just be undefined? Is setting the baseColor to black a good approach here? - emissiveTexture = baseColorTexture; - emissiveFactor = baseColorFactor.slice(0, 3); - baseColorTexture = undefined; - baseColorFactor = [0.0, 0.0, 0.0, baseColorFactor[3]]; - metallicRoughnessTexture = undefined; - metallicFactor = 0.0; - roughnessFactor = 0.0; - normalTexture = undefined; - } - var gltfMaterial = { - name : name, + name : materialName, pbrMetallicRoughness : { baseColorTexture : baseColorTexture, + metallicRoughnessTexture : metallicRoughnessTexture, baseColorFactor : baseColorFactor, metallicFactor : metallicFactor, - roughnessFactor : roughnessFactor, - metallicRoughnessTexture : metallicRoughnessTexture + roughnessFactor : roughnessFactor }, - normalTexture : normalTexture, emissiveTexture : emissiveTexture, + normalTexture : normalTexture, + occlusionTexture : occlusionTexture, emissiveFactor : emissiveFactor, alphaMode : alphaMode, - doubleSided : doubleSided, - extras : { - _obj2gltf : { - hasNormals : hasNormals - } - } + doubleSided : doubleSided }; + return gltfMaterial; +} + +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 + var specularIntensity = material.specularColor[0]; + var specularShininess = material.specularShininess; + + // Transform from 0-1000 range to 0-1 range. Then invert. + var roughnessFactor = specularShininess; + roughnessFactor = roughnessFactor / 1000.0; + roughnessFactor = 1.0 - roughnessFactor; + roughnessFactor = CesiumMath.clamp(roughnessFactor, 0.0, 1.0); + + // Low specular intensity values should produce a rough material even if shininess is high. + if (specularIntensity < 0.1) { + roughnessFactor *= specularIntensity; + } + + var metallicFactor = 0.0; + + material.specularColor = [metallicFactor, metallicFactor, metallicFactor, 1.0]; + material.specularShiness = roughnessFactor; +} + +function addMaterial(gltf, images, material, options) { + var gltfMaterial; + if (options.inputSpecularGlossiness) { + gltfMaterial = createSpecularGlossinessMaterial(gltf, images, material, options); + } else if (options.inputMetallicRoughness) { + gltfMaterial = createMetallicRoughnessMaterial(gltf, images, material, options); + } else { + convertTraditionalToMetallicRoughness(material); + gltfMaterial = createMetallicRoughnessMaterial(gltf, images, material, options); + } + var materialIndex = gltf.materials.length; gltf.materials.push(gltfMaterial); return materialIndex; } - -function getMaterialIndex(gltf, name) { +function getMaterialIndex(gltf, materialName) { var materials = gltf.materials; var length = materials.length; for (var i = 0; i < length; ++i) { - if (materials[i].name === name) { + if (materials[i].name === materialName) { return i; } } return undefined; } -function getMaterial(gltf, materials, images, materialName, hasNormals, options) { +function getMaterial(gltf, materials, images, materialName, options) { if (!defined(materialName)) { // Create a default material if the primitive does not specify one materialName = 'default'; } - var material = materials[materialName]; - material = defined(material) ? material : new Material(); - var materialIndex = getMaterialIndex(gltf, materialName); - - // Check if this material has already been added but with incompatible shading - if (defined(materialIndex)) { - var gltfMaterial = gltf.materials[materialIndex]; - var normalShading = gltfMaterial.extras._obj2gltf.hasNormals; - if (hasNormals !== normalShading) { - materialName += (hasNormals ? '_shaded' : '_constant'); - materialIndex = getMaterialIndex(gltf, materialName); + var material; + var materialsLength = materials.length; + for (var i = 0; i < materialsLength; ++i) { + if (materials[i].name === materialName) { + material = materials[i]; } } + if (!defined(material)) { + material = new Material(); + material.name = materialName; + } + + var materialIndex = getMaterialIndex(gltf, materialName); + if (!defined(materialIndex)) { - materialIndex = addMaterial(gltf, images, material, materialName, hasNormals, options); + materialIndex = addMaterial(gltf, images, material, options); } return materialIndex; @@ -555,7 +750,7 @@ function addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, opti var indexAccessorIndex = addIndexArray(gltf, bufferState, primitive.indices, uint32Indices); primitive.indices = undefined; // Unload resources - var materialIndex = getMaterial(gltf, materials, images, primitive.material, hasNormals, options); + var materialIndex = getMaterial(gltf, materials, images, primitive.material, options); gltfPrimitives.push({ attributes : attributes, diff --git a/lib/loadImage.js b/lib/loadImage.js index 85007ec..58f99ab 100644 --- a/lib/loadImage.js +++ b/lib/loadImage.js @@ -36,6 +36,7 @@ function loadImage(imagePath, options) { transparent : false, source : data, extension : extension, + path : imagePath, decoded : undefined, width : undefined, height : undefined diff --git a/lib/loadMtl.js b/lib/loadMtl.js index 106202d..76aaaa0 100644 --- a/lib/loadMtl.js +++ b/lib/loadMtl.js @@ -18,14 +18,15 @@ function loadMtl(mtlPath) { var values; var value; var mtlDirectory = path.dirname(mtlPath); - var materials = {}; + var materials = []; function parseLine(line) { line = line.trim(); if (/^newmtl /i.test(line)) { var name = line.substring(7).trim(); material = new Material(); - materials[name] = material; + material.name = name; + materials.push(material); } else if (/^Ka /i.test(line)) { values = line.substring(3).trim().split(' '); material.ambientColor = [ diff --git a/lib/loadObj.js b/lib/loadObj.js index db0d007..13c6a8c 100644 --- a/lib/loadObj.js +++ b/lib/loadObj.js @@ -307,7 +307,7 @@ function loadMaterials(mtlPaths, objPath, options) { var secure = options.secure; var logger = options.logger; var objDirectory = path.dirname(objPath); - var materials = {}; + var materials = []; return Promise.map(mtlPaths, function(mtlPath) { mtlPath = path.resolve(objDirectory, mtlPath); if (secure && outsideDirectory(mtlPath, objPath)) { @@ -316,19 +316,21 @@ function loadMaterials(mtlPaths, objPath, options) { } return loadMtl(mtlPath) .then(function(materialsInMtl) { - materials = Object.assign(materials, materialsInMtl); + materials = materials.concat(materialsInMtl); }) .catch(function() { logger('Could not read mtl file at ' + mtlPath + '. Using default material instead.'); }); }, {concurrency : 10}) - .thenReturn(materials); + .then(function() { + return materials; + }); } function loadImages(imagesOptions, objPath, options) { var secure = options.secure; var logger = options.logger; - var images = {}; + var images = []; return Promise.map(imagesOptions, function(imageOptions) { var imagePath = imageOptions.imagePath; if (secure && outsideDirectory(imagePath, objPath)) { @@ -337,53 +339,54 @@ function loadImages(imagesOptions, objPath, options) { } return loadImage(imagePath, imageOptions) .then(function(image) { - images[imagePath] = image; + images.push(image); }) .catch(function() { logger('Could not read image file at ' + imagePath + '. Material will ignore this image.'); }); }, {concurrency : 10}) - .thenReturn(images); + .then(function() { + return images; + }); } function getImagesOptions(materials, options) { var imagesOptions = []; - for (var name in materials) { - if (materials.hasOwnProperty(name)) { - var material = materials[name]; - if (defined(material.ambientTexture)) { - imagesOptions.push({ - imagePath : material.ambientTexture - }); - } - if (defined(material.emissiveTexture)) { - imagesOptions.push({ - imagePath : material.emissiveTexture - }); - } - if (defined(material.diffuseTexture)) { - imagesOptions.push({ - imagePath : material.diffuseTexture, - checkTransparency : options.checkTransparency - }); - } - if (defined(material.specularTexture)) { - imagesOptions.push({ - imagePath : material.specularTexture, - decode : true - }); - } - if (defined(material.specularShininessTexture)) { - imagesOptions.push({ - imagePath : material.specularShininessTexture, - decode : true - }); - } - if (defined(material.normalTexture)) { - imagesOptions.push({ - imagePath : material.normalTexture - }); - } + var materialsLength = materials.length; + for (var i = 0; i < materialsLength; ++i) { + var material = materials[i]; + if (defined(material.ambientTexture)) { + imagesOptions.push({ + imagePath : material.ambientTexture + }); + } + if (defined(material.emissiveTexture)) { + imagesOptions.push({ + imagePath : material.emissiveTexture + }); + } + if (defined(material.diffuseTexture)) { + imagesOptions.push({ + imagePath : material.diffuseTexture, + checkTransparency : options.checkTransparency + }); + } + if (defined(material.specularTexture)) { + imagesOptions.push({ + imagePath : material.specularTexture, + decode : true + }); + } + if (defined(material.specularShininessTexture)) { + imagesOptions.push({ + imagePath : material.specularShininessTexture, + decode : true + }); + } + if (defined(material.normalTexture)) { + imagesOptions.push({ + imagePath : material.normalTexture + }); } } return imagesOptions; diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index c362989..f998f1a 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -35,6 +35,9 @@ module.exports = obj2gltf; * @param {Boolean} [options.bypassPipeline=false] Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above. * @param {Boolean} [options.checkTransparency=false] Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. * @param {Boolean} [options.secure=false] Prevent the converter from reading image or mtl files outside of the input obj directory. + * @param {Boolean} [options.packOcclusion=false] Pack the occlusion texture in the red channel of metallic-roughness texture. + * @param {Boolean} [options.inputMetallicRoughness=false] The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots. + * @param {Boolean} [options.inputSpecularGlossiness=false] The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension. * @param {Logger} [options.logger] A callback function for handling logged messages. Defaults to console.log. */ function obj2gltf(objPath, gltfPath, options) { @@ -53,15 +56,20 @@ function obj2gltf(objPath, gltfPath, options) { var bypassPipeline = defaultValue(options.bypassPipeline, defaults.bypassPipeline); var checkTransparency = defaultValue(options.checkTransparency, defaults.checkTransparency); var secure = defaultValue(options.secure, defaults.secure); + var packOcclusion = defaultValue(options.packOcclusion, defaults.packOcclusion); + var inputMetallicRoughness = defaultValue(options.inputMetallicRoughness, defaults.inputMetallicRoughness); + var inputSpecularGlossiness = defaultValue(options.inputSpecularGlossiness, defaults.inputSpecularGlossiness); var logger = defaultValue(options.logger, defaults.logger); options.separate = separate; options.separateTextures = separateTextures; options.checkTransparency = checkTransparency; options.secure = secure; + options.packOcclusion = packOcclusion; + options.inputMetallicRoughness = inputMetallicRoughness; + options.inputSpecularGlossiness = inputSpecularGlossiness; options.logger = logger; - if (!defined(objPath)) { throw new DeveloperError('objPath is required'); } @@ -81,6 +89,10 @@ function obj2gltf(objPath, gltfPath, options) { throw new DeveloperError('--bypassPipeline does not convert to binary glTF'); } + if (inputMetallicRoughness && inputSpecularGlossiness) { + throw new DeveloperError('--inputMetallicRoughness and --inputSpecularGlossiness cannot both be set.'); + } + gltfPath = path.join(path.dirname(gltfPath), modelName + extension); var aoOptions = ao ? {} : undefined; @@ -201,6 +213,25 @@ obj2gltf.defaults = { * @default false */ secure: false, + /** + * Gets or sets whether to pack the occlusion texture in the red channel of the metallic-roughness texture. + * @type Boolean + * @default false + */ + packOcclusion: 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. + * @type Boolean + * @default false + */ + inputMetallicRoughness: 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. + * @type Boolean + * @default false + */ + inputSpecularGlossiness: false, + /** * @private */ diff --git a/lib/writeUris.js b/lib/writeUris.js index d05715c..8bf4955 100644 --- a/lib/writeUris.js +++ b/lib/writeUris.js @@ -61,6 +61,7 @@ function writeUris(gltf, gltfPath, options) { return Promise.all(promises) .then(function() { deleteExtras(gltf); + cleanup(gltf); return gltf; }); } @@ -82,6 +83,18 @@ function deleteExtras(gltf) { } } +function cleanup(gltf) { + // Remove empty arrays from top-level items + for (var key in gltf) { + if (gltf.hasOwnProperty(key)) { + var property = gltf[key]; + if (Array.isArray(property) && property.length === 0) { + delete gltf[key]; + } + } + } +} + function writeSeparateBuffer(gltf, gltfPath) { var buffer = gltf.buffers[0]; var source = buffer.extras._obj2gltf.source; diff --git a/package.json b/package.json index b03c260..56f87bf 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "gltf-pipeline": "^0.1.0-alpha11", "jpeg-js": "^0.2.0", "mime": "^1.3.4", - "pngjs": "^3.0.1", + "pngjs": "^3.2.0", "yargs": "^7.0.1" }, "devDependencies": {