diff --git a/README.md b/README.md index c930878..de7e357 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 1eb8262..4f454bc 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 32db616..0000000 Binary files a/doc/cesium.png and /dev/null differ diff --git a/lib/createGltf.js b/lib/createGltf.js index 985f764..86ebbe2 100644 --- a/lib/createGltf.js +++ b/lib/createGltf.js @@ -613,7 +613,7 @@ function convertTraditionalToMetallicRoughness(material) { 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); @@ -649,10 +649,9 @@ function createMaterialsCommonMaterial(gltf, images, material, hasNormals, optio var doubleSided = transparent; - if (!hasNormals && !options.generateNormals) { + 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'; @@ -689,7 +688,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); diff --git a/lib/obj2gltf.js b/lib/obj2gltf.js index b432ecf..c9ee36f 100644 --- a/lib/obj2gltf.js +++ b/lib/obj2gltf.js @@ -1,12 +1,10 @@ '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 uuid = require('uuid'); var createGltf = require('./createGltf'); +var gltfToGlb = require('./gltfToGlb'); var loadObj = require('./loadObj'); var writeUris = require('./writeUris'); @@ -26,13 +24,6 @@ module.exports = obj2gltf; * @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.separateTextures=false] Write out separate textures only. - * @param {Boolean} [options.compress=false] Quantize positions, compress texture coordinates, and oct-encode normals. - * @param {Boolean} [options.optimize=false] Optimize the glTF for size and runtime performance. - * @param {Boolean} [options.optimizeForCesium=false] Optimize the glTF for Cesium by using the sun as a default light source. - * @param {Boolean} [options.generateNormals=false] Generate normals if they are missing. - * @param {Boolean} [options.ao=false] Apply ambient occlusion to the converted model. - * @param {Boolean} [options.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. * @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'. @@ -42,6 +33,7 @@ module.exports = obj2gltf; * @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. + * @return {Promise} A promise that resolves when the glTF file is saved. */ function obj2gltf(objPath, gltfPath, options) { var defaults = obj2gltf.defaults; @@ -50,13 +42,6 @@ function obj2gltf(objPath, gltfPath, options) { var binary = defaultValue(options.binary, defaults.binary); var separate = defaultValue(options.separate, defaults.separate); var separateTextures = defaultValue(options.separateTextures, defaults.separateTextures) || separate; - var compress = defaultValue(options.compress, defaults.compress); - var optimize = defaultValue(options.optimize, defaults.optimize); - var optimizeForCesium = defaultValue(options.optimizeForCesium, defaults.optimizeForCesium); - var generateNormals = defaultValue(options.generateNormals, defaults.generateNormals); - var ao = defaultValue(options.ao, defaults.ao); - var textureCompressionOptions = options.textureCompressionOptions; - var bypassPipeline = defaultValue(options.bypassPipeline, defaults.bypassPipeline); var checkTransparency = defaultValue(options.checkTransparency, defaults.checkTransparency); var secure = defaultValue(options.secure, defaults.secure); var inputUpAxis = defaultValue(options.inputUpAxis, defaults.inputUpAxis); @@ -67,7 +52,6 @@ function obj2gltf(objPath, gltfPath, options) { var materialsCommon = defaultValue(options.materialsCommon, defaults.materialsCommon); var logger = defaultValue(options.logger, defaults.logger); - options.generateNormals = generateNormals; options.separate = separate; options.separateTextures = separateTextures; options.checkTransparency = checkTransparency; @@ -93,34 +77,18 @@ function obj2gltf(objPath, gltfPath, options) { if (extension === '.glb') { binary = true; } - - if (binary && bypassPipeline) { - return Promise.reject(new RuntimeError('--bypassPipeline does not convert to binary glTF')); + 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].')); } - 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 1dea8c2..c7e785b 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,18 +47,18 @@ function writeUris(gltf, gltfPath, resourcesDirectory, options) { var name = path.basename(gltfPath, path.extname(gltfPath)); - if (separate) { - promises.push(writeSeparateBuffer(gltf, resourcesDirectory, name)); - } else { - writeEmbeddedBuffer(gltf); - } - if (separateTextures) { - promises.push(writeSeparateTextures(gltf, resourcesDirectory)); + 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); @@ -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 4111da6..46fa4c4 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 dfc12bb..1ea5a0e 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,