From ce5221c80a425a3bc2729b3a2c07f7f46ad0f41e Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 18 Apr 2017 11:56:08 -0400 Subject: [PATCH 01/21] 2.0 --- lib/Material.js | 10 +- lib/createGltf.js | 804 ++++++++++++------- lib/loadImage.js | 97 ++- lib/loadMtl.js | 10 +- lib/loadObj.js | 50 +- lib/obj2gltf.js | 2 +- lib/writeUris.js | 43 +- package.json | 1 + specs/lib/{gltfSpec.js => createGltfSpec.js} | 38 +- specs/lib/{imageSpec.js => loadImageSpec.js} | 8 +- specs/lib/{mtlSpec.js => loadMtlSpec.js} | 12 +- specs/lib/{objSpec.js => loadObjSpec.js} | 2 +- 12 files changed, 676 insertions(+), 401 deletions(-) rename specs/lib/{gltfSpec.js => createGltfSpec.js} (92%) rename specs/lib/{imageSpec.js => loadImageSpec.js} (88%) rename specs/lib/{mtlSpec.js => loadMtlSpec.js} (79%) rename specs/lib/{objSpec.js => loadObjSpec.js} (99%) diff --git a/lib/Material.js b/lib/Material.js index 60d284dc..6a074ac1 100644 --- a/lib/Material.js +++ b/lib/Material.js @@ -4,16 +4,16 @@ 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.emissiveColor = [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.emissiveTexture = 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 + this.specularShininessTexture = undefined; // map_Ns + this.normalTexture = undefined; // map_Bump + this.alphaTexture = undefined; // map_d } diff --git a/lib/createGltf.js b/lib/createGltf.js index eb6aa7c3..c16d4a3c 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -1,6 +1,7 @@ 'use strict'; var Cesium = require('cesium'); var path = require('path'); +var PNG = require('pngjs').PNG; var Material = require('./Material'); var defined = Cesium.defined; @@ -13,328 +14,585 @@ module.exports = createGltf; * Create a glTF from obj data. * * @param {Object} objData Output of obj.js, containing an array of nodes containing geometry information, materials, and images. - * @returns {Object} A glTF asset with the KHR_materials_common extension. + * @param {Object} options An object with the following properties: + * @param {Boolean} options.logger A callback function for handling logged messages. Defaults to console.log. + * @returns {Object} A glTF asset. * * @private */ -function createGltf(objData) { +function createGltf(objData, options) { var nodes = objData.nodes; var materials = objData.materials; var images = objData.images; - var sceneId = 'scene'; - var samplerId = 'sampler'; - var bufferId = 'buffer'; - var vertexBufferViewId = 'bufferView_vertex'; - var indexBufferViewId = 'bufferView_index'; var gltf = { - accessors : {}, + accessors : [], asset : {}, - buffers : {}, - bufferViews : {}, - extensionsUsed : ['KHR_materials_common'], - images : {}, - materials : {}, - meshes : {}, - nodes : {}, - samplers : {}, - scene : sceneId, - scenes : {}, - textures : {} + buffers : [], + bufferViews : [], + images : [], + materials : [], + meshes : [], + nodes : [], + samplers : [], + scene : 0, + scenes : [], + textures : [] }; gltf.asset = { generator : 'obj2gltf', - profile : { - api : 'WebGL', - version : '1.0' - }, - version: '1.0' + version: '2.0' }; - gltf.scenes[sceneId] = { + gltf.scenes.push({ nodes : [] + }); + + var bufferState = { + vertexBuffers : [], + vertexBufferByteOffset : 0, + vertexBufferViewIndex : 0, + indexBuffers : [], + indexBufferByteOffset : 0, + indexBufferViewIndex : 1 }; - function getImageId(imagePath) { - return path.basename(imagePath, path.extname(imagePath)); - } - - function getTextureId(imagePath) { - if (!defined(imagePath) || !defined(images[imagePath])) { - return undefined; - } - return 'texture_' + getImageId(imagePath); - } - - function createMaterial(material, hasNormals) { - 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); - - var transparent; - var transparency = 1.0; - if (typeof diffuse === 'string') { - transparency = alpha; - transparent = images[material.diffuseTexture].transparent || (transparency < 1.0); - } else { - diffuse[3] = alpha; - transparent = diffuse[3] < 1.0; - } - - var doubleSided = transparent; + var uint32Indices = requiresUint32Indices(nodes); - if (!hasNormals) { - // Constant technique only factors in ambient and emission sources - set emission to diffuse - emission = diffuse; - diffuse = [0, 0, 0, 1]; - } + var nodesLength = nodes.length; + for (var i = 0; i < nodesLength; ++i) { + var node = nodes[i]; + var meshes = node.meshes; + var meshesLength = meshes.length; + var meshIndex; - var technique = hasNormals ? (hasSpecular ? 'PHONG' : 'LAMBERT') : 'CONSTANT'; - return { - extensions : { - KHR_materials_common : { - technique : technique, - transparent : transparent, - doubleSided : doubleSided, - values : { - ambient : ambient, - diffuse : diffuse, - emission : emission, - specular : specular, - shininess : shininess, - transparency : transparency, - transparent : transparent, - doubleSided : doubleSided - } - } + if (meshesLength === 1) { + meshIndex = addMesh(gltf, materials, images, bufferState, uint32Indices, meshes[0], options); + addNode(gltf, node.name, meshIndex); + } else { + // Add meshes as child nodes + var parentIndex = addNode(gltf, node.name); + for (var j = 0; j < meshesLength; ++j) { + var mesh = meshes[j]; + meshIndex = addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, options); + addNode(gltf, mesh.name, meshIndex, parentIndex); } - }; + } } - if (Object.keys(images).length > 0) { - gltf.samplers[samplerId] = { + if (Object.keys(gltf.images).length > 0) { + gltf.samplers.push({ magFilter : WebGLConstants.LINEAR, minFilter : WebGLConstants.LINEAR, wrapS : WebGLConstants.REPEAT, wrapT : WebGLConstants.REPEAT - }; - } - - for (var imagePath in images) { - if (images.hasOwnProperty(imagePath)) { - var image = images[imagePath]; - var imageId = getImageId(imagePath); - var textureId = getTextureId(imagePath); - - gltf.images[imageId] = { - name : imageId, - extras : { - _obj2gltf : { - source : image.source, - extension : image.extension - } - } - }; - gltf.textures[textureId] = { - format : image.format, - internalFormat : image.format, - sampler : samplerId, - source : imageId, - target : WebGLConstants.TEXTURE_2D, - type : WebGLConstants.UNSIGNED_BYTE - }; - } + }); } - var vertexBuffers = []; - var vertexBufferByteOffset = 0; - var indexBuffers = []; - var indexBufferByteOffset = 0; - var accessorCount = 0; - - function addVertexAttribute(array, components) { - var count = array.length / components; - var buffer = array.toFloatBuffer(); - var minMax = array.getMinMax(components); - - var type = (components === 3 ? 'VEC3' : 'VEC2'); - var accessor = { - bufferView : vertexBufferViewId, - byteOffset : vertexBufferByteOffset, - byteStride : 0, - componentType : WebGLConstants.FLOAT, - count : count, - min : minMax.min, - max : minMax.max, - type : type - }; - - vertexBufferByteOffset += buffer.length; - vertexBuffers.push(buffer); - var accessorId = 'accessor_' + accessorCount++; - gltf.accessors[accessorId] = accessor; - return accessorId; - } - - function addIndexArray(array, uint32Indices) { - var buffer = uint32Indices ? array.toUint32Buffer() : array.toUint16Buffer(); - var componentType = uint32Indices ? WebGLConstants.UNSIGNED_INT : WebGLConstants.UNSIGNED_SHORT; - var length = array.length; - var minMax = array.getMinMax(1); - var accessor = { - bufferView : indexBufferViewId, - byteOffset : indexBufferByteOffset, - byteStride : 0, - componentType : componentType, - count : length, - min : minMax.min, - max : minMax.max, - type : 'SCALAR' - }; - - indexBufferByteOffset += buffer.length; - indexBuffers.push(buffer); - - var accessorId = 'accessor_' + accessorCount++; - gltf.accessors[accessorId] = accessor; - return accessorId; - } - - function requiresUint32Indices(nodes) { - var nodesLength = nodes.length; - for (var i = 0; i < nodesLength; ++i) { - var meshes = nodes[i].meshes; - var meshesLength = meshes.length; - for (var j = 0; j < meshesLength; ++j) { - // Reserve the 65535 index for primitive restart - var vertexCount = meshes[j].positions.length / 3; - if (vertexCount > 65534) { - return true; - } + addBuffers(gltf, bufferState); + + return gltf; +} + +function addBuffers(gltf, bufferState) { + var bufferName = 'buffer'; + var vertexBufferViewName = 'bufferView_vertex'; + var indexBufferViewName = 'bufferView_index'; + + var vertexBuffers = bufferState.vertexBuffers; + var indexBuffers = bufferState.indexBuffers; + var vertexBufferByteLength = bufferState.vertexBufferByteOffset; + var indexBufferByteLength = bufferState.indexBufferByteOffset; + + var buffers = []; + buffers = buffers.concat(vertexBuffers, indexBuffers); + var buffer = Buffer.concat(buffers); + + gltf.buffers.push({ + name : bufferName, + byteLength : buffer.byteLength, + extras : { + _obj2gltf : { + source : buffer } } - return false; + }); + + gltf.bufferViews.push({ + name : vertexBufferViewName, + buffer : 0, + byteLength : vertexBufferByteLength, + byteOffset : 0, + target : WebGLConstants.ARRAY_BUFFER + }); + + gltf.bufferViews.push({ + name : indexBufferViewName, + buffer : 0, + byteLength : indexBufferByteLength, + byteOffset : vertexBufferByteLength, + target : WebGLConstants.ELEMENT_ARRAY_BUFFER + }); +} + +function getImage(images, imagePath) { + if (!defined(imagePath) || !defined(images[imagePath])) { + return undefined; } + return images[imagePath]; +} - var uint32Indices = requiresUint32Indices(nodes); - var gltfSceneNodes = gltf.scenes[sceneId].nodes; - var nodesLength = nodes.length; - for (var i = 0; i < nodesLength; ++i) { - // Add node - var node = nodes[i]; - var nodeId = node.name; - gltfSceneNodes.push(nodeId); - var gltfNodeMeshes = []; - gltf.nodes[nodeId] = { - name : nodeId, - meshes : gltfNodeMeshes - }; - - // Add meshes to node - var meshes = node.meshes; - var meshesLength = meshes.length; - for (var j = 0; j < meshesLength; ++j) { - var mesh = meshes[j]; - var meshId = mesh.name; - gltfNodeMeshes.push(meshId); +function getImageName(imagePath) { + return path.basename(imagePath, path.extname(imagePath)); +} - var hasPositions = mesh.positions.length > 0; - var hasNormals = mesh.normals.length > 0; - var hasUVs = mesh.uvs.length > 0; +function getTextureName(imagePath) { + return getImageName(imagePath); +} - var attributes = {}; - if (hasPositions) { - attributes.POSITION = addVertexAttribute(mesh.positions, 3); - } - if (hasNormals) { - attributes.NORMAL = addVertexAttribute(mesh.normals, 3); - } - if (hasUVs) { - attributes.TEXCOORD_0 = addVertexAttribute(mesh.uvs, 2); - } +function addTexture(gltf, image, imagePath) { + var imageName = getImageName(imagePath); + var textureName = getTextureName(imagePath); + var imageIndex = gltf.images.length; + var textureIndex = gltf.textures.length; - // Unload resources - mesh.positions = undefined; - mesh.normals = undefined; - mesh.uvs = undefined; - - var gltfMeshPrimitives = []; - gltf.meshes[meshId] = { - name : meshId, - primitives : gltfMeshPrimitives - }; - - // Add primitives to mesh - var primitives = mesh.primitives; - var primitivesLength = primitives.length; - for (var k = 0; k < primitivesLength; ++k) { - var primitive = primitives[k]; - var indexAccessorId = addIndexArray(primitive.indices, uint32Indices); - primitive.indices = undefined; // Unload resources - var materialId = primitive.material; - - if (!defined(materialId)) { - // Create a default material if the primitive does not specify one - materialId = 'default'; - } - - 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 - var normalShading = (gltfMaterial.extensions.KHR_materials_common.technique !== 'CONSTANT'); - if (hasNormals !== normalShading) { - materialId += (hasNormals ? '_shaded' : '_constant'); - gltfMaterial = gltf.materials[materialId]; - } - } - - if (!defined(gltfMaterial)) { - gltf.materials[materialId] = createMaterial(material, hasNormals); - } - - gltfMeshPrimitives.push({ - attributes : attributes, - indices : indexAccessorId, - material : materialId, - mode : WebGLConstants.TRIANGLES - }); + gltf.images.push({ + name : imageName, + extras : { + _obj2gltf : { + source : image.source, + extension : image.extension } } + }); + + gltf.textures.push({ + name : textureName, + sampler : 0, + source : imageIndex + }); + + return textureIndex; +} + +function getTextureIndex(gltf, imagePath) { + var name = getTextureName(imagePath); + var textures = gltf.textures; + var length = textures.length; + for (var i = 0; i < length; ++i) { + if (textures[i].name === name) { + return i; + } } +} - var buffers = []; - buffers = buffers.concat(vertexBuffers, indexBuffers); - var buffer = Buffer.concat(buffers); +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); + } + return textureIndex; +} - gltf.buffers[bufferId] = { - byteLength : buffer.byteLength, +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); + var blue = Math.min(left[2] + right[2], 1.0); + return [red, green, blue]; +} + +function resizeChannel(sourcePixels, sourceWidth, sourceHeight, targetWidth, targetHeight) { + // Nearest neighbor sampling + var targetPixels = Buffer.alloc(targetWidth * targetHeight); + var widthRatio = sourceWidth / targetWidth; + var heightRatio = sourceHeight / targetHeight; + + for (var y = 0; y < targetHeight; ++y) { + for (var x = 0; x < targetWidth; ++x) { + var targetIndex = y * targetWidth + x; + var sourceY = Math.round(y * heightRatio); + var sourceX = Math.round(x * widthRatio); + var sourceIndex = sourceY * sourceWidth + sourceX; + var sourceValue = sourcePixels.readUInt8(sourceIndex); + targetPixels.writeUInt8(sourceValue, targetIndex); + } + } + return targetPixels; +} + +var scratchColor = new Array(3); + +function getGrayscaleChannel(image, targetWidth, targetHeight) { + var pixels = image.decoded; // RGBA + var width = image.width; + var height = image.height; + var pixelsLength = width * height; + var grayPixels = 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); + } + if (width !== targetWidth || height !== targetHeight) { + grayPixels = resizeChannel(grayPixels, width, height, targetWidth, targetHeight); + } + return grayPixels; +} + +function writeChannel(pixels, channel, index, width, height) { + var pixelsLength = width * height; + for (var i = 0; i < pixelsLength; ++i) { + var value = channel.readUInt8(i); + pixels.writeUInt8(value, i * 4 + index); + } +} + +function createMetallicRoughnessTexture(gltf, materialName, metallicImage, roughnessImage, options) { + if (!defined(metallicImage) && !defined(roughnessImage)) { + return undefined; + } + + 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; + } + + 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); + } + + var pngInput = { + data : pixels, + width : width, + height : height + }; + + var pngOptions = { + width : width, + height : height, + colorType : 2, // RGB + inputHasAlpha : true + }; + + var encoded = PNG.sync.write(pngInput, pngOptions); + + var image = { + transparent : false, + source : encoded, + extension : '.png' + }; + + var imageName = materialName + '-' + 'MetallicRoughness'; + return addTexture(gltf, image, imageName); +} + +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); + + // Emissive and ambient represent roughly the same concept, so chose whichever is defined. + emissiveTexture = defaultValue(emissiveTexture, ambientTexture); + + 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]; + + if (!defined(baseColorTexture)) { + baseColorFactor = material.diffuseColor; + } + + if (!defined(metallicImage)) { + metallicFactor = luminance(material.specularColor); + } + + if (!defined(roughnessImage)) { + var specularShininess = material.specularShininess; + if (specularShininess > 1.0) { + specularShininess /= 1000.0; + } + roughnessFactor = specularShininess; + } + + 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); + } + + var alpha = material.alpha; + baseColorFactor[3] = alpha; + + var transparent = alpha < 1.0; + if (defined(material.diffuseTexture)) { + transparent |= images[material.diffuseTexture].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, + pbrMetallicRoughness : { + baseColorTexture : baseColorTexture, + baseColorFactor : baseColorFactor, + metallicFactor : metallicFactor, + roughnessFactor : roughnessFactor, + metallicRoughnessTexture : metallicRoughnessTexture + }, + normalTexture : normalTexture, + emissiveTexture : emissiveTexture, + emissiveFactor : emissiveFactor, + alphaMode : alphaMode, + doubleSided : doubleSided, extras : { _obj2gltf : { - source : buffer + hasNormals : hasNormals } } }; - gltf.bufferViews[vertexBufferViewId] = { - buffer : bufferId, - byteLength : vertexBufferByteOffset, - byteOffset : 0, - target : WebGLConstants.ARRAY_BUFFER + var materialIndex = gltf.materials.length; + gltf.materials.push(gltfMaterial); + return materialIndex; +} + + +function getMaterialIndex(gltf, name) { + var materials = gltf.materials; + var length = materials.length; + for (var i = 0; i < length; ++i) { + if (materials[i].name === name) { + return i; + } + } + return undefined; +} + +function getMaterial(gltf, materials, images, materialName, hasNormals, 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); + } + } + + if (!defined(materialIndex)) { + materialIndex = addMaterial(gltf, images, material, materialName, hasNormals, options); + } + + return materialIndex; +} + +function addVertexAttribute(gltf, bufferState, array, components) { + var buffer = array.toFloatBuffer(); + var count = array.length / components; + var minMax = array.getMinMax(components); + var type = (components === 3 ? 'VEC3' : 'VEC2'); + + var accessor = { + bufferView : bufferState.vertexBufferViewIndex, + byteOffset : bufferState.vertexBufferByteOffset, + componentType : WebGLConstants.FLOAT, + count : count, + min : minMax.min, + max : minMax.max, + type : type }; - gltf.bufferViews[indexBufferViewId] = { - buffer : bufferId, - byteLength : indexBufferByteOffset, - byteOffset : vertexBufferByteOffset, - target : WebGLConstants.ELEMENT_ARRAY_BUFFER + bufferState.vertexBufferByteOffset += buffer.length; + bufferState.vertexBuffers.push(buffer); + + var accessorIndex = gltf.accessors.length; + gltf.accessors.push(accessor); + return accessorIndex; +} + +function addIndexArray(gltf, bufferState, array, uint32Indices) { + var buffer = uint32Indices ? array.toUint32Buffer() : array.toUint16Buffer(); + var componentType = uint32Indices ? WebGLConstants.UNSIGNED_INT : WebGLConstants.UNSIGNED_SHORT; + var count = array.length; + var minMax = array.getMinMax(1); + + var accessor = { + bufferView : bufferState.indexBufferViewIndex, + byteOffset : bufferState.indexBufferByteOffset, + componentType : componentType, + count : count, + min : minMax.min, + max : minMax.max, + type : 'SCALAR' }; - return gltf; + bufferState.indexBufferByteOffset += buffer.length; + bufferState.indexBuffers.push(buffer); + + var accessorIndex = gltf.accessors.length; + gltf.accessors.push(accessor); + return accessorIndex; +} + +function requiresUint32Indices(nodes) { + var nodesLength = nodes.length; + for (var i = 0; i < nodesLength; ++i) { + var meshes = nodes[i].meshes; + var meshesLength = meshes.length; + for (var j = 0; j < meshesLength; ++j) { + // Reserve the 65535 index for primitive restart + var vertexCount = meshes[j].positions.length / 3; + if (vertexCount > 65534) { + return true; + } + } + } + return false; +} + +function addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, options) { + var hasPositions = mesh.positions.length > 0; + var hasNormals = mesh.normals.length > 0; + var hasUVs = mesh.uvs.length > 0; + + var attributes = {}; + if (hasPositions) { + attributes.POSITION = addVertexAttribute(gltf, bufferState, mesh.positions, 3); + } + if (hasNormals) { + attributes.NORMAL = addVertexAttribute(gltf, bufferState, mesh.normals, 3); + } + if (hasUVs) { + attributes.TEXCOORD_0 = addVertexAttribute(gltf, bufferState, mesh.uvs, 2); + } + + // Unload resources + mesh.positions = undefined; + mesh.normals = undefined; + mesh.uvs = undefined; + + var gltfPrimitives = []; + var primitives = mesh.primitives; + var primitivesLength = primitives.length; + for (var i = 0; i < primitivesLength; ++i) { + var primitive = primitives[i]; + var indexAccessorIndex = addIndexArray(gltf, bufferState, primitive.indices, uint32Indices); + primitive.indices = undefined; // Unload resources + + var materialIndex = getMaterial(gltf, materials, images, primitive.material, hasNormals, options); + + gltfPrimitives.push({ + attributes : attributes, + indices : indexAccessorIndex, + material : materialIndex, + mode : WebGLConstants.TRIANGLES + }); + } + + var gltfMesh = { + name : mesh.name, + primitives : gltfPrimitives + }; + + var meshIndex = gltf.meshes.length; + gltf.meshes.push(gltfMesh); + return meshIndex; +} + +function addNode(gltf, name, meshIndex, parentIndex) { + var node = { + name : name, + mesh : meshIndex + }; + + var nodeIndex = gltf.nodes.length; + gltf.nodes.push(node); + + if (defined(parentIndex)) { + var parentNode = gltf.nodes[parentIndex]; + if (!defined(parentNode.children)) { + parentNode.children = []; + } + parentNode.children.push(nodeIndex); + } else { + gltf.scenes[gltf.scene].nodes.push(nodeIndex); + } + + return nodeIndex; } diff --git a/lib/loadImage.js b/lib/loadImage.js index 8a3b6867..85007ec9 100644 --- a/lib/loadImage.js +++ b/lib/loadImage.js @@ -1,14 +1,14 @@ 'use strict'; var Cesium = require('cesium'); var fsExtra = require('fs-extra'); +var jpeg = require('jpeg-js'); var path = require('path'); var PNG = require('pngjs').PNG; var Promise = require('bluebird'); var fsExtraReadFile = Promise.promisify(fsExtra.readFile); -var defined = Cesium.defined; -var WebGLConstants = Cesium.WebGLConstants; +var defaultValue = Cesium.defaultValue; module.exports = loadImage; @@ -17,67 +17,49 @@ module.exports = loadImage; * * @param {String} imagePath Path to the image file. * @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.checkTransparency=false] Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. + * @param {Boolean} [options.decode=false] Decode image. * @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, {}); + options.checkTransparency = defaultValue(options.checkTransparency, false); + options.decode = defaultValue(options.decode, false); + return fsExtraReadFile(imagePath) .then(function(data) { var extension = path.extname(imagePath).toLowerCase(); var info = { transparent : false, - format : getFormat(3), source : data, - extension : extension + extension : extension, + decoded : undefined, + width : undefined, + height : undefined }; if (extension === '.png') { return getPngInfo(data, info, options); + } else if (extension === '.jpg' || extension === '.jpeg') { + return getJpegInfo(data, info, options); } return info; }); } -function getPngInfo(data, info, options) { - // Color type is encoded in the 25th bit of the png - var colorType = data[25]; - var channels = getChannels(colorType); - info.format = getFormat(channels); - - if (channels === 4) { - if (options.checkTransparency) { - return isTransparent(data) - .then(function(transparent) { - info.transparent = transparent; - return info; - }); +function hasTransparency(info) { + var pixels = info.decoded; + var pixelsLength = info.width * info.height; + for (var i = 0; i < pixelsLength; ++i) { + if (pixels[i * 4 + 3] < 255) { + return true; } } - return info; -} - -function isTransparent(data) { - return new Promise(function(resolve, reject) { - new PNG().parse(data, function(error, data) { - if (defined(error)) { - reject(error); - return; - } - var pixels = data.data; - var pixelsLength = data.width * data.height; - for (var i = 0; i < pixelsLength; ++i) { - if (pixels[i * 4 + 3] < 255) { - resolve(true); - return; - } - } - resolve(false); - }); - }); + return false; } function getChannels(colorType) { @@ -95,15 +77,32 @@ function getChannels(colorType) { } } -function getFormat(channels) { - switch (channels) { - case 1: - return WebGLConstants.ALPHA; - case 2: - return WebGLConstants.LUMINANCE_ALPHA; - case 3: - return WebGLConstants.RGB; - case 4: - return WebGLConstants.RGBA; +function getPngInfo(data, info, options) { + // Color type is encoded in the 25th bit of the png + var colorType = data[25]; + var channels = getChannels(colorType); + + var checkTransparency = (channels === 4 && options.checkTransparency); + 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 info; +} + +function getJpegInfo(data, info, options) { + if (options.decode) { + var decodedResults = jpeg.decode(data); + info.decoded = decodedResults.data; + info.width = decodedResults.width; + info.height = decodedResults.height; } + return info; } diff --git a/lib/loadMtl.js b/lib/loadMtl.js index 6d3f506e..106202d8 100644 --- a/lib/loadMtl.js +++ b/lib/loadMtl.js @@ -36,7 +36,7 @@ function loadMtl(mtlPath) { ]; } else if (/^Ke /i.test(line)) { values = line.substring(3).trim().split(' '); - material.emissionColor = [ + material.emissiveColor = [ parseFloat(values[0]), parseFloat(values[1]), parseFloat(values[2]), @@ -70,17 +70,17 @@ function loadMtl(mtlPath) { } else if (/^map_Ka /i.test(line)) { material.ambientTexture = path.resolve(mtlDirectory, line.substring(7).trim()); } else if (/^map_Ke /i.test(line)) { - material.emissionTexture = path.resolve(mtlDirectory, line.substring(7).trim()); + material.emissiveTexture = path.resolve(mtlDirectory, line.substring(7).trim()); } else if (/^map_Kd /i.test(line)) { material.diffuseTexture = path.resolve(mtlDirectory, line.substring(7).trim()); } else if (/^map_Ks /i.test(line)) { material.specularTexture = path.resolve(mtlDirectory, line.substring(7).trim()); } else if (/^map_Ns /i.test(line)) { - material.specularShininessMap = path.resolve(mtlDirectory, line.substring(7).trim()); + material.specularShininessTexture = path.resolve(mtlDirectory, line.substring(7).trim()); } else if (/^map_Bump /i.test(line)) { - material.normalMap = path.resolve(mtlDirectory, line.substring(9).trim()); + material.normalTexture = path.resolve(mtlDirectory, line.substring(9).trim()); } else if (/^map_d /i.test(line)) { - material.alphaMap = path.resolve(mtlDirectory, line.substring(6).trim()); + material.alphaTexture = path.resolve(mtlDirectory, line.substring(6).trim()); } } diff --git a/lib/loadObj.js b/lib/loadObj.js index 09c8afcb..db0d0074 100644 --- a/lib/loadObj.js +++ b/lib/loadObj.js @@ -287,8 +287,8 @@ function finishLoading(nodes, mtlPaths, objPath, options) { } return loadMaterials(mtlPaths, objPath, options) .then(function(materials) { - var imagePaths = getImagePaths(materials); - return loadImages(imagePaths, objPath, options) + var imagesOptions = getImagesOptions(materials, options); + return loadImages(imagesOptions, objPath, options) .then(function(images) { return { nodes : nodes, @@ -325,16 +325,17 @@ function loadMaterials(mtlPaths, objPath, options) { .thenReturn(materials); } -function loadImages(imagePaths, objPath, options) { +function loadImages(imagesOptions, objPath, options) { var secure = options.secure; var logger = options.logger; var images = {}; - return Promise.map(imagePaths, function(imagePath) { + return Promise.map(imagesOptions, function(imageOptions) { + var imagePath = imageOptions.imagePath; 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) + return loadImage(imagePath, imageOptions) .then(function(image) { images[imagePath] = image; }) @@ -345,26 +346,47 @@ function loadImages(imagePaths, objPath, options) { .thenReturn(images); } -function getImagePaths(materials) { - var imagePaths = {}; +function getImagesOptions(materials, options) { + var imagesOptions = []; for (var name in materials) { if (materials.hasOwnProperty(name)) { var material = materials[name]; if (defined(material.ambientTexture)) { - imagePaths[material.ambientTexture] = true; + imagesOptions.push({ + imagePath : material.ambientTexture + }); } - if (defined(material.diffuseTexture)) { - imagePaths[material.diffuseTexture] = true; + if (defined(material.emissiveTexture)) { + imagesOptions.push({ + imagePath : material.emissiveTexture + }); } - if (defined(material.emissionTexture)) { - imagePaths[material.emissionTexture] = true; + if (defined(material.diffuseTexture)) { + imagesOptions.push({ + imagePath : material.diffuseTexture, + checkTransparency : options.checkTransparency + }); } if (defined(material.specularTexture)) { - imagePaths[material.specularTexture] = true; + 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 Object.keys(imagePaths); + return imagesOptions; } function removeEmptyMeshes(meshes) { diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index d6c74fc5..eeffc243 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -107,7 +107,7 @@ function obj2gltf(objPath, gltfPath, options) { return loadObj(objPath, options) .then(function(objData) { - return createGltf(objData); + return createGltf(objData, options); }) .then(function(gltf) { return writeUris(gltf, gltfPath, options); diff --git a/lib/writeUris.js b/lib/writeUris.js index 53e23c39..d05715c6 100644 --- a/lib/writeUris.js +++ b/lib/writeUris.js @@ -29,15 +29,14 @@ function writeUris(gltf, gltfPath, options) { var promises = []; - var buffer = gltf.buffers[Object.keys(gltf.buffers)[0]]; + var buffer = gltf.buffers[0]; var bufferByteLength = buffer.extras._obj2gltf.source.length; var texturesByteLength = 0; var images = gltf.images; - for (var id in images) { - if (images.hasOwnProperty(id)) { - texturesByteLength += images[id].extras._obj2gltf.source.length; - } + 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 @@ -67,20 +66,24 @@ function writeUris(gltf, gltfPath, options) { } function deleteExtras(gltf) { - var buffer = gltf.buffers[Object.keys(gltf.buffers)[0]]; + var buffer = gltf.buffers[0]; delete buffer.extras; var images = gltf.images; - for (var id in images) { - if (images.hasOwnProperty(id)) { - var image = images[id]; - delete image.extras; - } + var imagesLength = images.length; + for (var i = 0; i < imagesLength; ++i) { + delete images[i].extras; + } + + var materials = gltf.materials; + var materialsLength = materials.length; + for (var j = 0; j < materialsLength; ++j) { + delete materials[j].extras; } } function writeSeparateBuffer(gltf, gltfPath) { - var buffer = gltf.buffers[Object.keys(gltf.buffers)[0]]; + var buffer = gltf.buffers[0]; var source = buffer.extras._obj2gltf.source; var bufferName = path.basename(gltfPath, path.extname(gltfPath)); var bufferUri = bufferName + '.bin'; @@ -91,8 +94,7 @@ function writeSeparateBuffer(gltf, gltfPath) { function writeSeparateTextures(gltf, gltfPath) { var images = gltf.images; - return Promise.map(Object.keys(images), function(id) { - var image = images[id]; + return Promise.map(images, function(image) { var extras = image.extras._obj2gltf; var imageUri = image.name + extras.extension; image.uri = imageUri; @@ -102,19 +104,18 @@ function writeSeparateTextures(gltf, gltfPath) { } function writeEmbeddedBuffer(gltf) { - var buffer = gltf.buffers[Object.keys(gltf.buffers)[0]]; + var buffer = gltf.buffers[0]; var source = buffer.extras._obj2gltf.source; buffer.uri = 'data:application/octet-stream;base64,' + source.toString('base64'); } function writeEmbeddedTextures(gltf) { var images = gltf.images; - for (var id in images) { - if (images.hasOwnProperty(id)) { - var image = images[id]; - var extras = image.extras._obj2gltf; - image.uri = 'data:' + mime.lookup(extras.extension) + ';base64,' + extras.source.toString('base64'); - } + var imagesLength = images.length; + for (var i = 0; i < imagesLength; ++i) { + var image = images[i]; + var extras = image.extras._obj2gltf; + image.uri = 'data:' + mime.lookup(extras.extension) + ';base64,' + extras.source.toString('base64'); } } diff --git a/package.json b/package.json index 74fd41fc..b03c2606 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "event-stream": "^3.3.4", "fs-extra": "^2.0.0", "gltf-pipeline": "^0.1.0-alpha11", + "jpeg-js": "^0.2.0", "mime": "^1.3.4", "pngjs": "^3.0.1", "yargs": "^7.0.1" diff --git a/specs/lib/gltfSpec.js b/specs/lib/createGltfSpec.js similarity index 92% rename from specs/lib/gltfSpec.js rename to specs/lib/createGltfSpec.js index 84d36928..c75084cd 100644 --- a/specs/lib/gltfSpec.js +++ b/specs/lib/createGltfSpec.js @@ -26,7 +26,7 @@ var defaultOptions = obj2gltf.defaults; var checkTransparencyOptions = clone(defaultOptions); checkTransparencyOptions.checkTransparency = true; -describe('gltf', function() { +describe('createGltf', function() { var boxObjData; var duplicateBoxObjData; var groupObjData; @@ -69,7 +69,7 @@ describe('gltf', function() { }); it('simple gltf', function(done) { - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); expect(writeUris(gltf, boxGltfUrl, defaultOptions) .then(function() { expect(gltf).toEqual(boxGltf); @@ -77,7 +77,7 @@ describe('gltf', function() { }); it('multiple nodes, meshes, and primitives', function(done) { - var gltf = createGltf(groupObjData); + var gltf = createGltf(groupObjData, defaultOptions); expect(writeUris(gltf, groupGltfUrl, defaultOptions) .then(function() { @@ -99,7 +99,7 @@ describe('gltf', function() { it('sets default material values', function() { boxObjData.materials.Material = new Material(); - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var material = gltf.materials.Material; var kmc = material.extensions.KHR_materials_common; var values = kmc.values; @@ -118,7 +118,7 @@ describe('gltf', function() { boxObjData.materials.Material = material; boxObjData.images[diffuseTextureUrl] = diffuseTexture; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var kmc = gltf.materials.Material.extensions.KHR_materials_common; var texture = gltf.textures.texture_cesium; var image = gltf.images.cesium; @@ -156,7 +156,7 @@ describe('gltf', function() { material.alpha = 0.4; boxObjData.materials.Material = material; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 0.4]); @@ -173,7 +173,7 @@ describe('gltf', function() { boxObjData.images[diffuseTextureUrl] = diffuseTexture; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.values.diffuse).toEqual('texture_cesium'); @@ -189,7 +189,7 @@ describe('gltf', function() { boxObjData.images[transparentDiffuseTextureUrl] = transparentDiffuseTexture; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.values.diffuse).toBe('texture_diffuse'); @@ -204,7 +204,7 @@ describe('gltf', function() { material.specularShininess = 0.1; boxObjData.materials.Material = material; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.technique).toBe('PHONG'); @@ -221,7 +221,7 @@ describe('gltf', function() { boxObjData.images[diffuseTextureUrl] = diffuseTexture; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.technique).toBe('CONSTANT'); @@ -233,7 +233,7 @@ describe('gltf', function() { material.diffuseTexture = diffuseTextureUrl; boxObjData.materials.Material = material; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]); @@ -243,7 +243,7 @@ describe('gltf', function() { boxObjData.nodes[0].meshes[0].primitives[0].material = undefined; // Creates a material called "default" - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); 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]); @@ -253,7 +253,7 @@ describe('gltf', function() { boxObjData.materials = {}; // Uses the original name of the material - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var kmc = gltf.materials.Material.extensions.KHR_materials_common; expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]); @@ -264,7 +264,7 @@ describe('gltf', function() { boxObjData.nodes.push(duplicateBoxObjData.nodes[0]); boxObjData.nodes[1].meshes[0].normals.length = 0; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var kmc1 = gltf.materials.Material.extensions.KHR_materials_common; var kmc2 = gltf.materials.Material_constant.extensions.KHR_materials_common; @@ -277,7 +277,7 @@ describe('gltf', function() { boxObjData.nodes.push(duplicateBoxObjData.nodes[0]); boxObjData.nodes[0].meshes[0].normals.length = 0; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var kmc1 = gltf.materials.Material.extensions.KHR_materials_common; var kmc2 = gltf.materials.Material_shaded.extensions.KHR_materials_common; @@ -288,7 +288,7 @@ describe('gltf', function() { it('runs without normals', function() { boxObjData.nodes[0].meshes[0].normals.length = 0; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes; expect(attributes.POSITION).toBeDefined(); expect(attributes.NORMAL).toBeUndefined(); @@ -298,7 +298,7 @@ describe('gltf', function() { it('runs without uvs', function() { boxObjData.nodes[0].meshes[0].uvs.length = 0; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes; expect(attributes.POSITION).toBeDefined(); expect(attributes.NORMAL).toBeDefined(); @@ -309,7 +309,7 @@ describe('gltf', function() { boxObjData.nodes[0].meshes[0].normals.length = 0; boxObjData.nodes[0].meshes[0].uvs.length = 0; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes; expect(attributes.POSITION).toBeDefined(); expect(attributes.NORMAL).toBeUndefined(); @@ -349,7 +349,7 @@ describe('gltf', function() { var indicesLength = mesh.primitives[0].indices.length; var vertexCount = mesh.positions.length / 3; - var gltf = createGltf(boxObjData); + var gltf = createGltf(boxObjData, defaultOptions); var primitive = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0]; var indicesAccessor = gltf.accessors[primitive.indices]; expect(indicesAccessor.count).toBe(indicesLength); diff --git a/specs/lib/imageSpec.js b/specs/lib/loadImageSpec.js similarity index 88% rename from specs/lib/imageSpec.js rename to specs/lib/loadImageSpec.js index bb557459..047cd441 100644 --- a/specs/lib/imageSpec.js +++ b/specs/lib/loadImageSpec.js @@ -4,7 +4,6 @@ var obj2gltf = require('../../lib/obj2gltf'); var loadImage = require('../../lib/loadImage'); var clone = Cesium.clone; -var WebGLConstants = Cesium.WebGLConstants; var pngImage = 'specs/data/box-complex-material/shininess.png'; var jpgImage = 'specs/data/box-complex-material/emission.jpg'; @@ -15,12 +14,11 @@ var transparentImage = 'specs/data/box-complex-material/diffuse.png'; var defaultOptions = obj2gltf.defaults; -describe('image', function() { +describe('loadImage', function() { it('loads png image', function(done) { expect(loadImage(pngImage, defaultOptions) .then(function(info) { expect(info.transparent).toBe(false); - expect(info.format).toBe(WebGLConstants.RGB); expect(info.source).toBeDefined(); expect(info.extension).toBe('.png'); }), done).toResolve(); @@ -30,7 +28,6 @@ describe('image', function() { expect(loadImage(jpgImage, defaultOptions) .then(function(info) { expect(info.transparent).toBe(false); - expect(info.format).toBe(WebGLConstants.RGB); expect(info.source).toBeDefined(); expect(info.extension).toBe('.jpg'); }), done).toResolve(); @@ -40,7 +37,6 @@ describe('image', function() { expect(loadImage(jpegImage, defaultOptions) .then(function(info) { expect(info.transparent).toBe(false); - expect(info.format).toBe(WebGLConstants.RGB); expect(info.source).toBeDefined(); expect(info.extension).toBe('.jpeg'); }), done).toResolve(); @@ -50,7 +46,6 @@ describe('image', function() { expect(loadImage(gifImage, defaultOptions) .then(function(info) { expect(info.transparent).toBe(false); - expect(info.format).toBe(WebGLConstants.RGB); expect(info.source).toBeDefined(); expect(info.extension).toBe('.gif'); }), done).toResolve(); @@ -60,7 +55,6 @@ describe('image', function() { expect(loadImage(grayscaleImage, defaultOptions) .then(function(info) { expect(info.transparent).toBe(false); - expect(info.format).toBe(WebGLConstants.ALPHA); expect(info.source).toBeDefined(); expect(info.extension).toBe('.png'); }), done).toResolve(); diff --git a/specs/lib/mtlSpec.js b/specs/lib/loadMtlSpec.js similarity index 79% rename from specs/lib/mtlSpec.js rename to specs/lib/loadMtlSpec.js index e0e112d7..ddecf09f 100644 --- a/specs/lib/mtlSpec.js +++ b/specs/lib/loadMtlSpec.js @@ -9,25 +9,25 @@ function getImagePath(objPath, relativePath) { return path.resolve(path.dirname(objPath), relativePath); } -describe('mtl', function() { +describe('loadMtl', function() { it('loads complex material', function(done) { expect(loadMtl(complexMaterialUrl) .then(function(materials) { var material = materials.Material; expect(material).toBeDefined(); expect(material.ambientColor).toEqual([0.2, 0.2, 0.2, 1.0]); - expect(material.emissionColor).toEqual([0.1, 0.1, 0.1, 1.0]); + expect(material.emissiveColor).toEqual([0.1, 0.1, 0.1, 1.0]); expect(material.diffuseColor).toEqual([0.64, 0.64, 0.64, 1.0]); 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.ambientTexture).toEqual(getImagePath(complexMaterialUrl, 'ambient.gif')); - expect(material.emissionTexture).toEqual(getImagePath(complexMaterialUrl, 'emission.jpg')); + expect(material.emissiveTexture).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')); + expect(material.specularShininessTexture).toEqual(getImagePath(complexMaterialUrl, 'shininess.png')); + expect(material.normalTexture).toEqual(getImagePath(complexMaterialUrl, 'bump.png')); + expect(material.alphaTexture).toEqual(getImagePath(complexMaterialUrl, 'alpha.png')); }), done).toResolve(); }); diff --git a/specs/lib/objSpec.js b/specs/lib/loadObjSpec.js similarity index 99% rename from specs/lib/objSpec.js rename to specs/lib/loadObjSpec.js index 7535c837..3375e64f 100644 --- a/specs/lib/objSpec.js +++ b/specs/lib/loadObjSpec.js @@ -61,7 +61,7 @@ function getImagePath(objPath, relativePath) { var defaultOptions = obj2gltf.defaults; -describe('obj', function() { +describe('loadObj', function() { it('loads obj with positions, normals, and uvs', function(done) { expect(loadObj(objUrl, defaultOptions) .then(function(data) { From d6d0d392c6fb52df61f2c8791740a9ff2139a373 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 20 Apr 2017 10:23:00 -0400 Subject: [PATCH 02/21] Remove occurences of khr_materials_common for code (not tests yet) --- README.md | 3 +-- bin/obj2gltf.js | 8 +------- lib/obj2gltf.js | 12 +----------- specs/lib/obj2gltfSpec.js | 3 --- 4 files changed, 3 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 412c69af..18eff2dd 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,7 @@ Using obj2gltf as a command-line tool: |`-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`| +|`--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`| diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index 8e77725b..cf7204f6 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -76,13 +76,8 @@ var argv = yargs type: 'boolean', default: defaults.ao }, - kmc : { - describe: 'Output glTF with the KHR_materials_common extension.', - type: 'boolean', - 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.', + describe: 'Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above.', type: 'boolean', default: defaults.bypassPipeline }, @@ -116,7 +111,6 @@ var options = { optimizeForCesium : argv.optimizeForCesium, generateNormals : argv.generateNormals, ao : argv.ao, - kmc : argv.kmc, bypassPipeline : argv.bypassPipeline, checkTransparency : argv.checkTransparency, secure : argv.secure diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index eeffc243..c3629897 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -31,9 +31,8 @@ module.exports = obj2gltf; * @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.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 {Logger} [options.logger] A callback function for handling logged messages. Defaults to console.log. @@ -50,7 +49,6 @@ function obj2gltf(objPath, gltfPath, options) { var optimizeForCesium = defaultValue(options.optimizeForCesium, defaults.optimizeForCesium); var generateNormals = defaultValue(options.generateNormals, defaults.generateNormals); var ao = defaultValue(options.ao, defaults.ao); - var kmc = defaultValue(options.kmc, defaults.kmc); var textureCompressionOptions = options.textureCompressionOptions; var bypassPipeline = defaultValue(options.bypassPipeline, defaults.bypassPipeline); var checkTransparency = defaultValue(options.checkTransparency, defaults.checkTransparency); @@ -86,7 +84,6 @@ function obj2gltf(objPath, gltfPath, options) { gltfPath = path.join(path.dirname(gltfPath), modelName + extension); var aoOptions = ao ? {} : undefined; - var kmcOptions = kmc ? {} : undefined; var pipelineOptions = { createDirectory : false, @@ -101,7 +98,6 @@ function obj2gltf(objPath, gltfPath, options) { optimizeForCesium : optimizeForCesium, smoothNormals : generateNormals, aoOptions : aoOptions, - kmcOptions : kmcOptions, textureCompressionOptions : textureCompressionOptions }; @@ -187,12 +183,6 @@ obj2gltf.defaults = { * @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 diff --git a/specs/lib/obj2gltfSpec.js b/specs/lib/obj2gltfSpec.js index b152f9f0..e85d1b7f 100644 --- a/specs/lib/obj2gltfSpec.js +++ b/specs/lib/obj2gltfSpec.js @@ -39,7 +39,6 @@ describe('obj2gltf', function() { quantize : false, compressTextureCoordinates : false, aoOptions : undefined, - kmcOptions : undefined, smoothNormals : false, optimizeForCesium : false, textureCompressionOptions : undefined, @@ -64,7 +63,6 @@ describe('obj2gltf', function() { optimizeForCesium : true, generateNormals : true, ao : true, - kmc : true, textureCompressionOptions : textureCompressionOptions, checkTransparency : true, secure : true, @@ -85,7 +83,6 @@ describe('obj2gltf', function() { quantize : true, compressTextureCoordinates : true, aoOptions : {}, - kmcOptions : {}, smoothNormals : true, optimizeForCesium : true, textureCompressionOptions : textureCompressionOptions, From c9ad66fcdb25ed57a6fb0b27291cfdb36d8d0147 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 3 May 2017 17:59:24 -0400 Subject: [PATCH 03/21] Add metallicRoughness and specularGlosiness output --- README.md | 3 + bin/obj2gltf.js | 20 +- lib/Material.js | 1 + lib/createGltf.js | 491 ++++++++++++++++++++++++++++++++-------------- 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, 460 insertions(+), 196 deletions(-) diff --git a/README.md b/README.md index 18eff2dd..8d201efd 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 cf7204f6..a50d2b0b 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 6a074ac1..f5caa1d1 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 c16d4a3c..fa012160 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); + } + } + + 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(metallicImage) && !defined(metallicImage.decoded)) { - options.logger('Could not get decoded image data for ' + metallicImage + '. The material will be created without a metallicRoughness texture.'); + 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 : colorType, + inputColorType : inputColorType, + inputHasAlpha : inputHasAlpha + }; + + 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 (defined(roughnessImage) && !defined(roughnessImage.decoded)) { - options.logger('Could not get decoded image data for ' + roughnessImage + '. The material will be created without a metallicRoughness texture.'); + 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; } - var width; - var height; + 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 (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; + 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 (defined(metallicImage)) { + if (packMetallic) { // Write into the B channel - var metallicChannel = getGrayscaleChannel(metallicImage, width, height); + var metallicChannel = getImageChannel(metallicImage, 0, width, height); writeChannel(pixels, metallicChannel, 2, width, height); } - if (defined(roughnessImage)) { + if (packRoughness) { // Write into the G channel - var roughnessChannel = getGrayscaleChannel(roughnessImage, width, height); + var roughnessChannel = getImageChannel(roughnessImage, 0, width, height); writeChannel(pixels, roughnessChannel, 1, width, height); } - var pngInput = { - data : pixels, - width : width, - height : height - }; + if (packOcclusion) { + // Write into the R channel + var occlusionChannel = getImageChannel(occlusionImage, 0, width, height); + writeChannel(pixels, occlusionChannel, 0, width, height); + } - var pngOptions = { - width : width, - height : height, - colorType : 2, // RGB - inputHasAlpha : true + var imageName = materialName + '-' + 'MetallicRoughness'; + if (packOcclusion) { + imageName += 'Occlusion'; + } + + var pngSource = encodePng(pixels, width, height, 4, 3); + + var image = { + transparent : false, + source : pngSource, + path : imageName, + extension : '.png' }; - var encoded = PNG.sync.write(pngInput, pngOptions); + return addTexture(gltf, image); +} + +function createSpecularGlossinessTexture(gltf, materialName, specularImage, glossinessImage, options) { + var packSpecular = defined(specularImage); + var packGlossiness = defined(glossinessImage); + + 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 : 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 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; +} - // Emissive and ambient represent roughly the same concept, so chose whichever is defined. - emissiveTexture = defaultValue(emissiveTexture, ambientTexture); +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); + + 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(baseColorTexture)) { - baseColorFactor = material.diffuseColor; + 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 85007ec9..58f99ab7 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 106202d8..76aaaa0b 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 db0d0074..13c6a8c5 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 c3629897..f998f1a6 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 d05715c6..8bf49559 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 b03c2606..56f87bf2 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": { From 44e9d5fa3b693d270a03e14b8682eb3c32fb58dd Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 4 May 2017 15:39:01 -0400 Subject: [PATCH 04/21] Add back KHR_materials_common --- README.md | 5 ++- bin/obj2gltf.js | 17 +++++--- lib/createGltf.js | 105 +++++++++++++++++++++++++++++++++++++++------- lib/obj2gltf.js | 29 ++++++++----- 4 files changed, 123 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index aa209aff..df14f498 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,9 @@ Using obj2gltf as a command-line tool: |`--inputUpAxis`|Up axis of the obj. Choices are 'X', 'Y', and 'Z'.|No, default `Y`| |`--outputUpAxis`|Up axis of the converted glTF. Choices are 'X', 'Y', and 'Z'.|No, default `Y`| |`--packOcclusion`|Pack the occlusion texture in the red channel of metallic-roughness texture.|No, default `false`| -|`--inputMetallicRoughness`|The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots.|No, default `false`| -|`--inputSpecularGlossiness`|The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the `KHR_materials_pbrSpecularGlossiness` extension.|No, default `false`| +|`--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`| ## Build Instructions diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index 24b63117..1eb82629 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -108,15 +108,20 @@ var argv = yargs type: 'boolean', default: defaults.packOcclusion }, - inputMetallicRoughness : { + metallicRoughness : { 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 + default: defaults.metallicRoughness }, - inputSpecularGlossiness : { + specularGlossiness : { 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 + default: defaults.specularGlossiness + }, + materialsCommon : { + describe: 'The glTF will be saved with the KHR_materials_common extension.', + type: 'boolean', + default: defaults.materialsCommon } }).parse(args); @@ -144,8 +149,8 @@ var options = { inputUpAxis : argv.inputUpAxis, outputUpAxis : argv.outputUpAxis, packOcclusion : argv.packOcclusion, - inputMetallicRoughness : argv.inputMetallicRoughness, - inputSpecularGlossiness : argv.inputSpecularGlossiness + metallicRoughness : argv.metallicRoughness, + specularGlossiness : argv.specularGlossiness }; console.time('Total'); diff --git a/lib/createGltf.js b/lib/createGltf.js index fa012160..a568c4fc 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -5,6 +5,7 @@ var PNG = require('pngjs').PNG; var Material = require('./Material'); var CesiumMath = Cesium.Math; +var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; var WebGLConstants = Cesium.WebGLConstants; @@ -15,9 +16,10 @@ 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.packOcclusion] Pack the occlusion texture in the red channel of metallic-roughness texture. + * @param {Boolean} [options.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. + * @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 {Boolean} options.logger A callback function for handling logged messages. Defaults to console.log. * @returns {Object} A glTF asset. * @@ -33,6 +35,8 @@ function createGltf(objData, options) { asset : {}, buffers : [], bufferViews : [], + extensionsUsed : [], + extensionsRequired : [], images : [], materials : [], meshes : [], @@ -486,7 +490,10 @@ function createSpecularGlossinessMaterial(gltf, images, material, options) { var doubleSided = transparent; var alphaMode = transparent ? 'BLEND' : 'OPAQUE'; - var gltfMaterial = { + gltf.extensionsUsed.push('KHR_materials_pbrSpecularGlossiness'); + gltf.extensionsRequired.push('KHR_materials_pbrSpecularGlossiness'); + + return { name : materialName, extensions : { KHR_materials_pbrSpecularGlossiness: { @@ -504,8 +511,6 @@ function createSpecularGlossinessMaterial(gltf, images, material, options) { alphaMode : alphaMode, doubleSided : doubleSided }; - - return gltfMaterial; } function createMetallicRoughnessMaterial(gltf, images, material, options) { @@ -558,7 +563,7 @@ function createMetallicRoughnessMaterial(gltf, images, material, options) { var doubleSided = transparent; var alphaMode = transparent ? 'BLEND' : 'OPAQUE'; - var gltfMaterial = { + return { name : materialName, pbrMetallicRoughness : { baseColorTexture : baseColorTexture, @@ -574,14 +579,13 @@ function createMetallicRoughnessMaterial(gltf, images, material, options) { alphaMode : alphaMode, 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 + // This does not convert textures var specularIntensity = material.specularColor[0]; var specularShininess = material.specularShininess; @@ -602,12 +606,83 @@ function convertTraditionalToMetallicRoughness(material) { material.specularShiness = roughnessFactor; } -function addMaterial(gltf, images, material, options) { +function createMaterialsCommonMaterial(gltf, images, material, hasNormals, options) { + var materialName = material.name; + + var ambientImage = getImage(images, material.ambientTexture); + var diffuseImage = getImage(images, material.diffuseTexture); + var emissiveImage = getImage(images, material.emissiveTexture); + var specularImage = getImage(images, material.specularTexture); + + var ambient = defaultValue(getTexture(gltf, ambientImage), material.ambientColor); + var diffuse = defaultValue(getTexture(gltf, diffuseImage), material.diffuseColor); + var emission = defaultValue(getTexture(gltf, emissiveImage), material.emissiveColor); + var specular = defaultValue(getTexture(gltf, specularImage), material.specularColor); + + var alpha = material.alpha; + var shininess = material.specularShininess; + var hasSpecular = (shininess > 0.0) && (specular[0] > 0.0 || specular[1] > 0.0 || specular[2] > 0.0); + + var transparent; + var transparency = 1.0; + if (defined(diffuseImage)) { + transparency = alpha; + transparent = diffuseImage.transparent || (transparency < 1.0); + } else { + diffuse[3] = alpha; + transparent = alpha < 1.0; + } + + if (!defined(ambientImage)) { + // If ambient color is [1, 1, 1] assume it is a multiplier and instead change to [0, 0, 0] + if (ambient[0] === 1.0 && ambient[1] === 1.0 && ambient[2] === 1.0) { + ambient = [0.0, 0.0, 0.0, 1.0]; + } + } + + var doubleSided = transparent; + + if (!hasNormals) { + // Constant technique only factors in ambient and emission sources - set emission to diffuse + emission = diffuse; + diffuse = [0, 0, 0, 1]; + } + + var technique = hasNormals ? (hasSpecular ? 'PHONG' : 'LAMBERT') : 'CONSTANT'; + + gltf.extensionsUsed.push('KHR_materials_common'); + gltf.extensionsRequired.push('KHR_materials_common'); + + return { + name : materialName, + extensions : { + KHR_materials_common : { + technique : technique, + transparent : transparent, + doubleSided : doubleSided, + values : { + ambient : ambient, + diffuse : diffuse, + emission : emission, + specular : specular, + shininess : shininess, + transparency : transparency, + transparent : transparent, + doubleSided : doubleSided + } + } + } + }; +} + +function addMaterial(gltf, images, material, hasNormals, options) { var gltfMaterial; - if (options.inputSpecularGlossiness) { + if (options.specularGlossiness) { gltfMaterial = createSpecularGlossinessMaterial(gltf, images, material, options); - } else if (options.inputMetallicRoughness) { + } else if (options.metallicRoughness) { gltfMaterial = createMetallicRoughnessMaterial(gltf, images, material, options); + } else if (options.materialsCommon) { + gltfMaterial = createMaterialsCommonMaterial(gltf, images, material, hasNormals, options); } else { convertTraditionalToMetallicRoughness(material); gltfMaterial = createMetallicRoughnessMaterial(gltf, images, material, options); @@ -629,7 +704,7 @@ function getMaterialIndex(gltf, materialName) { return undefined; } -function getMaterial(gltf, materials, images, materialName, options) { +function getMaterial(gltf, materials, images, materialName, hasNormals, options) { if (!defined(materialName)) { // Create a default material if the primitive does not specify one materialName = 'default'; @@ -651,7 +726,7 @@ function getMaterial(gltf, materials, images, materialName, options) { var materialIndex = getMaterialIndex(gltf, materialName); if (!defined(materialIndex)) { - materialIndex = addMaterial(gltf, images, material, options); + materialIndex = addMaterial(gltf, images, material, hasNormals, options); } return materialIndex; @@ -750,7 +825,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, options); + var materialIndex = getMaterial(gltf, materials, images, primitive.material, hasNormals, options); gltfPrimitives.push({ attributes : attributes, diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index 1ed7fe41..a7bb46b8 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -40,8 +40,9 @@ module.exports = obj2gltf; * @param {String} [options.inputUpAxis='Y'] Up axis of the obj. Choices are 'X', 'Y', and 'Z'. * @param {String} [options.outputUpAxis='Y'] Up axis of the converted glTF. Choices are 'X', 'Y', and 'Z'. * @param {Boolean} [options.packOcclusion=false] Pack the occlusion texture in the red channel of metallic-roughness texture. - * @param {Boolean} [options.inputMetallicRoughness=false] The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots. - * @param {Boolean} [options.inputSpecularGlossiness=false] The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension. + * @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 {Logger} [options.logger] A callback function for handling logged messages. Defaults to console.log. */ function obj2gltf(objPath, gltfPath, options) { @@ -63,8 +64,9 @@ function obj2gltf(objPath, gltfPath, options) { var inputUpAxis = defaultValue(options.inputUpAxis, defaults.inputUpAxis); var outputUpAxis = defaultValue(options.outputUpAxis, defaults.outputUpAxis); var packOcclusion = defaultValue(options.packOcclusion, defaults.packOcclusion); - var inputMetallicRoughness = defaultValue(options.inputMetallicRoughness, defaults.inputMetallicRoughness); - var inputSpecularGlossiness = defaultValue(options.inputSpecularGlossiness, defaults.inputSpecularGlossiness); + var metallicRoughness = defaultValue(options.metallicRoughness, defaults.metallicRoughness); + var specularGlossiness = defaultValue(options.specularGlossiness, defaults.specularGlossiness); + var materialsCommon = defaultValue(options.materialsCommon, defaults.materialsCommon); var logger = defaultValue(options.logger, defaults.logger); options.separate = separate; @@ -74,8 +76,9 @@ function obj2gltf(objPath, gltfPath, options) { options.inputUpAxis = inputUpAxis; options.outputUpAxis = outputUpAxis; options.packOcclusion = packOcclusion; - options.inputMetallicRoughness = inputMetallicRoughness; - options.inputSpecularGlossiness = inputSpecularGlossiness; + options.metallicRoughness = metallicRoughness; + options.specularGlossiness = specularGlossiness; + options.materialsCommon = materialsCommon; options.logger = logger; if (!defined(objPath)) { @@ -96,8 +99,8 @@ function obj2gltf(objPath, gltfPath, options) { return Promise.reject(new RuntimeError('--bypassPipeline does not convert to binary glTF')); } - if (inputMetallicRoughness && inputSpecularGlossiness) { - throw new DeveloperError('--inputMetallicRoughness and --inputSpecularGlossiness cannot both be set.'); + if (metallicRoughness + specularGlossiness + materialsCommon > 1) { + throw new DeveloperError('Only one material type may be set from [--metallicRoughness, --specularGlossiness, --materialsCommon].'); } gltfPath = path.join(path.dirname(gltfPath), modelName + extension); @@ -243,13 +246,19 @@ obj2gltf.defaults = { * @type Boolean * @default false */ - inputMetallicRoughness: false, + metallicRoughness: 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, + 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. + * @type Boolean + * @default false + */ + materialsCommon: false, /** * @private From c2af8f5098bfe79632f6acb61abe1ab110e7fac5 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 4 May 2017 17:58:13 -0400 Subject: [PATCH 05/21] Tests --- lib/Material.js | 7 + lib/createGltf.js | 54 +-- lib/loadMtl.js | 10 +- lib/loadObj.js | 2 +- lib/obj2gltf.js | 2 +- lib/writeUris.js | 21 +- .../box-objects-groups-materials.gltf | 412 ++++++++---------- .../box-objects-groups-materials.obj | 6 +- specs/data/box/box.gltf | 158 +++---- specs/lib/createGltfSpec.js | 128 +++--- specs/lib/loadImageSpec.js | 61 ++- specs/lib/loadMtlSpec.js | 22 +- specs/lib/loadObjSpec.js | 52 ++- specs/lib/obj2gltfSpec.js | 23 +- 14 files changed, 497 insertions(+), 461 deletions(-) diff --git a/lib/Material.js b/lib/Material.js index f5caa1d1..10bfcdd8 100644 --- a/lib/Material.js +++ b/lib/Material.js @@ -2,6 +2,13 @@ module.exports = Material; +/** + * A material definition which maps to the .mtl format. + * + * The default value for specularShininess varies depending on the material type, @see loadMtl. + * + * @private + */ function Material() { this.name = ''; this.ambientColor = [0.0, 0.0, 0.0, 1.0]; // Ka diff --git a/lib/createGltf.js b/lib/createGltf.js index a568c4fc..9c8b3029 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -16,10 +16,10 @@ 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] Pack the occlusion texture in the red channel of metallic-roughness texture. - * @param {Boolean} [options.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. - * @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 {Boolean} options.packOcclusion Pack the occlusion texture in the red channel of metallic-roughness texture. + * @param {Boolean} options.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. + * @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 {Boolean} options.logger A callback function for handling logged messages. Defaults to console.log. * @returns {Object} A glTF asset. * @@ -597,16 +597,17 @@ function convertTraditionalToMetallicRoughness(material) { // Low specular intensity values should produce a rough material even if shininess is high. if (specularIntensity < 0.1) { - roughnessFactor *= specularIntensity; + roughnessFactor *= (1.0 - specularIntensity); } var metallicFactor = 0.0; + material.specularTexture = undefined; // For now just ignore the specular texture material.specularColor = [metallicFactor, metallicFactor, metallicFactor, 1.0]; - material.specularShiness = roughnessFactor; + material.specularShininess = roughnessFactor; } -function createMaterialsCommonMaterial(gltf, images, material, hasNormals, options) { +function createMaterialsCommonMaterial(gltf, images, material, hasNormals) { var materialName = material.name; var ambientImage = getImage(images, material.ambientTexture); @@ -682,7 +683,7 @@ function addMaterial(gltf, images, material, hasNormals, options) { } else if (options.metallicRoughness) { gltfMaterial = createMetallicRoughnessMaterial(gltf, images, material, options); } else if (options.materialsCommon) { - gltfMaterial = createMaterialsCommonMaterial(gltf, images, material, hasNormals, options); + gltfMaterial = createMaterialsCommonMaterial(gltf, images, material, hasNormals); } else { convertTraditionalToMetallicRoughness(material); gltfMaterial = createMetallicRoughnessMaterial(gltf, images, material, options); @@ -693,26 +694,16 @@ function addMaterial(gltf, images, material, hasNormals, options) { return materialIndex; } -function getMaterialIndex(gltf, materialName) { - var materials = gltf.materials; - var length = materials.length; - for (var i = 0; i < length; ++i) { - if (materials[i].name === materialName) { - return i; - } - } - return undefined; -} - function getMaterial(gltf, materials, images, materialName, hasNormals, options) { if (!defined(materialName)) { // Create a default material if the primitive does not specify one materialName = 'default'; } + var i; var material; var materialsLength = materials.length; - for (var i = 0; i < materialsLength; ++i) { + for (i = 0; i < materialsLength; ++i) { if (materials[i].name === materialName) { material = materials[i]; } @@ -723,7 +714,14 @@ function getMaterial(gltf, materials, images, materialName, hasNormals, options) material.name = materialName; } - var materialIndex = getMaterialIndex(gltf, materialName); + var materialIndex; + materialsLength = gltf.materials.length; + for (i = 0; i < materialsLength; ++i) { + if (gltf.materials[i].name === materialName) { + materialIndex = i; + break; + } + } if (!defined(materialIndex)) { materialIndex = addMaterial(gltf, images, material, hasNormals, options); @@ -732,13 +730,14 @@ function getMaterial(gltf, materials, images, materialName, hasNormals, options) return materialIndex; } -function addVertexAttribute(gltf, bufferState, array, components) { +function addVertexAttribute(gltf, bufferState, array, components, name) { var buffer = array.toFloatBuffer(); var count = array.length / components; var minMax = array.getMinMax(components); var type = (components === 3 ? 'VEC3' : 'VEC2'); var accessor = { + name : name, bufferView : bufferState.vertexBufferViewIndex, byteOffset : bufferState.vertexBufferByteOffset, componentType : WebGLConstants.FLOAT, @@ -756,13 +755,14 @@ function addVertexAttribute(gltf, bufferState, array, components) { return accessorIndex; } -function addIndexArray(gltf, bufferState, array, uint32Indices) { +function addIndexArray(gltf, bufferState, array, uint32Indices, name) { var buffer = uint32Indices ? array.toUint32Buffer() : array.toUint16Buffer(); var componentType = uint32Indices ? WebGLConstants.UNSIGNED_INT : WebGLConstants.UNSIGNED_SHORT; var count = array.length; var minMax = array.getMinMax(1); var accessor = { + name : name, bufferView : bufferState.indexBufferViewIndex, byteOffset : bufferState.indexBufferByteOffset, componentType : componentType, @@ -803,13 +803,13 @@ function addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, opti var attributes = {}; if (hasPositions) { - attributes.POSITION = addVertexAttribute(gltf, bufferState, mesh.positions, 3); + attributes.POSITION = addVertexAttribute(gltf, bufferState, mesh.positions, 3, mesh.name + '_positions'); } if (hasNormals) { - attributes.NORMAL = addVertexAttribute(gltf, bufferState, mesh.normals, 3); + attributes.NORMAL = addVertexAttribute(gltf, bufferState, mesh.normals, 3, mesh.name + '_normals'); } if (hasUVs) { - attributes.TEXCOORD_0 = addVertexAttribute(gltf, bufferState, mesh.uvs, 2); + attributes.TEXCOORD_0 = addVertexAttribute(gltf, bufferState, mesh.uvs, 2, mesh.name + '_texcoords'); } // Unload resources @@ -822,7 +822,7 @@ function addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, opti var primitivesLength = primitives.length; for (var i = 0; i < primitivesLength; ++i) { var primitive = primitives[i]; - var indexAccessorIndex = addIndexArray(gltf, bufferState, primitive.indices, uint32Indices); + var indexAccessorIndex = addIndexArray(gltf, bufferState, primitive.indices, uint32Indices, mesh.name + '_' + i + '_indices'); primitive.indices = undefined; // Unload resources var materialIndex = getMaterial(gltf, materials, images, primitive.material, hasNormals, options); diff --git a/lib/loadMtl.js b/lib/loadMtl.js index 76aaaa0b..4a4c49d3 100644 --- a/lib/loadMtl.js +++ b/lib/loadMtl.js @@ -9,23 +9,31 @@ module.exports = loadMtl; * Parse an mtl file. * * @param {String} mtlPath Path to the mtl file. + * @param {Object} options An object with the following properties: + * @param {Boolean} options.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. * @returns {Promise} A promise resolving to the materials. * * @private */ -function loadMtl(mtlPath) { +function loadMtl(mtlPath, options) { var material; var values; var value; var mtlDirectory = path.dirname(mtlPath); var materials = []; + var defaultSpecularShininess = 0.0; + if (options.metallicRoughness) { + defaultSpecularShininess = 1.0; // Fully rough + } + function parseLine(line) { line = line.trim(); if (/^newmtl /i.test(line)) { var name = line.substring(7).trim(); material = new Material(); material.name = name; + material.specularShininess = defaultSpecularShininess; materials.push(material); } else if (/^Ka /i.test(line)) { values = line.substring(3).trim().split(' '); diff --git a/lib/loadObj.js b/lib/loadObj.js index 89912899..2f6d2f5a 100644 --- a/lib/loadObj.js +++ b/lib/loadObj.js @@ -337,7 +337,7 @@ function loadMaterials(mtlPaths, 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.'); return; } - return loadMtl(mtlPath) + return loadMtl(mtlPath, options) .then(function(materialsInMtl) { materials = materials.concat(materialsInMtl); }) diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index a7bb46b8..588546d7 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -100,7 +100,7 @@ function obj2gltf(objPath, gltfPath, options) { } if (metallicRoughness + specularGlossiness + materialsCommon > 1) { - throw new DeveloperError('Only one material type may be set from [--metallicRoughness, --specularGlossiness, --materialsCommon].'); + return Promise.reject(new RuntimeError('Only one material type may be set from [--metallicRoughness, --specularGlossiness, --materialsCommon].')); } gltfPath = path.join(path.dirname(gltfPath), modelName + extension); diff --git a/lib/writeUris.js b/lib/writeUris.js index 43dee141..96636fcb 100644 --- a/lib/writeUris.js +++ b/lib/writeUris.js @@ -7,6 +7,7 @@ var Promise = require('bluebird'); var fsExtraOutputFile = Promise.promisify(fsExtra.outputFile); +var defined = Cesium.defined; var RuntimeError = Cesium.RuntimeError; module.exports = writeUris; @@ -86,16 +87,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 removeEmpty(json) { + Object.keys(json).forEach(function(key) { + if (!defined(json[key]) || (Array.isArray(json[key]) && json[key].length === 0)) { + delete json[key]; // Delete values that are undefined or [] + } else if (typeof json[key] === 'object') { + removeEmpty(json[key]); } - } + }); +} + +function cleanup(gltf) { + removeEmpty(gltf); } function writeSeparateBuffer(gltf, resourcesDirectory, name) { diff --git a/specs/data/box-objects-groups-materials/box-objects-groups-materials.gltf b/specs/data/box-objects-groups-materials/box-objects-groups-materials.gltf index 1714d030..1006b4ea 100644 --- a/specs/data/box-objects-groups-materials/box-objects-groups-materials.gltf +++ b/specs/data/box-objects-groups-materials/box-objects-groups-materials.gltf @@ -1,9 +1,9 @@ { - "accessors": { - "accessor_0": { - "bufferView": "bufferView_vertex", + "accessors": [ + { + "name": "Blue_positions", + "bufferView": 0, "byteOffset": 0, - "byteStride": 0, "componentType": 5126, "count": 24, "min": [ @@ -18,10 +18,10 @@ ], "type": "VEC3" }, - "accessor_1": { - "bufferView": "bufferView_vertex", + { + "name": "Blue_normals", + "bufferView": 0, "byteOffset": 288, - "byteStride": 0, "componentType": 5126, "count": 24, "min": [ @@ -36,10 +36,10 @@ ], "type": "VEC3" }, - "accessor_2": { - "bufferView": "bufferView_vertex", + { + "name": "Blue_texcoords", + "bufferView": 0, "byteOffset": 576, - "byteStride": 0, "componentType": 5126, "count": 24, "min": [ @@ -52,10 +52,10 @@ ], "type": "VEC2" }, - "accessor_3": { - "bufferView": "bufferView_index", + { + "name": "Blue_0_indices", + "bufferView": 1, "byteOffset": 0, - "byteStride": 0, "componentType": 5123, "count": 18, "min": [ @@ -66,10 +66,10 @@ ], "type": "SCALAR" }, - "accessor_4": { - "bufferView": "bufferView_index", + { + "name": "Blue_1_indices", + "bufferView": 1, "byteOffset": 36, - "byteStride": 0, "componentType": 5123, "count": 18, "min": [ @@ -80,10 +80,10 @@ ], "type": "SCALAR" }, - "accessor_5": { - "bufferView": "bufferView_vertex", + { + "name": "Green_positions", + "bufferView": 0, "byteOffset": 768, - "byteStride": 0, "componentType": 5126, "count": 24, "min": [ @@ -98,10 +98,10 @@ ], "type": "VEC3" }, - "accessor_6": { - "bufferView": "bufferView_vertex", + { + "name": "Green_normals", + "bufferView": 0, "byteOffset": 1056, - "byteStride": 0, "componentType": 5126, "count": 24, "min": [ @@ -116,10 +116,10 @@ ], "type": "VEC3" }, - "accessor_7": { - "bufferView": "bufferView_vertex", + { + "name": "Green_texcoords", + "bufferView": 0, "byteOffset": 1344, - "byteStride": 0, "componentType": 5126, "count": 24, "min": [ @@ -132,10 +132,10 @@ ], "type": "VEC2" }, - "accessor_8": { - "bufferView": "bufferView_index", + { + "name": "Green_0_indices", + "bufferView": 1, "byteOffset": 72, - "byteStride": 0, "componentType": 5123, "count": 18, "min": [ @@ -146,10 +146,10 @@ ], "type": "SCALAR" }, - "accessor_9": { - "bufferView": "bufferView_index", + { + "name": "Green_1_indices", + "bufferView": 1, "byteOffset": 108, - "byteStride": 0, "componentType": 5123, "count": 18, "min": [ @@ -160,10 +160,10 @@ ], "type": "SCALAR" }, - "accessor_10": { - "bufferView": "bufferView_vertex", + { + "name": "Red_positions", + "bufferView": 0, "byteOffset": 1536, - "byteStride": 0, "componentType": 5126, "count": 24, "min": [ @@ -178,10 +178,10 @@ ], "type": "VEC3" }, - "accessor_11": { - "bufferView": "bufferView_vertex", + { + "name": "Red_normals", + "bufferView": 0, "byteOffset": 1824, - "byteStride": 0, "componentType": 5126, "count": 24, "min": [ @@ -196,10 +196,10 @@ ], "type": "VEC3" }, - "accessor_12": { - "bufferView": "bufferView_vertex", + { + "name": "Red_texcoords", + "bufferView": 0, "byteOffset": 2112, - "byteStride": 0, "componentType": 5126, "count": 24, "min": [ @@ -212,10 +212,10 @@ ], "type": "VEC2" }, - "accessor_13": { - "bufferView": "bufferView_index", + { + "name": "Red_0_indices", + "bufferView": 1, "byteOffset": 144, - "byteStride": 0, "componentType": 5123, "count": 18, "min": [ @@ -226,10 +226,10 @@ ], "type": "SCALAR" }, - "accessor_14": { - "bufferView": "bufferView_index", + { + "name": "Red_1_indices", + "bufferView": 1, "byteOffset": 180, - "byteStride": 0, "componentType": 5123, "count": 18, "min": [ @@ -240,253 +240,201 @@ ], "type": "SCALAR" } - }, + ], "asset": { "generator": "obj2gltf", - "profile": { - "api": "WebGL", - "version": "1.0" - }, - "version": "1.0" + "version": "2.0" }, - "buffers": { - "buffer": { + "buffers": [ + { + "name": "buffer", "byteLength": 2520, "uri": "data:application/octet-stream;base64,AACAvwAAgL8AAIDAAACAvwAAgD8AAIDAAACAvwAAgD8AAMDAAACAvwAAgL8AAMDAAACAvwAAgL8AAMDAAACAvwAAgD8AAMDAAACAPwAAgD8AAMDAAACAPwAAgL8AAMDAAACAPwAAgL8AAMDAAACAPwAAgD8AAMDAAACAPwAAgD8AAIDAAACAPwAAgL8AAIDAAACAPwAAgL8AAIDAAACAPwAAgD8AAIDAAACAvwAAgD8AAIDAAACAvwAAgL8AAIDAAACAvwAAgL8AAMDAAACAPwAAgL8AAMDAAACAPwAAgL8AAIDAAACAvwAAgL8AAIDAAACAPwAAgD8AAMDAAACAvwAAgD8AAMDAAACAvwAAgD8AAIDAAACAPwAAgD8AAIDAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAACAQAAAgL8AAIA/AACAQAAAgD8AAIA/AACAQAAAgD8AAIC/AACAQAAAgL8AAIC/AACAQAAAgL8AAIC/AACAQAAAgD8AAIC/AADAQAAAgD8AAIC/AADAQAAAgL8AAIC/AADAQAAAgL8AAIC/AADAQAAAgD8AAIC/AADAQAAAgD8AAIA/AADAQAAAgL8AAIA/AADAQAAAgL8AAIA/AADAQAAAgD8AAIA/AACAQAAAgD8AAIA/AACAQAAAgL8AAIA/AACAQAAAgL8AAIC/AADAQAAAgL8AAIC/AADAQAAAgL8AAIA/AACAQAAAgL8AAIA/AADAQAAAgD8AAIC/AACAQAAAgD8AAIC/AACAQAAAgD8AAIA/AADAQAAAgD8AAIA/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIA/AACAvwAAgL8AAIA/AACAPwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIA/AACAPwAAgD8AAIA/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcA" } - }, - "bufferViews": { - "bufferView_vertex": { - "buffer": "buffer", + ], + "bufferViews": [ + { + "name": "bufferView_vertex", + "buffer": 0, "byteLength": 2304, "byteOffset": 0, "target": 34962 }, - "bufferView_index": { - "buffer": "buffer", + { + "name": "bufferView_index", + "buffer": 0, "byteLength": 216, "byteOffset": 2304, "target": 34963 } - }, - "extensionsUsed": [ - "KHR_materials_common" ], - "images": {}, - "materials": { - "Blue": { - "extensions": { - "KHR_materials_common": { - "technique": "PHONG", - "transparent": false, - "doubleSided": false, - "values": { - "ambient": [ - 0, - 0, - 0, - 1 - ], - "diffuse": [ - 0, - 0, - 0.64, - 1 - ], - "emission": [ - 0, - 0, - 0, - 1 - ], - "specular": [ - 0.5, - 0.5, - 0.5, - 1 - ], - "shininess": 96.078431, - "transparency": 1, - "transparent": false, - "doubleSided": false - } - } - } + "materials": [ + { + "name": "Blue", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0, + 0, + 0.64, + 1 + ], + "metallicFactor": 0, + "roughnessFactor": 0.903921569 + }, + "emissiveFactor": [ + 0, + 0, + 0 + ], + "alphaMode": "OPAQUE", + "doubleSided": false }, - "Green": { - "extensions": { - "KHR_materials_common": { - "technique": "PHONG", - "transparent": false, - "doubleSided": false, - "values": { - "ambient": [ - 0, - 0, - 0, - 1 - ], - "diffuse": [ - 0, - 0.64, - 0, - 1 - ], - "emission": [ - 0, - 0, - 0, - 1 - ], - "specular": [ - 0.5, - 0.5, - 0.5, - 1 - ], - "shininess": 96.078431, - "transparency": 1, - "transparent": false, - "doubleSided": false - } - } - } + { + "name": "Green", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0, + 0.64, + 0, + 1 + ], + "metallicFactor": 0, + "roughnessFactor": 0.903921569 + }, + "emissiveFactor": [ + 0, + 0, + 0 + ], + "alphaMode": "OPAQUE", + "doubleSided": false }, - "Red": { - "extensions": { - "KHR_materials_common": { - "technique": "PHONG", - "transparent": false, - "doubleSided": false, - "values": { - "ambient": [ - 0, - 0, - 0, - 1 - ], - "diffuse": [ - 0.64, - 0, - 0, - 1 - ], - "emission": [ - 0, - 0, - 0, - 1 - ], - "specular": [ - 0.5, - 0.5, - 0.5, - 1 - ], - "shininess": 96.078431, - "transparency": 1, - "transparent": false, - "doubleSided": false - } - } - } + { + "name": "Red", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.64, + 0, + 0, + 1 + ], + "metallicFactor": 0, + "roughnessFactor": 0.903921569 + }, + "emissiveFactor": [ + 0, + 0, + 0 + ], + "alphaMode": "OPAQUE", + "doubleSided": false } - }, - "meshes": { - "CubeBlue_CubeBlue_Blue": { - "name": "CubeBlue_CubeBlue_Blue", + ], + "meshes": [ + { + "name": "Blue", "primitives": [ { "attributes": { - "POSITION": "accessor_0", - "NORMAL": "accessor_1", - "TEXCOORD_0": "accessor_2" + "POSITION": 0, + "NORMAL": 1, + "TEXCOORD_0": 2 }, - "indices": "accessor_3", - "material": "Blue", + "indices": 3, + "material": 0, "mode": 4 }, { "attributes": { - "POSITION": "accessor_0", - "NORMAL": "accessor_1", - "TEXCOORD_0": "accessor_2" + "POSITION": 0, + "NORMAL": 1, + "TEXCOORD_0": 2 }, - "indices": "accessor_4", - "material": "Green", + "indices": 4, + "material": 1, "mode": 4 } ] }, - "CubeGreen_CubeGreen_Green": { - "name": "CubeGreen_CubeGreen_Green", + { + "name": "Green", "primitives": [ { "attributes": { - "POSITION": "accessor_5", - "NORMAL": "accessor_6", - "TEXCOORD_0": "accessor_7" + "POSITION": 5, + "NORMAL": 6, + "TEXCOORD_0": 7 }, - "indices": "accessor_8", - "material": "Green", + "indices": 8, + "material": 1, "mode": 4 }, { "attributes": { - "POSITION": "accessor_5", - "NORMAL": "accessor_6", - "TEXCOORD_0": "accessor_7" + "POSITION": 5, + "NORMAL": 6, + "TEXCOORD_0": 7 }, - "indices": "accessor_9", - "material": "Red", + "indices": 9, + "material": 2, "mode": 4 } ] }, - "CubeRed_CubeRed_Red": { - "name": "CubeRed_CubeRed_Red", + { + "name": "Red", "primitives": [ { "attributes": { - "POSITION": "accessor_10", - "NORMAL": "accessor_11", - "TEXCOORD_0": "accessor_12" + "POSITION": 10, + "NORMAL": 11, + "TEXCOORD_0": 12 }, - "indices": "accessor_13", - "material": "Red", + "indices": 13, + "material": 2, "mode": 4 }, { "attributes": { - "POSITION": "accessor_10", - "NORMAL": "accessor_11", - "TEXCOORD_0": "accessor_12" + "POSITION": 10, + "NORMAL": 11, + "TEXCOORD_0": 12 }, - "indices": "accessor_14", - "material": "Blue", + "indices": 14, + "material": 0, "mode": 4 } ] } - }, - "nodes": { - "Cube": { + ], + "nodes": [ + { "name": "Cube", - "meshes": [ - "CubeBlue_CubeBlue_Blue", - "CubeGreen_CubeGreen_Green", - "CubeRed_CubeRed_Red" + "children": [ + 1, + 2, + 3 ] + }, + { + "name": "Blue", + "mesh": 0 + }, + { + "name": "Green", + "mesh": 1 + }, + { + "name": "Red", + "mesh": 2 } - }, - "samplers": {}, - "scene": "scene", - "scenes": { - "scene": { + ], + "scene": 0, + "scenes": [ + { "nodes": [ - "Cube" + 0 ] } - }, - "textures": {} + ] } diff --git a/specs/data/box-objects-groups-materials/box-objects-groups-materials.obj b/specs/data/box-objects-groups-materials/box-objects-groups-materials.obj index 1bb76984..b2fb821f 100644 --- a/specs/data/box-objects-groups-materials/box-objects-groups-materials.obj +++ b/specs/data/box-objects-groups-materials/box-objects-groups-materials.obj @@ -36,7 +36,7 @@ vn 1.0000 0.0000 0.0000 vn 0.0000 0.0000 1.0000 vn 0.0000 -1.0000 0.0000 vn 0.0000 1.0000 0.0000 -g CubeBlue_CubeBlue_Blue +g Blue usemtl Blue f 1/1/1 2/2/1 4/3/1 3/4/1 f 3/5/2 4/6/2 8/7/2 7/8/2 @@ -79,7 +79,7 @@ vn 1.0000 0.0000 0.0000 vn 0.0000 0.0000 1.0000 vn 0.0000 -1.0000 0.0000 vn 0.0000 1.0000 0.0000 -g CubeGreen_CubeGreen_Green +g Green usemtl Green f 9/21/7 10/22/7 12/23/7 11/24/7 f 11/25/8 12/26/8 16/27/8 15/28/8 @@ -122,7 +122,7 @@ vn 1.0000 0.0000 0.0000 vn 0.0000 0.0000 1.0000 vn 0.0000 -1.0000 0.0000 vn 0.0000 1.0000 0.0000 -g CubeRed_CubeRed_Red +g Red usemtl Red f 17/41/13 18/42/13 20/43/13 19/44/13 f 19/45/14 20/46/14 24/47/14 23/48/14 diff --git a/specs/data/box/box.gltf b/specs/data/box/box.gltf index 112389c5..cc017b13 100644 --- a/specs/data/box/box.gltf +++ b/specs/data/box/box.gltf @@ -1,9 +1,9 @@ { - "accessors": { - "accessor_0": { - "bufferView": "bufferView_vertex", + "accessors": [ + { + "name": "Cube-Mesh_positions", + "bufferView": 0, "byteOffset": 0, - "byteStride": 0, "componentType": 5126, "count": 24, "min": [ @@ -18,10 +18,10 @@ ], "type": "VEC3" }, - "accessor_1": { - "bufferView": "bufferView_vertex", + { + "name": "Cube-Mesh_normals", + "bufferView": 0, "byteOffset": 288, - "byteStride": 0, "componentType": 5126, "count": 24, "min": [ @@ -36,10 +36,10 @@ ], "type": "VEC3" }, - "accessor_2": { - "bufferView": "bufferView_vertex", + { + "name": "Cube-Mesh_texcoords", + "bufferView": 0, "byteOffset": 576, - "byteStride": 0, "componentType": 5126, "count": 24, "min": [ @@ -52,10 +52,10 @@ ], "type": "VEC2" }, - "accessor_3": { - "bufferView": "bufferView_index", + { + "name": "Cube-Mesh_0_indices", + "bufferView": 1, "byteOffset": 0, - "byteStride": 0, "componentType": 5123, "count": 36, "min": [ @@ -66,113 +66,85 @@ ], "type": "SCALAR" } - }, + ], "asset": { "generator": "obj2gltf", - "profile": { - "api": "WebGL", - "version": "1.0" - }, - "version": "1.0" + "version": "2.0" }, - "buffers": { - "buffer": { + "buffers": [ + { + "name": "buffer", "byteLength": 840, "uri": "data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIA/AACAvwAAgL8AAIA/AACAPwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIA/AACAPwAAgD8AAIA/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcA" } - }, - "bufferViews": { - "bufferView_vertex": { - "buffer": "buffer", + ], + "bufferViews": [ + { + "name": "bufferView_vertex", + "buffer": 0, "byteLength": 768, "byteOffset": 0, "target": 34962 }, - "bufferView_index": { - "buffer": "buffer", + { + "name": "bufferView_index", + "buffer": 0, "byteLength": 72, "byteOffset": 768, "target": 34963 } - }, - "extensionsUsed": [ - "KHR_materials_common" ], - "images": {}, - "materials": { - "Material": { - "extensions": { - "KHR_materials_common": { - "technique": "PHONG", - "transparent": false, - "doubleSided": false, - "values": { - "ambient": [ - 0, - 0, - 0, - 1 - ], - "diffuse": [ - 0.64, - 0.64, - 0.64, - 1 - ], - "emission": [ - 0, - 0, - 0, - 1 - ], - "specular": [ - 0.5, - 0.5, - 0.5, - 1 - ], - "shininess": 96.078431, - "transparency": 1, - "transparent": false, - "doubleSided": false - } - } - } + "materials": [ + { + "name": "Material", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.64, + 0.64, + 0.64, + 1 + ], + "metallicFactor": 0, + "roughnessFactor": 0.903921569 + }, + "emissiveFactor": [ + 0, + 0, + 0 + ], + "alphaMode": "OPAQUE", + "doubleSided": false } - }, - "meshes": { - "Cube-Mesh": { + ], + "meshes": [ + { "name": "Cube-Mesh", "primitives": [ { "attributes": { - "POSITION": "accessor_0", - "NORMAL": "accessor_1", - "TEXCOORD_0": "accessor_2" + "POSITION": 0, + "NORMAL": 1, + "TEXCOORD_0": 2 }, - "indices": "accessor_3", - "material": "Material", + "indices": 3, + "material": 0, "mode": 4 } ] } - }, - "nodes": { - "Cube": { + ], + "nodes": [ + { "name": "Cube", - "meshes": [ - "Cube-Mesh" - ] + "mesh": 0 } - }, - "samplers": {}, - "scene": "scene", - "scenes": { - "scene": { + ], + "scene": 0, + "scenes": [ + { "nodes": [ - "Cube" + 0 ] } - }, - "textures": {} -} \ No newline at end of file + ] +} diff --git a/specs/lib/createGltfSpec.js b/specs/lib/createGltfSpec.js index 22c8c777..e2ce4b48 100644 --- a/specs/lib/createGltfSpec.js +++ b/specs/lib/createGltfSpec.js @@ -26,9 +26,16 @@ var defaultOptions = obj2gltf.defaults; var checkTransparencyOptions = clone(defaultOptions); checkTransparencyOptions.checkTransparency = true; +function setDefaultMaterial(objData) { + var originalMaterial = objData.materials[0]; + var defaultMaterial = new Material(); + defaultMaterial.name = originalMaterial.name; + objData.materials[0] = defaultMaterial; + return defaultMaterial; +} + describe('createGltf', function() { var boxObjData; - var duplicateBoxObjData; var groupObjData; var boxGltf; var groupGltf; @@ -41,10 +48,6 @@ describe('createGltf', function() { .then(function(data) { boxObjData = data; }), - loadObj(boxObjUrl, defaultOptions) - .then(function(data) { - duplicateBoxObjData = data; - }), loadObj(groupObjUrl, defaultOptions) .then(function(data) { groupObjData = data; @@ -82,25 +85,69 @@ describe('createGltf', function() { expect(writeUris(gltf, groupGltfUrl, path.dirname(groupGltfUrl), defaultOptions) .then(function() { expect(gltf).toEqual(groupGltf); - expect(Object.keys(gltf.materials).length).toBe(3); - expect(Object.keys(gltf.nodes).length).toBe(1); - expect(Object.keys(gltf.meshes).length).toBe(3); + expect(gltf.materials.length).toBe(3); + expect(gltf.nodes.length).toBe(4); + expect(gltf.nodes[0].mesh).toBeUndefined(); + expect(gltf.nodes[0].children.length).toBe(3); + expect(gltf.meshes.length).toBe(3); // Check for two primitives in each mesh - for (var id in gltf.meshes) { - if (gltf.meshes.hasOwnProperty(id)) { - var mesh = gltf.meshes[id]; - expect(mesh.primitives.length).toBe(2); - } + var length = gltf.meshes.length; + for (var i = 0; i < length; ++i) { + var mesh = gltf.meshes[i]; + expect(mesh.primitives.length).toBe(2); } }), done).toResolve(); }); it('sets default material values', function() { - boxObjData.materials.Material = new Material(); + // Will convert traditional material to metallic-roughness + setDefaultMaterial(boxObjData); var gltf = createGltf(boxObjData, defaultOptions); - var material = gltf.materials.Material; + var material = gltf.materials[0]; + var pbr = material.pbrMetallicRoughness; + expect(pbr.baseColorTexture).toBeUndefined(); + expect(pbr.metallicRoughnessTexture).toBeUndefined(); + expect(pbr.baseColorFactor).toEqual([0.5, 0.5, 0.5, 1.0]); + expect(pbr.metallicFactor).toBe(0.0); // No metallic + expect(pbr.roughnessFactor).toBe(1.0); // Fully rough + expect(material.emissiveTexture).toBe(undefined); + expect(material.normalTexture).toBe(undefined); + expect(material.occlusionTexture).toBe(undefined); + expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]); + }); + + it('sets default material values for metallicRoughness', function() { + // No conversion applied when metallicRoughness flag is set + var options = clone(defaultOptions); + options.metallicRoughness = true; + + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.specularShininess = 1.0; // This is the default set in loadMtl + + var gltf = createGltf(boxObjData, options); + var material = gltf.materials[0]; + var pbr = material.pbrMetallicRoughness; + expect(pbr.baseColorTexture).toBeUndefined(); + expect(pbr.metallicRoughnessTexture).toBeUndefined(); + expect(pbr.baseColorFactor).toEqual([0.5, 0.5, 0.5, 1.0]); + expect(pbr.metallicFactor).toBe(0.0); // No metallic + expect(pbr.roughnessFactor).toBe(1.0); // Fully rough + expect(material.emissiveTexture).toBe(undefined); + expect(material.normalTexture).toBe(undefined); + expect(material.occlusionTexture).toBe(undefined); + expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]); + }); + + it('sets default material values for materialsCommon', function() { + var options = clone(defaultOptions); + options.materialsCommon = true; + + setDefaultMaterial(boxObjData); + + var gltf = createGltf(boxObjData, options); + var material = gltf.materials[0]; var kmc = material.extensions.KHR_materials_common; var values = kmc.values; @@ -110,12 +157,15 @@ describe('createGltf', function() { expect(values.emission).toEqual([0.0, 0.0, 0.0, 1]); expect(values.specular).toEqual([0.0, 0.0, 0.0, 1]); expect(values.shininess).toEqual(0.0); + expect(values.transparency).toBe(1.0); + expect(values.transparent).toBe(false); + expect(values.doubleSided).toBe(false); }); it('sets material for diffuse texture', function() { var material = new Material(); material.diffuseTexture = diffuseTextureUrl; - boxObjData.materials.Material = material; + boxObjData.materials[0] = material; boxObjData.images[diffuseTextureUrl] = diffuseTexture; var gltf = createGltf(boxObjData, defaultOptions); @@ -154,7 +204,7 @@ describe('createGltf', function() { it('sets material for alpha less than 1', function() { var material = new Material(); material.alpha = 0.4; - boxObjData.materials.Material = material; + boxObjData.materials[0] = material; var gltf = createGltf(boxObjData, defaultOptions); var kmc = gltf.materials.Material.extensions.KHR_materials_common; @@ -169,7 +219,7 @@ describe('createGltf', function() { var material = new Material(); material.diffuseTexture = diffuseTextureUrl; material.alpha = 0.4; - boxObjData.materials.Material = material; + boxObjData.materials[0] = material; boxObjData.images[diffuseTextureUrl] = diffuseTexture; @@ -185,7 +235,7 @@ describe('createGltf', function() { it('sets material for transparent diffuse texture', function() { var material = new Material(); material.diffuseTexture = transparentDiffuseTextureUrl; - boxObjData.materials.Material = material; + boxObjData.materials[0] = material; boxObjData.images[transparentDiffuseTextureUrl] = transparentDiffuseTexture; @@ -202,7 +252,7 @@ describe('createGltf', function() { var material = new Material(); material.specularColor = [0.1, 0.1, 0.2, 1]; material.specularShininess = 0.1; - boxObjData.materials.Material = material; + boxObjData.materials[0] = material; var gltf = createGltf(boxObjData, defaultOptions); var kmc = gltf.materials.Material.extensions.KHR_materials_common; @@ -217,7 +267,7 @@ describe('createGltf', function() { var material = new Material(); material.diffuseTexture = diffuseTextureUrl; - boxObjData.materials.Material = material; + boxObjData.materials[0] = material; boxObjData.images[diffuseTextureUrl] = diffuseTexture; @@ -231,10 +281,10 @@ describe('createGltf', function() { it('sets default material when texture is missing', function() { var material = new Material(); material.diffuseTexture = diffuseTextureUrl; - boxObjData.materials.Material = material; + boxObjData.materials[0] = material; var gltf = createGltf(boxObjData, defaultOptions); - var kmc = gltf.materials.Material.extensions.KHR_materials_common; + var kmc = gltf.materials[0].extensions.KHR_materials_common; expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]); }); @@ -244,8 +294,8 @@ describe('createGltf', function() { // Creates a material called "default" var gltf = createGltf(boxObjData, defaultOptions); - expect(gltf.materials.default).toBeDefined(); - var kmc = gltf.materials.default.extensions.KHR_materials_common; + expect(gltf.materials[0].name).toBe('default'); + var kmc = gltf.materials[0].extensions.KHR_materials_common; expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]); }); @@ -254,37 +304,11 @@ describe('createGltf', function() { // Uses the original name of the material var gltf = createGltf(boxObjData, defaultOptions); - var kmc = gltf.materials.Material.extensions.KHR_materials_common; + var kmc = gltf.materials[0].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 (1)', function() { - // Two meshes - one with normals, and one without - boxObjData.nodes.push(duplicateBoxObjData.nodes[0]); - boxObjData.nodes[1].meshes[0].normals.length = 0; - - var gltf = createGltf(boxObjData, defaultOptions); - 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 - boxObjData.nodes.push(duplicateBoxObjData.nodes[0]); - boxObjData.nodes[0].meshes[0].normals.length = 0; - - var gltf = createGltf(boxObjData, defaultOptions); - 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() { boxObjData.nodes[0].meshes[0].normals.length = 0; diff --git a/specs/lib/loadImageSpec.js b/specs/lib/loadImageSpec.js index 047cd441..a1c6185f 100644 --- a/specs/lib/loadImageSpec.js +++ b/specs/lib/loadImageSpec.js @@ -1,10 +1,7 @@ 'use strict'; -var Cesium = require('cesium'); var obj2gltf = require('../../lib/obj2gltf'); var loadImage = require('../../lib/loadImage'); -var clone = Cesium.clone; - var pngImage = 'specs/data/box-complex-material/shininess.png'; var jpgImage = 'specs/data/box-complex-material/emission.jpg'; var jpegImage = 'specs/data/box-complex-material/specular.jpeg'; @@ -12,47 +9,58 @@ 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 defaultOptions = obj2gltf.defaults; - describe('loadImage', function() { it('loads png image', function(done) { - expect(loadImage(pngImage, defaultOptions) + expect(loadImage(pngImage) .then(function(info) { expect(info.transparent).toBe(false); expect(info.source).toBeDefined(); expect(info.extension).toBe('.png'); + expect(info.path).toBe(pngImage); + expect(info.decoded).toBeUndefined(); + expect(info.width).toBeUndefined(); + expect(info.height).toBeUndefined(); }), done).toResolve(); }); it('loads jpg image', function(done) { - expect(loadImage(jpgImage, defaultOptions) + expect(loadImage(jpgImage) .then(function(info) { expect(info.transparent).toBe(false); expect(info.source).toBeDefined(); expect(info.extension).toBe('.jpg'); + expect(info.decoded).toBeUndefined(); + expect(info.width).toBeUndefined(); + expect(info.height).toBeUndefined(); }), done).toResolve(); }); it('loads jpeg image', function(done) { - expect(loadImage(jpegImage, defaultOptions) + expect(loadImage(jpegImage) .then(function(info) { expect(info.transparent).toBe(false); expect(info.source).toBeDefined(); expect(info.extension).toBe('.jpeg'); + expect(info.decoded).toBeUndefined(); + expect(info.width).toBeUndefined(); + expect(info.height).toBeUndefined(); }), done).toResolve(); }); it('loads gif image', function(done) { - expect(loadImage(gifImage, defaultOptions) + expect(loadImage(gifImage) .then(function(info) { expect(info.transparent).toBe(false); expect(info.source).toBeDefined(); expect(info.extension).toBe('.gif'); + expect(info.decoded).toBeUndefined(); + expect(info.width).toBeUndefined(); + expect(info.height).toBeUndefined(); }), done).toResolve(); }); it('loads grayscale image', function(done) { - expect(loadImage(grayscaleImage, defaultOptions) + expect(loadImage(grayscaleImage) .then(function(info) { expect(info.transparent).toBe(false); expect(info.source).toBeDefined(); @@ -61,19 +69,46 @@ describe('loadImage', function() { }); it('loads image with alpha channel', function(done) { - expect(loadImage(transparentImage, defaultOptions) + expect(loadImage(transparentImage) .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; + var options = { + checkTransparency : true + }; expect(loadImage(transparentImage, options) .then(function(info) { expect(info.transparent).toBe(true); }), done).toResolve(); }); + + it('loads and decodes png', function(done) { + var options = { + decode : true + }; + + expect(loadImage(pngImage, options) + .then(function(info) { + expect(info.decoded).toBeDefined(); + expect(info.width).toBe(211); + expect(info.height).toBe(211); + }), done).toResolve(); + }); + + it('loads and decodes jpeg', function(done) { + var options = { + decode : true + }; + + expect(loadImage(jpegImage, options) + .then(function(info) { + expect(info.decoded).toBeDefined(); + expect(info.width).toBe(211); + expect(info.height).toBe(211); + }), done).toResolve(); + }); }); diff --git a/specs/lib/loadMtlSpec.js b/specs/lib/loadMtlSpec.js index ddecf09f..eca3c9e6 100644 --- a/specs/lib/loadMtlSpec.js +++ b/specs/lib/loadMtlSpec.js @@ -1,6 +1,7 @@ 'use strict'; var path = require('path'); var loadMtl = require('../../lib/loadMtl'); +var obj2gltf = require('../../lib/obj2gltf'); var complexMaterialUrl = 'specs/data/box-complex-material/box-complex-material.mtl'; var multipleMaterialsUrl = 'specs/data/box-multiple-materials/box-multiple-materials.mtl'; @@ -9,12 +10,14 @@ function getImagePath(objPath, relativePath) { return path.resolve(path.dirname(objPath), relativePath); } +var defaultOptions = obj2gltf.defaults; + describe('loadMtl', function() { it('loads complex material', function(done) { - expect(loadMtl(complexMaterialUrl) + expect(loadMtl(complexMaterialUrl, defaultOptions) .then(function(materials) { - var material = materials.Material; - expect(material).toBeDefined(); + var material = materials[0]; + expect(material.name).toBe('Material'); expect(material.ambientColor).toEqual([0.2, 0.2, 0.2, 1.0]); expect(material.emissiveColor).toEqual([0.1, 0.1, 0.1, 1.0]); expect(material.diffuseColor).toEqual([0.64, 0.64, 0.64, 1.0]); @@ -32,12 +35,15 @@ describe('loadMtl', function() { }); it('loads mtl with multiple materials', function(done) { - expect(loadMtl(multipleMaterialsUrl) + expect(loadMtl(multipleMaterialsUrl, defaultOptions) .then(function(materials) { - expect(Object.keys(materials).length).toBe(3); - expect(materials.Red.diffuseColor).toEqual([0.64, 0.0, 0.0, 1.0]); - expect(materials.Green.diffuseColor).toEqual([0.0, 0.64, 0.0, 1.0]); - expect(materials.Blue.diffuseColor).toEqual([0.0, 0.0, 0.64, 1.0]); + expect(materials.length).toBe(3); + expect(materials[0].name).toBe('Blue'); + expect(materials[0].diffuseColor).toEqual([0.0, 0.0, 0.64, 1.0]); + expect(materials[1].name).toBe('Green'); + expect(materials[1].diffuseColor).toEqual([0.0, 0.64, 0.0, 1.0]); + expect(materials[2].name).toBe('Red'); + expect(materials[2].diffuseColor).toEqual([0.64, 0.0, 0.0, 1.0]); }), done).toResolve(); }); }); diff --git a/specs/lib/loadObjSpec.js b/specs/lib/loadObjSpec.js index ca7d2a73..3b0833a8 100644 --- a/specs/lib/loadObjSpec.js +++ b/specs/lib/loadObjSpec.js @@ -77,8 +77,8 @@ describe('loadObj', function() { var meshes = getMeshes(data); var primitives = getPrimitives(data); - expect(Object.keys(images).length).toBe(0); - expect(materials.Material).toBeDefined(); + expect(images.length).toBe(0); + expect(materials.length).toBe(1); expect(nodes.length).toBe(1); expect(meshes.length).toBe(1); expect(primitives.length).toBe(1); @@ -268,17 +268,26 @@ describe('loadObj', function() { expect(loadObj(objMtllibUrl, defaultOptions) .then(function(data) { var materials = data.materials; - expect(Object.keys(materials).length).toBe(3); - expect(materials.Red.diffuseColor).toEqual([0.64, 0.0, 0.0, 1.0]); - expect(materials.Green.diffuseColor).toEqual([0.0, 0.64, 0.0, 1.0]); - expect(materials.Blue.diffuseColor).toEqual([0.0, 0.0, 0.64, 1.0]); + expect(materials.length).toBe(3); + + // .mtl files are loaded in an arbitrary order, so sort for testing purposes + materials.sort(function(a, b){ + return a.name.localeCompare(b.name); + }); + + expect(materials[0].name).toBe('Blue'); + expect(materials[0].diffuseColor).toEqual([0.0, 0.0, 0.64, 1.0]); + expect(materials[1].name).toBe('Green'); + expect(materials[1].diffuseColor).toEqual([0.0, 0.64, 0.0, 1.0]); + expect(materials[2].name).toBe('Red'); + expect(materials[2].diffuseColor).toEqual([0.64, 0.0, 0.0, 1.0]); }), done).toResolve(); }); it('loads obj with missing mtllib', function(done) { expect(loadObj(objMissingMtllibUrl, defaultOptions) .then(function(data) { - expect(data.materials).toEqual({}); + expect(data.materials.length).toBe(0); expect(console.log.calls.argsFor(0)[0].indexOf('Could not read mtl file') >= 0).toBe(true); }), done).toResolve(); }); @@ -287,8 +296,14 @@ describe('loadObj', function() { expect(loadObj(objExternalResourcesUrl, defaultOptions) .then(function(data) { var imagePath = getImagePath(objTexturedUrl, 'cesium.png'); - expect(data.images[imagePath]).toBeDefined(); - expect(data.materials.MaterialTextured.diffuseTexture).toEqual(imagePath); + expect(data.images[0].path).toBe(imagePath); + + var materials = data.materials; + expect(materials.length).toBe(2); + + // .mtl files are loaded in an arbitrary order, so find the "MaterialTextured" material + var materialTextured = materials[0].name === 'MaterialTextured' ? materials[0] : materials[1]; + expect(materialTextured.diffuseTexture).toEqual(imagePath); }), done).toResolve(); }); @@ -299,9 +314,8 @@ describe('loadObj', function() { expect(loadObj(objExternalResourcesUrl, options) .then(function(data) { var imagePath = getImagePath(objMissingTextureUrl, 'cesium.png'); - expect(data.images[imagePath]).toBeUndefined(); - expect(data.materials.MaterialTextured).toBeDefined(); - expect(data.materials.Material).toBeUndefined(); // Not in directory, so not included + expect(data.images.length).toBe(0); // obj references an image file that is outside the input directory + expect(data.materials.length).toBe(1); // obj references 2 materials, one of which is outside the input directory expect(console.log.calls.argsFor(0)[0].indexOf('Could not read mtl file') >= 0).toBe(true); expect(console.log.calls.argsFor(1)[0].indexOf('Could not read image file') >= 0).toBe(true); }), done).toResolve(); @@ -311,8 +325,8 @@ describe('loadObj', function() { expect(loadObj(objTexturedUrl, defaultOptions) .then(function(data) { var imagePath = getImagePath(objTexturedUrl, 'cesium.png'); - expect(data.images[imagePath]).toBeDefined(); - expect(data.materials.Material.diffuseTexture).toEqual(imagePath); + expect(data.images[0].path).toBe(imagePath); + expect(data.materials[0].diffuseTexture).toEqual(imagePath); }), done).toResolve(); }); @@ -320,8 +334,8 @@ describe('loadObj', function() { expect(loadObj(objMissingTextureUrl, defaultOptions) .then(function(data) { var imagePath = getImagePath(objMissingTextureUrl, 'cesium.png'); - expect(data.images[imagePath]).toBeUndefined(); - expect(data.materials.Material.diffuseTexture).toEqual(imagePath); + expect(data.images.length).toBe(0); + expect(data.materials[0].diffuseTexture).toEqual(imagePath); expect(console.log.calls.argsFor(0)[0].indexOf('Could not read image file') >= 0).toBe(true); }), done).toResolve(); }); @@ -330,8 +344,8 @@ describe('loadObj', function() { 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.diffuseTexture).toEqual(imagePath); + expect(data.images[0].path).toBe(imagePath); + expect(data.materials[0].diffuseTexture).toEqual(imagePath); }), done).toResolve(); }); @@ -339,7 +353,7 @@ describe('loadObj', function() { 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 + expect(images.length).toBe(6); }), done).toResolve(); }); diff --git a/specs/lib/obj2gltfSpec.js b/specs/lib/obj2gltfSpec.js index 22a9bddb..8054fbed 100644 --- a/specs/lib/obj2gltfSpec.js +++ b/specs/lib/obj2gltfSpec.js @@ -1,4 +1,5 @@ 'use strict'; +var Cesium = require('Cesium'); var fsExtra = require('fs-extra'); var GltfPipeline = require('gltf-pipeline').Pipeline; var os = require('os'); @@ -7,6 +8,8 @@ var Promise = require('bluebird'); var obj2gltf = require('../../lib/obj2gltf'); var writeUris = require('../../lib/writeUris'); +var RuntimeError = Cesium.RuntimeError; + 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'; @@ -37,7 +40,7 @@ describe('obj2gltf', function() { var options = args[2]; expect(path.normalize(outputPath)).toEqual(path.normalize(gltfPath)); expect(gltf).toBeDefined(); - expect(gltf.images.cesium).toBeDefined(); + expect(gltf.images.length).toBe(1); expect(options).toEqual({ basePath : tempDirectory, createDirectory : false, @@ -131,9 +134,25 @@ describe('obj2gltf', function() { }).toThrowDeveloperError(); }); - it('rejects if gltfPath is undefined', function() { + it('throws if gltfPath is undefined', function() { expect(function() { obj2gltf(objPath, undefined); }).toThrowDeveloperError(); }); + + it('rejects if both bpypassPipeline and binary are true', function(done) { + var options = { + bypassPipeline : true, + binary : true + }; + expect(obj2gltf(objPath, gltfPath, options), done).toRejectWith(RuntimeError); + }); + + it('rejects if more than one material type is set', function(done) { + var options = { + metallicRoughness : true, + specularGlossiness : true + }; + expect(obj2gltf(objPath, gltfPath, options), done).toRejectWith(RuntimeError); + }); }); From 608234dcc4b9d4b694fd69a83f99cbad243d13c6 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 17 Jul 2017 17:45:58 -0400 Subject: [PATCH 06/21] Rounded out materialCommon, pbrMetallicRoughness, and pbrSpecularGlosiness tests --- lib/createGltf.js | 39 ++-- lib/loadImage.js | 2 - specs/lib/createGltfSpec.js | 356 +++++++++++++++++++++++++++--------- specs/lib/loadImageSpec.js | 1 - specs/lib/loadObjSpec.js | 1 - 5 files changed, 289 insertions(+), 110 deletions(-) diff --git a/lib/createGltf.js b/lib/createGltf.js index d31cfad8..985f764d 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -184,7 +184,9 @@ function addTexture(gltf, image) { source : imageIndex }); - return textureIndex; + return { + index : textureIndex + }; } function getTexture(gltf, image) { @@ -279,18 +281,14 @@ function getMinimumDimensions(images, options) { 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); - } + width = Math.min(image.width, width); + height = Math.min(image.height, height); } 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 (image.width !== width || image.height !== height) { + options.logger('Image ' + image.path + ' will be scaled from ' + image.width + 'x' + image.height + ' to ' + width + 'x' + height + '.'); } } @@ -347,7 +345,11 @@ function createMetallicRoughnessTexture(gltf, materialName, metallicImage, rough return undefined; } - var dimensions = getMinimumDimensions([metallicImage, roughnessImage, occlusionImage], options); + var packedImages = [metallicImage, roughnessImage, occlusionImage].filter(function(image) { + return defined(image) && defined(image.decoded); + }); + + var dimensions = getMinimumDimensions(packedImages, options); var width = dimensions[0]; var height = dimensions[1]; var pixelsLength = width * height; @@ -406,7 +408,11 @@ function createSpecularGlossinessTexture(gltf, materialName, specularImage, glos return undefined; } - var dimensions = getMinimumDimensions([specularImage, glossinessImage], options); + var packedImages = [specularImage, glossinessImage].filter(function(image) { + return defined(image) && defined(image.decoded); + }); + + var dimensions = getMinimumDimensions(packedImages, options); var width = dimensions[0]; var height = dimensions[1]; var pixelsLength = width * height; @@ -460,7 +466,7 @@ function createSpecularGlossinessMaterial(gltf, images, material, options) { var emissiveFactor = getEmissiveFactor(material); var diffuseFactor = material.diffuseColor; - var specularFactor = material.specularColor; + var specularFactor = material.specularColor.slice(0, 3); var glossinessFactor = material.specularShininess; if (defined(emissiveTexture)) { @@ -472,7 +478,7 @@ function createSpecularGlossinessMaterial(gltf, images, material, options) { } if (defined(specularImage)) { - specularFactor = 1.0; + specularFactor = [1.0, 1.0, 1.0]; } if (defined(glossinessImage)) { @@ -484,7 +490,7 @@ function createSpecularGlossinessMaterial(gltf, images, material, options) { var transparent = alpha < 1.0; if (defined(diffuseImage)) { - transparent |= diffuseImage.transparent; + transparent = transparent || diffuseImage.transparent; } var doubleSided = transparent; @@ -528,7 +534,7 @@ function createMetallicRoughnessMaterial(gltf, images, material, options) { var baseColorTexture = getTexture(gltf, baseColorImage); var metallicRoughnessTexture = createMetallicRoughnessTexture(gltf, materialName, metallicImage, roughnessImage, occlusionImage, options); - var packOcclusion = defined(occlusionImage) || options.packOcclusion; + var packOcclusion = defined(occlusionImage) && options.packOcclusion; var occlusionTexture = packOcclusion ? metallicRoughnessTexture : getTexture(gltf, occlusionImage); var emissiveFactor = getEmissiveFactor(material); @@ -557,7 +563,7 @@ function createMetallicRoughnessMaterial(gltf, images, material, options) { var transparent = alpha < 1.0; if (defined(baseColorImage)) { - transparent |= baseColorImage.transparent; + transparent = transparent || baseColorImage.transparent; } var doubleSided = transparent; @@ -706,6 +712,7 @@ function getMaterial(gltf, materials, images, materialName, hasNormals, options) for (i = 0; i < materialsLength; ++i) { if (materials[i].name === materialName) { material = materials[i]; + break; } } diff --git a/lib/loadImage.js b/lib/loadImage.js index 3c1e707f..410a133b 100644 --- a/lib/loadImage.js +++ b/lib/loadImage.js @@ -6,8 +6,6 @@ var path = require('path'); var PNG = require('pngjs').PNG; var defaultValue = Cesium.defaultValue; -var defined = Cesium.defined; -var WebGLConstants = Cesium.WebGLConstants; module.exports = loadImage; diff --git a/specs/lib/createGltfSpec.js b/specs/lib/createGltfSpec.js index cc7a7d54..835bf4c4 100644 --- a/specs/lib/createGltfSpec.js +++ b/specs/lib/createGltfSpec.js @@ -14,10 +14,17 @@ var boxObjUrl = 'specs/data/box/box.obj'; var groupObjUrl = 'specs/data/box-objects-groups-materials/box-objects-groups-materials.obj'; var diffuseTextureUrl = 'specs/data/box-textured/cesium.png'; var transparentDiffuseTextureUrl = 'specs/data/box-complex-material/diffuse.png'; +var ambientTextureUrl = 'specs/data/box-complex-material/ambient.gif'; +var normalTextureUrl = 'specs/data/box-complex-material/bump.png'; +var emissiveTextureUrl = 'specs/data/box-complex-material/emission.jpg'; +var metallicTextureUrl = 'specs/data/box-complex-material/specular.jpeg'; +var roughnessTextureUrl = 'specs/data/box-complex-material/shininess.png'; var defaultOptions = obj2gltf.defaults; var checkTransparencyOptions = clone(defaultOptions); checkTransparencyOptions.checkTransparency = true; +var decodeOptions = clone(defaultOptions); +decodeOptions.decode = true; function setDefaultMaterial(objData) { var originalMaterial = objData.materials[0]; @@ -32,24 +39,50 @@ describe('createGltf', function() { var groupObjData; var diffuseTexture; var transparentDiffuseTexture; + var ambientTexture; + var normalTexture; + var emissiveTexture; + var metallicTexture; + var roughnessTexture; beforeEach(function(done) { + spyOn(console, 'log'); return Promise.all([ - loadObj(boxObjUrl, defaultOptions) + loadObj(boxObjUrl, decodeOptions) .then(function(data) { boxObjData = data; }), - loadObj(groupObjUrl, defaultOptions) + loadObj(groupObjUrl, decodeOptions) .then(function(data) { groupObjData = data; }), - loadImage(diffuseTextureUrl, defaultOptions) + loadImage(diffuseTextureUrl, decodeOptions) .then(function(image) { diffuseTexture = image; }), loadImage(transparentDiffuseTextureUrl, checkTransparencyOptions) .then(function(image) { transparentDiffuseTexture = image; + }), + loadImage(ambientTextureUrl, decodeOptions) + .then(function(image) { + ambientTexture = image; + }), + loadImage(normalTextureUrl, decodeOptions) + .then(function(image) { + normalTexture = image; + }), + loadImage(emissiveTextureUrl, decodeOptions) + .then(function(image) { + emissiveTexture = image; + }), + loadImage(metallicTextureUrl, decodeOptions) + .then(function(image) { + metallicTexture = image; + }), + loadImage(roughnessTextureUrl, decodeOptions) + .then(function(image) { + roughnessTexture = image; }) ]).then(done); }); @@ -93,46 +126,6 @@ describe('createGltf', function() { } }); - it('sets default material values', function() { - // Will convert traditional material to metallic-roughness - setDefaultMaterial(boxObjData); - - var gltf = createGltf(boxObjData, defaultOptions); - var material = gltf.materials[0]; - var pbr = material.pbrMetallicRoughness; - expect(pbr.baseColorTexture).toBeUndefined(); - expect(pbr.metallicRoughnessTexture).toBeUndefined(); - expect(pbr.baseColorFactor).toEqual([0.5, 0.5, 0.5, 1.0]); - expect(pbr.metallicFactor).toBe(0.0); // No metallic - expect(pbr.roughnessFactor).toBe(1.0); // Fully rough - expect(material.emissiveTexture).toBe(undefined); - expect(material.normalTexture).toBe(undefined); - expect(material.occlusionTexture).toBe(undefined); - expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]); - }); - - it('sets default material values for metallicRoughness', function() { - // No conversion applied when metallicRoughness flag is set - var options = clone(defaultOptions); - options.metallicRoughness = true; - - var defaultMaterial = setDefaultMaterial(boxObjData); - defaultMaterial.specularShininess = 1.0; // This is the default set in loadMtl - - var gltf = createGltf(boxObjData, options); - var material = gltf.materials[0]; - var pbr = material.pbrMetallicRoughness; - expect(pbr.baseColorTexture).toBeUndefined(); - expect(pbr.metallicRoughnessTexture).toBeUndefined(); - expect(pbr.baseColorFactor).toEqual([0.5, 0.5, 0.5, 1.0]); - expect(pbr.metallicFactor).toBe(0.0); // No metallic - expect(pbr.roughnessFactor).toBe(1.0); // Fully rough - expect(material.emissiveTexture).toBe(undefined); - expect(material.normalTexture).toBe(undefined); - expect(material.occlusionTexture).toBe(undefined); - expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]); - }); - it('runs without normals', function() { boxObjData.nodes[0].meshes[0].normals.length = 0; @@ -208,6 +201,201 @@ describe('createGltf', function() { expect(positionAccessor.count).toBe(vertexCount); }); + describe('metallicRoughness', function() { + it('sets default material values', function() { + // Will convert traditional material to metallic-roughness + setDefaultMaterial(boxObjData); + + var gltf = createGltf(boxObjData, defaultOptions); + var material = gltf.materials[0]; + var pbr = material.pbrMetallicRoughness; + expect(pbr.baseColorTexture).toBeUndefined(); + expect(pbr.metallicRoughnessTexture).toBeUndefined(); + expect(pbr.baseColorFactor).toEqual([0.5, 0.5, 0.5, 1.0]); + expect(pbr.metallicFactor).toBe(0.0); // No metallic + expect(pbr.roughnessFactor).toBe(1.0); // Fully rough + expect(material.emissiveTexture).toBe(undefined); + expect(material.normalTexture).toBe(undefined); + expect(material.occlusionTexture).toBe(undefined); + expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]); + }); + + it('sets default material values for metallicRoughness', function() { + // No conversion applied when metallicRoughness flag is set + var options = clone(defaultOptions); + options.metallicRoughness = true; + + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.specularShininess = 1.0; // This is the default set in loadMtl + + var gltf = createGltf(boxObjData, options); + var material = gltf.materials[0]; + var pbr = material.pbrMetallicRoughness; + expect(pbr.baseColorTexture).toBeUndefined(); + expect(pbr.metallicRoughnessTexture).toBeUndefined(); + expect(pbr.baseColorFactor).toEqual([0.5, 0.5, 0.5, 1.0]); + expect(pbr.metallicFactor).toBe(0.0); // No metallic + expect(pbr.roughnessFactor).toBe(1.0); // Fully rough + expect(material.emissiveTexture).toBe(undefined); + expect(material.normalTexture).toBe(undefined); + expect(material.occlusionTexture).toBe(undefined); + expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]); + expect(material.alphaMode).toBe('OPAQUE'); + expect(material.doubleSided).toBe(false); + }); + + it('complex material', function() { + var options = clone(defaultOptions); + options.metallicRoughness = true; + + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.diffuseTexture = diffuseTextureUrl; + defaultMaterial.ambientTexture = ambientTextureUrl; + defaultMaterial.normalTexture = normalTextureUrl; + defaultMaterial.emissiveTexture = emissiveTextureUrl; + defaultMaterial.specularTexture = metallicTextureUrl; + defaultMaterial.specularShininessTexture = roughnessTextureUrl; + boxObjData.images.push(diffuseTexture, ambientTexture, normalTexture, emissiveTexture, metallicTexture, roughnessTexture); + + var gltf = createGltf(boxObjData, options); + var material = gltf.materials[0]; + var pbr = material.pbrMetallicRoughness; + var textureIndexes = [pbr.baseColorTexture.index, pbr.metallicRoughnessTexture.index, material.occlusionTexture.index, material.emissiveTexture.index, material.normalTexture.index].sort(); + expect(textureIndexes).toEqual([0, 1, 2, 3, 4]); + expect(pbr.baseColorFactor).toEqual([1.0, 1.0, 1.0, 1.0]); + expect(pbr.metallicFactor).toBe(1.0); + expect(pbr.roughnessFactor).toBe(1.0); + expect(material.emissiveFactor).toEqual([1.0, 1.0, 1.0]); + }); + + it('packs occlusion in metallic roughness texture', function() { + var options = clone(defaultOptions); + options.metallicRoughness = true; + options.packOcclusion = true; + + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.ambientTexture = diffuseTextureUrl; + defaultMaterial.specularTexture = metallicTextureUrl; + defaultMaterial.specularShininessTexture = roughnessTextureUrl; + boxObjData.images.push(diffuseTexture, metallicTexture, roughnessTexture); + + var gltf = createGltf(boxObjData, options); + var material = gltf.materials[0]; + var pbr = material.pbrMetallicRoughness; + expect(pbr.metallicRoughnessTexture).toEqual({index : 0}); + expect(material.occlusionTexture).toEqual({index : 0}); + }); + + it('does not create metallic roughness texture if decoded image data is not available', function() { + var options = clone(defaultOptions); + options.metallicRoughness = true; + options.packOcclusion = true; + + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.ambientTexture = ambientTextureUrl; // is a .gif which can't be decoded + defaultMaterial.specularTexture = metallicTextureUrl; + defaultMaterial.specularShininessTexture = roughnessTextureUrl; + boxObjData.images.push(ambientTexture, metallicTexture, roughnessTexture); + + var gltf = createGltf(boxObjData, options); + var material = gltf.materials[0]; + var pbr = material.pbrMetallicRoughness; + expect(pbr.metallicRoughnessTexture).toBeUndefined(); + expect(material.occlusionTexture).toBeUndefined(); + }); + + it('sets material for transparent diffuse texture', function() { + var options = clone(defaultOptions); + options.metallicRoughness = true; + + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.diffuseTexture = transparentDiffuseTextureUrl; + boxObjData.images.push(transparentDiffuseTexture); + + var gltf = createGltf(boxObjData, options); + var material = gltf.materials[0]; + expect(material.alphaMode).toBe('BLEND'); + expect(material.doubleSided).toBe(true); + }); + }); + + describe('specularGlosiness', function() { + it('sets default material values for specularGlossiness', function() { + var options = clone(defaultOptions); + options.specularGlossiness = true; + + setDefaultMaterial(boxObjData); + + var gltf = createGltf(boxObjData, options); + var material = gltf.materials[0]; + var pbr = material.extensions.KHR_materials_pbrSpecularGlossiness; + expect(pbr.diffuseTexture).toBeUndefined(); + expect(pbr.specularGlossinessTexture).toBeUndefined(); + expect(pbr.diffuseFactor).toEqual([0.5, 0.5, 0.5, 1.0]); + expect(pbr.specularFactor).toEqual([0.0, 0.0, 0.0]); // No specular color + expect(pbr.glossinessFactor).toEqual(0.0); // Rough surface + expect(material.emissiveTexture).toBe(undefined); + expect(material.normalTexture).toBe(undefined); + expect(material.occlusionTexture).toBe(undefined); + expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]); + expect(material.alphaMode).toBe('OPAQUE'); + expect(material.doubleSided).toBe(false); + }); + + it('complex material', function() { + var options = clone(defaultOptions); + options.specularGlossiness = true; + + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.diffuseTexture = diffuseTextureUrl; + defaultMaterial.ambientTexture = ambientTextureUrl; + defaultMaterial.normalTexture = normalTextureUrl; + defaultMaterial.emissiveTexture = emissiveTextureUrl; + defaultMaterial.specularTexture = metallicTextureUrl; + defaultMaterial.specularShininessTexture = roughnessTextureUrl; + boxObjData.images.push(diffuseTexture, ambientTexture, normalTexture, emissiveTexture, metallicTexture, roughnessTexture); + + var gltf = createGltf(boxObjData, options); + var material = gltf.materials[0]; + var pbr = material.extensions.KHR_materials_pbrSpecularGlossiness; + var textureIndexes = [pbr.diffuseTexture.index, pbr.specularGlossinessTexture.index, material.occlusionTexture.index, material.emissiveTexture.index, material.normalTexture.index].sort(); + expect(textureIndexes).toEqual([0, 1, 2, 3, 4]); + expect(pbr.diffuseFactor).toEqual([1.0, 1.0, 1.0, 1.0]); + expect(pbr.specularFactor).toEqual([1.0, 1.0, 1.0]); + expect(pbr.glossinessFactor).toEqual(1.0); + expect(material.emissiveFactor).toEqual([1.0, 1.0, 1.0]); + }); + + it('does not create metallic roughness texture if decoded image data is not available', function() { + var options = clone(defaultOptions); + options.specularGlossiness = true; + + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.specularTexture = ambientTextureUrl; // is a .gif which can't be decoded; + defaultMaterial.specularShininessTexture = roughnessTextureUrl; + boxObjData.images.push(ambientTexture, roughnessTexture); + + var gltf = createGltf(boxObjData, options); + var material = gltf.materials[0]; + var pbr = material.extensions.KHR_materials_pbrSpecularGlossiness; + expect(pbr.specularGlossinessTexture).toBeUndefined(); + }); + + it('sets material for transparent diffuse texture', function() { + var options = clone(defaultOptions); + options.specularGlossiness = true; + + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.diffuseTexture = transparentDiffuseTextureUrl; + boxObjData.images.push(transparentDiffuseTexture); + + var gltf = createGltf(boxObjData, options); + var material = gltf.materials[0]; + expect(material.alphaMode).toBe('BLEND'); + expect(material.doubleSided).toBe(true); + }); + }); + describe('materialsCommon', function() { it('sets default material values for materialsCommon', function() { var options = clone(defaultOptions); @@ -237,26 +425,23 @@ describe('createGltf', function() { var defaultMaterial = setDefaultMaterial(boxObjData); defaultMaterial.diffuseTexture = diffuseTextureUrl; - boxObjData.images[diffuseTextureUrl] = diffuseTexture; + boxObjData.images.push(diffuseTexture); var gltf = createGltf(boxObjData, options); var kmc = gltf.materials[0].extensions.KHR_materials_common; - var texture = gltf.textures.texture_cesium; - var image = gltf.images.cesium; + var texture = gltf.textures[0]; + var image = gltf.images[0]; expect(kmc.technique).toBe('LAMBERT'); - expect(kmc.values.diffuse).toEqual('texture_cesium'); + expect(kmc.values.diffuse).toEqual({index : 0}); expect(kmc.values.transparency).toBe(1.0); expect(kmc.values.transparent).toBe(false); expect(kmc.values.doubleSided).toBe(false); expect(texture).toEqual({ - format : WebGLConstants.RGB, - internalFormat : WebGLConstants.RGB, - sampler : 'sampler', - source : 'cesium', - target : WebGLConstants.TEXTURE_2D, - type : WebGLConstants.UNSIGNED_BYTE + name : 'cesium', + sampler : 0, + source : 0 }); expect(image).toBeDefined(); @@ -264,7 +449,7 @@ describe('createGltf', function() { expect(image.extras._obj2gltf.source).toBeDefined(); expect(image.extras._obj2gltf.extension).toBe('.png'); - expect(gltf.samplers.sampler).toEqual({ + expect(gltf.samplers[0]).toEqual({ magFilter : WebGLConstants.LINEAR, minFilter : WebGLConstants.NEAREST_MIPMAP_LINEAR, wrapS : WebGLConstants.REPEAT, @@ -276,12 +461,11 @@ describe('createGltf', function() { var options = clone(defaultOptions); options.materialsCommon = true; - var material = new Material(); - material.alpha = 0.4; - boxObjData.materials[0] = material; + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.alpha = 0.4; var gltf = createGltf(boxObjData, options); - var kmc = gltf.materials.Material.extensions.KHR_materials_common; + var kmc = gltf.materials[0].extensions.KHR_materials_common; expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 0.4]); expect(kmc.values.transparency).toBe(1.0); @@ -293,17 +477,15 @@ describe('createGltf', function() { var options = clone(defaultOptions); options.materialsCommon = true; - var material = new Material(); - material.diffuseTexture = diffuseTextureUrl; - material.alpha = 0.4; - boxObjData.materials[0] = material; - - boxObjData.images[diffuseTextureUrl] = diffuseTexture; + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.diffuseTexture = diffuseTextureUrl; + defaultMaterial.alpha = 0.4; + boxObjData.images.push(diffuseTexture); var gltf = createGltf(boxObjData, options); - var kmc = gltf.materials.Material.extensions.KHR_materials_common; + var kmc = gltf.materials[0].extensions.KHR_materials_common; - expect(kmc.values.diffuse).toEqual('texture_cesium'); + expect(kmc.values.diffuse).toEqual({index : 0}); expect(kmc.values.transparency).toBe(0.4); expect(kmc.values.transparent).toBe(true); expect(kmc.values.doubleSided).toBe(true); @@ -313,16 +495,14 @@ describe('createGltf', function() { var options = clone(defaultOptions); options.materialsCommon = true; - var material = new Material(); - material.diffuseTexture = transparentDiffuseTextureUrl; - boxObjData.materials[0] = material; - - boxObjData.images[transparentDiffuseTextureUrl] = transparentDiffuseTexture; + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.diffuseTexture = transparentDiffuseTextureUrl; + boxObjData.images.push(transparentDiffuseTexture); var gltf = createGltf(boxObjData, options); - var kmc = gltf.materials.Material.extensions.KHR_materials_common; + var kmc = gltf.materials[0].extensions.KHR_materials_common; - expect(kmc.values.diffuse).toBe('texture_diffuse'); + expect(kmc.values.diffuse).toEqual({index : 0}); expect(kmc.values.transparency).toBe(1.0); expect(kmc.values.transparent).toBe(true); expect(kmc.values.doubleSided).toBe(true); @@ -332,13 +512,12 @@ describe('createGltf', function() { var options = clone(defaultOptions); options.materialsCommon = true; - var material = new Material(); - material.specularColor = [0.1, 0.1, 0.2, 1]; - material.specularShininess = 0.1; - boxObjData.materials[0] = material; + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.specularColor = [0.1, 0.1, 0.2, 1]; + defaultMaterial.specularShininess = 0.1; var gltf = createGltf(boxObjData, options); - var kmc = gltf.materials.Material.extensions.KHR_materials_common; + var kmc = gltf.materials[0].extensions.KHR_materials_common; expect(kmc.technique).toBe('PHONG'); expect(kmc.values.specular).toEqual([0.1, 0.1, 0.2, 1]); @@ -351,26 +530,23 @@ describe('createGltf', function() { boxObjData.nodes[0].meshes[0].normals.length = 0; - var material = new Material(); - material.diffuseTexture = diffuseTextureUrl; - boxObjData.materials[0] = material; - - boxObjData.images[diffuseTextureUrl] = diffuseTexture; + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.diffuseTexture = diffuseTextureUrl; + boxObjData.images.push(diffuseTexture); var gltf = createGltf(boxObjData, options); - var kmc = gltf.materials.Material.extensions.KHR_materials_common; + var kmc = gltf.materials[0].extensions.KHR_materials_common; expect(kmc.technique).toBe('CONSTANT'); - expect(kmc.values.emission).toEqual('texture_cesium'); + expect(kmc.values.emission).toEqual({index : 0}); }); it('sets default material when texture is missing', function() { var options = clone(defaultOptions); options.materialsCommon = true; - var material = new Material(); - material.diffuseTexture = diffuseTextureUrl; - boxObjData.materials[0] = material; + var defaultMaterial = setDefaultMaterial(boxObjData); + defaultMaterial.diffuseTexture = diffuseTextureUrl; var gltf = createGltf(boxObjData, options); var kmc = gltf.materials[0].extensions.KHR_materials_common; @@ -408,10 +584,10 @@ describe('createGltf', function() { var options = clone(defaultOptions); options.materialsCommon = true; - boxObjData.materials.Material.ambientColor = [1.0, 1.0, 1.0, 1.0]; + boxObjData.materials[0].ambientColor = [1.0, 1.0, 1.0, 1.0]; var gltf = createGltf(boxObjData, options); - var ambient = gltf.materials.Material.extensions.KHR_materials_common.values.ambient; + var ambient = gltf.materials[0].extensions.KHR_materials_common.values.ambient; expect(ambient).toEqual([0.0, 0.0, 0.0, 1.0]); }); diff --git a/specs/lib/loadImageSpec.js b/specs/lib/loadImageSpec.js index a1c6185f..369ef51e 100644 --- a/specs/lib/loadImageSpec.js +++ b/specs/lib/loadImageSpec.js @@ -1,5 +1,4 @@ 'use strict'; -var obj2gltf = require('../../lib/obj2gltf'); var loadImage = require('../../lib/loadImage'); var pngImage = 'specs/data/box-complex-material/shininess.png'; diff --git a/specs/lib/loadObjSpec.js b/specs/lib/loadObjSpec.js index 3b0833a8..404a1feb 100644 --- a/specs/lib/loadObjSpec.js +++ b/specs/lib/loadObjSpec.js @@ -313,7 +313,6 @@ describe('loadObj', function() { expect(loadObj(objExternalResourcesUrl, options) .then(function(data) { - var imagePath = getImagePath(objMissingTextureUrl, 'cesium.png'); expect(data.images.length).toBe(0); // obj references an image file that is outside the input directory expect(data.materials.length).toBe(1); // obj references 2 materials, one of which is outside the input directory expect(console.log.calls.argsFor(0)[0].indexOf('Could not read mtl file') >= 0).toBe(true); From ab6786e463d448e4f8fd34d35d4eaf17a23c9632 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 19 Jul 2017 13:23:06 -0400 Subject: [PATCH 07/21] Remove dependence on gltf-pipeline and added gltfToGlb function --- README.md | 6 -- bin/obj2gltf.js | 39 ------------- doc/cesium.png | Bin 4664 -> 0 bytes lib/createGltf.js | 7 +-- lib/obj2gltf.js | 115 +++++--------------------------------- lib/writeUris.js | 33 ++++++----- package.json | 1 - specs/lib/obj2gltfSpec.js | 114 +++++++++---------------------------- 8 files changed, 62 insertions(+), 253 deletions(-) delete mode 100644 doc/cesium.png diff --git a/README.md b/README.md index c9308783..de7e357e 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,6 @@ Using obj2gltf as a command-line tool: |`-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`| -|`--bypassPipeline`|Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above.|No, default `false`| |`--checkTransparency`|Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. By default textures are considered to be opaque.|No, default `false`| |`--secure`|Prevent the converter from reading image or mtl files outside of the input obj directory.|No, default `false`| |`--inputUpAxis`|Up axis of the obj. Choices are 'X', 'Y', and 'Z'.|No, default `Y`| diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index 1eb82629..4f454bc9 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -48,39 +48,6 @@ var argv = yargs type: 'boolean', default: defaults.separateTextures }, - compress : { - alias: 'c', - describe: 'Quantize positions, compress texture coordinates, and oct-encode normals.', - type: 'boolean', - default: defaults.compress - }, - optimize : { - alias: 'z', - describe: 'Optimize the glTF for size and runtime performance.', - type: 'boolean', - default: defaults.optimize - }, - optimizeForCesium : { - describe: 'Optimize the glTF for Cesium by using the sun as a default light source.', - type: 'boolean', - default: defaults.optimizeForCesium - }, - generateNormals : { - alias: 'n', - describe: 'Generate normals if they are missing.', - type: 'boolean', - default: defaults.generateNormals - }, - ao : { - describe: 'Apply ambient occlusion to the converted model.', - type: 'boolean', - default: defaults.ao - }, - bypassPipeline : { - describe: 'Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above.', - type: 'boolean', - default: defaults.bypassPipeline - }, 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', @@ -138,12 +105,6 @@ var options = { binary : argv.binary, separate : argv.separate, separateTextures : argv.separateTextures, - compress : argv.compress, - optimize : argv.optimize, - optimizeForCesium : argv.optimizeForCesium, - generateNormals : argv.generateNormals, - ao : argv.ao, - bypassPipeline : argv.bypassPipeline, checkTransparency : argv.checkTransparency, secure : argv.secure, inputUpAxis : argv.inputUpAxis, diff --git a/doc/cesium.png b/doc/cesium.png deleted file mode 100644 index 32db61698f08c8d7ef37b002e0ab294df1c34a37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4664 zcmV-8636X{P)*h{so)r31ylG8s-{FD=?-ay?&mm@S*bN2Rtta7@QZdFV`KdV z7odjhl8QL0szf4*hK!;rdvdpanqRh> zGndtR=5NPD8xG?{-MRB31J>(YtWmf}esM{aMpc$bB#|(Gd--=@G;ie06$>qY@3{D= z;;h)?X%K%1W7_Vo%p%Wns&>>k9FA`dRn_4Ro9##Os`?ANV*gdVru~K!9h+pg*(cbC z*soVr_4|-TSle_V_G`xcVxggm;-Df!F)(4skhla@y&2Cr-lpBb<;BXT{l=lHw?Voe z4vmW&Liu&YMUdupV_pe1`>*ksFT@A@eG-&$uTju`-=>hLiC2{IT;9+J6bjLC3~>xK z*3~HJ3%63ZWoVkFa%BZbQN~bs_ed}FJj|J@FKVvKeLT`T@MFe|`IiL6es{d0<;N@P ziFj2L-Bgo9QTN8H_GhqzP%%NEDVVgD%VE<}smLJgarPg8;Wj%|?Mp6gR8rJx$nzC6 zJ`}-T^hS&raWTZ!2B+1|B_t$#mBMr&zx_dRAiLduJ%vOK)H9FE8~%Pjh3GgG?FlZ< zATM4xD2Wvef^t7($_bRBy-M-jBc;Io4reMe&2@RipZ9$!_E$EFvYKYG@90_a(w=IO zS{UBrvPgxG;o}@$^nq~R zPB?zv7;mB4;R3SWXk4V98XZ1wZ4ZP8eb&)nSCoI}$_SDbMxVRo=+{#sn6~2@`$T;! zt*`?@6E1k;D4Q-?<&+#3XJ8%gkJsy6NYxLCqA1^iMsl-_XR)B;%%@^I=uM-;^zd4UTKHy#sBaE5ossH> zR*_X4-p#eHw0LKwXgS|`DeeI=yIgoD%*I7aKDf z){*_&=vpZ^i-`p2o6m50v$DeInJ&*v&SXZV!L%GlY=3I&Pl)|T-WT;vwe%MsIHkvp zF zo?KA&EN3*M{{BTB+0DmChj)tUOGk+Z7AJ~F|8$f1qH;eS@sWQZyd7_xl0kS^-k*0r z&J$CYjMU3Zeqo~5wR`=gbP6mm!raI<8VEXV9W>r*_^_wNYuY4-qTGTvt#DNG7yNCU zez#pwe(BJ(N1#Kl33c&+{FYMjI;JB!EGH-=CctR%3$|Pk-b74HObpp}X84vus)x^4 zE1@u*{%&+o%{5nFeK{qM2(vnw$_k^W6_jn{%;v9kd#Yi&OpXI8PnC)pIXCF0yP)Ki z6GUC(DT)yKvvOQtbNO{8yz%DG=M^7ynEZlP6=jC(1a?)O&NiB=H9F!PQNIy~A1+Sa zn0)aA`3r^V^p?@#+<(iK3rbMcxfB)^aKW1g`Rp_bI`29P(djQnhtK*5_jg^2_97(@ z8@LGn7}A=|w$#Jy->ooudXcAqGus6{{Z@r&YHcUW3DMGaPW*1e9Ra4hpv8ZA2rdYn zu{Dit;)w{?>(9MgDbD+EsA;UxFCxaW7QJ#mKH4$GYz+h-@aq&3HP9*k$mI_ZG^H;&Hl@#d%ABo{qoXg*+C$^P~Q)k;Kr2xaXiRZS=V9Rm7 zJUQg^OlZ(GY@@061r!oB&{401PI#SBxb~tDonAEh?QFR|9l}SB*y$h?9}~soJNo+1 z)LmVaV6*=m{%tl2Z=)=_#9@QwADai%2aZ9V?&FSGV)S%Z`DV^^D16JzpIrwFS=0F| zDSNm$RJNN^_$WG5%jWN|lmfRnP}xW+e7OGcB3lM(dH(Dh=@eL?abDvZ4I7j~VonI7 zpM#E7Vuc{D4OCnQILff)b_zRfEf~xL`W%f0d3^|-_8l0dT-dRo)2)M_x$t)!(H6k( zX`pGivkeVse8OO)KN%ek4}^NZ$sgCe4n_HyFBfxY$!BsiQ$09w;>52&onmM7l>D+k za>m>6&y&Lc!B)k|&pU3ub*0%W#*5Ppoo}h(R6O^+s-QCbg*Wf~`P8#lBrO@i=8xLA z-HBUKO@Rh_J*dHajrJgg#2izjTPpP+71sfp3_8^@KHehX#Y$Co4Ar!osMrAdnb8Nt z+iWA@BG|^oKwEzVAz#5bGoZW`Tw0J9ZwOON?2JCI*fZN`I(%)kxvg`peXixKcw*gc ztl`Y`?5huo_V&(a?zK&AVjfJ*jb)+mrk($~{LI-?V$RARvH7AN&bi?nRo@h7oP4g) zurVnl=Jb})kpxwJoR7CwxK72*IaF*1{mtkIqOD&a5OG1?&}m9 z0Gin(P0KJH&;8@bjM%Tg42_wo3h+6|4>uvaX zNT1d#+G+T`9`e8!bXwqNxAw$4w7}hY{n6+M{GP$bp>Z`Qg>`^?Ft}JQC{0D-Y=~ewSFTxaMqbjp!uN9V8{?V4Fexr_E)3htH*GmWcN|vg;Pg=T?uwQ|AyI>mrV=ktywoV;)GdV3WBPb4R^a>L&_O?C&q>wx zQJ9VuM#tTa6cROHJ{Olanuo&IKY1nQp2pObo+LUbZD`agr>K+y36_V7o8S>Es&2Fwa0F**?HJ@WG~ zGa7{>J6seSnBzE_A2GH85_6*?4NYe@lpa4M@;_K19?AUyYqIVNceuT=cfGzOsHv~g zx3<3e_r+p*PI!;)9?Txz{^a_}VNF%xgLulM(LSTiB2fdKXA_qWhP{}vS2DI7I-+ZAmw%t~fM>@*PGAPkakueh24|6T{5=}ALA{yIL2%k?_r`;D(Et^zTsX zz|`m#%lO3;`rTf96_<&PnH2jRDqbfNqmQ_9#5dvZr-Nc+Brx21D!PMcjE>+%lTQ~c zFmlMREAKI*BdIU^UK*wFh z#l}R6dZs%YW)SG)kHYa#UzTBspg+`e%a>@44(Do%zCO5dcFECOAecf~r(Hb4`ZAjwM`m5`FdfMm%oOx3&p0#}J+nn)w zQb|jOw`8rIbO%){YH$+>G#2iZYYb|{=;ejZ@fnzG9&Vzbq5|kmqa(iO-mqR(wXmc88+;l!^-=3Zv^_esJ-4A1@$5X`>X5rcmx{2{v0i6%#~vjP5&EllRO)(Eb(I zfA4$Wpm0-q+~~;K;#rr>TsF3ZGhk0BDSJd~`s!OP-L5)#@Zd}El|tOE2_I|@G$1|# z*$NF@1RvCPqu}q`Ag&O8Zh_bhzBEcE{C*ehfC~8p0Czq?C;My6)y4DXM7#qHb%t~L zJ^aMiPhVs6=}P+UI4u{~%3J2a2;0n=(rV4fx#5$iHccK$#Y-fT zo+N9{uWp>4GkPCqPAim@J+gWB$_Yz0Zk*cXG)p3p#1N;`c~Q!$pG*V8mvLr|Mle%o zef`R&s-vr^fA^JJ1=?ZZRdA$iqo&ySm^wANr^M$7w1VN5{X12 ukw_#Gi9{liNF)-8L?V$$Bz=Ja1O6AzGCweLB9QU`0000 1) { return Promise.reject(new RuntimeError('Only one material type may be set from [--metallicRoughness, --specularGlossiness, --materialsCommon].')); } - gltfPath = path.join(path.dirname(gltfPath), modelName + extension); - var resourcesDirectory = options.bypassPipeline ? path.dirname(gltfPath) : obj2gltf._getTempDirectory(); - - var aoOptions = ao ? {} : undefined; - - var pipelineOptions = { - createDirectory : false, - basePath : resourcesDirectory, - binary : binary, - embed : !separate, - embedImage : !separateTextures, - quantize : compress, - compressTextureCoordinates : compress, - encodeNormals : compress, - preserve : !optimize, - optimizeForCesium : optimizeForCesium, - smoothNormals : generateNormals, - aoOptions : aoOptions, - textureCompressionOptions : textureCompressionOptions + var jsonOptions = { + spaces : 2 }; return loadObj(objPath, options) @@ -128,28 +96,17 @@ function obj2gltf(objPath, gltfPath, options) { return createGltf(objData, options); }) .then(function(gltf) { - return writeUris(gltf, gltfPath, resourcesDirectory, options); + return writeUris(gltf, gltfPath, options); }) .then(function(gltf) { - if (bypassPipeline) { - return fsExtra.outputJson(gltfPath, gltf); + if (binary) { + var glb = gltfToGlb(gltf); + return fsExtra.outputFile(gltfPath, glb); } - return GltfPipeline.processJSONToDisk(gltf, gltfPath, pipelineOptions); - }) - .finally(function() { - return cleanup(resourcesDirectory, options); + return fsExtra.outputJson(gltfPath, gltf, jsonOptions); }); } -function cleanup(resourcesDirectory, options) { - if (!options.bypassPipeline && options.separate) { - fsExtra.remove(resourcesDirectory, function () { - // Don't fail simply because we couldn't - // clean up the temporary files. - }); - } -} - /** * Default values that will be used when calling obj2gltf(options) unless specified in the options object. */ @@ -173,42 +130,6 @@ obj2gltf.defaults = { * @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 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 @@ -266,19 +187,9 @@ obj2gltf.defaults = { } }; -/** - * Exposed for testing - * - * @private - */ -obj2gltf._getTempDirectory = function () { - return path.join(os.tmpdir(), uuid()); -}; - /** * A callback function that logs messages. * @callback Logger * * @param {String} message The message to log. */ - diff --git a/lib/writeUris.js b/lib/writeUris.js index 1dea8c2e..c7e785bf 100644 --- a/lib/writeUris.js +++ b/lib/writeUris.js @@ -15,7 +15,6 @@ module.exports = writeUris; * * @param {Object} gltf The glTF asset. * @param {String} gltfPath Path where the glTF will be saved. - * @param {String} resourcesDirectory Path where separate resources will be saved. * @param {Object} options An object with the following properties: * @param {Boolean} options.separate Writes out separate buffers. * @param {Boolean} options.separateTextures Write out separate textures only. @@ -23,7 +22,7 @@ module.exports = writeUris; * * @private */ -function writeUris(gltf, gltfPath, resourcesDirectory, options) { +function writeUris(gltf, gltfPath, options) { var separate = options.separate; var separateTextures = options.separateTextures; @@ -48,16 +47,16 @@ function writeUris(gltf, gltfPath, resourcesDirectory, options) { var name = path.basename(gltfPath, path.extname(gltfPath)); - if (separate) { - promises.push(writeSeparateBuffer(gltf, resourcesDirectory, name)); + if (separateTextures) { + promises.push(writeSeparateTextures(gltf, gltfPath)); } else { - writeEmbeddedBuffer(gltf); + writeEmbeddedTextures(gltf); } - if (separateTextures) { - promises.push(writeSeparateTextures(gltf, resourcesDirectory)); + if (separate) { + promises.push(writeSeparateBuffer(gltf, gltfPath, name)); } else { - writeEmbeddedTextures(gltf); + writeEmbeddedBuffer(gltf); } return Promise.all(promises) @@ -99,22 +98,22 @@ function cleanup(gltf) { removeEmpty(gltf); } -function writeSeparateBuffer(gltf, resourcesDirectory, name) { +function writeSeparateBuffer(gltf, gltfPath, name) { var buffer = gltf.buffers[0]; var source = buffer.extras._obj2gltf.source; var bufferUri = name + '.bin'; buffer.uri = bufferUri; - var bufferPath = path.join(resourcesDirectory, bufferUri); + var bufferPath = path.join(path.dirname(gltfPath), bufferUri); return fsExtra.outputFile(bufferPath, source); } -function writeSeparateTextures(gltf, resourcesDirectory) { +function writeSeparateTextures(gltf, gltfPath) { var images = gltf.images; return Promise.map(images, function(image) { var extras = image.extras._obj2gltf; var imageUri = image.name + extras.extension; image.uri = imageUri; - var imagePath = path.join(resourcesDirectory, imageUri); + var imagePath = path.join(path.dirname(gltfPath), imageUri); return fsExtra.outputFile(imagePath, extras.source); }, {concurrency : 10}); } @@ -126,11 +125,19 @@ function writeEmbeddedBuffer(gltf) { } function writeEmbeddedTextures(gltf) { + var bufferSource = gltf.buffers[0].extras._obj2gltf.source; var images = gltf.images; var imagesLength = images.length; for (var i = 0; i < imagesLength; ++i) { var image = images[i]; var extras = image.extras._obj2gltf; - image.uri = 'data:' + mime.lookup(extras.extension) + ';base64,' + extras.source.toString('base64'); + var imageSource = extras.source; + image.mimeType = mime.lookup(extras.extension); + gltf.bufferViews.push({ + buffer : 0, + byteOffset : bufferSource.length, + byteLength : imageSource.byteLength + }); + bufferSource = Buffer.concat([bufferSource, imageSource]); } } diff --git a/package.json b/package.json index 4111da6b..46fa4c4c 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "bluebird": "^3.5.0", "cesium": "^1.35.2", "fs-extra": "^4.0.0", - "gltf-pipeline": "^1.0.0", "jpeg-js": "^0.3.3", "mime": "^1.3.6", "pngjs": "^3.2.0", diff --git a/specs/lib/obj2gltfSpec.js b/specs/lib/obj2gltfSpec.js index dfc12bbc..1ea5a0ef 100644 --- a/specs/lib/obj2gltfSpec.js +++ b/specs/lib/obj2gltfSpec.js @@ -1,8 +1,6 @@ 'use strict'; var Cesium = require('Cesium'); var fsExtra = require('fs-extra'); -var GltfPipeline = require('gltf-pipeline').Pipeline; -var os = require('os'); var path = require('path'); var Promise = require('bluebird'); var obj2gltf = require('../../lib/obj2gltf'); @@ -15,111 +13,59 @@ var glbPath = 'specs/data/box-textured/box-textured.glb'; var objPathNonExistent = 'specs/data/non-existent.obj'; describe('obj2gltf', function() { - var tempDirectory; - - beforeAll(function() { - expect(obj2gltf._getTempDirectory()).toContain(os.tmpdir()); - tempDirectory = path.join(os.tmpdir(), 'testPath'); - spyOn(obj2gltf, '_getTempDirectory').and.returnValue(tempDirectory); - spyOn(fsExtra, 'outputJson'); - spyOn(fsExtra, 'outputFile'); - spyOn(fsExtra, 'remove'); - }); - beforeEach(function() { - spyOn(GltfPipeline, 'processJSONToDisk').and.returnValue(Promise.resolve()); + spyOn(fsExtra, 'outputJson').and.returnValue(Promise.resolve()); + spyOn(fsExtra, 'outputFile').and.returnValue(Promise.resolve()); }); - it('converts an obj to gltf', function(done) { + it('converts obj to gltf', function(done) { expect(obj2gltf(objPath, gltfPath) .then(function() { - var args = GltfPipeline.processJSONToDisk.calls.first().args; - var gltf = args[0]; - var outputPath = args[1]; - var options = args[2]; + var args = fsExtra.outputJson.calls.first().args; + var outputPath = args[0]; + var gltf = args[1]; expect(path.normalize(outputPath)).toEqual(path.normalize(gltfPath)); expect(gltf).toBeDefined(); expect(gltf.images.length).toBe(1); - expect(options).toEqual({ - basePath : tempDirectory, - createDirectory : false, - binary : false, - embed : true, - embedImage : true, - encodeNormals : false, - quantize : false, - compressTextureCoordinates : false, - aoOptions : undefined, - smoothNormals : false, - optimizeForCesium : false, - textureCompressionOptions : undefined, - preserve : true - }); }), done).toResolve(); }); - it('sets options', function(done) { - var textureCompressionOptions = { - format : 'dxt1', - quality : 10 - }; + it('converts obj to glb', function(done) { var options = { - binary : true, - separate : true, - separateTextures : true, - compress : true, - optimize : true, - optimizeForCesium : true, - generateNormals : true, - ao : true, - textureCompressionOptions : textureCompressionOptions, - checkTransparency : true, - secure : true, - inputUpAxis : 'Z', - outputUpAxis : 'X', - logger : obj2gltf.defaults.logger + binary : true }; - expect(obj2gltf(objPath, gltfPath, options) .then(function() { - var args = GltfPipeline.processJSONToDisk.calls.first().args; - var options = args[2]; - expect(options).toEqual({ - basePath : tempDirectory, - createDirectory : false, - binary : true, - embed : false, - embedImage : false, - encodeNormals : true, - quantize : true, - compressTextureCoordinates : true, - aoOptions : {}, - smoothNormals : true, - optimizeForCesium : true, - textureCompressionOptions : textureCompressionOptions, - preserve : false - }); - expect(fsExtra.outputFile.calls.count()).toBe(2); // Saves out .png and .bin + var args = fsExtra.outputFile.calls.first().args; + var outputPath = args[0]; + var glb = args[1]; + expect(path.extname(outputPath)).toBe('.glb'); + var magic = glb.toString('utf8', 0, 4); + expect(magic).toBe('glTF'); }), done).toResolve(); }); - it('saves as binary if gltfPath has a .glb extension', function(done) { + it('converts obj to glb when gltfPath has a .glb extension', function(done) { expect(obj2gltf(objPath, glbPath) .then(function() { - var args = GltfPipeline.processJSONToDisk.calls.first().args; - var options = args[2]; - expect(options.binary).toBe(true); + var args = fsExtra.outputFile.calls.first().args; + var outputPath = args[0]; + var glb = args[1]; + expect(path.extname(outputPath)).toBe('.glb'); + var magic = glb.toString('utf8', 0, 4); + expect(magic).toBe('glTF'); }), done).toResolve(); }); - it('bypassPipeline flag bypasses gltf-pipeline', function(done) { + it('writes out separate resources', function(done) { var options = { - bypassPipeline : true + separate : true, + separateTextures : true }; expect(obj2gltf(objPath, gltfPath, options) .then(function() { - expect(fsExtra.outputJson).toHaveBeenCalled(); - expect(GltfPipeline.processJSONToDisk).not.toHaveBeenCalled(); + expect(fsExtra.outputFile.calls.count()).toBe(2); // Saves out .png and .bin + expect(fsExtra.outputJson.calls.count()).toBe(1); // Saves out .gltf }), done).toResolve(); }); @@ -139,14 +85,6 @@ describe('obj2gltf', function() { }).toThrowDeveloperError(); }); - it('rejects if both bpypassPipeline and binary are true', function(done) { - var options = { - bypassPipeline : true, - binary : true - }; - expect(obj2gltf(objPath, gltfPath, options), done).toRejectWith(RuntimeError); - }); - it('rejects if more than one material type is set', function(done) { var options = { metallicRoughness : true, From aaf44e75dc099ff93716ae4230850b0bb9377523 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 19 Jul 2017 17:56:24 -0400 Subject: [PATCH 08/21] Fix incompatible byte strides --- bin/obj2gltf.js | 18 +++--- lib/createGltf.js | 117 ++++++++++++++++++++----------------- lib/getBufferPadded.js | 19 ++++++ lib/getJsonBufferPadded.js | 34 +++++++++++ lib/gltfToGlb.js | 67 +++++++++++++++++++++ lib/obj2gltf.js | 12 ++-- lib/writeUris.js | 22 +++++-- 7 files changed, 218 insertions(+), 71 deletions(-) create mode 100644 lib/getBufferPadded.js create mode 100644 lib/getJsonBufferPadded.js create mode 100644 lib/gltfToGlb.js diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index 4f454bc9..a2dc0dd3 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -116,10 +116,14 @@ var options = { console.time('Total'); -obj2gltf(objPath, gltfPath, options) - .then(function() { - console.timeEnd('Total'); - }) - .catch(function(error) { - console.log(error); - }); +try { + obj2gltf(objPath, gltfPath, options) + .then(function() { + console.timeEnd('Total'); + }) + .catch(function(error) { + console.log(error); + }); +} catch(error) { + console.log(error); +} diff --git a/lib/createGltf.js b/lib/createGltf.js index 86ebbe29..59777f2c 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -2,6 +2,7 @@ var Cesium = require('cesium'); var path = require('path'); var PNG = require('pngjs').PNG; +var getBufferPadded = require('./getBufferPadded'); var Material = require('./Material'); var CesiumMath = Cesium.Math; @@ -57,12 +58,14 @@ function createGltf(objData, options) { }); var bufferState = { - vertexBuffers : [], - vertexBufferByteOffset : 0, - vertexBufferViewIndex : 0, + positionBuffers : [], + normalBuffers : [], + uvBuffers : [], indexBuffers : [], - indexBufferByteOffset : 0, - indexBufferViewIndex : 1 + positionAccessors : [], + normalAccessors : [], + uvAccessors : [], + indexAccessors : [] }; var uint32Indices = requiresUint32Indices(nodes); @@ -98,49 +101,55 @@ function createGltf(objData, options) { } addBuffers(gltf, bufferState); - return gltf; } -function addBuffers(gltf, bufferState) { - var bufferName = 'buffer'; - var vertexBufferViewName = 'bufferView_vertex'; - var indexBufferViewName = 'bufferView_index'; +function addBufferView(gltf, buffers, accessors, byteStride, target) { + var length = buffers.length; + if (length === 0) { + return; + } + var bufferViewIndex = gltf.bufferViews.length; + var previousBufferView = gltf.bufferViews[bufferViewIndex - 1]; + var byteOffset = defined(previousBufferView) ? previousBufferView.byteOffset + previousBufferView.byteLength : 0; + var byteLength = 0; + for (var i = 0; i < length; ++i) { + var accessor = gltf.accessors[accessors[i]]; + accessor.bufferView = bufferViewIndex; + accessor.byteOffset = byteLength; + byteLength += buffers[i].length; + } + gltf.bufferViews.push({ + name : 'bufferView_' + bufferViewIndex, + buffer : 0, + byteLength : byteLength, + byteOffset : byteOffset, + byteStride : byteStride, + target : target + }); +} - var vertexBuffers = bufferState.vertexBuffers; - var indexBuffers = bufferState.indexBuffers; - var vertexBufferByteLength = bufferState.vertexBufferByteOffset; - var indexBufferByteLength = bufferState.indexBufferByteOffset; +function addBuffers(gltf, bufferState) { + // Positions and normals share the same byte stride so they can share the same bufferView + var positionsAndNormalsAccessors = bufferState.positionAccessors.concat(bufferState.normalAccessors); + var positionsAndNormalsBuffers = bufferState.positionBuffers.concat(bufferState.normalBuffers); + addBufferView(gltf, positionsAndNormalsBuffers, positionsAndNormalsAccessors, 12, WebGLConstants.ARRAY_BUFFER); + addBufferView(gltf, bufferState.uvBuffers, bufferState.uvAccessors, 8, WebGLConstants.ARRAY_BUFFER); + addBufferView(gltf, bufferState.indexBuffers, bufferState.indexAccessors, undefined, WebGLConstants.ELEMENT_ARRAY_BUFFER); var buffers = []; - buffers = buffers.concat(vertexBuffers, indexBuffers); - var buffer = Buffer.concat(buffers); + buffers = buffers.concat(bufferState.positionBuffers, bufferState.normalBuffers, bufferState.uvBuffers, bufferState.indexBuffers); + var buffer = getBufferPadded(Buffer.concat(buffers)); gltf.buffers.push({ - name : bufferName, - byteLength : buffer.byteLength, + name : 'buffer', + byteLength : buffer.length, extras : { _obj2gltf : { source : buffer } } }); - - gltf.bufferViews.push({ - name : vertexBufferViewName, - buffer : 0, - byteLength : vertexBufferByteLength, - byteOffset : 0, - target : WebGLConstants.ARRAY_BUFFER - }); - - gltf.bufferViews.push({ - name : indexBufferViewName, - buffer : 0, - byteLength : indexBufferByteLength, - byteOffset : vertexBufferByteLength, - target : WebGLConstants.ELEMENT_ARRAY_BUFFER - }); } function getImage(images, imagePath) { @@ -304,7 +313,7 @@ function encodePng(pixels, width, height, inputChannels, outputChannels) { // Constants defined by pngjs var rgbColorType = 2; - var rgbaColorType = 4; + var rgbaColorType = 6; var colorType = outputChannels === 4 ? rgbaColorType : rgbColorType; var inputColorType = inputChannels === 4 ? rgbaColorType : rgbColorType; @@ -736,16 +745,13 @@ function getMaterial(gltf, materials, images, materialName, hasNormals, options) return materialIndex; } -function addVertexAttribute(gltf, bufferState, array, components, name) { - var buffer = array.toFloatBuffer(); +function addVertexAttribute(gltf, array, components, name) { var count = array.length / components; var minMax = array.getMinMax(components); var type = (components === 3 ? 'VEC3' : 'VEC2'); var accessor = { name : name, - bufferView : bufferState.vertexBufferViewIndex, - byteOffset : bufferState.vertexBufferByteOffset, componentType : WebGLConstants.FLOAT, count : count, min : minMax.min, @@ -753,24 +759,18 @@ function addVertexAttribute(gltf, bufferState, array, components, name) { type : type }; - bufferState.vertexBufferByteOffset += buffer.length; - bufferState.vertexBuffers.push(buffer); - var accessorIndex = gltf.accessors.length; gltf.accessors.push(accessor); return accessorIndex; } -function addIndexArray(gltf, bufferState, array, uint32Indices, name) { - var buffer = uint32Indices ? array.toUint32Buffer() : array.toUint16Buffer(); +function addIndexArray(gltf, array, uint32Indices, name) { var componentType = uint32Indices ? WebGLConstants.UNSIGNED_INT : WebGLConstants.UNSIGNED_SHORT; var count = array.length; var minMax = array.getMinMax(1); var accessor = { name : name, - bufferView : bufferState.indexBufferViewIndex, - byteOffset : bufferState.indexBufferByteOffset, componentType : componentType, count : count, min : minMax.min, @@ -778,9 +778,6 @@ function addIndexArray(gltf, bufferState, array, uint32Indices, name) { type : 'SCALAR' }; - bufferState.indexBufferByteOffset += buffer.length; - bufferState.indexBuffers.push(buffer); - var accessorIndex = gltf.accessors.length; gltf.accessors.push(accessor); return accessorIndex; @@ -807,15 +804,25 @@ function addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, opti var hasNormals = mesh.normals.length > 0; var hasUVs = mesh.uvs.length > 0; + var accessorIndex; var attributes = {}; if (hasPositions) { - attributes.POSITION = addVertexAttribute(gltf, bufferState, mesh.positions, 3, mesh.name + '_positions'); + accessorIndex = addVertexAttribute(gltf, mesh.positions, 3, mesh.name + '_positions'); + attributes.POSITION = accessorIndex; + bufferState.positionBuffers.push(mesh.positions.toFloatBuffer()); + bufferState.positionAccessors.push(accessorIndex); } if (hasNormals) { - attributes.NORMAL = addVertexAttribute(gltf, bufferState, mesh.normals, 3, mesh.name + '_normals'); + accessorIndex = addVertexAttribute(gltf, mesh.normals, 3, mesh.name + '_normals'); + attributes.NORMAL = accessorIndex; + bufferState.normalBuffers.push(mesh.normals.toFloatBuffer()); + bufferState.normalAccessors.push(accessorIndex); } if (hasUVs) { - attributes.TEXCOORD_0 = addVertexAttribute(gltf, bufferState, mesh.uvs, 2, mesh.name + '_texcoords'); + accessorIndex = addVertexAttribute(gltf, mesh.uvs, 2, mesh.name + '_texcoords'); + attributes.TEXCOORD_0 = accessorIndex; + bufferState.uvBuffers.push(mesh.uvs.toFloatBuffer()); + bufferState.uvAccessors.push(accessorIndex); } // Unload resources @@ -828,7 +835,11 @@ function addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, opti var primitivesLength = primitives.length; for (var i = 0; i < primitivesLength; ++i) { var primitive = primitives[i]; - var indexAccessorIndex = addIndexArray(gltf, bufferState, primitive.indices, uint32Indices, mesh.name + '_' + i + '_indices'); + var indexAccessorIndex = addIndexArray(gltf, primitive.indices, uint32Indices, mesh.name + '_' + i + '_indices'); + var indexBuffer = uint32Indices ? primitive.indices.toUint32Buffer() : primitive.indices.toUint16Buffer(); + bufferState.indexBuffers.push(indexBuffer); + bufferState.indexAccessors.push(indexAccessorIndex); + primitive.indices = undefined; // Unload resources var materialIndex = getMaterial(gltf, materials, images, primitive.material, hasNormals, options); diff --git a/lib/getBufferPadded.js b/lib/getBufferPadded.js new file mode 100644 index 00000000..a5287a52 --- /dev/null +++ b/lib/getBufferPadded.js @@ -0,0 +1,19 @@ +'use strict'; +module.exports = getBufferPadded; + +/** + * Pad the buffer to the next 4-byte boundary to ensure proper alignment for the section that follows. + * + * @param {Buffer} buffer The buffer. + * @returns {Buffer} The padded buffer. + * + * @private + */ +function getBufferPadded(buffer) { + var boundary = 4; + var byteLength = buffer.length; + var remainder = byteLength % boundary; + var padding = (remainder === 0) ? 0 : boundary - remainder; + var emptyBuffer = Buffer.alloc(padding); + return Buffer.concat([buffer, emptyBuffer]); +} diff --git a/lib/getJsonBufferPadded.js b/lib/getJsonBufferPadded.js new file mode 100644 index 00000000..135b8f49 --- /dev/null +++ b/lib/getJsonBufferPadded.js @@ -0,0 +1,34 @@ +'use strict'; +var Cesium = require('cesium'); + +var defaultValue = Cesium.defaultValue; +var defined = Cesium.defined; + +module.exports = getJsonBufferPadded; + +/** + * Convert the JSON object to a padded buffer. + * + * Pad the JSON with extra whitespace to fit the next 4-byte boundary. This ensures proper alignment + * for the section that follows. + * + * @param {Object} [json] The JSON object. + * @returns {Buffer} The padded JSON buffer. + * + * @private + */ +function getJsonBufferPadded(json) { + var string = JSON.stringify(json); + + var boundary = 8; + var byteLength = Buffer.byteLength(string); + var remainder = byteLength % boundary; + var padding = (remainder === 0) ? 0 : boundary - remainder; + var whitespace = ''; + for (var i = 0; i < padding; ++i) { + whitespace += ' '; + } + string += whitespace; + + return Buffer.from(string); +} diff --git a/lib/gltfToGlb.js b/lib/gltfToGlb.js new file mode 100644 index 00000000..350f7877 --- /dev/null +++ b/lib/gltfToGlb.js @@ -0,0 +1,67 @@ +'use strict'; +var Cesium = require('cesium'); +var getJsonBufferPadded = require('./getJsonBufferPadded'); + +var isDataUri = Cesium.isDataUri; + +module.exports = gltfToGlb; + +/** + * Convert a glTF to binary glTF. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @returns {Promise} A promise that resolves to a buffer containing the binary glTF. + * + * @private + */ +function gltfToGlb(gltf) { + var buffer = gltf.buffers[0]; + var binaryBuffer; + if (isDataUri(buffer.uri)) { + binaryBuffer = dataUriToBuffer(buffer.uri); + delete buffer.uri; + } else { + binaryBuffer = Buffer.alloc(0); + } + + // Create padded binary scene string + var jsonBuffer = getJsonBufferPadded(gltf); + + // Allocate buffer (Global header) + (JSON chunk header) + (JSON chunk) + (Binary chunk header) + (Binary chunk) + var glbLength = 12 + 8 + jsonBuffer.length + 8 + binaryBuffer.length; + var glb = Buffer.alloc(glbLength); + + // Write binary glTF header (magic, version, length) + var byteOffset = 0; + glb.writeUInt32LE(0x46546C67, byteOffset); + byteOffset += 4; + glb.writeUInt32LE(2, byteOffset); + byteOffset += 4; + glb.writeUInt32LE(glbLength, byteOffset); + byteOffset += 4; + + // Write JSON Chunk header (length, type) + glb.writeUInt32LE(jsonBuffer.length, byteOffset); + byteOffset += 4; + glb.writeUInt32LE(0x4E4F534A, byteOffset); // JSON + byteOffset += 4; + + // Write JSON Chunk + jsonBuffer.copy(glb, byteOffset); + byteOffset += jsonBuffer.length; + + // Write Binary Chunk header (length, type) + glb.writeUInt32LE(binaryBuffer.length, byteOffset); + byteOffset += 4; + glb.writeUInt32LE(0x004E4942, byteOffset); // BIN + byteOffset += 4; + + // Write Binary Chunk + binaryBuffer.copy(glb, byteOffset); + return glb; +} + +function dataUriToBuffer(dataUri) { + var data = dataUri.slice(dataUri.indexOf(',')); + return Buffer.from(data, 'base64'); +} diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index c9ee36f4..6dbcdcaf 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -72,21 +72,19 @@ function obj2gltf(objPath, gltfPath, options) { throw new DeveloperError('gltfPath is required'); } + if (metallicRoughness + specularGlossiness + materialsCommon > 1) { + throw new DeveloperError('Only one material type may be set from [--metallicRoughness, --specularGlossiness, --materialsCommon].'); + } + var extension = path.extname(gltfPath).toLowerCase(); var modelName = path.basename(gltfPath, path.extname(gltfPath)); - if (extension === '.glb') { + if (binary || extension === '.glb') { binary = true; - } - if (binary) { extension = '.glb'; } gltfPath = path.join(path.dirname(gltfPath), modelName + extension); - if (metallicRoughness + specularGlossiness + materialsCommon > 1) { - return Promise.reject(new RuntimeError('Only one material type may be set from [--metallicRoughness, --specularGlossiness, --materialsCommon].')); - } - var jsonOptions = { spaces : 2 }; diff --git a/lib/writeUris.js b/lib/writeUris.js index c7e785bf..a74b63fb 100644 --- a/lib/writeUris.js +++ b/lib/writeUris.js @@ -4,6 +4,7 @@ var fsExtra = require('fs-extra'); var mime = require('mime'); var path = require('path'); var Promise = require('bluebird'); +var getBufferPadded = require('./getBufferPadded'); var defined = Cesium.defined; var RuntimeError = Cesium.RuntimeError; @@ -125,19 +126,32 @@ function writeEmbeddedBuffer(gltf) { } function writeEmbeddedTextures(gltf) { - var bufferSource = gltf.buffers[0].extras._obj2gltf.source; + var buffer = gltf.buffers[0]; + var bufferExtras = buffer.extras._obj2gltf; + var bufferSource = bufferExtras.source; var images = gltf.images; var imagesLength = images.length; + var sources = [bufferSource]; + var byteOffset = bufferSource.length; + for (var i = 0; i < imagesLength; ++i) { var image = images[i]; var extras = image.extras._obj2gltf; var imageSource = extras.source; + var imageByteLength = imageSource.length; + image.mimeType = mime.lookup(extras.extension); + image.bufferView = gltf.bufferViews.length; gltf.bufferViews.push({ buffer : 0, - byteOffset : bufferSource.length, - byteLength : imageSource.byteLength + byteOffset : byteOffset, + byteLength : imageByteLength }); - bufferSource = Buffer.concat([bufferSource, imageSource]); + byteOffset += imageByteLength; + sources.push(imageSource); } + + var source = getBufferPadded(Buffer.concat(sources)); + bufferExtras.source = source; + buffer.byteLength = source.length; } From b52f63543ebd7faee52cfb91315338ccb171aafc Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 24 Jul 2017 09:36:45 -0400 Subject: [PATCH 09/21] Small fixes --- lib/getBufferPadded.js | 3 + lib/getJsonBufferPadded.js | 7 +- specs/data/box/box.gltf | 150 ------------------------------------- 3 files changed, 4 insertions(+), 156 deletions(-) delete mode 100644 specs/data/box/box.gltf diff --git a/lib/getBufferPadded.js b/lib/getBufferPadded.js index a5287a52..9b5b6455 100644 --- a/lib/getBufferPadded.js +++ b/lib/getBufferPadded.js @@ -13,6 +13,9 @@ function getBufferPadded(buffer) { var boundary = 4; var byteLength = buffer.length; var remainder = byteLength % boundary; + if (remainder === 0) { + return buffer; + } var padding = (remainder === 0) ? 0 : boundary - remainder; var emptyBuffer = Buffer.alloc(padding); return Buffer.concat([buffer, emptyBuffer]); diff --git a/lib/getJsonBufferPadded.js b/lib/getJsonBufferPadded.js index 135b8f49..f51b4c3d 100644 --- a/lib/getJsonBufferPadded.js +++ b/lib/getJsonBufferPadded.js @@ -1,9 +1,4 @@ 'use strict'; -var Cesium = require('cesium'); - -var defaultValue = Cesium.defaultValue; -var defined = Cesium.defined; - module.exports = getJsonBufferPadded; /** @@ -20,7 +15,7 @@ module.exports = getJsonBufferPadded; function getJsonBufferPadded(json) { var string = JSON.stringify(json); - var boundary = 8; + var boundary = 4; var byteLength = Buffer.byteLength(string); var remainder = byteLength % boundary; var padding = (remainder === 0) ? 0 : boundary - remainder; diff --git a/specs/data/box/box.gltf b/specs/data/box/box.gltf deleted file mode 100644 index cc017b13..00000000 --- a/specs/data/box/box.gltf +++ /dev/null @@ -1,150 +0,0 @@ -{ - "accessors": [ - { - "name": "Cube-Mesh_positions", - "bufferView": 0, - "byteOffset": 0, - "componentType": 5126, - "count": 24, - "min": [ - -1, - -1, - -1 - ], - "max": [ - 1, - 1, - 1 - ], - "type": "VEC3" - }, - { - "name": "Cube-Mesh_normals", - "bufferView": 0, - "byteOffset": 288, - "componentType": 5126, - "count": 24, - "min": [ - -1, - -1, - -1 - ], - "max": [ - 1, - 1, - 1 - ], - "type": "VEC3" - }, - { - "name": "Cube-Mesh_texcoords", - "bufferView": 0, - "byteOffset": 576, - "componentType": 5126, - "count": 24, - "min": [ - 0, - 0 - ], - "max": [ - 1, - 1 - ], - "type": "VEC2" - }, - { - "name": "Cube-Mesh_0_indices", - "bufferView": 1, - "byteOffset": 0, - "componentType": 5123, - "count": 36, - "min": [ - 0 - ], - "max": [ - 23 - ], - "type": "SCALAR" - } - ], - "asset": { - "generator": "obj2gltf", - "version": "2.0" - }, - "buffers": [ - { - "name": "buffer", - "byteLength": 840, - "uri": "data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIA/AACAvwAAgL8AAIA/AACAPwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIA/AACAPwAAgD8AAIA/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcA" - } - ], - "bufferViews": [ - { - "name": "bufferView_vertex", - "buffer": 0, - "byteLength": 768, - "byteOffset": 0, - "target": 34962 - }, - { - "name": "bufferView_index", - "buffer": 0, - "byteLength": 72, - "byteOffset": 768, - "target": 34963 - } - ], - "materials": [ - { - "name": "Material", - "pbrMetallicRoughness": { - "baseColorFactor": [ - 0.64, - 0.64, - 0.64, - 1 - ], - "metallicFactor": 0, - "roughnessFactor": 0.903921569 - }, - "emissiveFactor": [ - 0, - 0, - 0 - ], - "alphaMode": "OPAQUE", - "doubleSided": false - } - ], - "meshes": [ - { - "name": "Cube-Mesh", - "primitives": [ - { - "attributes": { - "POSITION": 0, - "NORMAL": 1, - "TEXCOORD_0": 2 - }, - "indices": 3, - "material": 0, - "mode": 4 - } - ] - } - ], - "nodes": [ - { - "name": "Cube", - "mesh": 0 - } - ], - "scene": 0, - "scenes": [ - { - "nodes": [ - 0 - ] - } - ] -} From 3b4e30d5e35d2ef5a9a2f60b5c465bc12266ec3b Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 24 Jul 2017 18:21:01 -0400 Subject: [PATCH 10/21] Supply images on the command line --- README.md | 6 +++ bin/obj2gltf.js | 30 ++++++++++++++ lib/createGltf.js | 87 ++++++++++++++++++++++++++------------- lib/obj2gltf.js | 45 +++++++++++++++++++- specs/lib/obj2gltfSpec.js | 25 ++++++++++- 5 files changed, 161 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index de7e357e..44274848 100644 --- a/README.md +++ b/README.md @@ -45,6 +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. ## Build Instructions diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index a2dc0dd3..2011bff0 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -89,6 +89,36 @@ var argv = yargs describe: 'The glTF will be saved with the KHR_materials_common extension.', type: 'boolean', 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.', + 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.', + 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.', + 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.', + 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.', + 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.', + type: 'string', + normalize: true } }).parse(args); diff --git a/lib/createGltf.js b/lib/createGltf.js index 59777f2c..7c20b35e 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -21,6 +21,12 @@ module.exports = createGltf; * @param {Boolean} options.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. * @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 {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 {Boolean} options.logger A callback function for handling logged messages. Defaults to console.log. * @returns {Object} A glTF asset. * @@ -152,7 +158,9 @@ function addBuffers(gltf, bufferState) { }); } -function getImage(images, imagePath) { +function getImage(images, imagePath, overrideImagePath, options) { + images = options.overridingImages.concat(images); + imagePath = defaultValue(overrideImagePath, imagePath); var imagesLength = images.length; for (var i = 0; i < imagesLength; ++i) { var image = images[i]; @@ -330,7 +338,7 @@ function encodePng(pixels, width, height, inputChannels, outputChannels) { return PNG.sync.write(pngInput, pngOptions); } -function createMetallicRoughnessTexture(gltf, materialName, metallicImage, roughnessImage, occlusionImage, options) { +function createMetallicRoughnessTexture(gltf, metallicImage, roughnessImage, occlusionImage, options) { var packMetallic = defined(metallicImage); var packRoughness = defined(roughnessImage); var packOcclusion = defined(occlusionImage) && options.packOcclusion; @@ -382,10 +390,13 @@ function createMetallicRoughnessTexture(gltf, materialName, metallicImage, rough writeChannel(pixels, occlusionChannel, 0, width, height); } - var imageName = materialName + '-' + 'MetallicRoughness'; - if (packOcclusion) { - imageName += 'Occlusion'; + 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)); } + var imageName = imageNames.join('_'); var pngSource = encodePng(pixels, width, height, 4, 3); @@ -399,7 +410,7 @@ function createMetallicRoughnessTexture(gltf, materialName, metallicImage, rough return addTexture(gltf, image); } -function createSpecularGlossinessTexture(gltf, materialName, specularImage, glossinessImage, options) { +function createSpecularGlossinessTexture(gltf, specularImage, glossinessImage, options) { var packSpecular = defined(specularImage); var packGlossiness = defined(glossinessImage); @@ -443,7 +454,13 @@ function createSpecularGlossinessTexture(gltf, materialName, specularImage, glos writeChannel(pixels, glossinessChannel, 3, width, height); } - var imageName = materialName + '-' + 'SpecularGlossiness'; + 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)); + } + var imageName = imageNames.join('_'); var pngSource = encodePng(pixels, width, height, 4, 4); @@ -460,18 +477,25 @@ function createSpecularGlossinessTexture(gltf, materialName, specularImage, glos 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); + // The texture paths supplied in the .mtl may be overriden by the texture paths supplied in options + var emissiveImage = getImage(images, material.emissiveTexture, options.emissiveTexture, options); + var normalImage = getImage(images, material.normalTexture, options.normalTexture, options); + var occlusionImage = getImage(images, material.ambientTexture, options.occlusionTexture, options); + var diffuseImage = getImage(images, material.diffuseTexture, options.baseColorTexture, options); + var specularImage = getImage(images, material.specularTexture, options.specularGlossinessTexture, options); + var glossinessImage = getImage(images, material.specularShininessTexture, options.specularGlossinessTexture, options); 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 specularGlossinessTexture; + if (defined(options.specularGlossinessTexture)) { + specularGlossinessTexture = getTexture(gltf, specularImage); + } else { + specularGlossinessTexture = createSpecularGlossinessTexture(gltf, specularImage, glossinessImage, options); + } var emissiveFactor = getEmissiveFactor(material); var diffuseFactor = material.diffuseColor; @@ -531,19 +555,26 @@ function createSpecularGlossinessMaterial(gltf, images, material, options) { 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); + // The texture paths supplied in the .mtl may be overriden by the texture paths supplied in options + var emissiveImage = getImage(images, material.emissiveTexture, options.emissiveTexture, options); + var normalImage = getImage(images, material.normalTexture, options.normalTexture, options); + var occlusionImage = getImage(images, material.ambientTexture, options.metallicRoughnessOcclusionTexture, options); + var baseColorImage = getImage(images, material.diffuseTexture, options.baseColorTexture, options); + var metallicImage = getImage(images, material.specularTexture, options.metallicRoughnessOcclusionTexture, options); + var roughnessImage = getImage(images, material.specularShininessTexture, options.metallicRoughnessOcclusionTexture, options); var emissiveTexture = getTexture(gltf, emissiveImage); var normalTexture = getTexture(gltf, normalImage); var baseColorTexture = getTexture(gltf, baseColorImage); - var metallicRoughnessTexture = createMetallicRoughnessTexture(gltf, materialName, metallicImage, roughnessImage, occlusionImage, options); - var packOcclusion = defined(occlusionImage) && options.packOcclusion; + var metallicRoughnessTexture; + if (defined(options.metallicRoughnessOcclusionTexture)) { + metallicRoughnessTexture = getTexture(gltf, metallicImage); + } else { + metallicRoughnessTexture = createMetallicRoughnessTexture(gltf, metallicImage, roughnessImage, occlusionImage, options); + } + + var packOcclusion = defined(occlusionImage) && options.packOcclusion || defined(options.metallicRoughnessOcclusionTexture); var occlusionTexture = packOcclusion ? metallicRoughnessTexture : getTexture(gltf, occlusionImage); var emissiveFactor = getEmissiveFactor(material); @@ -622,13 +653,13 @@ function convertTraditionalToMetallicRoughness(material) { material.specularShininess = roughnessFactor; } -function createMaterialsCommonMaterial(gltf, images, material, hasNormals) { +function createMaterialsCommonMaterial(gltf, images, material, hasNormals, options) { var materialName = material.name; - var ambientImage = getImage(images, material.ambientTexture); - var diffuseImage = getImage(images, material.diffuseTexture); - var emissiveImage = getImage(images, material.emissiveTexture); - var specularImage = getImage(images, material.specularTexture); + var ambientImage = getImage(images, material.ambientTexture, undefined, options); + var diffuseImage = getImage(images, material.diffuseTexture, undefined, options); + var emissiveImage = getImage(images, material.emissiveTexture, undefined, options); + var specularImage = getImage(images, material.specularTexture, undefined, options); var ambient = defaultValue(getTexture(gltf, ambientImage), material.ambientColor); var diffuse = defaultValue(getTexture(gltf, diffuseImage), material.diffuseColor); @@ -697,7 +728,7 @@ function addMaterial(gltf, images, material, hasNormals, options) { } else if (options.metallicRoughness) { gltfMaterial = createMetallicRoughnessMaterial(gltf, images, material, options); } else if (options.materialsCommon) { - gltfMaterial = createMaterialsCommonMaterial(gltf, images, material, hasNormals); + gltfMaterial = createMaterialsCommonMaterial(gltf, images, material, hasNormals, options); } else { convertTraditionalToMetallicRoughness(material); gltfMaterial = createMetallicRoughnessMaterial(gltf, images, material, options); diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index 6dbcdcaf..851e2b44 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -5,13 +5,13 @@ var path = require('path'); var Promise = require('bluebird'); var createGltf = require('./createGltf'); var gltfToGlb = require('./gltfToGlb'); +var loadImage = require('./loadImage'); var loadObj = require('./loadObj'); var writeUris = require('./writeUris'); var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; var DeveloperError = Cesium.DeveloperError; -var RuntimeError = Cesium.RuntimeError; module.exports = obj2gltf; @@ -32,6 +32,13 @@ 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 {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. */ @@ -76,6 +83,22 @@ function obj2gltf(objPath, gltfPath, options) { throw new DeveloperError('Only one material type may be set from [--metallicRoughness, --specularGlossiness, --materialsCommon].'); } + if (defined(options.metallicRoughnessOcclusionTexture) && defined(options.specularGlossinessTexture)) { + throw new DeveloperError('options.metallicRoughnessOcclusionTexture and options.specularGlossinessTexture cannot both be defined.'); + } + + if (defined(options.metallicRoughnessOcclusionTexture)) { + options.metallicRoughness = true; + options.specularGlossiness = false; + options.materialsCommon = false; + } + + if (defined(options.specularGlossinessTexture)) { + options.metallicRoughness = false; + options.specularGlossiness = true; + options.materialsCommon = false; + } + var extension = path.extname(gltfPath).toLowerCase(); var modelName = path.basename(gltfPath, path.extname(gltfPath)); if (binary || extension === '.glb') { @@ -89,7 +112,10 @@ function obj2gltf(objPath, gltfPath, options) { spaces : 2 }; - return loadObj(objPath, options) + return loadOverridingImages(options) + .then(function() { + return loadObj(objPath, options); + }) .then(function(objData) { return createGltf(objData, options); }) @@ -105,6 +131,21 @@ function obj2gltf(objPath, gltfPath, options) { }); } +function loadOverridingImages(options) { + // The texture paths supplied in the .mtl may be overriden by the texture path supplied in options + var checkTransparencyOptions = { + checkTransparency : options.checkTransparency + }; + var imagePaths = [options.metallicRoughnessOcclusionTexture, options.specularGlossinessTexture, options.occlusionTexture, options.normalTexture, options.baseColorTexture, options.emissiveTexture]; + imagePaths = imagePaths.filter(function(imagePath) {return defined(imagePath);}); + return Promise.map(imagePaths, function(imagePath) { + var imageOptions = (imagePath === options.baseColorTexture) ? checkTransparencyOptions : undefined; + return loadImage(imagePath, imageOptions); + }).then(function(images) { + options.overridingImages = images; + }); +} + /** * Default values that will be used when calling obj2gltf(options) unless specified in the options object. */ diff --git a/specs/lib/obj2gltfSpec.js b/specs/lib/obj2gltfSpec.js index 1ea5a0ef..55183d2e 100644 --- a/specs/lib/obj2gltfSpec.js +++ b/specs/lib/obj2gltfSpec.js @@ -85,11 +85,32 @@ describe('obj2gltf', function() { }).toThrowDeveloperError(); }); - it('rejects if more than one material type is set', function(done) { + it('throws if more than one material type is set', function() { var options = { metallicRoughness : true, specularGlossiness : true }; - expect(obj2gltf(objPath, gltfPath, options), done).toRejectWith(RuntimeError); + expect(function() { + obj2gltf(objPath, gltfPath, options); + }).toThrowDeveloperError(); + }); + + it('throws if occlusionTexture is defined and specularGlossinessTexture is undefined', function() { + var options = { + occlusionTexture : 'path/to/occlusion/texture' + }; + expect(function() { + obj2gltf(objPath, gltfPath, options); + }).toThrowDeveloperError(); + }); + + it('throws if metallicRoughnessOcclusionTexture and specularGlossinessTexture are both defined', function() { + var options = { + metallicRoughnessOcclusionTexture : 'path/to/metallic-roughness-occlusion/texture', + specularGlossinessTexture : 'path/to/specular-glossiness/texture' + }; + expect(function() { + obj2gltf(objPath, gltfPath, options); + }).toThrowDeveloperError(); }); }); From 8d491afc7064040674c7a75f9f66d031abe11780 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 24 Jul 2017 21:32:51 -0400 Subject: [PATCH 11/21] bin script tweaks --- bin/obj2gltf.js | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index 2011bff0..a8936a5e 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -131,29 +131,16 @@ if (!defined(gltfPath)) { gltfPath = path.join(path.dirname(objPath), modelName + extension); } -var options = { - binary : argv.binary, - separate : argv.separate, - separateTextures : argv.separateTextures, - checkTransparency : argv.checkTransparency, - secure : argv.secure, - inputUpAxis : argv.inputUpAxis, - outputUpAxis : argv.outputUpAxis, - packOcclusion : argv.packOcclusion, - metallicRoughness : argv.metallicRoughness, - specularGlossiness : argv.specularGlossiness -}; - console.time('Total'); try { - obj2gltf(objPath, gltfPath, options) + obj2gltf(objPath, gltfPath, argv) .then(function() { console.timeEnd('Total'); }) .catch(function(error) { - console.log(error); + console.log(error.message); }); } catch(error) { - console.log(error); + console.log(error.message); } From b8c5ebc8aa8c601ed48a43ac0a51a48f9a447927 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 25 Jul 2017 11:41:39 -0400 Subject: [PATCH 12/21] Fix texture index when texture is shared by multiple material values --- lib/createGltf.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/createGltf.js b/lib/createGltf.js index 7c20b35e..f26bee2e 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -201,9 +201,7 @@ function addTexture(gltf, image) { source : imageIndex }); - return { - index : textureIndex - }; + return textureIndex; } function getTexture(gltf, image) { @@ -225,7 +223,9 @@ function getTexture(gltf, image) { if (!defined(textureIndex)) { textureIndex = addTexture(gltf, image); } - return textureIndex; + return { + index : textureIndex + }; } function addColors(left, right) { @@ -407,7 +407,7 @@ function createMetallicRoughnessTexture(gltf, metallicImage, roughnessImage, occ extension : '.png' }; - return addTexture(gltf, image); + return getTexture(gltf, image); } function createSpecularGlossinessTexture(gltf, specularImage, glossinessImage, options) { @@ -471,7 +471,7 @@ function createSpecularGlossinessTexture(gltf, specularImage, glossinessImage, o extension : '.png' }; - return addTexture(gltf, image); + return getTexture(gltf, image); } function createSpecularGlossinessMaterial(gltf, images, material, options) { @@ -574,7 +574,7 @@ function createMetallicRoughnessMaterial(gltf, images, material, options) { metallicRoughnessTexture = createMetallicRoughnessTexture(gltf, metallicImage, roughnessImage, occlusionImage, options); } - var packOcclusion = defined(occlusionImage) && options.packOcclusion || defined(options.metallicRoughnessOcclusionTexture); + var packOcclusion = (defined(occlusionImage) && options.packOcclusion) || defined(options.metallicRoughnessOcclusionTexture); var occlusionTexture = packOcclusion ? metallicRoughnessTexture : getTexture(gltf, occlusionImage); var emissiveFactor = getEmissiveFactor(material); From 2a44e0d1a8b9a81928be438821f3e08e350deae7 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 25 Jul 2017 12:32:24 -0400 Subject: [PATCH 13/21] Misc fixes --- .gitignore | 3 +- .npmignore | 3 +- doc/cesium.png | Bin 0 -> 4664 bytes lib/createGltf.js | 1 + .../box-objects-groups-materials.gltf | 440 ------------------ specs/lib/createGltfSpec.js | 3 +- specs/lib/obj2gltfSpec.js | 11 - 7 files changed, 7 insertions(+), 454 deletions(-) create mode 100644 doc/cesium.png delete mode 100644 specs/data/box-objects-groups-materials/box-objects-groups-materials.gltf diff --git a/.gitignore b/.gitignore index 53cace90..4ffd6811 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ npm-debug.log # Generate data .eslintcache coverage -doc *.tgz .nyc_output +doc/* +!doc/cesium.png diff --git a/.npmignore b/.npmignore index 3480bcec..33f72365 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,6 @@ /.idea /coverage -/doc +/doc/* /specs /test .editorconfig @@ -12,3 +12,4 @@ .travis.yml gulpfile.js *.tgz +!doc/cesium.png diff --git a/doc/cesium.png b/doc/cesium.png new file mode 100644 index 0000000000000000000000000000000000000000..32db61698f08c8d7ef37b002e0ab294df1c34a37 GIT binary patch literal 4664 zcmV-8636X{P)*h{so)r31ylG8s-{FD=?-ay?&mm@S*bN2Rtta7@QZdFV`KdV z7odjhl8QL0szf4*hK!;rdvdpanqRh> zGndtR=5NPD8xG?{-MRB31J>(YtWmf}esM{aMpc$bB#|(Gd--=@G;ie06$>qY@3{D= z;;h)?X%K%1W7_Vo%p%Wns&>>k9FA`dRn_4Ro9##Os`?ANV*gdVru~K!9h+pg*(cbC z*soVr_4|-TSle_V_G`xcVxggm;-Df!F)(4skhla@y&2Cr-lpBb<;BXT{l=lHw?Voe z4vmW&Liu&YMUdupV_pe1`>*ksFT@A@eG-&$uTju`-=>hLiC2{IT;9+J6bjLC3~>xK z*3~HJ3%63ZWoVkFa%BZbQN~bs_ed}FJj|J@FKVvKeLT`T@MFe|`IiL6es{d0<;N@P ziFj2L-Bgo9QTN8H_GhqzP%%NEDVVgD%VE<}smLJgarPg8;Wj%|?Mp6gR8rJx$nzC6 zJ`}-T^hS&raWTZ!2B+1|B_t$#mBMr&zx_dRAiLduJ%vOK)H9FE8~%Pjh3GgG?FlZ< zATM4xD2Wvef^t7($_bRBy-M-jBc;Io4reMe&2@RipZ9$!_E$EFvYKYG@90_a(w=IO zS{UBrvPgxG;o}@$^nq~R zPB?zv7;mB4;R3SWXk4V98XZ1wZ4ZP8eb&)nSCoI}$_SDbMxVRo=+{#sn6~2@`$T;! zt*`?@6E1k;D4Q-?<&+#3XJ8%gkJsy6NYxLCqA1^iMsl-_XR)B;%%@^I=uM-;^zd4UTKHy#sBaE5ossH> zR*_X4-p#eHw0LKwXgS|`DeeI=yIgoD%*I7aKDf z){*_&=vpZ^i-`p2o6m50v$DeInJ&*v&SXZV!L%GlY=3I&Pl)|T-WT;vwe%MsIHkvp zF zo?KA&EN3*M{{BTB+0DmChj)tUOGk+Z7AJ~F|8$f1qH;eS@sWQZyd7_xl0kS^-k*0r z&J$CYjMU3Zeqo~5wR`=gbP6mm!raI<8VEXV9W>r*_^_wNYuY4-qTGTvt#DNG7yNCU zez#pwe(BJ(N1#Kl33c&+{FYMjI;JB!EGH-=CctR%3$|Pk-b74HObpp}X84vus)x^4 zE1@u*{%&+o%{5nFeK{qM2(vnw$_k^W6_jn{%;v9kd#Yi&OpXI8PnC)pIXCF0yP)Ki z6GUC(DT)yKvvOQtbNO{8yz%DG=M^7ynEZlP6=jC(1a?)O&NiB=H9F!PQNIy~A1+Sa zn0)aA`3r^V^p?@#+<(iK3rbMcxfB)^aKW1g`Rp_bI`29P(djQnhtK*5_jg^2_97(@ z8@LGn7}A=|w$#Jy->ooudXcAqGus6{{Z@r&YHcUW3DMGaPW*1e9Ra4hpv8ZA2rdYn zu{Dit;)w{?>(9MgDbD+EsA;UxFCxaW7QJ#mKH4$GYz+h-@aq&3HP9*k$mI_ZG^H;&Hl@#d%ABo{qoXg*+C$^P~Q)k;Kr2xaXiRZS=V9Rm7 zJUQg^OlZ(GY@@061r!oB&{401PI#SBxb~tDonAEh?QFR|9l}SB*y$h?9}~soJNo+1 z)LmVaV6*=m{%tl2Z=)=_#9@QwADai%2aZ9V?&FSGV)S%Z`DV^^D16JzpIrwFS=0F| zDSNm$RJNN^_$WG5%jWN|lmfRnP}xW+e7OGcB3lM(dH(Dh=@eL?abDvZ4I7j~VonI7 zpM#E7Vuc{D4OCnQILff)b_zRfEf~xL`W%f0d3^|-_8l0dT-dRo)2)M_x$t)!(H6k( zX`pGivkeVse8OO)KN%ek4}^NZ$sgCe4n_HyFBfxY$!BsiQ$09w;>52&onmM7l>D+k za>m>6&y&Lc!B)k|&pU3ub*0%W#*5Ppoo}h(R6O^+s-QCbg*Wf~`P8#lBrO@i=8xLA z-HBUKO@Rh_J*dHajrJgg#2izjTPpP+71sfp3_8^@KHehX#Y$Co4Ar!osMrAdnb8Nt z+iWA@BG|^oKwEzVAz#5bGoZW`Tw0J9ZwOON?2JCI*fZN`I(%)kxvg`peXixKcw*gc ztl`Y`?5huo_V&(a?zK&AVjfJ*jb)+mrk($~{LI-?V$RARvH7AN&bi?nRo@h7oP4g) zurVnl=Jb})kpxwJoR7CwxK72*IaF*1{mtkIqOD&a5OG1?&}m9 z0Gin(P0KJH&;8@bjM%Tg42_wo3h+6|4>uvaX zNT1d#+G+T`9`e8!bXwqNxAw$4w7}hY{n6+M{GP$bp>Z`Qg>`^?Ft}JQC{0D-Y=~ewSFTxaMqbjp!uN9V8{?V4Fexr_E)3htH*GmWcN|vg;Pg=T?uwQ|AyI>mrV=ktywoV;)GdV3WBPb4R^a>L&_O?C&q>wx zQJ9VuM#tTa6cROHJ{Olanuo&IKY1nQp2pObo+LUbZD`agr>K+y36_V7o8S>Es&2Fwa0F**?HJ@WG~ zGa7{>J6seSnBzE_A2GH85_6*?4NYe@lpa4M@;_K19?AUyYqIVNceuT=cfGzOsHv~g zx3<3e_r+p*PI!;)9?Txz{^a_}VNF%xgLulM(LSTiB2fdKXA_qWhP{}vS2DI7I-+ZAmw%t~fM>@*PGAPkakueh24|6T{5=}ALA{yIL2%k?_r`;D(Et^zTsX zz|`m#%lO3;`rTf96_<&PnH2jRDqbfNqmQ_9#5dvZr-Nc+Brx21D!PMcjE>+%lTQ~c zFmlMREAKI*BdIU^UK*wFh z#l}R6dZs%YW)SG)kHYa#UzTBspg+`e%a>@44(Do%zCO5dcFECOAecf~r(Hb4`ZAjwM`m5`FdfMm%oOx3&p0#}J+nn)w zQb|jOw`8rIbO%){YH$+>G#2iZYYb|{=;ejZ@fnzG9&Vzbq5|kmqa(iO-mqR(wXmc88+;l!^-=3Zv^_esJ-4A1@$5X`>X5rcmx{2{v0i6%#~vjP5&EllRO)(Eb(I zfA4$Wpm0-q+~~;K;#rr>TsF3ZGhk0BDSJd~`s!OP-L5)#@Zd}El|tOE2_I|@G$1|# z*$NF@1RvCPqu}q`Ag&O8Zh_bhzBEcE{C*ehfC~8p0Czq?C;My6)y4DXM7#qHb%t~L zJ^aMiPhVs6=}P+UI4u{~%3J2a2;0n=(rV4fx#5$iHccK$#Y-fT zo+N9{uWp>4GkPCqPAim@J+gWB$_Yz0Zk*cXG)p3p#1N;`c~Q!$pG*V8mvLr|Mle%o zef`R&s-vr^fA^JJ1=?ZZRdA$iqo&ySm^wANr^M$7w1VN5{X12 ukw_#Gi9{liNF)-8L?V$$Bz=Ja1O6AzGCweLB9QU`0000 Date: Tue, 25 Jul 2017 13:03:56 -0400 Subject: [PATCH 14/21] Updates to package.json, README, and CHANGES --- .idea/encodings.xml | 1 + CHANGES.md | 6 ++++++ README.md | 2 +- package.json | 2 +- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 97626ba4..ff5f482b 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index a2f93930..c6c4eed1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,12 @@ Change Log ========== +### 2.0.0 2017-07-XX + +* Breaking changes + * Obj models now convert to glTF 2.0. Possible material profiles are `metallicRoughness`, `specGlossiness` (using the `KHR_materials_pbrSpecularGlossiness` extension), and `materialsCommon` (using the `KHR_materials_common` extension). + * Removed `gltf-pipeline` dependency. The following options have been removed: `compress`, `optimize`, `generateNormals`, `optimizeForCesium`, `ao`, and `bypassPipeline`. + ### 1.2.0 2017-07-11 * Change texture sampling to use `NEAREST_MIPMAP_LINEAR` by default [#83](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/83). diff --git a/README.md b/README.md index 44274848..e9045e36 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OBJ2GLTF -Convert OBJ assets to [glTF](https://www.khronos.org/gltf) 1.0. +Convert OBJ assets to [glTF](https://www.khronos.org/gltf) 2.0. ## Getting Started diff --git a/package.json b/package.json index 46fa4c4c..2795d304 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obj2gltf", - "version": "1.2.0", + "version": "2.0.0", "description": "Convert OBJ model format to glTF", "license": "Apache-2.0", "contributors": [ From 125edddef9d73b6a0cf9c81c7132f250e8a4d0f7 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 25 Jul 2017 13:04:38 -0400 Subject: [PATCH 15/21] Fix eslint error --- specs/lib/obj2gltfSpec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/specs/lib/obj2gltfSpec.js b/specs/lib/obj2gltfSpec.js index 4aba492e..3776b0c0 100644 --- a/specs/lib/obj2gltfSpec.js +++ b/specs/lib/obj2gltfSpec.js @@ -1,5 +1,4 @@ 'use strict'; -var Cesium = require('Cesium'); var fsExtra = require('fs-extra'); var path = require('path'); var Promise = require('bluebird'); From e54f3af37f0a47852c32c087c1238b79efaeaedf Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 27 Jul 2017 11:23:12 -0400 Subject: [PATCH 16/21] 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 | 123 ++++++++++++++++++++--------- specs/lib/createGltfSpec.js | 8 +- 9 files changed, 212 insertions(+), 149 deletions(-) diff --git a/README.md b/README.md index e9045e36..7bb0e0fd 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 a8936a5e..dab8dc8e 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 2cd0d926..e3771567 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 350f7877..e8d455bd 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 410a133b..7ed720d0 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 2f6d2f5a..b7d9441d 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 851e2b44..740317b6 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 a74b63fb..ab171a76 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,48 +25,100 @@ 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; - } + return encodeImages(gltf) + .then(function() { + 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; + }); + }); +} - // 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); +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 + }); - if (exceedsMaximum && !separate) { - return Promise.reject(new RuntimeError('Buffers and textures are too large to encode in the glTF. Use the --separate flag instead.')); - } + png.data = image.decoded; - var name = path.basename(gltfPath, path.extname(gltfPath)); + 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); + }); +} - if (separateTextures) { - promises.push(writeSeparateTextures(gltf, gltfPath)); - } else { - writeEmbeddedTextures(gltf); +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; + }); } +} - if (separate) { - promises.push(writeSeparateBuffer(gltf, gltfPath, name)); - } else { - writeEmbeddedBuffer(gltf); +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(promises) - .then(function() { - deleteExtras(gltf); - cleanup(gltf); - return gltf; - }); + return Promise.all(encodePromises); } function deleteExtras(gltf) { diff --git a/specs/lib/createGltfSpec.js b/specs/lib/createGltfSpec.js index 55d80ffc..3fe2974a 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 }); From 3da691df6289ec23a2a24acbf83e00a3dd42b71e Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 28 Jul 2017 16:56:28 -0400 Subject: [PATCH 17/21] Cleanup overriding images --- bin/obj2gltf.js | 26 ++++++++++- lib/Image.js | 18 ++++++++ lib/createGltf.js | 86 +++++++++++++++++------------------ lib/loadImage.js | 65 +++++++++++++-------------- lib/loadMtl.js | 2 +- lib/obj2gltf.js | 61 ++++++++++++++++--------- specs/lib/createGltfSpec.js | 2 +- specs/lib/loadImageSpec.js | 90 ++++++++++++++++++------------------- specs/lib/obj2gltfSpec.js | 24 ++++++++++ 9 files changed, 227 insertions(+), 147 deletions(-) create mode 100644 lib/Image.js diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index dab8dc8e..fdd27042 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -131,10 +131,34 @@ if (!defined(gltfPath)) { gltfPath = path.join(path.dirname(objPath), modelName + extension); } +var overridingImages = { + metallicRoughnessOcclusionTexture : argv.metallicRoughnessOcclusionTexture, + specularGlossinessTexture : argv.specularGlossinessTexture, + occlusionTexture : argv.occlusionTexture, + normalTexture : argv.normalTexture, + baseColorTexture : argv.baseColorTexture, + emissiveTexture : argv.emissiveTexture +}; + +var options = { + binary : argv.binary, + separate : argv.separate, + separateTextures : argv.separateTextures, + checkTransparency : argv.checkTransparency, + secure : argv.secure, + inputUpAxis : argv.inputUpAxis, + outputUpAxis : argv.outputUpAxis, + packOcclusion : argv.packOcclusion, + metallicRoughness : argv.metallicRoughness, + specularGlossiness : argv.specularGlossiness, + materialsCommon : argv.materialsCommon, + overridingImages : overridingImages +}; + console.time('Total'); try { - obj2gltf(objPath, gltfPath, argv) + obj2gltf(objPath, gltfPath, options) .then(function() { console.timeEnd('Total'); }) diff --git a/lib/Image.js b/lib/Image.js new file mode 100644 index 00000000..afcc4dab --- /dev/null +++ b/lib/Image.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports = Image; + +/** + * Stores image data and properties. + * + * @private + */ +function Image() { + this.transparent = false; + this.source = undefined; + this.extension = undefined; + this.path = undefined; + this.decoded = undefined; + this.width = undefined; + this.height = undefined; +} diff --git a/lib/createGltf.js b/lib/createGltf.js index e3771567..01c60933 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -2,6 +2,7 @@ var Cesium = require('cesium'); var path = require('path'); var getBufferPadded = require('./getBufferPadded'); +var Image = require('./Image'); var Material = require('./Material'); var CesiumMath = Cesium.Math; @@ -20,13 +21,13 @@ module.exports = createGltf; * @param {Boolean} options.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. * @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, 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 {Object} [options.overridingImages] An object containing image paths that override material values defined in the .mtl file. This is often convenient in workflows where the .mtl does not exist or is not set up to use PBR materials. Intended for models with a single material. + * @param {String} [options.overridingImages.metallicRoughnessOcclusionTexture] Path to the metallic-roughness-occlusion texture, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. The model will be saved with a pbrMetallicRoughness material. + * @param {String} [options.overridingImages.specularGlossinessTexture] Path to the specular-glossiness texture, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension. + * @param {String} [options.overridingImages.occlusionTexture] Path to the occlusion texture. Ignored if metallicRoughnessOcclusionTexture is also set. + * @param {String} [options.overridingImages.normalTexture] Path to the normal texture. + * @param {String} [options.overridingImages.baseColorTexture] Path to the baseColor/diffuse texture. + * @param {String} [options.overridingImages.emissiveTexture] Path to the emissive texture. * @param {Boolean} options.logger A callback function for handling logged messages. Defaults to console.log. * @returns {Object} A glTF asset. * @@ -158,9 +159,10 @@ function addBuffers(gltf, bufferState) { }); } -function getImage(images, imagePath, overrideImagePath, options) { - images = options.overridingImages.concat(images); - imagePath = defaultValue(overrideImagePath, imagePath); +function getImage(images, imagePath, overridingImage) { + if (defined(overridingImage)) { + return overridingImage; + } var imagesLength = images.length; for (var i = 0; i < imagesLength; ++i) { var image = images[i]; @@ -383,15 +385,12 @@ function createMetallicRoughnessTexture(gltf, metallicImage, roughnessImage, occ } var imageName = imageNames.join('_'); - var image = { - transparent : false, - source : undefined, - extension : '.png', - path : imageName, - decoded : pixels, - width : width, - height : height - }; + var image = new Image(); + image.extension = '.png'; + image.path = imageName; + image.decoded = pixels; + image.width = width; + image.height = height; return getTexture(gltf, image); } @@ -448,15 +447,12 @@ function createSpecularGlossinessTexture(gltf, specularImage, glossinessImage, o } var imageName = imageNames.join('_'); - var image = { - transparent : true, - source : undefined, - extension : '.png', - path : imageName, - decoded : pixels, - width : width, - height : height - }; + var image = new Image(); + image.extension = '.png'; + image.path = imageName; + image.decoded = pixels; + image.width = width; + image.height = height; return getTexture(gltf, image); } @@ -465,12 +461,13 @@ function createSpecularGlossinessMaterial(gltf, images, material, options) { var materialName = material.name; // The texture paths supplied in the .mtl may be overriden by the texture paths supplied in options - var emissiveImage = getImage(images, material.emissiveTexture, options.emissiveTexture, options); - var normalImage = getImage(images, material.normalTexture, options.normalTexture, options); - var occlusionImage = getImage(images, material.ambientTexture, options.occlusionTexture, options); - var diffuseImage = getImage(images, material.diffuseTexture, options.baseColorTexture, options); - var specularImage = getImage(images, material.specularTexture, options.specularGlossinessTexture, options); - var glossinessImage = getImage(images, material.specularShininessTexture, options.specularGlossinessTexture, options); + var overridingImages = options.overridingImages; + var emissiveImage = getImage(images, material.emissiveTexture, overridingImages.emissiveTexture, options); + var normalImage = getImage(images, material.normalTexture, overridingImages.normalTexture, options); + var occlusionImage = getImage(images, material.ambientTexture, overridingImages.occlusionTexture, options); + var diffuseImage = getImage(images, material.diffuseTexture, overridingImages.baseColorTexture, options); + var specularImage = getImage(images, material.specularTexture, overridingImages.specularGlossinessTexture, options); + var glossinessImage = getImage(images, material.specularShininessTexture, overridingImages.specularGlossinessTexture, options); var emissiveTexture = getTexture(gltf, emissiveImage); var normalTexture = getTexture(gltf, normalImage); @@ -478,7 +475,7 @@ function createSpecularGlossinessMaterial(gltf, images, material, options) { var diffuseTexture = getTexture(gltf, diffuseImage); var specularGlossinessTexture; - if (defined(options.specularGlossinessTexture)) { + if (defined(overridingImages.specularGlossinessTexture)) { specularGlossinessTexture = getTexture(gltf, specularImage); } else { specularGlossinessTexture = createSpecularGlossinessTexture(gltf, specularImage, glossinessImage, options); @@ -542,26 +539,27 @@ function createSpecularGlossinessMaterial(gltf, images, material, options) { function createMetallicRoughnessMaterial(gltf, images, material, options) { var materialName = material.name; - // The texture paths supplied in the .mtl may be overriden by the texture paths supplied in options - var emissiveImage = getImage(images, material.emissiveTexture, options.emissiveTexture, options); - var normalImage = getImage(images, material.normalTexture, options.normalTexture, options); - var occlusionImage = getImage(images, material.ambientTexture, options.metallicRoughnessOcclusionTexture, options); - var baseColorImage = getImage(images, material.diffuseTexture, options.baseColorTexture, options); - var metallicImage = getImage(images, material.specularTexture, options.metallicRoughnessOcclusionTexture, options); - var roughnessImage = getImage(images, material.specularShininessTexture, options.metallicRoughnessOcclusionTexture, options); + // The texture paths supplied in the .mtl may be over var overridingImages = options.overridingImages; + var overridingImages = options.overridingImages; + var emissiveImage = getImage(images, material.emissiveTexture, overridingImages.emissiveTexture); + var normalImage = getImage(images, material.normalTexture, overridingImages.normalTexture); + var occlusionImage = getImage(images, material.ambientTexture, overridingImages.metallicRoughnessOcclusionTexture); + var baseColorImage = getImage(images, material.diffuseTexture, overridingImages.baseColorTexture); + var metallicImage = getImage(images, material.specularTexture, overridingImages.metallicRoughnessOcclusionTexture); + var roughnessImage = getImage(images, material.specularShininessTexture, overridingImages.metallicRoughnessOcclusionTexture); var emissiveTexture = getTexture(gltf, emissiveImage); var normalTexture = getTexture(gltf, normalImage); var baseColorTexture = getTexture(gltf, baseColorImage); var metallicRoughnessTexture; - if (defined(options.metallicRoughnessOcclusionTexture)) { + if (defined(overridingImages.metallicRoughnessOcclusionTexture)) { metallicRoughnessTexture = getTexture(gltf, metallicImage); } else { metallicRoughnessTexture = createMetallicRoughnessTexture(gltf, metallicImage, roughnessImage, occlusionImage, options); } - var packOcclusion = (defined(occlusionImage) && options.packOcclusion) || defined(options.metallicRoughnessOcclusionTexture); + var packOcclusion = (defined(occlusionImage) && options.packOcclusion) || defined(overridingImages.metallicRoughnessOcclusionTexture); var occlusionTexture = packOcclusion ? metallicRoughnessTexture : getTexture(gltf, occlusionImage); var emissiveFactor = getEmissiveFactor(material); diff --git a/lib/loadImage.js b/lib/loadImage.js index 7ed720d0..73dfd99d 100644 --- a/lib/loadImage.js +++ b/lib/loadImage.js @@ -5,6 +5,7 @@ var jpeg = require('jpeg-js'); var path = require('path'); var PNG = require('pngjs').PNG; var Promise = require('bluebird'); +var Image = require('./Image'); var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; @@ -12,13 +13,13 @@ var defined = Cesium.defined; module.exports = loadImage; /** - * Load an image file and get information about it. + * Load an image file. * * @param {String} imagePath Path to the image file. * @param {Object} options An object with the following properties: * @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.decode=false] Decode image. - * @returns {Promise} A promise resolving to the image information, or undefined if the file doesn't exist. + * @returns {Promise} A promise resolving to an Image object. * * @private */ @@ -30,30 +31,29 @@ function loadImage(imagePath, options) { return fsExtra.readFile(imagePath) .then(function(data) { var extension = path.extname(imagePath).toLowerCase(); + var image = new Image(); + image.source = data; + image.extension = extension; + image.path = imagePath; - var info = { - transparent : false, - source : data, - extension : extension, - path : imagePath, - decoded : undefined, - width : undefined, - height : undefined - }; - + var decodePromise; if (extension === '.png') { - return getPngInfo(data, info, options); + decodePromise = decodePng(image, options); } else if (extension === '.jpg' || extension === '.jpeg') { - return getJpegInfo(data, info, options); + decodePromise = decodeJpeg(image, options); + } + + if (defined(decodePromise)) { + return decodePromise.thenReturn(image); } - return info; + return image; }); } -function hasTransparency(info) { - var pixels = info.decoded; - var pixelsLength = info.width * info.height; +function hasTransparency(image) { + var pixels = image.decoded; + var pixelsLength = image.width * image.height; for (var i = 0; i < pixelsLength; ++i) { if (pixels[i * 4 + 3] < 255) { return true; @@ -89,35 +89,34 @@ function parsePng(data) { }); } -function getPngInfo(data, info, options) { +function decodePng(image, options) { // Color type is encoded in the 25th bit of the png - var colorType = data[25]; + var source = image.source; + var colorType = source[25]; var channels = getChannels(colorType); var checkTransparency = (channels === 4 && options.checkTransparency); var decode = options.decode || checkTransparency; if (decode) { - return parsePng(data) + return parsePng(source) .then(function(decodedResults) { - info.decoded = decodedResults.data; - info.width = decodedResults.width; - info.height = decodedResults.height; + image.decoded = decodedResults.data; + image.width = decodedResults.width; + image.height = decodedResults.height; if (checkTransparency) { - info.transparent = hasTransparency(info); + image.transparent = hasTransparency(image); } - return info; }); } - return info; } -function getJpegInfo(data, info, options) { +function decodeJpeg(image, options) { if (options.decode) { - var decodedResults = jpeg.decode(data); - info.decoded = decodedResults.data; - info.width = decodedResults.width; - info.height = decodedResults.height; + var source = image.source; + var decodedResults = jpeg.decode(source); + image.decoded = decodedResults.data; + image.width = decodedResults.width; + image.height = decodedResults.height; } - return info; } diff --git a/lib/loadMtl.js b/lib/loadMtl.js index 2cb4d591..c72eb566 100644 --- a/lib/loadMtl.js +++ b/lib/loadMtl.js @@ -11,7 +11,7 @@ module.exports = loadMtl; * @param {String} mtlPath Path to the mtl file. * @param {Object} options An object with the following properties: * @param {Boolean} options.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. - * @returns {Promise} A promise resolving to the materials. + * @returns {Promise} A promise resolving to an array of materials. * * @private */ diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index 740317b6..3d032a5a 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -32,12 +32,13 @@ 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, 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 {Object} [options.overridingImages] An object containing image paths that override material values defined in the .mtl file. This is often convenient in workflows where the .mtl does not exist or is not set up to use PBR materials. Intended for models with a single material. + * @param {String} [options.overridingImages.metallicRoughnessOcclusionTexture] Path to the metallic-roughness-occlusion texture, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. The model will be saved with a pbrMetallicRoughness material. + * @param {String} [options.overridingImages.specularGlossinessTexture] Path to the specular-glossiness texture, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension. + * @param {String} [options.overridingImages.occlusionTexture] Path to the occlusion texture. Ignored if metallicRoughnessOcclusionTexture is also set. + * @param {String} [options.overridingImages.normalTexture] Path to the normal texture. + * @param {String} [options.overridingImages.baseColorTexture] Path to the baseColor/diffuse texture. + * @param {String} [options.overridingImages.emissiveTexture] Path to the emissive texture. * @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. @@ -57,6 +58,7 @@ function obj2gltf(objPath, gltfPath, options) { var metallicRoughness = defaultValue(options.metallicRoughness, defaults.metallicRoughness); var specularGlossiness = defaultValue(options.specularGlossiness, defaults.specularGlossiness); var materialsCommon = defaultValue(options.materialsCommon, defaults.materialsCommon); + var overridingImages = defaultValue(options.overridingImages, defaultValue.EMPTY_OBJECT); var logger = defaultValue(options.logger, defaults.logger); options.separate = separate; @@ -69,6 +71,7 @@ function obj2gltf(objPath, gltfPath, options) { options.metallicRoughness = metallicRoughness; options.specularGlossiness = specularGlossiness; options.materialsCommon = materialsCommon; + options.overridingImages = overridingImages; options.logger = logger; if (!defined(objPath)) { @@ -83,17 +86,17 @@ function obj2gltf(objPath, gltfPath, options) { throw new DeveloperError('Only one material type may be set from [--metallicRoughness, --specularGlossiness, --materialsCommon].'); } - if (defined(options.metallicRoughnessOcclusionTexture) && defined(options.specularGlossinessTexture)) { - throw new DeveloperError('options.metallicRoughnessOcclusionTexture and options.specularGlossinessTexture cannot both be defined.'); + if (defined(overridingImages.metallicRoughnessOcclusionTexture) && defined(overridingImages.specularGlossinessTexture)) { + throw new DeveloperError('options.overridingImages.metallicRoughnessOcclusionTexture and options.overridingImages.specularGlossinessTexture cannot both be defined.'); } - if (defined(options.metallicRoughnessOcclusionTexture)) { + if (defined(overridingImages.metallicRoughnessOcclusionTexture)) { options.metallicRoughness = true; options.specularGlossiness = false; options.materialsCommon = false; } - if (defined(options.specularGlossinessTexture)) { + if (defined(overridingImages.specularGlossinessTexture)) { options.metallicRoughness = false; options.specularGlossiness = true; options.materialsCommon = false; @@ -131,18 +134,32 @@ function obj2gltf(objPath, gltfPath, options) { } function loadOverridingImages(options) { - // The texture paths supplied in the .mtl may be overriden by the texture path supplied in options - var checkTransparencyOptions = { - checkTransparency : options.checkTransparency - }; - var imagePaths = [options.metallicRoughnessOcclusionTexture, options.specularGlossinessTexture, options.occlusionTexture, options.normalTexture, options.baseColorTexture, options.emissiveTexture]; - imagePaths = imagePaths.filter(function(imagePath) {return defined(imagePath);}); - return Promise.map(imagePaths, function(imagePath) { - var imageOptions = (imagePath === options.baseColorTexture) ? checkTransparencyOptions : undefined; - return loadImage(imagePath, imageOptions); - }).then(function(images) { - options.overridingImages = images; - }); + var overridingImages = options.overridingImages; + var promises = []; + for (var imageName in overridingImages) { + if (overridingImages.hasOwnProperty(imageName)) { + promises.push(loadOverridingImage(imageName, overridingImages, options)); + } + } + return Promise.all(promises); +} + +function loadOverridingImage(imageName, overridingImages, options) { + var imagePath = overridingImages[imageName]; + var imageOptions; + if (imageName === 'baseColorTexture') { + imageOptions = { + checkTransparency : options.checkTransparency + }; + } + return loadImage(imagePath, imageOptions) + .then(function(image) { + overridingImages[imageName] = image; + }) + .catch(function() { + delete overridingImages[imageName]; + options.logger('Could not read image file at ' + imagePath + '. This image will be ignored.'); + }); } /** diff --git a/specs/lib/createGltfSpec.js b/specs/lib/createGltfSpec.js index 3fe2974a..48812298 100644 --- a/specs/lib/createGltfSpec.js +++ b/specs/lib/createGltfSpec.js @@ -21,7 +21,7 @@ var metallicTextureUrl = 'specs/data/box-complex-material/specular.jpeg'; var roughnessTextureUrl = 'specs/data/box-complex-material/shininess.png'; var defaultOptions = clone(obj2gltf.defaults); -defaultOptions.overridingImages = []; +defaultOptions.overridingImages = {}; var checkTransparencyOptions = clone(defaultOptions); checkTransparencyOptions.checkTransparency = true; var decodeOptions = clone(defaultOptions); diff --git a/specs/lib/loadImageSpec.js b/specs/lib/loadImageSpec.js index 369ef51e..6d05426d 100644 --- a/specs/lib/loadImageSpec.js +++ b/specs/lib/loadImageSpec.js @@ -11,66 +11,66 @@ var transparentImage = 'specs/data/box-complex-material/diffuse.png'; describe('loadImage', function() { it('loads png image', function(done) { expect(loadImage(pngImage) - .then(function(info) { - expect(info.transparent).toBe(false); - expect(info.source).toBeDefined(); - expect(info.extension).toBe('.png'); - expect(info.path).toBe(pngImage); - expect(info.decoded).toBeUndefined(); - expect(info.width).toBeUndefined(); - expect(info.height).toBeUndefined(); + .then(function(image) { + expect(image.transparent).toBe(false); + expect(image.source).toBeDefined(); + expect(image.extension).toBe('.png'); + expect(image.path).toBe(pngImage); + expect(image.decoded).toBeUndefined(); + expect(image.width).toBeUndefined(); + expect(image.height).toBeUndefined(); }), done).toResolve(); }); it('loads jpg image', function(done) { expect(loadImage(jpgImage) - .then(function(info) { - expect(info.transparent).toBe(false); - expect(info.source).toBeDefined(); - expect(info.extension).toBe('.jpg'); - expect(info.decoded).toBeUndefined(); - expect(info.width).toBeUndefined(); - expect(info.height).toBeUndefined(); + .then(function(image) { + expect(image.transparent).toBe(false); + expect(image.source).toBeDefined(); + expect(image.extension).toBe('.jpg'); + expect(image.decoded).toBeUndefined(); + expect(image.width).toBeUndefined(); + expect(image.height).toBeUndefined(); }), done).toResolve(); }); it('loads jpeg image', function(done) { expect(loadImage(jpegImage) - .then(function(info) { - expect(info.transparent).toBe(false); - expect(info.source).toBeDefined(); - expect(info.extension).toBe('.jpeg'); - expect(info.decoded).toBeUndefined(); - expect(info.width).toBeUndefined(); - expect(info.height).toBeUndefined(); + .then(function(image) { + expect(image.transparent).toBe(false); + expect(image.source).toBeDefined(); + expect(image.extension).toBe('.jpeg'); + expect(image.decoded).toBeUndefined(); + expect(image.width).toBeUndefined(); + expect(image.height).toBeUndefined(); }), done).toResolve(); }); it('loads gif image', function(done) { expect(loadImage(gifImage) - .then(function(info) { - expect(info.transparent).toBe(false); - expect(info.source).toBeDefined(); - expect(info.extension).toBe('.gif'); - expect(info.decoded).toBeUndefined(); - expect(info.width).toBeUndefined(); - expect(info.height).toBeUndefined(); + .then(function(image) { + expect(image.transparent).toBe(false); + expect(image.source).toBeDefined(); + expect(image.extension).toBe('.gif'); + expect(image.decoded).toBeUndefined(); + expect(image.width).toBeUndefined(); + expect(image.height).toBeUndefined(); }), done).toResolve(); }); it('loads grayscale image', function(done) { expect(loadImage(grayscaleImage) - .then(function(info) { - expect(info.transparent).toBe(false); - expect(info.source).toBeDefined(); - expect(info.extension).toBe('.png'); + .then(function(image) { + expect(image.transparent).toBe(false); + expect(image.source).toBeDefined(); + expect(image.extension).toBe('.png'); }), done).toResolve(); }); it('loads image with alpha channel', function(done) { expect(loadImage(transparentImage) - .then(function(info) { - expect(info.transparent).toBe(false); + .then(function(image) { + expect(image.transparent).toBe(false); }), done).toResolve(); }); @@ -80,8 +80,8 @@ describe('loadImage', function() { }; expect(loadImage(transparentImage, options) - .then(function(info) { - expect(info.transparent).toBe(true); + .then(function(image) { + expect(image.transparent).toBe(true); }), done).toResolve(); }); @@ -91,10 +91,10 @@ describe('loadImage', function() { }; expect(loadImage(pngImage, options) - .then(function(info) { - expect(info.decoded).toBeDefined(); - expect(info.width).toBe(211); - expect(info.height).toBe(211); + .then(function(image) { + expect(image.decoded).toBeDefined(); + expect(image.width).toBe(211); + expect(image.height).toBe(211); }), done).toResolve(); }); @@ -104,10 +104,10 @@ describe('loadImage', function() { }; expect(loadImage(jpegImage, options) - .then(function(info) { - expect(info.decoded).toBeDefined(); - expect(info.width).toBe(211); - expect(info.height).toBe(211); + .then(function(image) { + expect(image.decoded).toBeDefined(); + expect(image.width).toBe(211); + expect(image.height).toBe(211); }), done).toResolve(); }); }); diff --git a/specs/lib/obj2gltfSpec.js b/specs/lib/obj2gltfSpec.js index 3776b0c0..3679b659 100644 --- a/specs/lib/obj2gltfSpec.js +++ b/specs/lib/obj2gltfSpec.js @@ -9,6 +9,10 @@ var gltfPath = 'specs/data/box-textured/box-textured.gltf'; var glbPath = 'specs/data/box-textured/box-textured.glb'; var objPathNonExistent = 'specs/data/non-existent.obj'; +var complexMaterialObjPath = 'specs/data/box-complex-material/box-complex-material.obj'; +var complexMaterialGltfPath = 'specs/data/box-complex-material/box-complex-material.gltf'; +var textureUrl = 'specs/data/box-textured/cesium.png'; + describe('obj2gltf', function() { beforeEach(function() { spyOn(fsExtra, 'outputJson').and.returnValue(Promise.resolve()); @@ -66,6 +70,26 @@ describe('obj2gltf', function() { }), done).toResolve(); }); + it('sets overriding images', function(done) { + var options = { + overridingImages : { + metallicRoughnessOcclusionTexture : textureUrl, + normalTexture : textureUrl, + baseColorTexture : textureUrl, + emissiveTexture : textureUrl + }, + separateTextures : true + }; + expect(obj2gltf(complexMaterialObjPath, complexMaterialGltfPath, options) + .then(function() { + var args = fsExtra.outputFile.calls.allArgs(); + var length = args.length; + for (var i = 0; i < length; ++i) { + expect(path.basename(args[i][0])).toBe(path.basename(textureUrl)); + } + }), done).toResolve(); + }); + it('rejects if obj path does not exist', function(done) { expect(obj2gltf(objPathNonExistent, gltfPath), done).toRejectWith(Error); }); From 487eca91f813332fd7072d93ae8a80498c478e31 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 28 Jul 2017 17:34:56 -0400 Subject: [PATCH 18/21] Move incompatible argument checking to bin file --- bin/obj2gltf.js | 28 +++++++++++++++++----------- lib/obj2gltf.js | 4 ++-- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index fdd27042..8d2e2765 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -122,6 +122,16 @@ var argv = yargs } }).parse(args); +if (argv.metallicRoughness + argv.specularGlossiness + argv.materialsCommon > 1) { + console.error('Only one material type may be set from [--metallicRoughness, --specularGlossiness, --materialsCommon].'); + process.exit(1); +} + +if (defined(argv.metallicRoughnessOcclusionTexture) && defined(argv.specularGlossinessTexture)) { + console.error('--metallicRoughnessOcclusionTexture and --specularGlossinessTexture cannot both be set.'); + process.exit(1); +} + var objPath = argv.i; var gltfPath = argv.o; @@ -157,14 +167,10 @@ var options = { console.time('Total'); -try { - obj2gltf(objPath, gltfPath, options) - .then(function() { - console.timeEnd('Total'); - }) - .catch(function(error) { - console.log(error.message); - }); -} catch(error) { - console.log(error.message); -} +obj2gltf(objPath, gltfPath, options) + .then(function() { + console.timeEnd('Total'); + }) + .catch(function(error) { + console.log(error.message); + }); diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index 3d032a5a..0bc8f0d2 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -83,11 +83,11 @@ function obj2gltf(objPath, gltfPath, options) { } if (metallicRoughness + specularGlossiness + materialsCommon > 1) { - throw new DeveloperError('Only one material type may be set from [--metallicRoughness, --specularGlossiness, --materialsCommon].'); + throw new DeveloperError('Only one material type may be set from [metallicRoughness, specularGlossiness, materialsCommon].'); } if (defined(overridingImages.metallicRoughnessOcclusionTexture) && defined(overridingImages.specularGlossinessTexture)) { - throw new DeveloperError('options.overridingImages.metallicRoughnessOcclusionTexture and options.overridingImages.specularGlossinessTexture cannot both be defined.'); + throw new DeveloperError('metallicRoughnessOcclusionTexture and specularGlossinessTexture cannot both be defined.'); } if (defined(overridingImages.metallicRoughnessOcclusionTexture)) { From cda657e9a6af9123f6cc72fc6b6fd5fdd738e8fc Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 28 Jul 2017 18:00:39 -0400 Subject: [PATCH 19/21] Fixed interleaving and added comment --- lib/createGltf.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/createGltf.js b/lib/createGltf.js index 01c60933..0bb96a47 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -427,10 +427,10 @@ function createSpecularGlossinessTexture(gltf, specularImage, glossinessImage, o if (packSpecular) { // Write into the R, G, B channels 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); + var greenChannel = getImageChannel(specularImage, 1, width, height, scratchChannel); writeChannel(pixels, greenChannel, 1); + var blueChannel = getImageChannel(specularImage, 2, width, height, scratchChannel); writeChannel(pixels, blueChannel, 2); } @@ -824,6 +824,7 @@ function addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, opti var hasNormals = mesh.normals.length > 0; var hasUVs = mesh.uvs.length > 0; + // Attributes are shared by all primitives in the mesh var accessorIndex; var attributes = {}; if (hasPositions) { From 60a080be4609fe8b4591480b91257cca96f7489a Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Sat, 29 Jul 2017 13:23:33 -0400 Subject: [PATCH 20/21] Reorganization of material loading and returning buffer rather than writing file --- .idea/encodings.xml | 1 + CHANGES.md | 2 + README.md | 85 ++- bin/obj2gltf.js | 164 +++--- lib/ArrayStorage.js | 1 - lib/Material.js | 27 - lib/{Image.js => Texture.js} | 9 +- lib/createGltf.js | 600 ++-------------------- lib/gltfToGlb.js | 22 +- lib/loadMtl.js | 597 ++++++++++++++++++++- lib/loadObj.js | 161 +----- lib/{loadImage.js => loadTexture.js} | 73 +-- lib/obj2gltf.js | 205 +++----- lib/outsideDirectory.js | 15 + lib/readLines.js | 4 +- lib/{writeUris.js => writeGltf.js} | 121 ++--- package.json | 18 +- specs/data/box-complex-material/alpha.png | Bin 1843 -> 2255 bytes specs/data/box/box.mtl | 4 +- specs/lib/createGltfSpec.js | 527 +++---------------- specs/lib/loadImageSpec.js | 113 ---- specs/lib/loadMtlSpec.js | 462 ++++++++++++++++- specs/lib/loadObjSpec.js | 211 +++----- specs/lib/loadTextureSpec.js | 117 +++++ specs/lib/obj2gltfSpec.js | 145 ++++-- 25 files changed, 1786 insertions(+), 1898 deletions(-) delete mode 100644 lib/Material.js rename lib/{Image.js => Texture.js} (58%) rename lib/{loadImage.js => loadTexture.js} (53%) create mode 100644 lib/outsideDirectory.js rename lib/{writeUris.js => writeGltf.js} (51%) delete mode 100644 specs/lib/loadImageSpec.js create mode 100644 specs/lib/loadTextureSpec.js diff --git a/.idea/encodings.xml b/.idea/encodings.xml index ff5f482b..65bf126c 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -2,6 +2,7 @@ + \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index c6c4eed1..0dc04e97 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,8 @@ Change Log * Breaking changes * Obj models now convert to glTF 2.0. Possible material profiles are `metallicRoughness`, `specGlossiness` (using the `KHR_materials_pbrSpecularGlossiness` extension), and `materialsCommon` (using the `KHR_materials_common` extension). * Removed `gltf-pipeline` dependency. The following options have been removed: `compress`, `optimize`, `generateNormals`, `optimizeForCesium`, `ao`, and `bypassPipeline`. + * Removed `inputUpAxis` and `outputUpAxis`. This stage will be incorporated into `gltf-pipeline` instead. + * `obj2gltf` no longer takes a `gltfPath` argument and saves a glTF file. Instead it returns a promise that resolves to the glTF JSON or glb buffer. ### 1.2.0 2017-07-11 diff --git a/README.md b/README.md index 7bb0e0fd..fafefdd1 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,64 @@ Install [Node.js](https://nodejs.org/en/) if you don't already have it, and then ``` npm install --save obj2gltf ``` -Using obj2gltf as a library: + +### Using obj2gltf as a command-line tool: + +`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.glb` + +### Using obj2gltf as a library: + +#### Converting an obj model to gltf: + +```javascript +var obj2gltf = require('obj2gltf'); +obj2gltf('model.obj') + .then(function(gltf) { + console.log(gltf.asset); + }); +``` + +#### Converting an obj model to glb + ```javascript var obj2gltf = require('obj2gltf'); var options = { - separateTextures : true // Don't embed textures in the converted glTF + binary : true } -obj2gltf('model.obj', 'model.gltf', options) - .then(function() { - console.log('Converted model'); +obj2gltf('model.obj', options) + .then(function(glb) { + console.log(glb.length); }); ``` -Using obj2gltf as a command-line tool: -`node bin/obj2gltf.js -i model.obj` +## Material types -`node bin/obj2gltf.js -i model.obj -o model.gltf` +Traditionally the .mtl file format describes the Blinn-Phong shading model. Meanwhile glTF 2.0 introduces physically-based +materials. + +There are three shading models supported by `obj2gltf`: + +* Metallic roughness PBR +* Specular glossiness PBR (via `KHR_materials_pbrSpecularGlossiness` extension) +* Materials common (via `KHR_materials_common` extension) + +If the material type is known in advance, it should be specified with either the `metallicRoughness`, `specularGlossiness`, or `materialsCommon` flag. + +In general, if a model is authored with traditional diffuse, specular, and shininess textures the `materialsCommon` flag should be passed in. +The glTF will be saved with the `KHR_materials_common` extension and the Blinn-Phong shading model will be used. + +However if the model is created with PBR textures, either the `metallicRoughness` or `specularGlossiness` flag should be passed in. +See the command line flags below for more information about how to specify PBR values inside the .mtl file. + +If none of these flags are provided, the .mtl is assumed to contain traditional Blinn-Phong materials which will be converted to metallic-roughness PBR. +There may be some quality loss as traditional materials do not map perfectly to PBR materials. + +Commonly in PBR workflows the the .mtl file may not exist or its values may be outdated or incorrect. +As a convenience the PBR textures may be supplied directly to the command line. See the options below. ## Usage @@ -33,24 +75,22 @@ Using obj2gltf as a command-line tool: |----|-----------|--------| |`-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`| +|`-o`, `--output`|Path of the converted glTF or glb file.|No| +|`-b`, `--binary`|Save as binary glTF (.glb).|No, default `false`| +|`-s`, `--separate`|Writes out separate buffers and textures instead of embedding them in the glTF file.|No, default `false`| |`-t`, `--separateTextures`|Write out separate textures only.|No, default `false`| |`--checkTransparency`|Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. By default textures are considered to be opaque.|No, default `false`| -|`--secure`|Prevent the converter from reading image or mtl files outside of the input obj directory.|No, default `false`| -|`--inputUpAxis`|Up axis of the obj. Choices are 'X', 'Y', and 'Z'.|No, default `Y`| -|`--outputUpAxis`|Up axis of the converted glTF. Choices are 'X', 'Y', and 'Z'.|No, default `Y`| +|`--secure`|Prevent the converter from reading texture 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`| |`--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, 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. +|`--metallicRoughnessOcclusionTexture`|Path to the metallic-roughness-occlusion texture that should override textures in the .mtl file, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. The model will be saved with a pbrMetallicRoughness material. This is often convenient in workflows where the .mtl does not exist or is not set up to use PBR materials. Intended for models with a single material.|No| +|`--specularGlossinessTexture`|Path to the specular-glossiness texture that should override textures in the .mtl file, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension.|No| +|`--occlusionTexture`|Path to the occlusion texture that should override textures in the .mtl file.|No| +|`--normalTexture`|Path to the normal texture that should override textures in the .mtl file.|No| +|`--baseColorTexture`|Path to the baseColor/diffuse texture that should override textures in the .mtl file.|No| +|`--emissiveTexture`|Path to the emissive texture that should override textures in the .mtl file.|No| ## Build Instructions @@ -86,11 +126,6 @@ npm run jsdoc The documentation will be placed in the `doc` folder. -## Debugging - -* To debug the tests in Webstorm, open the Gulp tab, right click the `test` task, and click `Debug 'test'`. -* To run a single test, change the test function from `it` to `fit`. - ## Contributions Pull requests are appreciated. Please use the same [Contributor License Agreement (CLA)](https://github.com/AnalyticalGraphicsInc/cesium/blob/master/CONTRIBUTING.md) used for [Cesium](http://cesiumjs.org/). diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js index 8d2e2765..58abdd94 100644 --- a/bin/obj2gltf.js +++ b/bin/obj2gltf.js @@ -1,6 +1,7 @@ #!/usr/bin/env node 'use strict'; var Cesium = require('cesium'); +var fsExtra = require('fs-extra'); var path = require('path'); var yargs = require('yargs'); var obj2gltf = require('../lib/obj2gltf'); @@ -18,107 +19,95 @@ var argv = yargs .alias('h', 'help') .options({ input : { - alias: 'i', - describe: 'Path to the obj file.', - type: 'string', - normalize: true, - demandOption: true + alias : 'i', + describe : 'Path to the obj file.', + type : 'string', + normalize : true, + demandOption : true }, output : { - alias: 'o', - describe: 'Path of the converted glTF file.', - type: 'string', - normalize: true + alias : 'o', + describe : 'Path of the converted glTF or glb file.', + type : 'string', + normalize : true }, binary : { - alias: 'b', - describe: 'Save as binary glTF.', - type: 'boolean', - default: defaults.binary + alias : 'b', + describe : 'Save as binary glTF (.glb)', + type : 'boolean', + 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: defaults.separate + alias : 's', + describe : 'Write separate buffers and textures instead of embedding them in the glTF.', + type : 'boolean', + default : defaults.separate }, separateTextures : { - alias: 't', - describe: 'Write out separate textures only.', - type: 'boolean', - default: defaults.separateTextures + alias : 't', + describe : 'Write out separate textures only.', + type : 'boolean', + default : defaults.separateTextures }, 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: defaults.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 : defaults.checkTransparency }, secure : { - describe: 'Prevent the converter from reading image or mtl files outside of the input obj directory.', - type: 'boolean', - default: defaults.secure - }, - inputUpAxis : { - describe: 'Up axis of the obj.', - choices: ['X', 'Y', 'Z'], - type: 'string', - default: 'Y' - }, - outputUpAxis : { - describe: 'Up axis of the converted glTF.', - choices: ['X', 'Y', 'Z'], - type: 'string', - default: 'Y' + describe : 'Prevent the converter from reading textures 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 + describe : 'Pack the occlusion texture in the red channel of metallic-roughness texture.', + type : 'boolean', + default : defaults.packOcclusion }, metallicRoughness : { - 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 + 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 }, specularGlossiness : { - 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 + 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 }, materialsCommon : { - describe: 'The glTF will be saved with the KHR_materials_common extension.', - type: 'boolean', - default: defaults.materialsCommon + describe : 'The glTF will be saved with the KHR_materials_common extension.', + type : 'boolean', + 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, and is intended for models that use one material. The model will be saved with a pbrMetallicRoughness material.', - type: 'string', - normalize: true + describe : 'Path to the metallic-roughness-occlusion texture that should override textures in the .mtl file, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. The model will be saved with a pbrMetallicRoughness material. This is often convenient in workflows where the .mtl does not exist or is not set up to use PBR materials. Intended for models with a single 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, 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 + describe : 'Path to the specular-glossiness texture that should override textures in the .mtl file, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. 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, and is intended for models that use one material. Ignored if metallicRoughnessOcclusionTexture is also set.', - type: 'string', - normalize: true + describe : 'Path to the occlusion texture that should override textures in the .mtl file.', + 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, and is intended for models that use one material.', - type: 'string', - normalize: true + describe : 'Path to the normal texture that should override textures in the .mtl file.', + 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, and is intended for models that use one material.', - type: 'string', - normalize: true + describe : 'Path to the baseColor/diffuse texture that should override textures in the .mtl file.', + 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, and is intended for models that use one material.', - type: 'string', - normalize: true + describe : 'Path to the emissive texture that should override textures in the .mtl file.', + type : 'string', + normalize : true } }).parse(args); @@ -132,16 +121,23 @@ if (defined(argv.metallicRoughnessOcclusionTexture) && defined(argv.specularGlos process.exit(1); } -var objPath = argv.i; -var gltfPath = argv.o; +var objPath = argv.input; +var gltfPath = argv.output; +var name = path.basename(objPath, path.extname(objPath)); if (!defined(gltfPath)) { - var extension = argv.b ? '.glb' : '.gltf'; - var modelName = path.basename(objPath, path.extname(objPath)); - gltfPath = path.join(path.dirname(objPath), modelName + extension); + gltfPath = path.join(path.dirname(objPath), name + '.gltf'); } -var overridingImages = { +var outputDirectory = path.dirname(gltfPath); +var extension = path.extname(gltfPath).toLowerCase(); +if (argv.binary || extension === '.glb') { + argv.binary = true; + extension = '.glb'; +} +gltfPath = path.join(outputDirectory, name + extension); + +var overridingTextures = { metallicRoughnessOcclusionTexture : argv.metallicRoughnessOcclusionTexture, specularGlossinessTexture : argv.specularGlossinessTexture, occlusionTexture : argv.occlusionTexture, @@ -156,21 +152,31 @@ var options = { separateTextures : argv.separateTextures, checkTransparency : argv.checkTransparency, secure : argv.secure, - inputUpAxis : argv.inputUpAxis, - outputUpAxis : argv.outputUpAxis, packOcclusion : argv.packOcclusion, metallicRoughness : argv.metallicRoughness, specularGlossiness : argv.specularGlossiness, materialsCommon : argv.materialsCommon, - overridingImages : overridingImages + overridingTextures : overridingTextures, + outputDirectory : outputDirectory }; console.time('Total'); -obj2gltf(objPath, gltfPath, options) +obj2gltf(objPath, options) + .then(function(gltf) { + if (argv.binary) { + // gltf is a glb buffer + return fsExtra.outputFile(gltfPath, gltf); + } + var jsonOptions = { + spaces : 2 + }; + return fsExtra.outputJson(gltfPath, gltf, jsonOptions); + }) .then(function() { console.timeEnd('Total'); }) .catch(function(error) { console.log(error.message); + process.exit(1); }); diff --git a/lib/ArrayStorage.js b/lib/ArrayStorage.js index c97fd432..3b9cfda9 100644 --- a/lib/ArrayStorage.js +++ b/lib/ArrayStorage.js @@ -14,7 +14,6 @@ var fixedExpansionLength = 33554432; // 2^25 (~134 MB for a Float32Array) * stored with double precision. The resizing mechanism is similar to std::vector. * * @param {ComponentDatatype} componentDatatype The data type. - * @constructor * * @private */ diff --git a/lib/Material.js b/lib/Material.js deleted file mode 100644 index 10bfcdd8..00000000 --- a/lib/Material.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -module.exports = Material; - -/** - * A material definition which maps to the .mtl format. - * - * The default value for specularShininess varies depending on the material type, @see loadMtl. - * - * @private - */ -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 - 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.emissiveTexture = undefined; // map_Ke - this.diffuseTexture = undefined; // map_Kd - this.specularTexture = undefined; // map_Ks - this.specularShininessTexture = undefined; // map_Ns - this.normalTexture = undefined; // map_Bump - this.alphaTexture = undefined; // map_d -} diff --git a/lib/Image.js b/lib/Texture.js similarity index 58% rename from lib/Image.js rename to lib/Texture.js index afcc4dab..aa440533 100644 --- a/lib/Image.js +++ b/lib/Texture.js @@ -1,18 +1,19 @@ 'use strict'; -module.exports = Image; +module.exports = Texture; /** - * Stores image data and properties. + * An object containing information about a texture. * * @private */ -function Image() { +function Texture() { this.transparent = false; this.source = undefined; + this.name = undefined; this.extension = undefined; this.path = undefined; - this.decoded = undefined; + this.pixels = undefined; this.width = undefined; this.height = undefined; } diff --git a/lib/createGltf.js b/lib/createGltf.js index 0bb96a47..5b6c0444 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -1,12 +1,9 @@ 'use strict'; var Cesium = require('cesium'); -var path = require('path'); var getBufferPadded = require('./getBufferPadded'); -var Image = require('./Image'); -var Material = require('./Material'); +var getDefaultMaterial = require('./loadMtl').getDefaultMaterial; +var Texture = require('./Texture'); -var CesiumMath = Cesium.Math; -var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; var WebGLConstants = Cesium.WebGLConstants; @@ -15,20 +12,8 @@ module.exports = createGltf; /** * Create a glTF from obj data. * - * @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 Pack the occlusion texture in the red channel of metallic-roughness texture. - * @param {Boolean} options.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. - * @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 object containing image paths that override material values defined in the .mtl file. This is often convenient in workflows where the .mtl does not exist or is not set up to use PBR materials. Intended for models with a single material. - * @param {String} [options.overridingImages.metallicRoughnessOcclusionTexture] Path to the metallic-roughness-occlusion texture, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. The model will be saved with a pbrMetallicRoughness material. - * @param {String} [options.overridingImages.specularGlossinessTexture] Path to the specular-glossiness texture, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension. - * @param {String} [options.overridingImages.occlusionTexture] Path to the occlusion texture. Ignored if metallicRoughnessOcclusionTexture is also set. - * @param {String} [options.overridingImages.normalTexture] Path to the normal texture. - * @param {String} [options.overridingImages.baseColorTexture] Path to the baseColor/diffuse texture. - * @param {String} [options.overridingImages.emissiveTexture] Path to the emissive texture. - * @param {Boolean} options.logger A callback function for handling logged messages. Defaults to console.log. + * @param {Object} objData An object containing an array of nodes containing geometry information and an array of materials. + * @param {Object} options The options object passed along from lib/obj2gltf.js * @returns {Object} A glTF asset. * * @private @@ -36,7 +21,7 @@ module.exports = createGltf; function createGltf(objData, options) { var nodes = objData.nodes; var materials = objData.materials; - var images = objData.images; + var name = objData.name; var gltf = { accessors : [], @@ -85,14 +70,14 @@ function createGltf(objData, options) { var meshIndex; if (meshesLength === 1) { - meshIndex = addMesh(gltf, materials, images, bufferState, uint32Indices, meshes[0], options); - addNode(gltf, node.name, meshIndex); + meshIndex = addMesh(gltf, materials, bufferState, uint32Indices, meshes[0], options); + addNode(gltf, node.name, meshIndex, undefined); } else { // Add meshes as child nodes var parentIndex = addNode(gltf, node.name); for (var j = 0; j < meshesLength; ++j) { var mesh = meshes[j]; - meshIndex = addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, options); + meshIndex = addMesh(gltf, materials, bufferState, uint32Indices, mesh, options); addNode(gltf, mesh.name, meshIndex, parentIndex); } } @@ -107,7 +92,16 @@ function createGltf(objData, options) { }); } - addBuffers(gltf, bufferState); + addBuffers(gltf, bufferState, name); + + if (options.specularGlossiness) { + gltf.extensionsUsed.push('KHR_materials_pbrSpecularGlossiness'); + gltf.extensionsRequired.push('KHR_materials_pbrSpecularGlossiness'); + } else if (options.materialsCommon) { + gltf.extensionsUsed.push('KHR_materials_common'); + gltf.extensionsRequired.push('KHR_materials_common'); + } + return gltf; } @@ -136,7 +130,7 @@ function addBufferView(gltf, buffers, accessors, byteStride, target) { }); } -function addBuffers(gltf, bufferState) { +function addBuffers(gltf, bufferState, name) { // Positions and normals share the same byte stride so they can share the same bufferView var positionsAndNormalsAccessors = bufferState.positionAccessors.concat(bufferState.normalAccessors); var positionsAndNormalsBuffers = bufferState.positionBuffers.concat(bufferState.normalBuffers); @@ -149,7 +143,7 @@ function addBuffers(gltf, bufferState) { var buffer = getBufferPadded(Buffer.concat(buffers)); gltf.buffers.push({ - name : 'buffer', + name : name, byteLength : buffer.length, extras : { _obj2gltf : { @@ -159,38 +153,16 @@ function addBuffers(gltf, bufferState) { }); } -function getImage(images, imagePath, overridingImage) { - if (defined(overridingImage)) { - return overridingImage; - } - var imagesLength = images.length; - for (var i = 0; i < imagesLength; ++i) { - var image = images[i]; - if (image.path === imagePath) { - return image; - } - } - return undefined; -} - -function getImageName(image) { - return path.basename(image.path, image.extension); -} - -function getTextureName(image) { - return getImageName(image) + '_texture'; -} - -function addTexture(gltf, image) { - var imageName = getImageName(image); - var textureName = getTextureName(image); +function addTexture(gltf, texture) { + var imageName = texture.name; + var textureName = texture.name; var imageIndex = gltf.images.length; var textureIndex = gltf.textures.length; gltf.images.push({ name : imageName, extras : { - _obj2gltf : image + _obj2gltf : texture } }); @@ -203,13 +175,9 @@ function addTexture(gltf, image) { return textureIndex; } -function getTexture(gltf, image) { - if (!defined(image)) { - return undefined; - } - +function getTexture(gltf, texture) { var textureIndex; - var name = getTextureName(image); + var name = texture.name; var textures = gltf.textures; var length = textures.length; for (var i = 0; i < length; ++i) { @@ -220,7 +188,7 @@ function getTexture(gltf, image) { } if (!defined(textureIndex)) { - textureIndex = addTexture(gltf, image); + textureIndex = addTexture(gltf, texture); } return { @@ -228,507 +196,27 @@ function getTexture(gltf, image) { }; } -function addColors(left, right) { - var red = Math.min(left[0] + right[0], 1.0); - var green = Math.min(left[1] + right[1], 1.0); - var blue = Math.min(left[2] + right[2], 1.0); - 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, targetPixels, targetWidth, targetHeight) { - // Nearest neighbor sampling - var widthRatio = sourceWidth / targetWidth; - var heightRatio = sourceHeight / targetHeight; - - for (var y = 0; y < targetHeight; ++y) { - for (var x = 0; x < targetWidth; ++x) { - var targetIndex = y * targetWidth + x; - var sourceY = Math.round(y * heightRatio); - var sourceX = Math.round(x * widthRatio); - var sourceIndex = sourceY * sourceWidth + sourceX; - var sourceValue = sourcePixels.readUInt8(sourceIndex); - targetPixels.writeUInt8(sourceValue, targetIndex); - } - } - return targetPixels; -} - -var scratchResizeChannel; - -function getImageChannel(image, index, targetWidth, targetHeight, targetChannel) { - var pixels = image.decoded; // RGBA - 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); - sourceChannel.writeUInt8(value, i); - } - - if (sourcePixelsLength > targetPixelsLength) { - resizeChannel(sourceChannel, sourceWidth, sourceHeight, targetChannel, targetWidth, targetHeight); - } - - return targetChannel; -} - -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); - } -} - -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]; - width = Math.min(image.width, width); - height = Math.min(image.height, height); - } - - for (i = 0; i < length; ++i) { - image = images[i]; - 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 + '.'); - } - } - - return [width, height]; -} - -function createMetallicRoughnessTexture(gltf, 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 packedImages = [metallicImage, roughnessImage, occlusionImage].filter(function(image) { - return defined(image) && defined(image.decoded); - }); - - var dimensions = getMinimumDimensions(packedImages, 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 - var scratchChannel = Buffer.alloc(pixelsLength); - - if (packMetallic) { - // Write into the B channel - 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, scratchChannel); - writeChannel(pixels, roughnessChannel, 1); - } - - if (packOcclusion) { - // Write into the R channel - 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) { - imageNames[i] = getImageName(packedImages[i]); - } - var imageName = imageNames.join('_'); - - var image = new Image(); - image.extension = '.png'; - image.path = imageName; - image.decoded = pixels; - image.width = width; - image.height = height; - - return getTexture(gltf, image); -} - -function createSpecularGlossinessTexture(gltf, specularImage, glossinessImage, options) { - var packSpecular = defined(specularImage); - var packGlossiness = defined(glossinessImage); - - 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 packedImages = [specularImage, glossinessImage].filter(function(image) { - return defined(image) && defined(image.decoded); - }); - - var dimensions = getMinimumDimensions(packedImages, 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 - var scratchChannel = Buffer.alloc(pixelsLength); - - if (packSpecular) { - // Write into the R, G, B channels - var redChannel = getImageChannel(specularImage, 0, width, height, scratchChannel); - writeChannel(pixels, redChannel, 0); - var greenChannel = getImageChannel(specularImage, 1, width, height, scratchChannel); - writeChannel(pixels, greenChannel, 1); - var blueChannel = getImageChannel(specularImage, 2, width, height, scratchChannel); - writeChannel(pixels, blueChannel, 2); - } - - if (packGlossiness) { - // Write into the A channel - 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) { - imageNames[i] = getImageName(packedImages[i]); - } - var imageName = imageNames.join('_'); - - var image = new Image(); - image.extension = '.png'; - image.path = imageName; - image.decoded = pixels; - image.width = width; - image.height = height; - - return getTexture(gltf, image); -} - -function createSpecularGlossinessMaterial(gltf, images, material, options) { - var materialName = material.name; - - // The texture paths supplied in the .mtl may be overriden by the texture paths supplied in options - var overridingImages = options.overridingImages; - var emissiveImage = getImage(images, material.emissiveTexture, overridingImages.emissiveTexture, options); - var normalImage = getImage(images, material.normalTexture, overridingImages.normalTexture, options); - var occlusionImage = getImage(images, material.ambientTexture, overridingImages.occlusionTexture, options); - var diffuseImage = getImage(images, material.diffuseTexture, overridingImages.baseColorTexture, options); - var specularImage = getImage(images, material.specularTexture, overridingImages.specularGlossinessTexture, options); - var glossinessImage = getImage(images, material.specularShininessTexture, overridingImages.specularGlossinessTexture, options); - - var emissiveTexture = getTexture(gltf, emissiveImage); - var normalTexture = getTexture(gltf, normalImage); - var occlusionTexture = getTexture(gltf, occlusionImage); - var diffuseTexture = getTexture(gltf, diffuseImage); - - var specularGlossinessTexture; - if (defined(overridingImages.specularGlossinessTexture)) { - specularGlossinessTexture = getTexture(gltf, specularImage); - } else { - specularGlossinessTexture = createSpecularGlossinessTexture(gltf, specularImage, glossinessImage, options); - } - - var emissiveFactor = getEmissiveFactor(material); - var diffuseFactor = material.diffuseColor; - var specularFactor = material.specularColor.slice(0, 3); - 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, 1.0, 1.0]; - } - - if (defined(glossinessImage)) { - glossinessFactor = 1.0; - } - - var alpha = material.alpha; - diffuseFactor[3] = alpha; - - var transparent = alpha < 1.0; - if (defined(diffuseImage)) { - transparent = transparent || diffuseImage.transparent; - } - - var doubleSided = transparent; - var alphaMode = transparent ? 'BLEND' : 'OPAQUE'; - - gltf.extensionsUsed.push('KHR_materials_pbrSpecularGlossiness'); - gltf.extensionsRequired.push('KHR_materials_pbrSpecularGlossiness'); - - return { - name : materialName, - extensions : { - KHR_materials_pbrSpecularGlossiness: { - diffuseTexture : diffuseTexture, - specularGlossinessTexture : specularGlossinessTexture, - diffuseFactor : diffuseFactor, - specularFactor : specularFactor, - glossinessFactor : glossinessFactor +function resolveTextures(gltf, material) { + for (var name in material) { + if (material.hasOwnProperty(name)) { + var property = material[name]; + if (property instanceof Texture) { + material[name] = getTexture(gltf, property); + } else if (!Array.isArray(property) && (typeof property === 'object')) { + resolveTextures(gltf, property); } - }, - emissiveTexture : emissiveTexture, - normalTexture : normalTexture, - occlusionTexture : occlusionTexture, - emissiveFactor : emissiveFactor, - alphaMode : alphaMode, - doubleSided : doubleSided - }; -} - -function createMetallicRoughnessMaterial(gltf, images, material, options) { - var materialName = material.name; - - // The texture paths supplied in the .mtl may be over var overridingImages = options.overridingImages; - var overridingImages = options.overridingImages; - var emissiveImage = getImage(images, material.emissiveTexture, overridingImages.emissiveTexture); - var normalImage = getImage(images, material.normalTexture, overridingImages.normalTexture); - var occlusionImage = getImage(images, material.ambientTexture, overridingImages.metallicRoughnessOcclusionTexture); - var baseColorImage = getImage(images, material.diffuseTexture, overridingImages.baseColorTexture); - var metallicImage = getImage(images, material.specularTexture, overridingImages.metallicRoughnessOcclusionTexture); - var roughnessImage = getImage(images, material.specularShininessTexture, overridingImages.metallicRoughnessOcclusionTexture); - - var emissiveTexture = getTexture(gltf, emissiveImage); - var normalTexture = getTexture(gltf, normalImage); - var baseColorTexture = getTexture(gltf, baseColorImage); - - var metallicRoughnessTexture; - if (defined(overridingImages.metallicRoughnessOcclusionTexture)) { - metallicRoughnessTexture = getTexture(gltf, metallicImage); - } else { - metallicRoughnessTexture = createMetallicRoughnessTexture(gltf, metallicImage, roughnessImage, occlusionImage, options); - } - - var packOcclusion = (defined(occlusionImage) && options.packOcclusion) || defined(overridingImages.metallicRoughnessOcclusionTexture); - 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(baseColorTexture)) { - baseColorFactor = [1.0, 1.0, 1.0, 1.0]; - } - - if (defined(metallicImage)) { - metallicFactor = 1.0; - } - - if (defined(roughnessImage)) { - roughnessFactor = 1.0; - } - - var alpha = material.alpha; - baseColorFactor[3] = alpha; - - var transparent = alpha < 1.0; - if (defined(baseColorImage)) { - transparent = transparent || baseColorImage.transparent; - } - - var doubleSided = transparent; - var alphaMode = transparent ? 'BLEND' : 'OPAQUE'; - - return { - name : materialName, - pbrMetallicRoughness : { - baseColorTexture : baseColorTexture, - metallicRoughnessTexture : metallicRoughnessTexture, - baseColorFactor : baseColorFactor, - metallicFactor : metallicFactor, - roughnessFactor : roughnessFactor - }, - emissiveTexture : emissiveTexture, - normalTexture : normalTexture, - occlusionTexture : occlusionTexture, - emissiveFactor : emissiveFactor, - alphaMode : alphaMode, - doubleSided : doubleSided - }; -} - -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 = luminance(material.specularColor); - 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 *= (1.0 - specularIntensity); - } - - var metallicFactor = 0.0; - - material.specularTexture = undefined; // For now just ignore the specular texture - material.specularColor = [metallicFactor, metallicFactor, metallicFactor, 1.0]; - material.specularShininess = roughnessFactor; -} - -function createMaterialsCommonMaterial(gltf, images, material, hasNormals, options) { - var materialName = material.name; - - var ambientImage = getImage(images, material.ambientTexture, undefined, options); - var diffuseImage = getImage(images, material.diffuseTexture, undefined, options); - var emissiveImage = getImage(images, material.emissiveTexture, undefined, options); - var specularImage = getImage(images, material.specularTexture, undefined, options); - - var ambient = defaultValue(getTexture(gltf, ambientImage), material.ambientColor); - var diffuse = defaultValue(getTexture(gltf, diffuseImage), material.diffuseColor); - var emission = defaultValue(getTexture(gltf, emissiveImage), material.emissiveColor); - var specular = defaultValue(getTexture(gltf, specularImage), material.specularColor); - - var alpha = material.alpha; - var shininess = material.specularShininess; - var hasSpecular = (shininess > 0.0) && (specular[0] > 0.0 || specular[1] > 0.0 || specular[2] > 0.0); - - var transparent; - var transparency = 1.0; - if (defined(diffuseImage)) { - transparency = alpha; - transparent = diffuseImage.transparent || (transparency < 1.0); - } else { - diffuse[3] = alpha; - transparent = alpha < 1.0; - } - - if (!defined(ambientImage)) { - // If ambient color is [1, 1, 1] assume it is a multiplier and instead change to [0, 0, 0] - if (ambient[0] === 1.0 && ambient[1] === 1.0 && ambient[2] === 1.0) { - ambient = [0.0, 0.0, 0.0, 1.0]; } } - - var doubleSided = transparent; - - if (!hasNormals) { - // Constant technique only factors in ambient and emission sources - set emission to diffuse - emission = diffuse; - } - - var technique = hasNormals ? (hasSpecular ? 'PHONG' : 'LAMBERT') : 'CONSTANT'; - - gltf.extensionsUsed.push('KHR_materials_common'); - gltf.extensionsRequired.push('KHR_materials_common'); - - return { - name : materialName, - extensions : { - KHR_materials_common : { - technique : technique, - transparent : transparent, - doubleSided : doubleSided, - values : { - ambient : ambient, - diffuse : diffuse, - emission : emission, - specular : specular, - shininess : shininess, - transparency : transparency, - transparent : transparent, - doubleSided : doubleSided - } - } - } - }; } -function addMaterial(gltf, images, material, hasNormals, options) { - var gltfMaterial; - if (options.specularGlossiness) { - gltfMaterial = createSpecularGlossinessMaterial(gltf, images, material, options); - } else if (options.metallicRoughness) { - gltfMaterial = createMetallicRoughnessMaterial(gltf, images, material, options); - } else if (options.materialsCommon) { - gltfMaterial = createMaterialsCommonMaterial(gltf, images, material, hasNormals, options); - } else { - convertTraditionalToMetallicRoughness(material); - gltfMaterial = createMetallicRoughnessMaterial(gltf, images, material, options); - } - +function addMaterial(gltf, material) { + resolveTextures(gltf, material); var materialIndex = gltf.materials.length; - gltf.materials.push(gltfMaterial); + gltf.materials.push(material); return materialIndex; } -function getMaterial(gltf, materials, images, materialName, hasNormals, options) { +function getMaterial(gltf, materials, materialName, options) { if (!defined(materialName)) { // Create a default material if the primitive does not specify one materialName = 'default'; @@ -745,7 +233,7 @@ function getMaterial(gltf, materials, images, materialName, hasNormals, options) } if (!defined(material)) { - material = new Material(); + material = getDefaultMaterial(options); material.name = materialName; } @@ -759,7 +247,7 @@ function getMaterial(gltf, materials, images, materialName, hasNormals, options) } if (!defined(materialIndex)) { - materialIndex = addMaterial(gltf, images, material, hasNormals, options); + materialIndex = addMaterial(gltf, material); } return materialIndex; @@ -819,12 +307,12 @@ function requiresUint32Indices(nodes) { return false; } -function addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, options) { +function addMesh(gltf, materials, bufferState, uint32Indices, mesh, options) { var hasPositions = mesh.positions.length > 0; var hasNormals = mesh.normals.length > 0; var hasUVs = mesh.uvs.length > 0; - // Attributes are shared by all primitives in the mesh + // Vertex attributes are shared by all primitives in the mesh var accessorIndex; var attributes = {}; if (hasPositions) { @@ -863,7 +351,7 @@ function addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, opti primitive.indices = undefined; // Unload resources - var materialIndex = getMaterial(gltf, materials, images, primitive.material, hasNormals, options); + var materialIndex = getMaterial(gltf, materials, primitive.material, options); gltfPrimitives.push({ attributes : attributes, diff --git a/lib/gltfToGlb.js b/lib/gltfToGlb.js index e8d455bd..85888b21 100644 --- a/lib/gltfToGlb.js +++ b/lib/gltfToGlb.js @@ -2,27 +2,24 @@ var Cesium = require('cesium'); var getJsonBufferPadded = require('./getJsonBufferPadded'); -var isDataUri = Cesium.isDataUri; +var defined = Cesium.defined; 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. + * The glTF is expected to have a single buffer and all embedded resources stored in bufferViews. * - * @param {Object} gltf A javascript object containing a glTF asset. - * @returns {Promise} A promise that resolves to a buffer containing the binary glTF. + * @param {Object} gltf The glTF asset. + * @param {Buffer} binaryBuffer The binary buffer. + * @returns {Buffer} The glb buffer. * * @private */ -function gltfToGlb(gltf) { +function gltfToGlb(gltf, binaryBuffer) { var buffer = gltf.buffers[0]; - var binaryBuffer; - if (isDataUri(buffer.uri)) { - binaryBuffer = dataUriToBuffer(buffer.uri); - delete buffer.uri; - } else { + if (defined(buffer.uri)) { binaryBuffer = Buffer.alloc(0); } @@ -62,8 +59,3 @@ function gltfToGlb(gltf) { binaryBuffer.copy(glb, byteOffset); return glb; } - -function dataUriToBuffer(dataUri) { - var data = dataUri.slice(dataUri.indexOf(',')); - return Buffer.from(data, 'base64'); -} diff --git a/lib/loadMtl.js b/lib/loadMtl.js index c72eb566..62a58edb 100644 --- a/lib/loadMtl.js +++ b/lib/loadMtl.js @@ -1,17 +1,26 @@ 'use strict'; +var Cesium = require('cesium'); var path = require('path'); -var Material = require('./Material'); +var Promise = require('bluebird'); +var loadTexture = require('./loadTexture'); +var outsideDirectory = require('./outsideDirectory'); var readLines = require('./readLines'); +var Texture = require('./Texture'); + +var CesiumMath = Cesium.Math; +var combine = Cesium.combine; +var defaultValue = Cesium.defaultValue; +var defined = Cesium.defined; module.exports = loadMtl; /** - * Parse an mtl file. + * Parse a .mtl file and load textures referenced within. Returns an array of glTF materials with Texture + * objects stored in the texture slots. * - * @param {String} mtlPath Path to the mtl file. - * @param {Object} options An object with the following properties: - * @param {Boolean} options.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. - * @returns {Promise} A promise resolving to an array of materials. + * @param {String} mtlPath Path to the .mtl file. + * @param {Object} options The options object passed along from lib/obj2gltf.js + * @returns {Promise} A promise resolving to an array of glTF materials with Texture objects stored in the texture slots. * * @private */ @@ -19,22 +28,54 @@ function loadMtl(mtlPath, options) { var material; var values; var value; + var texturePath; + var mtlDirectory = path.dirname(mtlPath); var materials = []; + var texturePromiseMap = {}; // Maps texture paths to load promises so that no texture is loaded twice + var texturePromises = []; + + var overridingTextures = options.overridingTextures; + var overridingSpecularTexture = defaultValue(overridingTextures.metallicRoughnessOcclusionTexture, overridingTextures.specularGlossinessTexture); + var overridingSpecularShininessTexture = defaultValue(overridingTextures.metallicRoughnessOcclusionTexture, overridingTextures.specularGlossinessTexture); + var overridingAmbientTexture = defaultValue(overridingTextures.metallicRoughnessOcclusionTexture, overridingTextures.occlusionTexture); + var overridingNormalTexture = overridingTextures.normalTexture; + var overridingDiffuseTexture = overridingTextures.baseColorTexture; + var overridingEmissiveTexture = overridingTextures.emissiveTexture; + + // Textures that are packed into PBR textures need to be decoded first + var decodeOptions = options.materialsCommon ? undefined : { + decode : true + }; + + var diffuseTextureOptions = { + checkTransparency : options.checkTransparency + }; - var defaultSpecularShininess = 0.0; - if (options.metallicRoughness) { - defaultSpecularShininess = 1.0; // Fully rough + var ambientTextureOptions = options.packOcclusion ? decodeOptions : undefined; + var specularTextureOptions = decodeOptions; + var specularShinessTextureOptions = decodeOptions; + var emissiveTextureOptions; + var normalTextureOptions; + + function createMaterial(name) { + material = new Material(); + material.name = name; + material.specularShininess = options.metallicRoughness ? 1.0 : 0.0; + loadMaterialTexture(material, 'specularTexture', overridingSpecularTexture, undefined, mtlDirectory, texturePromiseMap, texturePromises, options); + loadMaterialTexture(material, 'specularShininessTexture', overridingSpecularShininessTexture, undefined, mtlDirectory, texturePromiseMap, texturePromises, options); + loadMaterialTexture(material, 'ambientTexture', overridingAmbientTexture, undefined, mtlDirectory, texturePromiseMap, texturePromises, options); + loadMaterialTexture(material, 'normalTexture', overridingNormalTexture, undefined, mtlDirectory, texturePromiseMap, texturePromises, options); + loadMaterialTexture(material, 'diffuseTexture', overridingDiffuseTexture, diffuseTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); + loadMaterialTexture(material, 'emissiveTexture', overridingEmissiveTexture, undefined, mtlDirectory, texturePromiseMap, texturePromises, options); + materials.push(material); } function parseLine(line) { line = line.trim(); if (/^newmtl /i.test(line)) { var name = line.substring(7).trim(); - material = new Material(); - material.name = name; - material.specularShininess = defaultSpecularShininess; - materials.push(material); + createMaterial(name); } else if (/^Ka /i.test(line)) { values = line.substring(3).trim().split(' '); material.ambientColor = [ @@ -77,24 +118,536 @@ function loadMtl(mtlPath, options) { value = line.substring(3).trim(); material.alpha = 1.0 - parseFloat(value); } else if (/^map_Ka /i.test(line)) { - material.ambientTexture = path.resolve(mtlDirectory, line.substring(7).trim()); + if (!defined(overridingAmbientTexture)) { + texturePath = path.resolve(mtlDirectory, line.substring(7).trim()); + loadMaterialTexture(material, 'ambientTexture', texturePath, ambientTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); + } } else if (/^map_Ke /i.test(line)) { - material.emissiveTexture = path.resolve(mtlDirectory, line.substring(7).trim()); + if (!defined(overridingEmissiveTexture)) { + texturePath = path.resolve(mtlDirectory, line.substring(7).trim()); + loadMaterialTexture(material, 'emissiveTexture', texturePath, emissiveTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); + } } else if (/^map_Kd /i.test(line)) { - material.diffuseTexture = path.resolve(mtlDirectory, line.substring(7).trim()); + if (!defined(overridingDiffuseTexture)) { + texturePath = path.resolve(mtlDirectory, line.substring(7).trim()); + loadMaterialTexture(material, 'diffuseTexture', texturePath, diffuseTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); + } } else if (/^map_Ks /i.test(line)) { - material.specularTexture = path.resolve(mtlDirectory, line.substring(7).trim()); + if (!defined(overridingSpecularTexture)) { + texturePath = path.resolve(mtlDirectory, line.substring(7).trim()); + loadMaterialTexture(material, 'specularTexture', texturePath, specularTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); + } } else if (/^map_Ns /i.test(line)) { - material.specularShininessTexture = path.resolve(mtlDirectory, line.substring(7).trim()); + if (!defined(overridingSpecularShininessTexture)) { + texturePath = path.resolve(mtlDirectory, line.substring(7).trim()); + loadMaterialTexture(material, 'specularShininessTexture', texturePath, specularShinessTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); + } } else if (/^map_Bump /i.test(line)) { - material.normalTexture = path.resolve(mtlDirectory, line.substring(9).trim()); - } else if (/^map_d /i.test(line)) { - material.alphaTexture = path.resolve(mtlDirectory, line.substring(6).trim()); + if (!defined(overridingNormalTexture)) { + texturePath = path.resolve(mtlDirectory, line.substring(9).trim()); + loadMaterialTexture(material, 'normalTexture', texturePath, normalTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); + } } } return readLines(mtlPath, parseLine) .then(function() { - return materials; + return Promise.all(texturePromises); + }) + .then(function() { + return convertMaterials(materials, options); }); } + +function Material() { + this.name = undefined; + 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 + 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.emissiveTexture = undefined; // map_Ke + this.diffuseTexture = undefined; // map_Kd + this.specularTexture = undefined; // map_Ks + this.specularShininessTexture = undefined; // map_Ns + this.normalTexture = undefined; // map_Bump +} + +loadMtl.getDefaultMaterial = function(options) { + return convertMaterial(new Material(), options); +}; + +// Exposed for testing +loadMtl._createMaterial = function(materialOptions, options) { + return convertMaterial(combine(materialOptions, new Material()), options); +}; + +function loadMaterialTexture(material, name, texturePath, textureOptions, mtlDirectory, texturePromiseMap, texturePromises, options) { + if (!defined(texturePath)) { + return; + } + var texturePromise = texturePromiseMap[texturePath]; + if (!defined(texturePromise)) { + if (options.secure && outsideDirectory(texturePath, mtlDirectory)) { + options.logger('Could not read texture file at ' + texturePath + ' because it is outside of the mtl directory and the secure flag is true. This texture will be ignored.'); + texturePromise = Promise.resolve(); + } else { + texturePromise = loadTexture(texturePath, textureOptions) + .catch(function() { + options.logger('Could not read texture file at ' + texturePath + '. This texture will be ignored.'); + }); + } + texturePromiseMap[texturePath] = texturePromise; + } + + texturePromises.push(texturePromise + .then(function(texture) { + material[name] = texture; + })); +} + +function convertMaterial(material, options) { + if (options.specularGlossiness) { + return createSpecularGlossinessMaterial(material, options); + } else if (options.metallicRoughness) { + return createMetallicRoughnessMaterial(material, options); + } else if (options.materialsCommon) { + return createMaterialsCommonMaterial(material); + } + + // No material type specified, convert the material to metallic roughness + convertTraditionalToMetallicRoughness(material); + return createMetallicRoughnessMaterial(material, options); +} + +function convertMaterials(materials, options) { + return materials.map(function(material) { + return convertMaterial(material, options); + }); +} + +function resizeChannel(sourcePixels, sourceWidth, sourceHeight, targetPixels, targetWidth, targetHeight) { + // Nearest neighbor sampling + var widthRatio = sourceWidth / targetWidth; + var heightRatio = sourceHeight / targetHeight; + + for (var y = 0; y < targetHeight; ++y) { + for (var x = 0; x < targetWidth; ++x) { + var targetIndex = y * targetWidth + x; + var sourceY = Math.round(y * heightRatio); + var sourceX = Math.round(x * widthRatio); + var sourceIndex = sourceY * sourceWidth + sourceX; + var sourceValue = sourcePixels.readUInt8(sourceIndex); + targetPixels.writeUInt8(sourceValue, targetIndex); + } + } + return targetPixels; +} + +var scratchResizeChannel; + +function getTextureChannel(texture, index, targetWidth, targetHeight, targetChannel) { + var pixels = texture.pixels; // RGBA + var sourceWidth = texture.width; + var sourceHeight = texture.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); + sourceChannel.writeUInt8(value, i); + } + + if (sourcePixelsLength > targetPixelsLength) { + resizeChannel(sourceChannel, sourceWidth, sourceHeight, targetChannel, targetWidth, targetHeight); + } + + return targetChannel; +} + +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); + } +} + +function getMinimumDimensions(textures, options) { + var i; + var texture; + var width = Number.POSITIVE_INFINITY; + var height = Number.POSITIVE_INFINITY; + + var length = textures.length; + for (i = 0; i < length; ++i) { + texture = textures[i]; + width = Math.min(texture.width, width); + height = Math.min(texture.height, height); + } + + for (i = 0; i < length; ++i) { + texture = textures[i]; + if (texture.width !== width || texture.height !== height) { + options.logger('Texture ' + texture.path + ' will be scaled from ' + texture.width + 'x' + texture.height + ' to ' + width + 'x' + height + '.'); + } + } + + return [width, height]; +} + +function createMetallicRoughnessTexture(metallicTexture, roughnessTexture, occlusionTexture, options) { + if (defined(options.overridingTextures.metallicRoughnessOcclusionTexture)) { + return metallicTexture; + } + + var packMetallic = defined(metallicTexture); + var packRoughness = defined(roughnessTexture); + var packOcclusion = defined(occlusionTexture) && options.packOcclusion; + + if (!packMetallic && !packRoughness) { + return undefined; + } + + if (packMetallic && !defined(metallicTexture.pixels)) { + options.logger('Could not get decoded texture data for ' + metallicTexture.path + '. The material will be created without a metallicRoughness texture.'); + return undefined; + } + + if (packRoughness && !defined(roughnessTexture.pixels)) { + options.logger('Could not get decoded texture data for ' + roughnessTexture.path + '. The material will be created without a metallicRoughness texture.'); + return undefined; + } + + if (packOcclusion && !defined(occlusionTexture.pixels)) { + options.logger('Could not get decoded texture data for ' + occlusionTexture.path + '. The occlusion texture will not be packed in the metallicRoughness texture.'); + return undefined; + } + + var packedTextures = [metallicTexture, roughnessTexture, occlusionTexture].filter(function(texture) { + return defined(texture) && defined(texture.pixels); + }); + + var dimensions = getMinimumDimensions(packedTextures, 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 + var scratchChannel = Buffer.alloc(pixelsLength); + + if (packMetallic) { + // Write into the B channel + var metallicChannel = getTextureChannel(metallicTexture, 0, width, height, scratchChannel); + writeChannel(pixels, metallicChannel, 2); + } + + if (packRoughness) { + // Write into the G channel + var roughnessChannel = getTextureChannel(roughnessTexture, 0, width, height, scratchChannel); + writeChannel(pixels, roughnessChannel, 1); + } + + if (packOcclusion) { + // Write into the R channel + var occlusionChannel = getTextureChannel(occlusionTexture, 0, width, height, scratchChannel); + writeChannel(pixels, occlusionChannel, 0); + } + + var length = packedTextures.length; + var names = new Array(length); + for (var i = 0; i < length; ++i) { + names[i] = packedTextures[i].name; + } + var name = names.join('_'); + + var texture = new Texture(); + texture.name = name; + texture.extension = '.png'; + texture.pixels = pixels; + texture.width = width; + texture.height = height; + + return texture; +} + +function createSpecularGlossinessTexture(specularTexture, glossinessTexture, options) { + if (defined(options.overridingTextures.specularGlossinessTexture)) { + return specularTexture; + } + + var packSpecular = defined(specularTexture); + var packGlossiness = defined(glossinessTexture); + + if (!packSpecular && !packGlossiness) { + return undefined; + } + + if (packSpecular && !defined(specularTexture.pixels)) { + options.logger('Could not get decoded texture data for ' + specularTexture.path + '. The material will be created without a specularGlossiness texture.'); + return undefined; + } + + if (packGlossiness && !defined(glossinessTexture.pixels)) { + options.logger('Could not get decoded texture data for ' + glossinessTexture.path + '. The material will be created without a specularGlossiness texture.'); + return undefined; + } + + var packedTextures = [specularTexture, glossinessTexture].filter(function(texture) { + return defined(texture) && defined(texture.pixels); + }); + + var dimensions = getMinimumDimensions(packedTextures, 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 + var scratchChannel = Buffer.alloc(pixelsLength); + + if (packSpecular) { + // Write into the R, G, B channels + var redChannel = getTextureChannel(specularTexture, 0, width, height, scratchChannel); + writeChannel(pixels, redChannel, 0); + var greenChannel = getTextureChannel(specularTexture, 1, width, height, scratchChannel); + writeChannel(pixels, greenChannel, 1); + var blueChannel = getTextureChannel(specularTexture, 2, width, height, scratchChannel); + writeChannel(pixels, blueChannel, 2); + } + + if (packGlossiness) { + // Write into the A channel + var glossinessChannel = getTextureChannel(glossinessTexture, 0, width, height, scratchChannel); + writeChannel(pixels, glossinessChannel, 3); + } + + var length = packedTextures.length; + var names = new Array(length); + for (var i = 0; i < length; ++i) { + names[i] = packedTextures[i].name; + } + var name = names.join('_'); + + var texture = new Texture(); + texture.name = name; + texture.extension = '.png'; + texture.pixels = pixels; + texture.width = width; + texture.height = height; + + return texture; +} + +function createSpecularGlossinessMaterial(material, options) { + var emissiveTexture = material.emissiveTexture; + var normalTexture = material.normalTexture; + var occlusionTexture = material.ambientTexture; + var diffuseTexture = material.diffuseTexture; + var specularTexture = material.specularTexture; + var glossinessTexture = material.specularShininessTexture; + var specularGlossinessTexture = createSpecularGlossinessTexture(specularTexture, glossinessTexture, options); + + var emissiveFactor = material.emissiveColor.slice(0, 3); + var diffuseFactor = material.diffuseColor; + var specularFactor = material.specularColor.slice(0, 3); + 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(specularTexture)) { + specularFactor = [1.0, 1.0, 1.0]; + } + + if (defined(glossinessTexture)) { + glossinessFactor = 1.0; + } + + var alpha = material.alpha; + diffuseFactor[3] = alpha; + + var transparent = alpha < 1.0; + if (defined(diffuseTexture)) { + transparent = transparent || diffuseTexture.transparent; + } + + var doubleSided = transparent; + var alphaMode = transparent ? 'BLEND' : 'OPAQUE'; + + return { + name : material.name, + 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 + }; +} + +function createMetallicRoughnessMaterial(material, options) { + var emissiveTexture = material.emissiveTexture; + var normalTexture = material.normalTexture; + var occlusionTexture = material.ambientTexture; + var baseColorTexture = material.diffuseTexture; + var metallicTexture = material.specularTexture; + var roughnessTexture = material.specularShininessTexture; + var metallicRoughnessTexture = createMetallicRoughnessTexture(metallicTexture, roughnessTexture, occlusionTexture, options); + + if (options.packOcclusion) { + occlusionTexture = metallicRoughnessTexture; + } + + var emissiveFactor = material.emissiveColor.slice(0, 3); + 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(baseColorTexture)) { + baseColorFactor = [1.0, 1.0, 1.0, 1.0]; + } + + if (defined(metallicTexture)) { + metallicFactor = 1.0; + } + + if (defined(roughnessTexture)) { + roughnessFactor = 1.0; + } + + var alpha = material.alpha; + baseColorFactor[3] = alpha; + + var transparent = alpha < 1.0; + if (defined(baseColorTexture)) { + transparent = transparent || baseColorTexture.transparent; + } + + var doubleSided = transparent; + var alphaMode = transparent ? 'BLEND' : 'OPAQUE'; + + return { + name : material.name, + pbrMetallicRoughness : { + baseColorTexture : baseColorTexture, + metallicRoughnessTexture : metallicRoughnessTexture, + baseColorFactor : baseColorFactor, + metallicFactor : metallicFactor, + roughnessFactor : roughnessFactor + }, + emissiveTexture : emissiveTexture, + normalTexture : normalTexture, + occlusionTexture : occlusionTexture, + emissiveFactor : emissiveFactor, + alphaMode : alphaMode, + doubleSided : doubleSided + }; +} + +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 + // Textures are not converted for now + var specularIntensity = luminance(material.specularColor); + + // Transform from 0-1000 range to 0-1 range. Then invert. + var roughnessFactor = material.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 *= (1.0 - specularIntensity); + } + + var metallicFactor = 0.0; + + material.specularColor = [metallicFactor, metallicFactor, metallicFactor, 1.0]; + material.specularShininess = roughnessFactor; +} + +function createMaterialsCommonMaterial(material) { + var ambient = defaultValue(material.ambientTexture, material.ambientColor); + var diffuse = defaultValue(material.diffuseTexture, material.diffuseColor); + var emission = defaultValue(material.emissiveTexture, material.emissiveColor); + var specular = defaultValue(material.specularTexture, material.specularColor); + + var alpha = material.alpha; + var shininess = material.specularShininess; + var hasSpecular = (shininess > 0.0) && (defined(material.specularTexture) || (specular[0] > 0.0 || specular[1] > 0.0 || specular[2] > 0.0)); + + var transparent; + var transparency = 1.0; + if (defined(material.diffuseTexture)) { + transparency = alpha; + transparent = material.diffuseTexture.transparent || (transparency < 1.0); + } else { + diffuse[3] = alpha; + transparent = alpha < 1.0; + } + + if (!defined(material.ambientTexture)) { + // If ambient color is [1, 1, 1] assume it is a multiplier and instead change to [0, 0, 0] + if (ambient[0] === 1.0 && ambient[1] === 1.0 && ambient[2] === 1.0) { + ambient = [0.0, 0.0, 0.0, 1.0]; + } + } + + var doubleSided = transparent; + + var technique = hasSpecular ? 'PHONG' : 'LAMBERT'; + + return { + name : material.name, + extensions : { + KHR_materials_common : { + technique : technique, + transparent : transparent, + doubleSided : doubleSided, + values : { + ambient : ambient, + diffuse : diffuse, + emission : emission, + specular : specular, + shininess : shininess, + transparency : transparency, + transparent : transparent, + doubleSided : doubleSided + } + } + } + }; +} diff --git a/lib/loadObj.js b/lib/loadObj.js index b7d9441d..c1239677 100644 --- a/lib/loadObj.js +++ b/lib/loadObj.js @@ -4,16 +4,13 @@ var path = require('path'); var Promise = require('bluebird'); var ArrayStorage = require('./ArrayStorage'); -var loadImage = require('./loadImage'); var loadMtl = require('./loadMtl'); +var outsideDirectory = require('./outsideDirectory'); var readLines = require('./readLines'); -var Axis = Cesium.Axis; -var Cartesian3 = Cesium.Cartesian3; var ComponentDatatype = Cesium.ComponentDatatype; var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; -var Matrix4 = Cesium.Matrix4; var RuntimeError = Cesium.RuntimeError; module.exports = loadObj; @@ -49,26 +46,16 @@ var facePattern2 = /f( +(-?\d+)\/(-?\d+)\/?)( +(-?\d+)\/(-?\d+)\/?)( +(-?\d+)\/( var facePattern3 = /f( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))?/; // f vertex/uv/normal vertex/uv/normal vertex/uv/normal ... var facePattern4 = /f( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))?/; // f vertex//normal vertex//normal vertex//normal ... -var scratchCartesian = new Cartesian3(); - /** * Parse an obj file. * * @param {String} objPath Path to the obj file. - * @param {Object} options An object with the following properties: - * @param {Boolean} options.checkTransparency Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. - * @param {Boolean} options.secure Prevent the converter from reading image or mtl files outside of the input obj directory. - * @param {String} options.inputUpAxis Up axis of the obj. - * @param {String} options.outputUpAxis Up axis of the converted glTF. - * @param {Boolean} options.logger A callback function for handling logged messages. Defaults to console.log. - * @returns {Promise} A promise resolving to the obj data. - * @exception {RuntimeError} The file does not have any geometry information in it. + * @param {Object} options The options object passed along from lib/obj2gltf.js + * @returns {Promise} A promise resolving to the obj data, which includes an array of nodes containing geometry information and an array of materials. * * @private */ function loadObj(objPath, options) { - var axisTransform = getAxisTransform(options.inputUpAxis, options.outputUpAxis); - // Global store of vertex attributes listed in the obj file var positions = new ArrayStorage(ComponentDatatype.FLOAT); var normals = new ArrayStorage(ComponentDatatype.FLOAT); @@ -232,30 +219,16 @@ function loadObj(objPath, options) { var paths = line.substring(7).trim().split(' '); mtlPaths = mtlPaths.concat(paths); } else if ((result = vertexPattern.exec(line)) !== null) { - var position = scratchCartesian; - position.x = parseFloat(result[1]); - position.y = parseFloat(result[2]); - position.z = parseFloat(result[3]); - if (defined(axisTransform)) { - Matrix4.multiplyByPoint(axisTransform, position, position); - } - positions.push(position.x); - positions.push(position.y); - positions.push(position.z); + positions.push(parseFloat(result[1])); + positions.push(parseFloat(result[2])); + positions.push(parseFloat(result[3])); } else if ((result = normalPattern.exec(line) ) !== null) { - var normal = scratchCartesian; - normal.x = parseFloat(result[1]); - normal.y = parseFloat(result[2]); - normal.z = parseFloat(result[3]); - if (defined(axisTransform)) { - Matrix4.multiplyByPointAsVector(axisTransform, normal, normal); - } - normals.push(normal.x); - normals.push(normal.y); - normals.push(normal.z); + normals.push(parseFloat(result[1])); + normals.push(parseFloat(result[2])); + normals.push(parseFloat(result[3])); } else if ((result = uvPattern.exec(line)) !== null) { uvs.push(parseFloat(result[1])); - uvs.push(1.0 - parseFloat(result[2])); // Flip y so 0.0 is the bottom of the image + uvs.push(1.0 - parseFloat(result[2])); // Flip y so 0.0 is the bottom of the texture } else if ((result = facePattern1.exec(line)) !== null) { addFace( result[1], result[1], undefined, undefined, @@ -298,7 +271,7 @@ function loadObj(objPath, options) { normals = undefined; uvs = undefined; - // Load materials and images + // Load materials and textures return finishLoading(nodes, mtlPaths, objPath, options); }); } @@ -306,35 +279,26 @@ function loadObj(objPath, options) { function finishLoading(nodes, mtlPaths, objPath, options) { nodes = cleanNodes(nodes); if (nodes.length === 0) { - return Promise.reject(new RuntimeError(objPath + ' does not have any geometry data')); + throw new RuntimeError(objPath + ' does not have any geometry data'); } - return loadMaterials(mtlPaths, objPath, options) + var name = path.basename(objPath, path.extname(objPath)); + return loadMtls(mtlPaths, objPath, options) .then(function(materials) { - var imagesOptions = getImagesOptions(materials, options); - return loadImages(imagesOptions, objPath, options) - .then(function(images) { - return { - nodes : nodes, - materials : materials, - images : images - }; - }); + return { + nodes : nodes, + materials : materials, + name : name + }; }); } -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; +function loadMtls(mtlPaths, objPath, options) { var objDirectory = path.dirname(objPath); var materials = []; return Promise.map(mtlPaths, function(mtlPath) { 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.'); + if (options.secure && outsideDirectory(mtlPath, objDirectory)) { + 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.'); return; } return loadMtl(mtlPath, options) @@ -342,7 +306,7 @@ function loadMaterials(mtlPaths, objPath, options) { materials = materials.concat(materialsInMtl); }) .catch(function() { - logger('Could not read mtl file at ' + mtlPath + '. Using default material instead.'); + options.logger('Could not read mtl file at ' + mtlPath + '. Using default material instead.'); }); }, {concurrency : 10}) .then(function() { @@ -350,69 +314,6 @@ function loadMaterials(mtlPaths, objPath, options) { }); } -function loadImages(imagesOptions, objPath, options) { - var secure = options.secure; - var logger = options.logger; - var images = []; - return Promise.map(imagesOptions, function(imageOptions) { - var imagePath = imageOptions.imagePath; - 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, imageOptions) - .then(function(image) { - images.push(image); - }) - .catch(function() { - logger('Could not read image file at ' + imagePath + '. Material will ignore this image.'); - }); - }, {concurrency : 10}) - .thenReturn(images); -} - -function getImagesOptions(materials, options) { - var imagesOptions = []; - 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; -} - function removeEmptyMeshes(meshes) { return meshes.filter(function(mesh) { // Remove empty primitives @@ -492,19 +393,3 @@ function cleanNodes(nodes) { setDefaults(nodes); return nodes; } - -function getAxisTransform(inputUpAxis, outputUpAxis) { - if (inputUpAxis === 'X' && outputUpAxis === 'Y') { - return Axis.X_UP_TO_Y_UP; - } else if (inputUpAxis === 'X' && outputUpAxis === 'Z') { - return Axis.X_UP_TO_Z_UP; - } else if (inputUpAxis === 'Y' && outputUpAxis === 'X') { - return Axis.Y_UP_TO_X_UP; - } else if (inputUpAxis === 'Y' && outputUpAxis === 'Z') { - return Axis.Y_UP_TO_Z_UP; - } else if (inputUpAxis === 'Z' && outputUpAxis === 'X') { - return Axis.Z_UP_TO_X_UP; - } else if (inputUpAxis === 'Z' && outputUpAxis === 'Y') { - return Axis.Z_UP_TO_Y_UP; - } -} diff --git a/lib/loadImage.js b/lib/loadTexture.js similarity index 53% rename from lib/loadImage.js rename to lib/loadTexture.js index 73dfd99d..13b74648 100644 --- a/lib/loadImage.js +++ b/lib/loadTexture.js @@ -5,55 +5,56 @@ var jpeg = require('jpeg-js'); var path = require('path'); var PNG = require('pngjs').PNG; var Promise = require('bluebird'); -var Image = require('./Image'); +var Texture = require('./Texture'); var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; -module.exports = loadImage; +module.exports = loadTexture; /** - * Load an image file. + * Load a texture file. * - * @param {String} imagePath Path to the image file. - * @param {Object} options An object with the following properties: + * @param {String} texturePath Path to the texture file. + * @param {Object} [options] An object with the following properties: * @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.decode=false] Decode image. - * @returns {Promise} A promise resolving to an Image object. + * @param {Boolean} [options.decode=false] Whether to decode the texture. + * @returns {Promise} A promise resolving to a Texture object. * * @private */ -function loadImage(imagePath, options) { +function loadTexture(texturePath, options) { options = defaultValue(options, {}); options.checkTransparency = defaultValue(options.checkTransparency, false); options.decode = defaultValue(options.decode, false); - return fsExtra.readFile(imagePath) - .then(function(data) { - var extension = path.extname(imagePath).toLowerCase(); - var image = new Image(); - image.source = data; - image.extension = extension; - image.path = imagePath; + return fsExtra.readFile(texturePath) + .then(function(source) { + var name = path.basename(texturePath, path.extname(texturePath)); + var extension = path.extname(texturePath).toLowerCase(); + var texture = new Texture(); + texture.source = source; + texture.name = name; + texture.extension = extension; + texture.path = texturePath; var decodePromise; if (extension === '.png') { - decodePromise = decodePng(image, options); + decodePromise = decodePng(texture, options); } else if (extension === '.jpg' || extension === '.jpeg') { - decodePromise = decodeJpeg(image, options); + decodePromise = decodeJpeg(texture, options); } if (defined(decodePromise)) { - return decodePromise.thenReturn(image); + return decodePromise.thenReturn(texture); } - return image; + return texture; }); } -function hasTransparency(image) { - var pixels = image.decoded; - var pixelsLength = image.width * image.height; +function hasTransparency(pixels) { + var pixelsLength = pixels.length / 4; for (var i = 0; i < pixelsLength; ++i) { if (pixels[i * 4 + 3] < 255) { return true; @@ -89,9 +90,9 @@ function parsePng(data) { }); } -function decodePng(image, options) { +function decodePng(texture, options) { // Color type is encoded in the 25th bit of the png - var source = image.source; + var source = texture.source; var colorType = source[25]; var channels = getChannels(colorType); @@ -101,22 +102,26 @@ function decodePng(image, options) { if (decode) { return parsePng(source) .then(function(decodedResults) { - image.decoded = decodedResults.data; - image.width = decodedResults.width; - image.height = decodedResults.height; - if (checkTransparency) { - image.transparent = hasTransparency(image); + if (options.checkTransparency) { + texture.transparent = hasTransparency(decodedResults.data); + } + if (options.decode) { + texture.pixels = decodedResults.data; + texture.width = decodedResults.width; + texture.height = decodedResults.height; + texture.source = undefined; // Unload resources } }); } } -function decodeJpeg(image, options) { +function decodeJpeg(texture, options) { if (options.decode) { - var source = image.source; + var source = texture.source; var decodedResults = jpeg.decode(source); - image.decoded = decodedResults.data; - image.width = decodedResults.width; - image.height = decodedResults.height; + texture.pixels = decodedResults.data; + texture.width = decodedResults.width; + texture.height = decodedResults.height; + texture.source = undefined; // Unload resources } } diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index 0bc8f0d2..3dcd0928 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -2,12 +2,9 @@ var Cesium = require('cesium'); var fsExtra = require('fs-extra'); var path = require('path'); -var Promise = require('bluebird'); var createGltf = require('./createGltf'); -var gltfToGlb = require('./gltfToGlb'); -var loadImage = require('./loadImage'); var loadObj = require('./loadObj'); -var writeUris = require('./writeUris'); +var writeGltf = require('./writeGltf'); var defaultValue = Cesium.defaultValue; var defined = Cesium.defined; @@ -16,150 +13,98 @@ var DeveloperError = Cesium.DeveloperError; module.exports = obj2gltf; /** - * Converts an obj file to a glTF file. + * Converts an obj file to a glTF or glb. * * @param {String} objPath Path to the obj file. - * @param {String} gltfPath Path of the converted glTF file. * @param {Object} [options] An object with the following properties: - * @param {Boolean} [options.binary=false] Save as binary glTF. - * @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.binary=false] Convert to binary glTF. + * @param {Boolean} [options.separate=false] Write out separate buffer files and textures instead of embedding them in the glTF. * @param {Boolean} [options.separateTextures=false] Write out separate textures only. * @param {Boolean} [options.checkTransparency=false] Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. - * @param {Boolean} [options.secure=false] Prevent the converter from reading image or mtl files outside of the input obj directory. - * @param {String} [options.inputUpAxis='Y'] Up axis of the obj. Choices are 'X', 'Y', and 'Z'. - * @param {String} [options.outputUpAxis='Y'] Up axis of the converted glTF. Choices are 'X', 'Y', and 'Z'. - * @param {Boolean} [options.packOcclusion=false] Pack the occlusion texture in the red channel of metallic-roughness texture. + * @param {Boolean} [options.secure=false] Prevent the converter from reading textures or mtl files outside of the input obj directory. + * @param {Boolean} [options.packOcclusion=false] Pack the occlusion texture in the red channel of the metallic-roughness texture. * @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 {Object} [options.overridingImages] An object containing image paths that override material values defined in the .mtl file. This is often convenient in workflows where the .mtl does not exist or is not set up to use PBR materials. Intended for models with a single material. - * @param {String} [options.overridingImages.metallicRoughnessOcclusionTexture] Path to the metallic-roughness-occlusion texture, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. The model will be saved with a pbrMetallicRoughness material. - * @param {String} [options.overridingImages.specularGlossinessTexture] Path to the specular-glossiness texture, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension. - * @param {String} [options.overridingImages.occlusionTexture] Path to the occlusion texture. Ignored if metallicRoughnessOcclusionTexture is also set. - * @param {String} [options.overridingImages.normalTexture] Path to the normal texture. - * @param {String} [options.overridingImages.baseColorTexture] Path to the baseColor/diffuse texture. - * @param {String} [options.overridingImages.emissiveTexture] Path to the emissive texture. - + * @param {Object} [options.overridingTextures] An object containing texture paths that override textures defined in the .mtl file. This is often convenient in workflows where the .mtl does not exist or is not set up to use PBR materials. Intended for models with a single material. + * @param {String} [options.overridingTextures.metallicRoughnessOcclusionTexture] Path to the metallic-roughness-occlusion texture, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. The model will be saved with a pbrMetallicRoughness material. + * @param {String} [options.overridingTextures.specularGlossinessTexture] Path to the specular-glossiness texture, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension. + * @param {String} [options.overridingTextures.occlusionTexture] Path to the occlusion texture. Ignored if metallicRoughnessOcclusionTexture is also set. + * @param {String} [options.overridingTextures.normalTexture] Path to the normal texture. + * @param {String} [options.overridingTextures.baseColorTexture] Path to the baseColor/diffuse texture. + * @param {String} [options.overridingTextures.emissiveTexture] Path to the emissive texture. * @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. + * @param {Writer} [options.writer] A callback function that writes files that are saved as separate resources. + * @param {String} [options.outputDirectory] Output directory for writing separate resources when options.writer is not defined. + * @return {Promise} A promise that resolves to the glTF JSON or glb buffer. */ -function obj2gltf(objPath, gltfPath, options) { +function obj2gltf(objPath, options) { var defaults = obj2gltf.defaults; - options = defaultValue(options, {}); - var binary = defaultValue(options.binary, defaults.binary); - var separate = defaultValue(options.separate, defaults.separate); - var separateTextures = defaultValue(options.separateTextures, defaults.separateTextures) || separate; - var checkTransparency = defaultValue(options.checkTransparency, defaults.checkTransparency); - var secure = defaultValue(options.secure, defaults.secure); - var inputUpAxis = defaultValue(options.inputUpAxis, defaults.inputUpAxis); - var outputUpAxis = defaultValue(options.outputUpAxis, defaults.outputUpAxis); - var packOcclusion = defaultValue(options.packOcclusion, defaults.packOcclusion); - var metallicRoughness = defaultValue(options.metallicRoughness, defaults.metallicRoughness); - var specularGlossiness = defaultValue(options.specularGlossiness, defaults.specularGlossiness); - var materialsCommon = defaultValue(options.materialsCommon, defaults.materialsCommon); - var overridingImages = defaultValue(options.overridingImages, defaultValue.EMPTY_OBJECT); - var logger = defaultValue(options.logger, defaults.logger); - - options.separate = separate; - options.separateTextures = separateTextures; - options.checkTransparency = checkTransparency; - options.secure = secure; - options.inputUpAxis = inputUpAxis; - options.outputUpAxis = outputUpAxis; - options.packOcclusion = packOcclusion; - options.metallicRoughness = metallicRoughness; - options.specularGlossiness = specularGlossiness; - options.materialsCommon = materialsCommon; - options.overridingImages = overridingImages; - options.logger = logger; + options.binary = defaultValue(options.binary, defaults.binary); + options.separate = defaultValue(options.separate, defaults.separate); + options.separateTextures = defaultValue(options.separateTextures, defaults.separateTextures) || options.separate; + options.checkTransparency = defaultValue(options.checkTransparency, defaults.checkTransparency); + options.secure = defaultValue(options.secure, defaults.secure); + options.packOcclusion = defaultValue(options.packOcclusion, defaults.packOcclusion); + options.metallicRoughness = defaultValue(options.metallicRoughness, defaults.metallicRoughness); + options.specularGlossiness = defaultValue(options.specularGlossiness, defaults.specularGlossiness); + options.materialsCommon = defaultValue(options.materialsCommon, defaults.materialsCommon); + options.overridingTextures = defaultValue(options.overridingTextures, defaultValue.EMPTY_OBJECT); + options.logger = defaultValue(options.logger, getDefaultLogger()); + options.writer = defaultValue(options.writer, getDefaultWriter(options.outputDirectory)); if (!defined(objPath)) { throw new DeveloperError('objPath is required'); } - if (!defined(gltfPath)) { - throw new DeveloperError('gltfPath is required'); + if (options.separateTextures && !defined(options.writer)) { + throw new DeveloperError('Either options.writer or options.outputDirectory must be defined when writing separate resources.'); } - if (metallicRoughness + specularGlossiness + materialsCommon > 1) { + if (options.metallicRoughness + options.specularGlossiness + options.materialsCommon > 1) { throw new DeveloperError('Only one material type may be set from [metallicRoughness, specularGlossiness, materialsCommon].'); } - if (defined(overridingImages.metallicRoughnessOcclusionTexture) && defined(overridingImages.specularGlossinessTexture)) { + if (defined(options.overridingTextures.metallicRoughnessOcclusionTexture) && defined(options.overridingTextures.specularGlossinessTexture)) { throw new DeveloperError('metallicRoughnessOcclusionTexture and specularGlossinessTexture cannot both be defined.'); } - if (defined(overridingImages.metallicRoughnessOcclusionTexture)) { + if (defined(options.overridingTextures.metallicRoughnessOcclusionTexture)) { options.metallicRoughness = true; options.specularGlossiness = false; options.materialsCommon = false; + options.packOcclusion = true; } - if (defined(overridingImages.specularGlossinessTexture)) { + if (defined(options.overridingTextures.specularGlossinessTexture)) { options.metallicRoughness = false; options.specularGlossiness = true; options.materialsCommon = false; } - var extension = path.extname(gltfPath).toLowerCase(); - var modelName = path.basename(gltfPath, path.extname(gltfPath)); - if (binary || extension === '.glb') { - binary = true; - extension = '.glb'; - } - - gltfPath = path.join(path.dirname(gltfPath), modelName + extension); - - return loadOverridingImages(options) - .then(function() { - return loadObj(objPath, options); - }) + return loadObj(objPath, options) .then(function(objData) { return createGltf(objData, options); }) .then(function(gltf) { - return writeUris(gltf, gltfPath, options); - }) - .then(function(gltf) { - if (binary) { - var glb = gltfToGlb(gltf); - return fsExtra.outputFile(gltfPath, glb); - } - var jsonOptions = { - spaces : 2 - }; - return fsExtra.outputJson(gltfPath, gltf, jsonOptions); + return writeGltf(gltf, options); }); } -function loadOverridingImages(options) { - var overridingImages = options.overridingImages; - var promises = []; - for (var imageName in overridingImages) { - if (overridingImages.hasOwnProperty(imageName)) { - promises.push(loadOverridingImage(imageName, overridingImages, options)); - } - } - return Promise.all(promises); +function getDefaultLogger() { + return function(message) { + console.log(message); + }; } -function loadOverridingImage(imageName, overridingImages, options) { - var imagePath = overridingImages[imageName]; - var imageOptions; - if (imageName === 'baseColorTexture') { - imageOptions = { - checkTransparency : options.checkTransparency +function getDefaultWriter(outputDirectory) { + if (defined(outputDirectory)) { + return function(file, data) { + var outputFile = path.join(outputDirectory, file); + return fsExtra.outputFile(outputFile, data); }; } - return loadImage(imagePath, imageOptions) - .then(function(image) { - overridingImages[imageName] = image; - }) - .catch(function() { - delete overridingImages[imageName]; - options.logger('Could not read image file at ' + imagePath + '. This image will be ignored.'); - }); } /** @@ -167,79 +112,60 @@ function loadOverridingImage(imageName, overridingImages, options) { */ obj2gltf.defaults = { /** - * Gets or sets whether the model will be saved as binary glTF. + * Gets or sets whether the converter will return a glb. * @type Boolean * @default false */ - binary: false, + binary : false, /** - * Gets or sets whether to write out separate geometry/animation data files, + * Gets or sets whether to write out separate buffer and texture, * shader files, and textures instead of embedding them in the glTF. * @type Boolean * @default false */ - separate: false, + separate : false, /** * Gets or sets whether to write out separate textures only. * @type Boolean * @default false */ - separateTextures: false, + separateTextures : 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, + checkTransparency : false, /** * Gets or sets whether the source model can reference paths outside of its directory. * @type Boolean * @default false */ - secure: false, - /** - * Gets or sets the up axis of the obj. - * @type String - * @default 'Y' - */ - inputUpAxis: 'Y', - /** - * Gets or sets the up axis of the converted glTF. - * @type String - * @default 'Y' - */ - outputUpAxis: 'Y', + 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, + 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. + * Gets or sets whether rhe 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 */ - metallicRoughness: false, + metallicRoughness : 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. + * Gets or sets whether 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 */ - specularGlossiness: false, + 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. + * Gets or sets whether the glTF will be saved with the KHR_materials_common extension. * @type Boolean * @default false */ - materialsCommon: false, - - /** - * @private - */ - logger: function(message) { - console.log(message); - } + materialsCommon : false }; /** @@ -248,3 +174,12 @@ obj2gltf.defaults = { * * @param {String} message The message to log. */ + +/** + * A callback function that writes files that are saved as separate resources. + * @callback Writer + * + * @param {String} file The relative path of the file. + * @param {Buffer} data The file data to write. + * @returns {Promise} A promise that resolves when the file is written. + */ diff --git a/lib/outsideDirectory.js b/lib/outsideDirectory.js new file mode 100644 index 00000000..df2c8a1a --- /dev/null +++ b/lib/outsideDirectory.js @@ -0,0 +1,15 @@ +'use strict'; +var path = require('path'); + +module.exports = outsideDirectory; + +/** + * Checks if a file is outside of a directory. + * + * @param {String} file Path to the file. + * @param {String} directory Path to the directory. + * @returns {Boolean} Whether the file is outside of the directory. + */ +function outsideDirectory(file, directory) { + return (path.relative(directory, file).indexOf('..') === 0); +} diff --git a/lib/readLines.js b/lib/readLines.js index 073b85ce..185cead0 100644 --- a/lib/readLines.js +++ b/lib/readLines.js @@ -15,13 +15,13 @@ module.exports = readLines; * @private */ function readLines(path, callback) { - return new Promise(function (resolve, reject) { + return new Promise(function(resolve, reject) { var stream = fsExtra.createReadStream(path); stream.on('error', reject); stream.on('end', resolve); var lineReader = readline.createInterface({ - input: stream + input : stream }); lineReader.on('line', callback); }); diff --git a/lib/writeUris.js b/lib/writeGltf.js similarity index 51% rename from lib/writeUris.js rename to lib/writeGltf.js index ab171a76..36348895 100644 --- a/lib/writeUris.js +++ b/lib/writeGltf.js @@ -1,90 +1,73 @@ 'use strict'; 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'); +var gltfToGlb = require('./gltfToGlb'); var defined = Cesium.defined; var RuntimeError = Cesium.RuntimeError; -module.exports = writeUris; +module.exports = writeGltf; /** * Write glTF resources as embedded data uris or external files. * * @param {Object} gltf The glTF asset. - * @param {String} gltfPath Path where the glTF will be saved. - * @param {Object} options An object with the following properties: - * @param {Boolean} options.separate Writes out separate buffers. - * @param {Boolean} options.separateTextures Write out separate textures only. - * @returns {Promise} A promise that resolves to the glTF asset. + * @param {Object} options The options object passed along from lib/obj2gltf.js + * @returns {Promise} A promise that resolves to the glTF JSON or glb buffer. * * @private */ -function writeUris(gltf, gltfPath, options) { - return encodeImages(gltf) +function writeGltf(gltf, options) { + return encodeTextures(gltf) .then(function() { + var binary = options.binary; 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)); + promises.push(writeSeparateTextures(gltf, options)); } else { writeEmbeddedTextures(gltf); } if (separate) { - promises.push(writeSeparateBuffer(gltf, gltfPath, name)); - } else { + promises.push(writeSeparateBuffer(gltf, options)); + } else if (!binary) { writeEmbeddedBuffer(gltf); } + var binaryBuffer = gltf.buffers[0].extras._obj2gltf.source; + return Promise.all(promises) .then(function() { deleteExtras(gltf); - cleanup(gltf); + removeEmpty(gltf); + if (binary) { + return gltfToGlb(gltf, binaryBuffer); + } return gltf; }); }); } -function encodePng(image) { +function encodePng(texture) { // 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, + width : texture.width, + height : texture.height, + colorType : texture.transparent ? rgbaColorType : rgbColorType, inputColorType : rgbaColorType, inputHasAlpha : true }); - png.data = image.decoded; + png.data = texture.pixels; return new Promise(function(resolve, reject) { var chunks = []; @@ -99,24 +82,22 @@ function encodePng(image) { }); } -function encodeImage(image) { - var imageExtras = image.extras._obj2gltf; - if (!defined(imageExtras.source) && defined(imageExtras.decoded) && imageExtras.extension === '.png') { - return encodePng(imageExtras) +function encodeTexture(texture) { + if (!defined(texture.source) && defined(texture.pixels) && texture.extension === '.png') { + return encodePng(texture) .then(function(encoded) { - imageExtras.source = encoded; + texture.source = encoded; }); } } -function encodeImages(gltf) { - // Dynamically generated metallicRoughnessOcclusion and specularGlossiness - // textures need to be encoded to png's prior to being saved. +function encodeTextures(gltf) { + // Dynamically generated PBR textures need to be encoded to png 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])); + encodePromises.push(encodeTexture(images[i].extras._obj2gltf)); } return Promise.all(encodePromises); } @@ -130,12 +111,6 @@ function deleteExtras(gltf) { for (var i = 0; i < imagesLength; ++i) { delete images[i].extras; } - - var materials = gltf.materials; - var materialsLength = materials.length; - for (var j = 0; j < materialsLength; ++j) { - delete materials[j].extras; - } } function removeEmpty(json) { @@ -148,33 +123,33 @@ function removeEmpty(json) { }); } -function cleanup(gltf) { - removeEmpty(gltf); -} - -function writeSeparateBuffer(gltf, gltfPath, name) { +function writeSeparateBuffer(gltf, options) { var buffer = gltf.buffers[0]; var source = buffer.extras._obj2gltf.source; - var bufferUri = name + '.bin'; + var bufferUri = buffer.name + '.bin'; buffer.uri = bufferUri; - var bufferPath = path.join(path.dirname(gltfPath), bufferUri); - return fsExtra.outputFile(bufferPath, source); + return options.writer(bufferUri, source); } -function writeSeparateTextures(gltf, gltfPath) { +function writeSeparateTextures(gltf, options) { var images = gltf.images; return Promise.map(images, function(image) { - var extras = image.extras._obj2gltf; - var imageUri = image.name + extras.extension; + var texture = image.extras._obj2gltf; + var imageUri = image.name + texture.extension; image.uri = imageUri; - var imagePath = path.join(path.dirname(gltfPath), imageUri); - return fsExtra.outputFile(imagePath, extras.source); + return options.writer(imageUri, texture.source); }, {concurrency : 10}); } function writeEmbeddedBuffer(gltf) { var buffer = gltf.buffers[0]; var source = buffer.extras._obj2gltf.source; + + // Buffers larger than ~192MB cannot be base64 encoded due to a NodeJS limitation. Source: https://github.com/nodejs/node/issues/4266 + if (source.length > 201326580) { + throw new RuntimeError('Buffer is too large to embed in the glTF. Use the --separate flag instead.'); + } + buffer.uri = 'data:application/octet-stream;base64,' + source.toString('base64'); } @@ -189,19 +164,19 @@ function writeEmbeddedTextures(gltf) { for (var i = 0; i < imagesLength; ++i) { var image = images[i]; - var extras = image.extras._obj2gltf; - var imageSource = extras.source; - var imageByteLength = imageSource.length; + var texture = image.extras._obj2gltf; + var textureSource = texture.source; + var textureByteLength = textureSource.length; - image.mimeType = mime.lookup(extras.extension); + image.mimeType = mime.lookup(texture.extension); image.bufferView = gltf.bufferViews.length; gltf.bufferViews.push({ buffer : 0, byteOffset : byteOffset, - byteLength : imageByteLength + byteLength : textureByteLength }); - byteOffset += imageByteLength; - sources.push(imageSource); + byteOffset += textureByteLength; + sources.push(textureSource); } var source = getBufferPadded(Buffer.concat(sources)); diff --git a/package.json b/package.json index 2795d304..585c0e52 100644 --- a/package.json +++ b/package.json @@ -27,25 +27,25 @@ }, "dependencies": { "bluebird": "^3.5.0", - "cesium": "^1.35.2", - "fs-extra": "^4.0.0", + "cesium": "^1.36.0", + "fs-extra": "^4.0.1", "jpeg-js": "^0.3.3", "mime": "^1.3.6", - "pngjs": "^3.2.0", + "pngjs": "^3.3.0", "uuid": "^3.1.0", "yargs": "^8.0.2" }, "devDependencies": { "coveralls": "^2.13.1", - "eslint": "^4.2.0", + "eslint": "^4.4.1", "eslint-config-cesium": "^2.0.1", "gulp": "^3.9.1", - "jasmine": "^2.6.0", - "jasmine-spec-reporter": "^4.1.1", - "jsdoc": "^3.5.3", - "nyc": "^11.0.3", + "jasmine": "^2.7.0", + "jasmine-spec-reporter": "^4.2.0", + "jsdoc": "^3.5.4", + "nyc": "^11.1.0", "open": "^0.0.5", - "requirejs": "^2.3.3" + "requirejs": "^2.3.4" }, "scripts": { "jsdoc": "jsdoc ./lib -R ./README.md -d doc", diff --git a/specs/data/box-complex-material/alpha.png b/specs/data/box-complex-material/alpha.png index 8437c62e20eaafd489a2e1b2fda6784706d37591..d04eb82f52c40bc1af9a3a8f516c0de93da592d7 100644 GIT binary patch delta 2250 zcmV;*2sQV!4$l!GiBL{Q4GJ0x0000DNk~Le0002U0002U2mk;80Ejd3LXjaKe+UQ? z83bp?8@2!d2yRJ4K~#9!?cHl^Tjd!C@aNc$QHMc8Z-C z$M@K+?ROuLrg1Lzv3)M@gZ%&2f5=Y#%X2R8dwkyK02ES4A%zs8LKyjeb#bZLY&IE7 z^g4zl;<0Er6beP+s3291EzO3KGD~G;WqFzD`=5n_exJ`52*#FYrd>YzA;#5xt)^;Q z>ovkJ@mz9xqZ4N*V)R0~1~!!1I!$6;8X9$m?I$l%2+?o4v&PzH&gLzFe@VCdAEy#j zLALC(SX**=R$iIho_-xn*-D1Ly*k-Li&W^4-+d9t)wui!4;{wa|! zM^-6S73&{}ym`Dvk$rr?e?#QX+@_LjKUUTU@6}JdX_EbqWw}c;w zwK7(u_Tg9R71|MNcO)j4<@)8LkXgGN_2gshiML=2B%#P%+gUl29GS`=NMQks1 z6Na{mkaFwsNOmVg{jUdrItMVY>zBIh##%QVHvpB1+%S^Ord9LCYC0XDZ#)_oziH{V z-3+v5*MLU+rqwzH6sOK3wjzdyXm-G$PGm)xbt6=&F9g|L zhYHfPO9*lsvWn~wfLM%xD`qo4FpqSx`dCOW6<$PDmveQjQW*Vg8X+gbOj7 zaqc4~Hl(x^=TXbZhLj*Zj~LjHV!#gt*LwI5BQp!Ed>ci*pDelT%#C%w>cShm#BW_B(`-g#|7oiE(d_ zqX6NbR6crKiJ5B%VttTk2|H=Z?CKU~4esz~Y>mPyH#k0D(}pkW%x zf48E6HJ)@80oMr}L*V2IPx^|$#xp1&SNqOE+KRxJLnvk-A?aK$vGaKjFS+ zY&HFm^PDcz+=`Gv1Qp?aMxMp%x*_V<=6`iPWUdG#9-zJ;_k$P+oI6Un;}B0vAl|+R z9iwzZnt3kw@HPqyX*SX9*#8j`SrKTSf2O#DMZ(J0#hCwoGv^%?7vy%{Uf=wX8gAraCelcrz{=iMM+xa5#E0oj@|Le+hP|FL8B8A#VtHyB1V-ID0bRT`gus zlBZkU`-Y}_L_GgV)Y2-?1~q$Re~)eu^A)YQHDZf>>NS_P_ey%#zWHEWmGj{8J0d%I zihV-mmBYGDqta7G!gyYOLh>&)HTO*^{yfCd-^ua^8W>V|yE=WNOr`F6H>F@X5^u@( zjZ@_#i;78H_sjWcHeLTW3OVe(GOr%4e@=-RO6+O*$K1ue@;|(1YJ(yZe^bj|nH>)* z)qg^3lk<_s2UjS#ww~GKlZoN!Rr)gQJtQHyB0+wm!mzYxUrO46WIQ>jQo{!QbAhmA zj8Nc7EvOdF-r+?lhApXup*^5-w*PY?mM0@RerRjn`>X_%hR3d}&trTyKL6$G`QOVf z(NyWDgB4AkxxDd$!~2F^f0Co9WW&jgyK8M-SvBeMU2wV&o|UK}i=>m+HT~U|*7c&s z7yjYt!0>4aFV09k3pcb}+f-WD(w?T@`Q@=`Pk3fzM%qp+vRqCmw>H)rjO7*8)zy{d z-xr94{64SOyX0S9b~)WLv}KU-x`@8SWHK2I#YI|%B(KI|(P$*HAbbT1DWs4>3ITxs Y0`r_>be8EZx&QzG07*qoM6N<$g4muiSO5S3 delta 1835 zcmV+`2h{k_5wi{ zCofa#e4_vW2GU7HK~#9!?cHBYQ&$`Z@Y6yK6h^1kQt`@MB^}!Z90|m*m@ph8%Nimh z0Uq?lgqOy|bO{gJ3okw_Sxii}WvMK4F)=LTO;NJwrjVGhg@+9aPR9(aRA|v^3R=2u z_3W{K?zy-3w)fBZ-QMr3+MJKSe{)aIJ-zoFgp3$5V#J6M`eIV_!z*kKC&Msx4!g~| z3nk;RC!6b`P$;|^OJI1WDu(UoY!~OuD2aV#<8 zy>^PPO_wi%3rfW2GSMiiAHhXsq@FTR_0TFVI;%s~^oZPkEK~e$I)=Hfe*l&-fiBGl zLQ8)N%bZldMN3inAXX%U<(iG%9;{eA+KlaOL9AecZR(4h6IkI)IMo%Ed9e!dmZ?Z; zj$l<1X;y9g9>uC=OKa?xe`2nu_kKb&ru*l@ zP4#~xI`dkvP}8n82c@RXgUJHNi1W0PkmG!Tb^7oIqBowYLQJ(`M01>x0{Y$$RFedn z$8S2>Nw=xK_6Y9Ao#Z<9_Afz%Oq3`)Xka(JD zf^cgq@urttr_X_qf5;6b)~WFiC?@pgeUWwg8VrfpR8pOe{0_&2-kuj#r>_8!NR1`b ziCu(bLQP8|>hu{X5}~;!IBO81*CxzRrw{@S=G5txEodfGvz@0-AAuurnw(oF({n30 zQ@k1r)`^8jLRMC|@0~#ab4IZ50l60xzBkON(>owZInB;7f1QPCIxEoktS?}iP*t3_ zPVGQR*$vM&orY>U&D;0N{)1{lHb2d=4yukX#bfHf@~A)A0sA~;it z%)aLWOv-G0rs)`D(=oE?1Z2|*;`L;D3E70qm{6xzL6b6?pJ6%#+jNLzYJqKPA(=X0 zn>t9Q~?o3Ha{3GftT(iA53 zJ#-baDWmb3rn`_$8I9Nbvu+8%qxnLD-1pEoa7~#_&obSFYszf8?w`d@U4}}!oZ{96lPP*I^BRC0*G|Ab5v_$rpSpv1za+kZeda3N$G=B#U z=?<4vI(wkqzXFAHg+S8fo`U54%b)+#4TALPe~(CXopiZJX9C3Q{5?bx@AI&?0Q?v= zMcy8^wDu+rpQ4>~itmX-f&I&;>E0N7=3OM+q{ltkBOaPb9{%Tdh0Y9F-g}f<(w71& z#wB+aMUvWmsoI6!#1MW5B}nPCuUqiMx;$s_ti(Q`ko58Q#r^wBn7Ulb)8xG8bXk6- zf9(LM-;VxUq)CrwkD)EfwsLaiUC>^1L-lLAPn*P`} z$wrm07N}Rq`r^ z?dTNxIe2bnb}6LHYjKKpV^f9A;ba)5&SAG%ccElF_GEKC6bgkmV+o9m7%^hRh!NiW Z58b3RhlqILm;e9(07*qoM6N<$0fIY= 0).toBe(true); }), done).toResolve(); }); + + describe('metallicRoughness', function() { + it('creates default material', function() { + var material = loadMtl._createMaterial(undefined, options); + var pbr = material.pbrMetallicRoughness; + expect(pbr.baseColorTexture).toBeUndefined(); + expect(pbr.metallicRoughnessTexture).toBeUndefined(); + expect(pbr.baseColorFactor).toEqual([0.5, 0.5, 0.5, 1.0]); + expect(pbr.metallicFactor).toBe(0.0); // No metallic + expect(pbr.roughnessFactor).toBe(1.0); // Fully rough + expect(material.emissiveTexture).toBeUndefined(); + expect(material.normalTexture).toBeUndefined(); + expect(material.ambientTexture).toBeUndefined(); + expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]); + expect(material.alphaMode).toBe('OPAQUE'); + expect(material.doubleSided).toBe(false); + }); + + it('creates material with textures', function() { + options.metallicRoughness = true; + + var material = loadMtl._createMaterial({ + diffuseTexture : diffuseTexture, + ambientTexture : ambientTexture, + normalTexture : normalTexture, + emissiveTexture : emissiveTexture, + specularTexture : specularTexture, + specularShininessTexture : specularShininessTexture + }, options); + + var pbr = material.pbrMetallicRoughness; + expect(pbr.baseColorTexture).toBeDefined(); + expect(pbr.metallicRoughnessTexture).toBeDefined(); + expect(pbr.baseColorFactor).toEqual([1.0, 1.0, 1.0, 1.0]); + expect(pbr.metallicFactor).toBe(1.0); + expect(pbr.roughnessFactor).toBe(1.0); + expect(material.emissiveTexture).toBeDefined(); + expect(material.normalTexture).toBeDefined(); + expect(material.occlusionTexture).toBeDefined(); + expect(material.emissiveFactor).toEqual([1.0, 1.0, 1.0]); + expect(material.alphaMode).toBe('OPAQUE'); + expect(material.doubleSided).toBe(false); + }); + + it('packs occlusion in metallic roughness texture', function() { + options.metallicRoughness = true; + options.packOcclusion = true; + + var material = loadMtl._createMaterial({ + ambientTexture : alphaTexture, + specularTexture : specularTexture, + specularShininessTexture : specularShininessTexture + }, options); + + var pbr = material.pbrMetallicRoughness; + expect(pbr.metallicRoughnessTexture).toBeDefined(); + expect(pbr.metallicRoughnessTexture).toBe(material.occlusionTexture); + }); + + it('does not create metallic roughness texture if decoded texture data is not available', function() { + options.metallicRoughness = true; + options.packOcclusion = true; + + var material = loadMtl._createMaterial({ + ambientTexture : ambientTexture, // Is a .gif which can't be decoded + specularTexture : specularTexture, + specularShininessTexture : specularShininessTexture + }, options); + + var pbr = material.pbrMetallicRoughness; + expect(pbr.metallicRoughnessTexture).toBeUndefined(); + expect(material.occlusionTexture).toBeUndefined(); + }); + + it('sets material for transparent diffuse texture', function() { + options.metallicRoughness = true; + + var material = loadMtl._createMaterial({ + diffuseTexture : transparentDiffuseTexture + }, options); + expect(material.alphaMode).toBe('BLEND'); + expect(material.doubleSided).toBe(true); + }); + }); + + describe('specularGlossiness', function() { + it('creates default material', function() { + options.specularGlossiness = true; + var material = loadMtl._createMaterial(undefined, options); + var pbr = material.extensions.KHR_materials_pbrSpecularGlossiness; + expect(pbr.diffuseTexture).toBeUndefined(); + expect(pbr.specularGlossinessTexture).toBeUndefined(); + expect(pbr.diffuseFactor).toEqual([0.5, 0.5, 0.5, 1.0]); + expect(pbr.specularFactor).toEqual([0.0, 0.0, 0.0]); // No specular color + expect(pbr.glossinessFactor).toEqual(0.0); // Rough surface + expect(material.emissiveTexture).toBeUndefined(); + expect(material.normalTexture).toBeUndefined(); + expect(material.occlusionTexture).toBeUndefined(); + expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]); + expect(material.alphaMode).toBe('OPAQUE'); + expect(material.doubleSided).toBe(false); + }); + + it('creates material with textures', function() { + options.specularGlossiness = true; + + var material = loadMtl._createMaterial({ + diffuseTexture : diffuseTexture, + ambientTexture : ambientTexture, + normalTexture : normalTexture, + emissiveTexture : emissiveTexture, + specularTexture : specularTexture, + specularShininessTexture : specularShininessTexture + }, options); + + var pbr = material.extensions.KHR_materials_pbrSpecularGlossiness; + expect(pbr.diffuseTexture).toBeDefined(); + expect(pbr.specularGlossinessTexture).toBeDefined(); + expect(pbr.diffuseFactor).toEqual([1.0, 1.0, 1.0, 1.0]); + expect(pbr.specularFactor).toEqual([1.0, 1.0, 1.0]); + expect(pbr.glossinessFactor).toEqual(1.0); + expect(material.emissiveTexture).toBeDefined(); + expect(material.normalTexture).toBeDefined(); + expect(material.occlusionTexture).toBeDefined(); + expect(material.emissiveFactor).toEqual([1.0, 1.0, 1.0]); + expect(material.alphaMode).toBe('OPAQUE'); + expect(material.doubleSided).toBe(false); + }); + + it('does not create specular glossiness texture if decoded texture data is not available', function() { + options.specularGlossiness = true; + + var material = loadMtl._createMaterial({ + specularTexture : ambientTexture, // Is a .gif which can't be decoded + specularShininessTexture : specularShininessTexture + }, options); + + var pbr = material.extensions.KHR_materials_pbrSpecularGlossiness; + expect(pbr.specularGlossinessTexture).toBeUndefined(); + }); + + it('sets material for transparent diffuse texture', function() { + options.specularGlossiness = true; + + var material = loadMtl._createMaterial({ + diffuseTexture : transparentDiffuseTexture + }, options); + + expect(material.alphaMode).toBe('BLEND'); + expect(material.doubleSided).toBe(true); + }); + }); + + describe('materialsCommon', function() { + it('creates default material', function() { + options.materialsCommon = true; + var material = loadMtl._createMaterial(undefined, options); + var extension = material.extensions.KHR_materials_common; + var values = extension.values; + expect(extension.technique).toBe('LAMBERT'); + expect(extension.transparent).toBe(false); + expect(extension.doubleSided).toBe(false); + expect(values.ambient).toEqual([0.0, 0.0, 0.0, 1.0]); + expect(values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]); + expect(values.emission).toEqual([0.0, 0.0, 0.0, 1.0]); + expect(values.specular).toEqual([0.0, 0.0, 0.0, 1.0]); + expect(values.shininess).toEqual(0); + expect(values.transparency).toBe(1.0); + expect(values.transparent).toBe(false); + expect(values.doubleSided).toBe(false); + }); + + it('creates material with textures', function() { + options.materialsCommon = true; + + var material = loadMtl._createMaterial({ + diffuseTexture : diffuseTexture, + ambientTexture : ambientTexture, + normalTexture : normalTexture, + emissiveTexture : emissiveTexture, + specularTexture : specularTexture, + specularShininessTexture : specularShininessTexture + }, options); + + var extension = material.extensions.KHR_materials_common; + var values = extension.values; + expect(extension.technique).toBe('LAMBERT'); + expect(extension.transparent).toBe(false); + expect(extension.doubleSided).toBe(false); + expect(values.ambient instanceof Texture).toBe(true); + expect(values.diffuse instanceof Texture).toBe(true); + expect(values.emission instanceof Texture).toBe(true); + expect(values.specular instanceof Texture).toBe(true); + expect(values.shininess).toEqual(0); + expect(values.transparency).toBe(1.0); + expect(values.transparent).toBe(false); + expect(values.doubleSided).toBe(false); + }); + + it('sets material for alpha less than 1', function() { + options.materialsCommon = true; + + var material = loadMtl._createMaterial({ + alpha : 0.4 + }, options); + + var values = material.extensions.KHR_materials_common.values; + expect(values.diffuse).toEqual([0.5, 0.5, 0.5, 0.4]); + expect(values.transparency).toBe(1.0); + expect(values.transparent).toBe(true); + expect(values.doubleSided).toBe(true); + }); + + it('sets material for diffuse texture and alpha less than 1', function() { + options.materialsCommon = true; + + var material = loadMtl._createMaterial({ + diffuseTexture : diffuseTexture, + alpha : 0.4 + }, options); + + var values = material.extensions.KHR_materials_common.values; + + expect(values.diffuse instanceof Texture).toBe(true); + expect(values.transparency).toBe(0.4); + expect(values.transparent).toBe(true); + expect(values.doubleSided).toBe(true); + }); + + it('sets material for transparent diffuse texture', function() { + options.materialsCommon = true; + + var material = loadMtl._createMaterial({ + diffuseTexture : transparentDiffuseTexture + }, options); + + var values = material.extensions.KHR_materials_common.values; + + expect(values.diffuse instanceof Texture).toBe(true); + expect(values.transparency).toBe(1.0); + expect(values.transparent).toBe(true); + expect(values.doubleSided).toBe(true); + }); + + it('sets material for specular', function() { + options.materialsCommon = true; + + var material = loadMtl._createMaterial({ + specularColor : [0.1, 0.1, 0.2, 1], + specularShininess : 0.1 + }, options); + + var extension = material.extensions.KHR_materials_common; + var values = extension.values; + + expect(extension.technique).toBe('PHONG'); + expect(values.specular).toEqual([0.1, 0.1, 0.2, 1]); + expect(values.shininess).toEqual(0.1); + }); + + it('ambient of [1, 1, 1] is treated as [0, 0, 0]', function() { + options.materialsCommon = true; + + var material = loadMtl._createMaterial({ + ambientColor : [1.0, 1.0, 1.0, 1.0] + }, options); + + var values = material.extensions.KHR_materials_common.values; + expect(values.ambient).toEqual([0.0, 0.0, 0.0, 1.0]); + }); + }); }); diff --git a/specs/lib/loadObjSpec.js b/specs/lib/loadObjSpec.js index 404a1feb..9c03e9ad 100644 --- a/specs/lib/loadObjSpec.js +++ b/specs/lib/loadObjSpec.js @@ -1,37 +1,33 @@ 'use strict'; var Cesium = require('cesium'); -var path = require('path'); var Promise = require('bluebird'); var loadObj = require('../../lib/loadObj'); var obj2gltf = require('../../lib/obj2gltf'); -var Cartesian3 = Cesium.Cartesian3; var clone = Cesium.clone; var RuntimeError = Cesium.RuntimeError; -var objUrl = 'specs/data/box/box.obj'; -var objRotatedUrl = 'specs/data/box-rotated/box-rotated.obj'; -var objNormalsUrl = 'specs/data/box-normals/box-normals.obj'; -var objUvsUrl = 'specs/data/box-uvs/box-uvs.obj'; -var objPositionsOnlyUrl = 'specs/data/box-positions-only/box-positions-only.obj'; -var objNegativeIndicesUrl = 'specs/data/box-negative-indices/box-negative-indices.obj'; -var objTrianglesUrl = 'specs/data/box-triangles/box-triangles.obj'; -var objObjectsUrl = 'specs/data/box-objects/box-objects.obj'; -var objGroupsUrl = 'specs/data/box-groups/box-groups.obj'; -var objObjectsGroupsUrl = 'specs/data/box-objects-groups/box-objects-groups.obj'; -var objUsemtlUrl = 'specs/data/box-usemtl/box-usemtl.obj'; -var objNoMaterialsUrl = 'specs/data/box-no-materials/box-no-materials.obj'; -var objMultipleMaterialsUrl = 'specs/data/box-multiple-materials/box-multiple-materials.obj'; -var objUncleanedUrl = 'specs/data/box-uncleaned/box-uncleaned.obj'; -var objMtllibUrl = 'specs/data/box-mtllib/box-mtllib.obj'; -var objMissingMtllibUrl = 'specs/data/box-missing-mtllib/box-missing-mtllib.obj'; -var objExternalResourcesUrl = 'specs/data/box-external-resources/box-external-resources.obj'; -var objTexturedUrl = 'specs/data/box-textured/box-textured.obj'; -var objMissingTextureUrl = 'specs/data/box-missing-texture/box-missing-texture.obj'; -var objSubdirectoriesUrl = 'specs/data/box-subdirectories/box-textured.obj'; -var objComplexMaterialUrl = 'specs/data/box-complex-material/box-complex-material.obj'; -var objInvalidContentsUrl = 'specs/data/box/box.mtl'; -var objInvalidUrl = 'invalid.obj'; +var objPath = 'specs/data/box/box.obj'; +var objNormalsPath = 'specs/data/box-normals/box-normals.obj'; +var objUvsPath = 'specs/data/box-uvs/box-uvs.obj'; +var objPositionsOnlyPath = 'specs/data/box-positions-only/box-positions-only.obj'; +var objNegativeIndicesPath = 'specs/data/box-negative-indices/box-negative-indices.obj'; +var objTrianglesPath = 'specs/data/box-triangles/box-triangles.obj'; +var objObjectsPath = 'specs/data/box-objects/box-objects.obj'; +var objGroupsPath = 'specs/data/box-groups/box-groups.obj'; +var objObjectsGroupsPath = 'specs/data/box-objects-groups/box-objects-groups.obj'; +var objUsemtlPath = 'specs/data/box-usemtl/box-usemtl.obj'; +var objNoMaterialsPath = 'specs/data/box-no-materials/box-no-materials.obj'; +var objMultipleMaterialsPath = 'specs/data/box-multiple-materials/box-multiple-materials.obj'; +var objUncleanedPath = 'specs/data/box-uncleaned/box-uncleaned.obj'; +var objMtllibPath = 'specs/data/box-mtllib/box-mtllib.obj'; +var objMissingMtllibPath = 'specs/data/box-missing-mtllib/box-missing-mtllib.obj'; +var objExternalResourcesPath = 'specs/data/box-external-resources/box-external-resources.obj'; +var objTexturedPath = 'specs/data/box-textured/box-textured.obj'; +var objMissingTexturePath = 'specs/data/box-missing-texture/box-missing-texture.obj'; +var objSubdirectoriesPath = 'specs/data/box-subdirectories/box-textured.obj'; +var objInvalidContentsPath = 'specs/data/box/box.mtl'; +var objInvalidPath = 'invalid.obj'; function getMeshes(data) { var meshes = []; @@ -57,27 +53,25 @@ function getPrimitives(data) { return primitives; } -function getImagePath(objPath, relativePath) { - return path.resolve(path.dirname(objPath), relativePath); -} - -var defaultOptions = obj2gltf.defaults; +var options; describe('loadObj', function() { beforeEach(function() { - spyOn(console, 'log'); + options = clone(obj2gltf.defaults); + options.overridingTextures = {}; + options.logger = function() {}; }); it('loads obj with positions, normals, and uvs', function(done) { - expect(loadObj(objUrl, defaultOptions) + expect(loadObj(objPath, options) .then(function(data) { - var images = data.images; var materials = data.materials; var nodes = data.nodes; + var name = data.name; var meshes = getMeshes(data); var primitives = getPrimitives(data); - expect(images.length).toBe(0); + expect(name).toBe('box'); expect(materials.length).toBe(1); expect(nodes.length).toBe(1); expect(meshes.length).toBe(1); @@ -98,7 +92,7 @@ describe('loadObj', function() { }); it('loads obj with normals', function(done) { - expect(loadObj(objNormalsUrl, defaultOptions) + expect(loadObj(objNormalsPath, options) .then(function(data) { var mesh = getMeshes(data)[0]; expect(mesh.positions.length / 3).toBe(24); @@ -108,7 +102,7 @@ describe('loadObj', function() { }); it('loads obj with uvs', function(done) { - expect(loadObj(objUvsUrl, defaultOptions) + expect(loadObj(objUvsPath, options) .then(function(data) { var mesh = getMeshes(data)[0]; expect(mesh.positions.length / 3).toBe(20); @@ -119,8 +113,8 @@ describe('loadObj', function() { it('loads obj with negative indices', function(done) { expect(Promise.all([ - loadObj(objPositionsOnlyUrl, defaultOptions), - loadObj(objNegativeIndicesUrl, defaultOptions) + loadObj(objPositionsOnlyPath, options), + loadObj(objNegativeIndicesPath, options) ]) .then(function(results) { var positionsReference = getMeshes(results[0])[0].positions.toFloatBuffer(); @@ -130,7 +124,7 @@ describe('loadObj', function() { }); it('loads obj with triangle faces', function(done) { - expect(loadObj(objTrianglesUrl, defaultOptions) + expect(loadObj(objTrianglesPath, options) .then(function(data) { var mesh = getMeshes(data)[0]; var primitive = getPrimitives(data)[0]; @@ -140,7 +134,7 @@ describe('loadObj', function() { }); it('loads obj with objects', function(done) { - expect(loadObj(objObjectsUrl, defaultOptions) + expect(loadObj(objObjectsPath, options) .then(function(data) { var nodes = data.nodes; expect(nodes.length).toBe(3); @@ -157,7 +151,7 @@ describe('loadObj', function() { }); it('loads obj with groups', function(done) { - expect(loadObj(objGroupsUrl, defaultOptions) + expect(loadObj(objGroupsPath, options) .then(function(data) { var nodes = data.nodes; expect(nodes.length).toBe(3); @@ -174,7 +168,7 @@ describe('loadObj', function() { }); it('loads obj with objects and groups', function(done) { - expect(loadObj(objObjectsGroupsUrl, defaultOptions) + expect(loadObj(objObjectsGroupsPath, options) .then(function(data) { var nodes = data.nodes; expect(nodes.length).toBe(3); @@ -197,7 +191,7 @@ describe('loadObj', function() { }); it('loads obj with usemtl only', function(done) { - expect(loadObj(objUsemtlUrl, defaultOptions) + expect(loadObj(objUsemtlPath, options) .then(function(data) { var nodes = data.nodes; expect(nodes.length).toBe(1); @@ -216,7 +210,7 @@ describe('loadObj', function() { }); it('loads obj with no materials', function(done) { - expect(loadObj(objNoMaterialsUrl, defaultOptions) + expect(loadObj(objNoMaterialsPath, options) .then(function(data) { var nodes = data.nodes; expect(nodes.length).toBe(1); @@ -229,7 +223,7 @@ describe('loadObj', function() { it('loads obj with multiple materials', function(done) { // The usemtl markers are interleaved, but should condense to just three primitives - expect(loadObj(objMultipleMaterialsUrl, defaultOptions) + expect(loadObj(objMultipleMaterialsPath, options) .then(function(data) { var nodes = data.nodes; expect(nodes.length).toBe(1); @@ -249,7 +243,7 @@ describe('loadObj', 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, defaultOptions) + expect(loadObj(objUncleanedPath, options) .then(function(data) { var nodes = data.nodes; var meshes = getMeshes(data); @@ -265,7 +259,7 @@ describe('loadObj', function() { }); it('loads obj with multiple mtllibs', function(done) { - expect(loadObj(objMtllibUrl, defaultOptions) + expect(loadObj(objMtllibPath, options) .then(function(data) { var materials = data.materials; expect(materials.length).toBe(3); @@ -276,138 +270,87 @@ describe('loadObj', function() { }); expect(materials[0].name).toBe('Blue'); - expect(materials[0].diffuseColor).toEqual([0.0, 0.0, 0.64, 1.0]); + expect(materials[0].pbrMetallicRoughness.baseColorFactor).toEqual([0.0, 0.0, 0.64, 1.0]); expect(materials[1].name).toBe('Green'); - expect(materials[1].diffuseColor).toEqual([0.0, 0.64, 0.0, 1.0]); + expect(materials[1].pbrMetallicRoughness.baseColorFactor).toEqual([0.0, 0.64, 0.0, 1.0]); expect(materials[2].name).toBe('Red'); - expect(materials[2].diffuseColor).toEqual([0.64, 0.0, 0.0, 1.0]); + expect(materials[2].pbrMetallicRoughness.baseColorFactor).toEqual([0.64, 0.0, 0.0, 1.0]); }), done).toResolve(); }); it('loads obj with missing mtllib', function(done) { - expect(loadObj(objMissingMtllibUrl, defaultOptions) + var spy = jasmine.createSpy('logger'); + options.logger = spy; + + expect(loadObj(objMissingMtllibPath, options) .then(function(data) { expect(data.materials.length).toBe(0); - expect(console.log.calls.argsFor(0)[0].indexOf('Could not read mtl file') >= 0).toBe(true); + expect(spy.calls.argsFor(0)[0].indexOf('Could not read mtl file') >= 0).toBe(true); }), done).toResolve(); }); - it('loads resources outside of the obj directory', function(done) { - expect(loadObj(objExternalResourcesUrl, defaultOptions) + it('loads .mtl outside of the obj directory', function(done) { + expect(loadObj(objExternalResourcesPath, options) .then(function(data) { - var imagePath = getImagePath(objTexturedUrl, 'cesium.png'); - expect(data.images[0].path).toBe(imagePath); - var materials = data.materials; expect(materials.length).toBe(2); // .mtl files are loaded in an arbitrary order, so find the "MaterialTextured" material var materialTextured = materials[0].name === 'MaterialTextured' ? materials[0] : materials[1]; - expect(materialTextured.diffuseTexture).toEqual(imagePath); + var baseColorTexture = materialTextured.pbrMetallicRoughness.baseColorTexture; + expect(baseColorTexture.source).toBeDefined(); + expect(baseColorTexture.name).toEqual('cesium'); }), done).toResolve(); }); - it('does not load resources outside of the obj directory when secure is true', function(done) { - var options = clone(defaultOptions); + it('does not load .mtl outside of the obj directory when secure is true', function(done) { + var spy = jasmine.createSpy('logger'); + options.logger = spy; options.secure = true; - expect(loadObj(objExternalResourcesUrl, options) + expect(loadObj(objExternalResourcesPath, options) .then(function(data) { - expect(data.images.length).toBe(0); // obj references an image file that is outside the input directory expect(data.materials.length).toBe(1); // obj references 2 materials, one of which is outside the input directory - expect(console.log.calls.argsFor(0)[0].indexOf('Could not read mtl file') >= 0).toBe(true); - expect(console.log.calls.argsFor(1)[0].indexOf('Could not read image file') >= 0).toBe(true); + expect(spy.calls.argsFor(0)[0].indexOf('Could not read mtl file') >= 0).toBe(true); + expect(spy.calls.argsFor(1)[0].indexOf('Could not read texture file') >= 0).toBe(true); }), done).toResolve(); }); it('loads obj with texture', function(done) { - expect(loadObj(objTexturedUrl, defaultOptions) + expect(loadObj(objTexturedPath, options) .then(function(data) { - var imagePath = getImagePath(objTexturedUrl, 'cesium.png'); - expect(data.images[0].path).toBe(imagePath); - expect(data.materials[0].diffuseTexture).toEqual(imagePath); + var baseColorTexture = data.materials[0].pbrMetallicRoughness.baseColorTexture; + expect(baseColorTexture.name).toBe('cesium'); + expect(baseColorTexture.source).toBeDefined(); }), done).toResolve(); }); it('loads obj with missing texture', function(done) { - expect(loadObj(objMissingTextureUrl, defaultOptions) - .then(function(data) { - var imagePath = getImagePath(objMissingTextureUrl, 'cesium.png'); - expect(data.images.length).toBe(0); - expect(data.materials[0].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, defaultOptions) - .then(function(data) { - var imagePath = getImagePath(objSubdirectoriesUrl, path.join('materials', 'images', 'cesium.png')); - expect(data.images[0].path).toBe(imagePath); - expect(data.materials[0].diffuseTexture).toEqual(imagePath); - }), done).toResolve(); - }); + var spy = jasmine.createSpy('logger'); + options.logger = spy; - it('loads obj with complex material', function(done) { - expect(loadObj(objComplexMaterialUrl, defaultOptions) + expect(loadObj(objMissingTexturePath, options) .then(function(data) { - var images = data.images; - expect(images.length).toBe(6); + var baseColorTexture = data.materials[0].pbrMetallicRoughness.baseColorTexture; + expect(baseColorTexture).toBeUndefined(); + expect(spy.calls.argsFor(0)[0].indexOf('Could not read texture file') >= 0).toBe(true); }), done).toResolve(); }); - function getFirstPosition(data) { - var positions = data.nodes[0].meshes[0].positions; - return new Cartesian3(positions.get(0), positions.get(1), positions.get(2)); - } - - function getFirstNormal(data) { - var normals = data.nodes[0].meshes[0].normals; - return new Cartesian3(normals.get(0), normals.get(1), normals.get(2)); - } - - function checkAxisConversion(inputUpAxis, outputUpAxis, position, normal) { - var sameAxis = (inputUpAxis === outputUpAxis); - var options = clone(defaultOptions); - options.inputUpAxis = inputUpAxis; - options.outputUpAxis = outputUpAxis; - return loadObj(objRotatedUrl, options) - .then(function(data) { - var rotatedPosition = getFirstPosition(data); - var rotatedNormal = getFirstNormal(data); - if (sameAxis) { - expect(rotatedPosition).toEqual(position); - expect(rotatedNormal).toEqual(normal); - } else { - expect(rotatedPosition).not.toEqual(position); - expect(rotatedNormal).not.toEqual(normal); - } - }); - } - - it('performs up axis conversion', function(done) { - expect(loadObj(objRotatedUrl, defaultOptions) + it('loads obj with subdirectories', function(done) { + expect(loadObj(objSubdirectoriesPath, options) .then(function(data) { - var position = getFirstPosition(data); - var normal = getFirstNormal(data); - - var axes = ['X', 'Y', 'Z']; - var axesLength = axes.length; - var promises = []; - for (var i = 0; i < axesLength; ++i) { - for (var j = 0; j < axesLength; ++j) { - promises.push(checkAxisConversion(axes[i], axes[j], position, normal)); - } - } - return Promise.all(promises); + var baseColorTexture = data.materials[0].pbrMetallicRoughness.baseColorTexture; + expect(baseColorTexture.name).toBe('cesium'); + expect(baseColorTexture.source).toBeDefined(); }), done).toResolve(); }); it('throws when file has invalid contents', function(done) { - expect(loadObj(objInvalidContentsUrl, defaultOptions), done).toRejectWith(RuntimeError); + expect(loadObj(objInvalidContentsPath, options), done).toRejectWith(RuntimeError); }); it('throw when reading invalid file', function(done) { - expect(loadObj(objInvalidUrl, defaultOptions), done).toRejectWith(Error); + expect(loadObj(objInvalidPath, options), done).toRejectWith(Error); }); }); diff --git a/specs/lib/loadTextureSpec.js b/specs/lib/loadTextureSpec.js new file mode 100644 index 00000000..b8e33b9c --- /dev/null +++ b/specs/lib/loadTextureSpec.js @@ -0,0 +1,117 @@ +'use strict'; +var loadTexture = require('../../lib/loadTexture'); + +var pngTexturePath = 'specs/data/box-complex-material/shininess.png'; +var jpgTexturePath = 'specs/data/box-complex-material/emission.jpg'; +var jpegTexturePath = 'specs/data/box-complex-material/specular.jpeg'; +var gifTexturePath = 'specs/data/box-complex-material/ambient.gif'; +var grayscaleTexturePath = 'specs/data/box-complex-material/alpha.png'; +var transparentTexturePath = 'specs/data/box-complex-material/diffuse.png'; + +describe('loadTexture', function() { + it('loads png texture', function(done) { + expect(loadTexture(pngTexturePath) + .then(function(texture) { + expect(texture.transparent).toBe(false); + expect(texture.source).toBeDefined(); + expect(texture.name).toBe('shininess'); + expect(texture.extension).toBe('.png'); + expect(texture.path).toBe(pngTexturePath); + expect(texture.pixels).toBeUndefined(); + expect(texture.width).toBeUndefined(); + expect(texture.height).toBeUndefined(); + }), done).toResolve(); + }); + + it('loads jpg texture', function(done) { + expect(loadTexture(jpgTexturePath) + .then(function(texture) { + expect(texture.transparent).toBe(false); + expect(texture.source).toBeDefined(); + expect(texture.name).toBe('emission'); + expect(texture.extension).toBe('.jpg'); + expect(texture.path).toBe(jpgTexturePath); + expect(texture.pixels).toBeUndefined(); + expect(texture.width).toBeUndefined(); + expect(texture.height).toBeUndefined(); + }), done).toResolve(); + }); + + it('loads jpeg texture', function(done) { + expect(loadTexture(jpegTexturePath) + .then(function(texture) { + expect(texture.transparent).toBe(false); + expect(texture.source).toBeDefined(); + expect(texture.name).toBe('specular'); + expect(texture.extension).toBe('.jpeg'); + expect(texture.path).toBe(jpegTexturePath); + expect(texture.pixels).toBeUndefined(); + expect(texture.width).toBeUndefined(); + expect(texture.height).toBeUndefined(); + }), done).toResolve(); + }); + + it('loads gif texture', function(done) { + expect(loadTexture(gifTexturePath) + .then(function(texture) { + expect(texture.transparent).toBe(false); + expect(texture.source).toBeDefined(); + expect(texture.name).toBe('ambient'); + expect(texture.extension).toBe('.gif'); + expect(texture.path).toBe(gifTexturePath); + expect(texture.pixels).toBeUndefined(); + expect(texture.width).toBeUndefined(); + expect(texture.height).toBeUndefined(); + }), done).toResolve(); + }); + + it('loads grayscale texture', function(done) { + expect(loadTexture(grayscaleTexturePath) + .then(function(texture) { + expect(texture.transparent).toBe(false); + expect(texture.source).toBeDefined(); + expect(texture.extension).toBe('.png'); + }), done).toResolve(); + }); + + it('loads texture with alpha channel', function(done) { + expect(loadTexture(transparentTexturePath) + .then(function(texture) { + expect(texture.transparent).toBe(false); + }), done).toResolve(); + }); + + it('loads texture with checkTransparency flag', function(done) { + var options = { + checkTransparency : true + }; + expect(loadTexture(transparentTexturePath, options) + .then(function(texture) { + expect(texture.transparent).toBe(true); + }), done).toResolve(); + }); + + it('loads and decodes png', function(done) { + var options = { + decode : true + }; + expect(loadTexture(pngTexturePath, options) + .then(function(texture) { + expect(texture.pixels).toBeDefined(); + expect(texture.width).toBe(211); + expect(texture.height).toBe(211); + }), done).toResolve(); + }); + + it('loads and decodes jpeg', function(done) { + var options = { + decode : true + }; + expect(loadTexture(jpegTexturePath, options) + .then(function(texture) { + expect(texture.pixels).toBeDefined(); + expect(texture.width).toBe(211); + expect(texture.height).toBe(211); + }), done).toResolve(); + }); +}); diff --git a/specs/lib/obj2gltfSpec.js b/specs/lib/obj2gltfSpec.js index 3679b659..be16cf2d 100644 --- a/specs/lib/obj2gltfSpec.js +++ b/specs/lib/obj2gltfSpec.js @@ -4,28 +4,22 @@ var path = require('path'); var Promise = require('bluebird'); var obj2gltf = require('../../lib/obj2gltf'); -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 objPathNonExistent = 'specs/data/non-existent.obj'; +var texturedObjPath = 'specs/data/box-textured/box-textured.obj'; +var complexObjPath = 'specs/data/box-complex-material/box-complex-material.obj'; +var missingMtllibObjPath = 'specs/data/box-missing-mtllib/box-missing-mtllib.obj'; + +var outputDirectory = 'output'; -var complexMaterialObjPath = 'specs/data/box-complex-material/box-complex-material.obj'; -var complexMaterialGltfPath = 'specs/data/box-complex-material/box-complex-material.gltf'; var textureUrl = 'specs/data/box-textured/cesium.png'; describe('obj2gltf', function() { beforeEach(function() { - spyOn(fsExtra, 'outputJson').and.returnValue(Promise.resolve()); spyOn(fsExtra, 'outputFile').and.returnValue(Promise.resolve()); }); it('converts obj to gltf', function(done) { - expect(obj2gltf(objPath, gltfPath) - .then(function() { - var args = fsExtra.outputJson.calls.first().args; - var outputPath = args[0]; - var gltf = args[1]; - expect(path.normalize(outputPath)).toEqual(path.normalize(gltfPath)); + expect(obj2gltf(texturedObjPath) + .then(function(gltf) { expect(gltf).toBeDefined(); expect(gltf.images.length).toBe(1); }), done).toResolve(); @@ -35,52 +29,83 @@ describe('obj2gltf', function() { var options = { binary : true }; - expect(obj2gltf(objPath, gltfPath, options) - .then(function() { - var args = fsExtra.outputFile.calls.first().args; - var outputPath = args[0]; - var glb = args[1]; - expect(path.extname(outputPath)).toBe('.glb'); + expect(obj2gltf(texturedObjPath, options) + .then(function(glb) { var magic = glb.toString('utf8', 0, 4); expect(magic).toBe('glTF'); }), done).toResolve(); }); - it('converts obj to glb when gltfPath has a .glb extension', function(done) { - expect(obj2gltf(objPath, glbPath) + it('convert obj to gltf with separate resources', function(done) { + var options = { + separate : true, + separateTextures : true, + outputDirectory : outputDirectory + }; + expect(obj2gltf(texturedObjPath, options) .then(function() { - var args = fsExtra.outputFile.calls.first().args; - var outputPath = args[0]; - var glb = args[1]; - expect(path.extname(outputPath)).toBe('.glb'); - var magic = glb.toString('utf8', 0, 4); - expect(magic).toBe('glTF'); + expect(fsExtra.outputFile.calls.count()).toBe(2); // Saves out .png and .bin }), done).toResolve(); }); - it('writes out separate resources', function(done) { + it('converts obj to glb with separate resources', function(done) { var options = { separate : true, - separateTextures : true + separateTextures : true, + outputDirectory : outputDirectory, + binary : true }; - expect(obj2gltf(objPath, gltfPath, options) + expect(obj2gltf(texturedObjPath, options) .then(function() { expect(fsExtra.outputFile.calls.count()).toBe(2); // Saves out .png and .bin - expect(fsExtra.outputJson.calls.count()).toBe(1); // Saves out .gltf }), done).toResolve(); }); - it('sets overriding images', function(done) { + it('converts obj with multiple textures', function(done) { + var options = { + separateTextures : true, + outputDirectory : outputDirectory + }; + expect(obj2gltf(complexObjPath, options) + .then(function() { + expect(fsExtra.outputFile.calls.count()).toBe(5); // baseColor, metallicRoughness, occlusion, emission, normal + }), done).toResolve(); + }); + + it('sets overriding textures (1)', function(done) { var options = { - overridingImages : { + overridingTextures : { metallicRoughnessOcclusionTexture : textureUrl, normalTexture : textureUrl, baseColorTexture : textureUrl, emissiveTexture : textureUrl }, - separateTextures : true + separateTextures : true, + outputDirectory : outputDirectory + }; + expect(obj2gltf(complexObjPath, options) + .then(function() { + var args = fsExtra.outputFile.calls.allArgs(); + var length = args.length; + for (var i = 0; i < length; ++i) { + expect(path.basename(args[i][0])).toBe(path.basename(textureUrl)); + } + }), done).toResolve(); + }); + + it('sets overriding textures (2)', function(done) { + var options = { + overridingTextures : { + specularGlossinessTexture : textureUrl, + occlusionTexture : textureUrl, + normalTexture : textureUrl, + baseColorTexture : textureUrl, + emissiveTexture : textureUrl + }, + separateTextures : true, + outputDirectory : outputDirectory }; - expect(obj2gltf(complexMaterialObjPath, complexMaterialGltfPath, options) + expect(obj2gltf(complexObjPath, options) .then(function() { var args = fsExtra.outputFile.calls.allArgs(); var length = args.length; @@ -90,19 +115,49 @@ describe('obj2gltf', function() { }), done).toResolve(); }); - it('rejects if obj path does not exist', function(done) { - expect(obj2gltf(objPathNonExistent, gltfPath), done).toRejectWith(Error); + it('uses a custom logger', function(done) { + var lastMessage; + var options = { + logger : function(message) { + lastMessage = message; + } + }; + expect(obj2gltf(missingMtllibObjPath, options) + .then(function() { + expect(lastMessage.indexOf('Could not read mtl file') >= 0).toBe(true); + }), done).toResolve(); + }); + + it('uses a custom writer', function(done) { + var filePaths = []; + var fileContents = []; + var options = { + separate : true, + writer : function(relativePath, contents) { + filePaths.push(relativePath); + fileContents.push(contents); + } + }; + expect(obj2gltf(texturedObjPath, options) + .then(function() { + expect(filePaths).toEqual(['box-textured.bin', 'cesium.png']); + expect(fileContents[0]).toBeDefined(); + expect(fileContents[1]).toBeDefined(); + }), done).toResolve(); }); it('throws if objPath is undefined', function() { expect(function() { - obj2gltf(undefined, gltfPath); + obj2gltf(undefined); }).toThrowDeveloperError(); }); - it('throws if gltfPath is undefined', function() { + it('throws if both options.writer and options.outputDirectory are undefined when writing separate resources', function() { + var options = { + separateTextures : true + }; expect(function() { - obj2gltf(objPath, undefined); + obj2gltf(texturedObjPath, options); }).toThrowDeveloperError(); }); @@ -112,17 +167,19 @@ describe('obj2gltf', function() { specularGlossiness : true }; expect(function() { - obj2gltf(objPath, gltfPath, options); + obj2gltf(texturedObjPath, options); }).toThrowDeveloperError(); }); it('throws if metallicRoughnessOcclusionTexture and specularGlossinessTexture are both defined', function() { var options = { - metallicRoughnessOcclusionTexture : 'path/to/metallic-roughness-occlusion/texture', - specularGlossinessTexture : 'path/to/specular-glossiness/texture' + overridingTextures : { + metallicRoughnessOcclusionTexture : textureUrl, + specularGlossinessTexture : textureUrl + } }; expect(function() { - obj2gltf(objPath, gltfPath, options); + obj2gltf(texturedObjPath, options); }).toThrowDeveloperError(); }); }); From b2e4fb6deb4b67f20dc91e7fff17b2ab69cc4738 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 11 Aug 2017 12:01:23 -0400 Subject: [PATCH 21/21] Add doc --- lib/loadMtl.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/loadMtl.js b/lib/loadMtl.js index 62a58edb..bc10fbaa 100644 --- a/lib/loadMtl.js +++ b/lib/loadMtl.js @@ -17,6 +17,11 @@ module.exports = loadMtl; /** * Parse a .mtl file and load textures referenced within. Returns an array of glTF materials with Texture * objects stored in the texture slots. + *

+ * Packed PBR textures (like metallicRoughnessOcclusion and specularGlossiness) require all input textures to be decoded before hand. + * If a texture is of an unsupported format like .gif or .tga it can't be packed and a metallicRoughness texture will not be created. + * Similarly if a texture cannot be found it will be ignored and a default value will be used instead. + *

* * @param {String} mtlPath Path to the .mtl file. * @param {Object} options The options object passed along from lib/obj2gltf.js