Reorganization of material loading and returning buffer rather than writing file

This commit is contained in:
Sean Lilley 2017-07-29 13:23:33 -04:00
parent cda657e9a6
commit 60a080be46
25 changed files with 1795 additions and 1907 deletions

View File

@ -2,6 +2,7 @@
<project version="4"> <project version="4">
<component name="Encoding"> <component name="Encoding">
<file url="file://$PROJECT_DIR$/CHANGES.md" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/CHANGES.md" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/README.md" charset="UTF-8" />
<file url="PROJECT" charset="UTF-8" /> <file url="PROJECT" charset="UTF-8" />
</component> </component>
</project> </project>

View File

@ -6,6 +6,8 @@ Change Log
* Breaking changes * 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). * 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 `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 ### 1.2.0 2017-07-11

View File

@ -8,23 +8,65 @@ Install [Node.js](https://nodejs.org/en/) if you don't already have it, and then
``` ```
npm install --save obj2gltf npm install --save obj2gltf
``` ```
Using obj2gltf as a library:
```javascript ### Using obj2gltf as a command-line tool:
var obj2gltf = require('obj2gltf');
var options = {
separateTextures : true // Don't embed textures in the converted glTF
}
obj2gltf('model.obj', 'model.gltf', options)
.then(function() {
console.log('Converted model');
});
```
Using obj2gltf as a command-line tool:
`node bin/obj2gltf.js -i model.obj` `node bin/obj2gltf.js -i model.obj`
`node bin/obj2gltf.js -i model.obj -o model.gltf` `node bin/obj2gltf.js -i model.obj -o model.gltf`
`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 = {
binary : true
}
obj2gltf('model.obj', options)
.then(function(glb) {
console.log(glb.length);
});
```
## Material types
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 ## Usage
### Command line flags: ### Command line flags:
@ -33,24 +75,22 @@ Using obj2gltf as a command-line tool:
|----|-----------|--------| |----|-----------|--------|
|`-h`, `--help`|Display help.|No| |`-h`, `--help`|Display help.|No|
|`-i`, `--input`|Path to the obj file.| :white_check_mark: Yes| |`-i`, `--input`|Path to the obj file.| :white_check_mark: Yes|
|`-o`, `--output`|Path of the converted glTF file.|No| |`-o`, `--output`|Path of the converted glTF or glb file.|No|
|`-b`, `--binary`|Save as binary glTF.|No, default `false`| |`-b`, `--binary`|Save as binary glTF (.glb).|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`| |`-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`| |`-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`| |`--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`| |`--secure`|Prevent the converter from reading texture 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`|
|`--packOcclusion`|Pack the occlusion texture in the red channel of metallic-roughness texture.|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`| |`--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`| |`--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`| |`--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. |`--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 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. |`--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 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. |`--occlusionTexture`|Path to the occlusion texture that should override textures in the .mtl file.|No|
|`--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. |`--normalTexture`|Path to the normal texture that should override textures in the .mtl file.|No|
|`--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. |`--baseColorTexture`|Path to the baseColor/diffuse texture that should override textures in the .mtl file.|No|
|`--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. |`--emissiveTexture`|Path to the emissive texture that should override textures in the .mtl file.|No|
## Build Instructions ## Build Instructions
@ -86,11 +126,6 @@ npm run jsdoc
The documentation will be placed in the `doc` folder. 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 ## 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/). 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/).

View File

@ -1,6 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict'; 'use strict';
var Cesium = require('cesium'); var Cesium = require('cesium');
var fsExtra = require('fs-extra');
var path = require('path'); var path = require('path');
var yargs = require('yargs'); var yargs = require('yargs');
var obj2gltf = require('../lib/obj2gltf'); var obj2gltf = require('../lib/obj2gltf');
@ -18,107 +19,95 @@ var argv = yargs
.alias('h', 'help') .alias('h', 'help')
.options({ .options({
input : { input : {
alias: 'i', alias : 'i',
describe: 'Path to the obj file.', describe : 'Path to the obj file.',
type: 'string', type : 'string',
normalize: true, normalize : true,
demandOption: true demandOption : true
}, },
output : { output : {
alias: 'o', alias : 'o',
describe: 'Path of the converted glTF file.', describe : 'Path of the converted glTF or glb file.',
type: 'string', type : 'string',
normalize: true normalize : true
}, },
binary : { binary : {
alias: 'b', alias : 'b',
describe: 'Save as binary glTF.', describe : 'Save as binary glTF (.glb)',
type: 'boolean', type : 'boolean',
default: defaults.binary default : defaults.binary
}, },
separate : { separate : {
alias: 's', alias : 's',
describe: 'Write separate geometry data files, shader files, and textures instead of embedding them in the glTF.', describe : 'Write separate buffers and textures instead of embedding them in the glTF.',
type: 'boolean', type : 'boolean',
default: defaults.separate default : defaults.separate
}, },
separateTextures : { separateTextures : {
alias: 't', alias : 't',
describe: 'Write out separate textures only.', describe : 'Write out separate textures only.',
type: 'boolean', type : 'boolean',
default: defaults.separateTextures default : defaults.separateTextures
}, },
checkTransparency : { 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.', 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', type : 'boolean',
default: defaults.checkTransparency default : defaults.checkTransparency
}, },
secure : { secure : {
describe: 'Prevent the converter from reading image or mtl files outside of the input obj directory.', describe : 'Prevent the converter from reading textures or mtl files outside of the input obj directory.',
type: 'boolean', type : 'boolean',
default: defaults.secure 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'
}, },
packOcclusion : { packOcclusion : {
describe: 'Pack the occlusion texture in the red channel of metallic-roughness texture.', describe : 'Pack the occlusion texture in the red channel of metallic-roughness texture.',
type: 'boolean', type : 'boolean',
default: defaults.packOcclusion default : defaults.packOcclusion
}, },
metallicRoughness : { 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.', 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', type : 'boolean',
default: defaults.metallicRoughness default : defaults.metallicRoughness
}, },
specularGlossiness : { 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.', 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', type : 'boolean',
default: defaults.specularGlossiness default : defaults.specularGlossiness
}, },
materialsCommon : { materialsCommon : {
describe: 'The glTF will be saved with the KHR_materials_common extension.', describe : 'The glTF will be saved with the KHR_materials_common extension.',
type: 'boolean', type : 'boolean',
default: defaults.materialsCommon default : defaults.materialsCommon
}, },
metallicRoughnessOcclusionTexture : { 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.', 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', type : 'string',
normalize: true normalize : true
}, },
specularGlossinessTexture : { 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.', 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', type : 'string',
normalize: true normalize : true
}, },
occlusionTexture : { 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.', describe : 'Path to the occlusion texture that should override textures in the .mtl file.',
type: 'string', type : 'string',
normalize: true normalize : true
}, },
normalTexture : { 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.', describe : 'Path to the normal texture that should override textures in the .mtl file.',
type: 'string', type : 'string',
normalize: true normalize : true
}, },
baseColorTexture : { 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.', describe : 'Path to the baseColor/diffuse texture that should override textures in the .mtl file.',
type: 'string', type : 'string',
normalize: true normalize : true
}, },
emissiveTexture : { 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.', describe : 'Path to the emissive texture that should override textures in the .mtl file.',
type: 'string', type : 'string',
normalize: true normalize : true
} }
}).parse(args); }).parse(args);
@ -132,16 +121,23 @@ if (defined(argv.metallicRoughnessOcclusionTexture) && defined(argv.specularGlos
process.exit(1); process.exit(1);
} }
var objPath = argv.i; var objPath = argv.input;
var gltfPath = argv.o; var gltfPath = argv.output;
var name = path.basename(objPath, path.extname(objPath));
if (!defined(gltfPath)) { if (!defined(gltfPath)) {
var extension = argv.b ? '.glb' : '.gltf'; gltfPath = path.join(path.dirname(objPath), name + '.gltf');
var modelName = path.basename(objPath, path.extname(objPath));
gltfPath = path.join(path.dirname(objPath), modelName + extension);
} }
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, metallicRoughnessOcclusionTexture : argv.metallicRoughnessOcclusionTexture,
specularGlossinessTexture : argv.specularGlossinessTexture, specularGlossinessTexture : argv.specularGlossinessTexture,
occlusionTexture : argv.occlusionTexture, occlusionTexture : argv.occlusionTexture,
@ -156,21 +152,31 @@ var options = {
separateTextures : argv.separateTextures, separateTextures : argv.separateTextures,
checkTransparency : argv.checkTransparency, checkTransparency : argv.checkTransparency,
secure : argv.secure, secure : argv.secure,
inputUpAxis : argv.inputUpAxis,
outputUpAxis : argv.outputUpAxis,
packOcclusion : argv.packOcclusion, packOcclusion : argv.packOcclusion,
metallicRoughness : argv.metallicRoughness, metallicRoughness : argv.metallicRoughness,
specularGlossiness : argv.specularGlossiness, specularGlossiness : argv.specularGlossiness,
materialsCommon : argv.materialsCommon, materialsCommon : argv.materialsCommon,
overridingImages : overridingImages overridingTextures : overridingTextures,
outputDirectory : outputDirectory
}; };
console.time('Total'); 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() { .then(function() {
console.timeEnd('Total'); console.timeEnd('Total');
}) })
.catch(function(error) { .catch(function(error) {
console.log(error.message); console.log(error.message);
process.exit(1);
}); });

View File

@ -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. * stored with double precision. The resizing mechanism is similar to std::vector.
* *
* @param {ComponentDatatype} componentDatatype The data type. * @param {ComponentDatatype} componentDatatype The data type.
* @constructor
* *
* @private * @private
*/ */

View File

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

View File

@ -1,18 +1,19 @@
'use strict'; 'use strict';
module.exports = Image; module.exports = Texture;
/** /**
* Stores image data and properties. * An object containing information about a texture.
* *
* @private * @private
*/ */
function Image() { function Texture() {
this.transparent = false; this.transparent = false;
this.source = undefined; this.source = undefined;
this.name = undefined;
this.extension = undefined; this.extension = undefined;
this.path = undefined; this.path = undefined;
this.decoded = undefined; this.pixels = undefined;
this.width = undefined; this.width = undefined;
this.height = undefined; this.height = undefined;
} }

View File

@ -1,12 +1,9 @@
'use strict'; 'use strict';
var Cesium = require('cesium'); var Cesium = require('cesium');
var path = require('path');
var getBufferPadded = require('./getBufferPadded'); var getBufferPadded = require('./getBufferPadded');
var Image = require('./Image'); var getDefaultMaterial = require('./loadMtl').getDefaultMaterial;
var Material = require('./Material'); var Texture = require('./Texture');
var CesiumMath = Cesium.Math;
var defaultValue = Cesium.defaultValue;
var defined = Cesium.defined; var defined = Cesium.defined;
var WebGLConstants = Cesium.WebGLConstants; var WebGLConstants = Cesium.WebGLConstants;
@ -15,20 +12,8 @@ module.exports = createGltf;
/** /**
* Create a glTF from obj data. * 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} objData An object containing an array of nodes containing geometry information and an array of materials.
* @param {Object} options An object with the following properties: * @param {Object} options The options object passed along from lib/obj2gltf.js
* @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.
* @returns {Object} A glTF asset. * @returns {Object} A glTF asset.
* *
* @private * @private
@ -36,7 +21,7 @@ module.exports = createGltf;
function createGltf(objData, options) { function createGltf(objData, options) {
var nodes = objData.nodes; var nodes = objData.nodes;
var materials = objData.materials; var materials = objData.materials;
var images = objData.images; var name = objData.name;
var gltf = { var gltf = {
accessors : [], accessors : [],
@ -85,14 +70,14 @@ function createGltf(objData, options) {
var meshIndex; var meshIndex;
if (meshesLength === 1) { if (meshesLength === 1) {
meshIndex = addMesh(gltf, materials, images, bufferState, uint32Indices, meshes[0], options); meshIndex = addMesh(gltf, materials, bufferState, uint32Indices, meshes[0], options);
addNode(gltf, node.name, meshIndex); addNode(gltf, node.name, meshIndex, undefined);
} else { } else {
// Add meshes as child nodes // Add meshes as child nodes
var parentIndex = addNode(gltf, node.name); var parentIndex = addNode(gltf, node.name);
for (var j = 0; j < meshesLength; ++j) { for (var j = 0; j < meshesLength; ++j) {
var mesh = meshes[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); 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; 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 // Positions and normals share the same byte stride so they can share the same bufferView
var positionsAndNormalsAccessors = bufferState.positionAccessors.concat(bufferState.normalAccessors); var positionsAndNormalsAccessors = bufferState.positionAccessors.concat(bufferState.normalAccessors);
var positionsAndNormalsBuffers = bufferState.positionBuffers.concat(bufferState.normalBuffers); var positionsAndNormalsBuffers = bufferState.positionBuffers.concat(bufferState.normalBuffers);
@ -149,7 +143,7 @@ function addBuffers(gltf, bufferState) {
var buffer = getBufferPadded(Buffer.concat(buffers)); var buffer = getBufferPadded(Buffer.concat(buffers));
gltf.buffers.push({ gltf.buffers.push({
name : 'buffer', name : name,
byteLength : buffer.length, byteLength : buffer.length,
extras : { extras : {
_obj2gltf : { _obj2gltf : {
@ -159,38 +153,16 @@ function addBuffers(gltf, bufferState) {
}); });
} }
function getImage(images, imagePath, overridingImage) { function addTexture(gltf, texture) {
if (defined(overridingImage)) { var imageName = texture.name;
return overridingImage; var textureName = texture.name;
}
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);
var imageIndex = gltf.images.length; var imageIndex = gltf.images.length;
var textureIndex = gltf.textures.length; var textureIndex = gltf.textures.length;
gltf.images.push({ gltf.images.push({
name : imageName, name : imageName,
extras : { extras : {
_obj2gltf : image _obj2gltf : texture
} }
}); });
@ -203,13 +175,9 @@ function addTexture(gltf, image) {
return textureIndex; return textureIndex;
} }
function getTexture(gltf, image) { function getTexture(gltf, texture) {
if (!defined(image)) {
return undefined;
}
var textureIndex; var textureIndex;
var name = getTextureName(image); var name = texture.name;
var textures = gltf.textures; var textures = gltf.textures;
var length = textures.length; var length = textures.length;
for (var i = 0; i < length; ++i) { for (var i = 0; i < length; ++i) {
@ -220,7 +188,7 @@ function getTexture(gltf, image) {
} }
if (!defined(textureIndex)) { if (!defined(textureIndex)) {
textureIndex = addTexture(gltf, image); textureIndex = addTexture(gltf, texture);
} }
return { return {
@ -228,507 +196,27 @@ function getTexture(gltf, image) {
}; };
} }
function addColors(left, right) { function resolveTextures(gltf, material) {
var red = Math.min(left[0] + right[0], 1.0); for (var name in material) {
var green = Math.min(left[1] + right[1], 1.0); if (material.hasOwnProperty(name)) {
var blue = Math.min(left[2] + right[2], 1.0); var property = material[name];
return [red, green, blue]; if (property instanceof Texture) {
} material[name] = getTexture(gltf, property);
} else if (!Array.isArray(property) && (typeof property === 'object')) {
function getEmissiveFactor(material) { resolveTextures(gltf, property);
// 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
}
},
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) { function addMaterial(gltf, material) {
var gltfMaterial; resolveTextures(gltf, material);
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);
}
var materialIndex = gltf.materials.length; var materialIndex = gltf.materials.length;
gltf.materials.push(gltfMaterial); gltf.materials.push(material);
return materialIndex; return materialIndex;
} }
function getMaterial(gltf, materials, images, materialName, hasNormals, options) { function getMaterial(gltf, materials, materialName, options) {
if (!defined(materialName)) { if (!defined(materialName)) {
// Create a default material if the primitive does not specify one // Create a default material if the primitive does not specify one
materialName = 'default'; materialName = 'default';
@ -745,7 +233,7 @@ function getMaterial(gltf, materials, images, materialName, hasNormals, options)
} }
if (!defined(material)) { if (!defined(material)) {
material = new Material(); material = getDefaultMaterial(options);
material.name = materialName; material.name = materialName;
} }
@ -759,7 +247,7 @@ function getMaterial(gltf, materials, images, materialName, hasNormals, options)
} }
if (!defined(materialIndex)) { if (!defined(materialIndex)) {
materialIndex = addMaterial(gltf, images, material, hasNormals, options); materialIndex = addMaterial(gltf, material);
} }
return materialIndex; return materialIndex;
@ -819,12 +307,12 @@ function requiresUint32Indices(nodes) {
return false; 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 hasPositions = mesh.positions.length > 0;
var hasNormals = mesh.normals.length > 0; var hasNormals = mesh.normals.length > 0;
var hasUVs = mesh.uvs.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 accessorIndex;
var attributes = {}; var attributes = {};
if (hasPositions) { if (hasPositions) {
@ -863,7 +351,7 @@ function addMesh(gltf, materials, images, bufferState, uint32Indices, mesh, opti
primitive.indices = undefined; // Unload resources 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({ gltfPrimitives.push({
attributes : attributes, attributes : attributes,

View File

@ -2,27 +2,24 @@
var Cesium = require('cesium'); var Cesium = require('cesium');
var getJsonBufferPadded = require('./getJsonBufferPadded'); var getJsonBufferPadded = require('./getJsonBufferPadded');
var isDataUri = Cesium.isDataUri; var defined = Cesium.defined;
module.exports = gltfToGlb; module.exports = gltfToGlb;
/** /**
* Convert a glTF to binary glTF. * 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. * @param {Object} gltf The glTF asset.
* @returns {Promise} A promise that resolves to a buffer containing the binary glTF. * @param {Buffer} binaryBuffer The binary buffer.
* @returns {Buffer} The glb buffer.
* *
* @private * @private
*/ */
function gltfToGlb(gltf) { function gltfToGlb(gltf, binaryBuffer) {
var buffer = gltf.buffers[0]; var buffer = gltf.buffers[0];
var binaryBuffer; if (defined(buffer.uri)) {
if (isDataUri(buffer.uri)) {
binaryBuffer = dataUriToBuffer(buffer.uri);
delete buffer.uri;
} else {
binaryBuffer = Buffer.alloc(0); binaryBuffer = Buffer.alloc(0);
} }
@ -62,8 +59,3 @@ function gltfToGlb(gltf) {
binaryBuffer.copy(glb, byteOffset); binaryBuffer.copy(glb, byteOffset);
return glb; return glb;
} }
function dataUriToBuffer(dataUri) {
var data = dataUri.slice(dataUri.indexOf(','));
return Buffer.from(data, 'base64');
}

View File

@ -1,17 +1,26 @@
'use strict'; 'use strict';
var Cesium = require('cesium');
var path = require('path'); 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 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; 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 {String} mtlPath Path to the .mtl file.
* @param {Object} options An object with the following properties: * @param {Object} options The options object passed along from lib/obj2gltf.js
* @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 glTF materials with Texture objects stored in the texture slots.
* @returns {Promise} A promise resolving to an array of materials.
* *
* @private * @private
*/ */
@ -19,22 +28,54 @@ function loadMtl(mtlPath, options) {
var material; var material;
var values; var values;
var value; var value;
var texturePath;
var mtlDirectory = path.dirname(mtlPath); var mtlDirectory = path.dirname(mtlPath);
var materials = []; var materials = [];
var texturePromiseMap = {}; // Maps texture paths to load promises so that no texture is loaded twice
var texturePromises = [];
var defaultSpecularShininess = 0.0; var overridingTextures = options.overridingTextures;
if (options.metallicRoughness) { var overridingSpecularTexture = defaultValue(overridingTextures.metallicRoughnessOcclusionTexture, overridingTextures.specularGlossinessTexture);
defaultSpecularShininess = 1.0; // Fully rough 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 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) { function parseLine(line) {
line = line.trim(); line = line.trim();
if (/^newmtl /i.test(line)) { if (/^newmtl /i.test(line)) {
var name = line.substring(7).trim(); var name = line.substring(7).trim();
material = new Material(); createMaterial(name);
material.name = name;
material.specularShininess = defaultSpecularShininess;
materials.push(material);
} else if (/^Ka /i.test(line)) { } else if (/^Ka /i.test(line)) {
values = line.substring(3).trim().split(' '); values = line.substring(3).trim().split(' ');
material.ambientColor = [ material.ambientColor = [
@ -77,24 +118,536 @@ function loadMtl(mtlPath, options) {
value = line.substring(3).trim(); value = line.substring(3).trim();
material.alpha = 1.0 - parseFloat(value); material.alpha = 1.0 - parseFloat(value);
} else if (/^map_Ka /i.test(line)) { } 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)) { } 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)) { } 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)) { } 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)) { } 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)) { } else if (/^map_Bump /i.test(line)) {
material.normalTexture = path.resolve(mtlDirectory, line.substring(9).trim()); if (!defined(overridingNormalTexture)) {
} else if (/^map_d /i.test(line)) { texturePath = path.resolve(mtlDirectory, line.substring(9).trim());
material.alphaTexture = path.resolve(mtlDirectory, line.substring(6).trim()); loadMaterialTexture(material, 'normalTexture', texturePath, normalTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options);
}
} }
} }
return readLines(mtlPath, parseLine) return readLines(mtlPath, parseLine)
.then(function() { .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
}
}
}
};
}

View File

@ -4,16 +4,13 @@ var path = require('path');
var Promise = require('bluebird'); var Promise = require('bluebird');
var ArrayStorage = require('./ArrayStorage'); var ArrayStorage = require('./ArrayStorage');
var loadImage = require('./loadImage');
var loadMtl = require('./loadMtl'); var loadMtl = require('./loadMtl');
var outsideDirectory = require('./outsideDirectory');
var readLines = require('./readLines'); var readLines = require('./readLines');
var Axis = Cesium.Axis;
var Cartesian3 = Cesium.Cartesian3;
var ComponentDatatype = Cesium.ComponentDatatype; var ComponentDatatype = Cesium.ComponentDatatype;
var defaultValue = Cesium.defaultValue; var defaultValue = Cesium.defaultValue;
var defined = Cesium.defined; var defined = Cesium.defined;
var Matrix4 = Cesium.Matrix4;
var RuntimeError = Cesium.RuntimeError; var RuntimeError = Cesium.RuntimeError;
module.exports = loadObj; 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 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 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. * Parse an obj file.
* *
* @param {String} objPath Path to the obj file. * @param {String} objPath Path to the obj file.
* @param {Object} options An object with the following properties: * @param {Object} options The options object passed along from lib/obj2gltf.js
* @param {Boolean} options.checkTransparency Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. * @returns {Promise} A promise resolving to the obj data, which includes an array of nodes containing geometry information and an array of materials.
* @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.
* *
* @private * @private
*/ */
function loadObj(objPath, options) { function loadObj(objPath, options) {
var axisTransform = getAxisTransform(options.inputUpAxis, options.outputUpAxis);
// Global store of vertex attributes listed in the obj file // Global store of vertex attributes listed in the obj file
var positions = new ArrayStorage(ComponentDatatype.FLOAT); var positions = new ArrayStorage(ComponentDatatype.FLOAT);
var normals = 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(' '); var paths = line.substring(7).trim().split(' ');
mtlPaths = mtlPaths.concat(paths); mtlPaths = mtlPaths.concat(paths);
} else if ((result = vertexPattern.exec(line)) !== null) { } else if ((result = vertexPattern.exec(line)) !== null) {
var position = scratchCartesian; positions.push(parseFloat(result[1]));
position.x = parseFloat(result[1]); positions.push(parseFloat(result[2]));
position.y = parseFloat(result[2]); positions.push(parseFloat(result[3]));
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);
} else if ((result = normalPattern.exec(line) ) !== null) { } else if ((result = normalPattern.exec(line) ) !== null) {
var normal = scratchCartesian; normals.push(parseFloat(result[1]));
normal.x = parseFloat(result[1]); normals.push(parseFloat(result[2]));
normal.y = parseFloat(result[2]); normals.push(parseFloat(result[3]));
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);
} else if ((result = uvPattern.exec(line)) !== null) { } else if ((result = uvPattern.exec(line)) !== null) {
uvs.push(parseFloat(result[1])); 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) { } else if ((result = facePattern1.exec(line)) !== null) {
addFace( addFace(
result[1], result[1], undefined, undefined, result[1], result[1], undefined, undefined,
@ -298,7 +271,7 @@ function loadObj(objPath, options) {
normals = undefined; normals = undefined;
uvs = undefined; uvs = undefined;
// Load materials and images // Load materials and textures
return finishLoading(nodes, mtlPaths, objPath, options); return finishLoading(nodes, mtlPaths, objPath, options);
}); });
} }
@ -306,35 +279,26 @@ function loadObj(objPath, options) {
function finishLoading(nodes, mtlPaths, objPath, options) { function finishLoading(nodes, mtlPaths, objPath, options) {
nodes = cleanNodes(nodes); nodes = cleanNodes(nodes);
if (nodes.length === 0) { 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) { .then(function(materials) {
var imagesOptions = getImagesOptions(materials, options); return {
return loadImages(imagesOptions, objPath, options) nodes : nodes,
.then(function(images) { materials : materials,
return { name : name
nodes : nodes, };
materials : materials,
images : images
};
});
}); });
} }
function outsideDirectory(filePath, objPath) { function loadMtls(mtlPaths, objPath, options) {
return (path.relative(path.dirname(objPath), filePath).indexOf('..') === 0);
}
function loadMaterials(mtlPaths, objPath, options) {
var secure = options.secure;
var logger = options.logger;
var objDirectory = path.dirname(objPath); var objDirectory = path.dirname(objPath);
var materials = []; var materials = [];
return Promise.map(mtlPaths, function(mtlPath) { return Promise.map(mtlPaths, function(mtlPath) {
mtlPath = path.resolve(objDirectory, mtlPath); mtlPath = path.resolve(objDirectory, mtlPath);
if (secure && outsideDirectory(mtlPath, objPath)) { if (options.secure && outsideDirectory(mtlPath, objDirectory)) {
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.'); 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;
} }
return loadMtl(mtlPath, options) return loadMtl(mtlPath, options)
@ -342,7 +306,7 @@ function loadMaterials(mtlPaths, objPath, options) {
materials = materials.concat(materialsInMtl); materials = materials.concat(materialsInMtl);
}) })
.catch(function() { .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}) }, {concurrency : 10})
.then(function() { .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) { function removeEmptyMeshes(meshes) {
return meshes.filter(function(mesh) { return meshes.filter(function(mesh) {
// Remove empty primitives // Remove empty primitives
@ -492,19 +393,3 @@ function cleanNodes(nodes) {
setDefaults(nodes); setDefaults(nodes);
return 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;
}
}

View File

@ -5,55 +5,56 @@ var jpeg = require('jpeg-js');
var path = require('path'); var path = require('path');
var PNG = require('pngjs').PNG; var PNG = require('pngjs').PNG;
var Promise = require('bluebird'); var Promise = require('bluebird');
var Image = require('./Image'); var Texture = require('./Texture');
var defaultValue = Cesium.defaultValue; var defaultValue = Cesium.defaultValue;
var defined = Cesium.defined; 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 {String} texturePath Path to the texture file.
* @param {Object} options An object with the following properties: * @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.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. * @param {Boolean} [options.decode=false] Whether to decode the texture.
* @returns {Promise} A promise resolving to an Image object. * @returns {Promise} A promise resolving to a Texture object.
* *
* @private * @private
*/ */
function loadImage(imagePath, options) { function loadTexture(texturePath, options) {
options = defaultValue(options, {}); options = defaultValue(options, {});
options.checkTransparency = defaultValue(options.checkTransparency, false); options.checkTransparency = defaultValue(options.checkTransparency, false);
options.decode = defaultValue(options.decode, false); options.decode = defaultValue(options.decode, false);
return fsExtra.readFile(imagePath) return fsExtra.readFile(texturePath)
.then(function(data) { .then(function(source) {
var extension = path.extname(imagePath).toLowerCase(); var name = path.basename(texturePath, path.extname(texturePath));
var image = new Image(); var extension = path.extname(texturePath).toLowerCase();
image.source = data; var texture = new Texture();
image.extension = extension; texture.source = source;
image.path = imagePath; texture.name = name;
texture.extension = extension;
texture.path = texturePath;
var decodePromise; var decodePromise;
if (extension === '.png') { if (extension === '.png') {
decodePromise = decodePng(image, options); decodePromise = decodePng(texture, options);
} else if (extension === '.jpg' || extension === '.jpeg') { } else if (extension === '.jpg' || extension === '.jpeg') {
decodePromise = decodeJpeg(image, options); decodePromise = decodeJpeg(texture, options);
} }
if (defined(decodePromise)) { if (defined(decodePromise)) {
return decodePromise.thenReturn(image); return decodePromise.thenReturn(texture);
} }
return image; return texture;
}); });
} }
function hasTransparency(image) { function hasTransparency(pixels) {
var pixels = image.decoded; var pixelsLength = pixels.length / 4;
var pixelsLength = image.width * image.height;
for (var i = 0; i < pixelsLength; ++i) { for (var i = 0; i < pixelsLength; ++i) {
if (pixels[i * 4 + 3] < 255) { if (pixels[i * 4 + 3] < 255) {
return true; 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 // Color type is encoded in the 25th bit of the png
var source = image.source; var source = texture.source;
var colorType = source[25]; var colorType = source[25];
var channels = getChannels(colorType); var channels = getChannels(colorType);
@ -101,22 +102,26 @@ function decodePng(image, options) {
if (decode) { if (decode) {
return parsePng(source) return parsePng(source)
.then(function(decodedResults) { .then(function(decodedResults) {
image.decoded = decodedResults.data; if (options.checkTransparency) {
image.width = decodedResults.width; texture.transparent = hasTransparency(decodedResults.data);
image.height = decodedResults.height; }
if (checkTransparency) { if (options.decode) {
image.transparent = hasTransparency(image); 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) { if (options.decode) {
var source = image.source; var source = texture.source;
var decodedResults = jpeg.decode(source); var decodedResults = jpeg.decode(source);
image.decoded = decodedResults.data; texture.pixels = decodedResults.data;
image.width = decodedResults.width; texture.width = decodedResults.width;
image.height = decodedResults.height; texture.height = decodedResults.height;
texture.source = undefined; // Unload resources
} }
} }

View File

@ -2,12 +2,9 @@
var Cesium = require('cesium'); var Cesium = require('cesium');
var fsExtra = require('fs-extra'); var fsExtra = require('fs-extra');
var path = require('path'); var path = require('path');
var Promise = require('bluebird');
var createGltf = require('./createGltf'); var createGltf = require('./createGltf');
var gltfToGlb = require('./gltfToGlb');
var loadImage = require('./loadImage');
var loadObj = require('./loadObj'); var loadObj = require('./loadObj');
var writeUris = require('./writeUris'); var writeGltf = require('./writeGltf');
var defaultValue = Cesium.defaultValue; var defaultValue = Cesium.defaultValue;
var defined = Cesium.defined; var defined = Cesium.defined;
@ -16,150 +13,98 @@ var DeveloperError = Cesium.DeveloperError;
module.exports = obj2gltf; 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} 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 {Object} [options] An object with the following properties:
* @param {Boolean} [options.binary=false] Save as binary glTF. * @param {Boolean} [options.binary=false] Convert to 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.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.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.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.secure=false] Prevent the converter from reading textures 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 {Boolean} [options.packOcclusion=false] Pack the occlusion texture in the red channel of the metallic-roughness texture.
* @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.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.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.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 {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 {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.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.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.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.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.overridingImages.occlusionTexture] Path to the occlusion texture. Ignored if metallicRoughnessOcclusionTexture is also set. * @param {String} [options.overridingTextures.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.overridingTextures.normalTexture] Path to the normal texture.
* @param {String} [options.overridingImages.baseColorTexture] Path to the baseColor/diffuse texture. * @param {String} [options.overridingTextures.baseColorTexture] Path to the baseColor/diffuse texture.
* @param {String} [options.overridingImages.emissiveTexture] Path to the emissive 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. * @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; var defaults = obj2gltf.defaults;
options = defaultValue(options, {}); options = defaultValue(options, {});
var binary = defaultValue(options.binary, defaults.binary); options.binary = defaultValue(options.binary, defaults.binary);
var separate = defaultValue(options.separate, defaults.separate); options.separate = defaultValue(options.separate, defaults.separate);
var separateTextures = defaultValue(options.separateTextures, defaults.separateTextures) || separate; options.separateTextures = defaultValue(options.separateTextures, defaults.separateTextures) || options.separate;
var checkTransparency = defaultValue(options.checkTransparency, defaults.checkTransparency); options.checkTransparency = defaultValue(options.checkTransparency, defaults.checkTransparency);
var secure = defaultValue(options.secure, defaults.secure); options.secure = defaultValue(options.secure, defaults.secure);
var inputUpAxis = defaultValue(options.inputUpAxis, defaults.inputUpAxis); options.packOcclusion = defaultValue(options.packOcclusion, defaults.packOcclusion);
var outputUpAxis = defaultValue(options.outputUpAxis, defaults.outputUpAxis); options.metallicRoughness = defaultValue(options.metallicRoughness, defaults.metallicRoughness);
var packOcclusion = defaultValue(options.packOcclusion, defaults.packOcclusion); options.specularGlossiness = defaultValue(options.specularGlossiness, defaults.specularGlossiness);
var metallicRoughness = defaultValue(options.metallicRoughness, defaults.metallicRoughness); options.materialsCommon = defaultValue(options.materialsCommon, defaults.materialsCommon);
var specularGlossiness = defaultValue(options.specularGlossiness, defaults.specularGlossiness); options.overridingTextures = defaultValue(options.overridingTextures, defaultValue.EMPTY_OBJECT);
var materialsCommon = defaultValue(options.materialsCommon, defaults.materialsCommon); options.logger = defaultValue(options.logger, getDefaultLogger());
var overridingImages = defaultValue(options.overridingImages, defaultValue.EMPTY_OBJECT); options.writer = defaultValue(options.writer, getDefaultWriter(options.outputDirectory));
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;
if (!defined(objPath)) { if (!defined(objPath)) {
throw new DeveloperError('objPath is required'); throw new DeveloperError('objPath is required');
} }
if (!defined(gltfPath)) { if (options.separateTextures && !defined(options.writer)) {
throw new DeveloperError('gltfPath is required'); 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].'); 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.'); throw new DeveloperError('metallicRoughnessOcclusionTexture and specularGlossinessTexture cannot both be defined.');
} }
if (defined(overridingImages.metallicRoughnessOcclusionTexture)) { if (defined(options.overridingTextures.metallicRoughnessOcclusionTexture)) {
options.metallicRoughness = true; options.metallicRoughness = true;
options.specularGlossiness = false; options.specularGlossiness = false;
options.materialsCommon = false; options.materialsCommon = false;
options.packOcclusion = true;
} }
if (defined(overridingImages.specularGlossinessTexture)) { if (defined(options.overridingTextures.specularGlossinessTexture)) {
options.metallicRoughness = false; options.metallicRoughness = false;
options.specularGlossiness = true; options.specularGlossiness = true;
options.materialsCommon = false; options.materialsCommon = false;
} }
var extension = path.extname(gltfPath).toLowerCase(); return loadObj(objPath, options)
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);
})
.then(function(objData) { .then(function(objData) {
return createGltf(objData, options); return createGltf(objData, options);
}) })
.then(function(gltf) { .then(function(gltf) {
return writeUris(gltf, gltfPath, options); return writeGltf(gltf, 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);
}); });
} }
function loadOverridingImages(options) { function getDefaultLogger() {
var overridingImages = options.overridingImages; return function(message) {
var promises = []; console.log(message);
for (var imageName in overridingImages) { };
if (overridingImages.hasOwnProperty(imageName)) {
promises.push(loadOverridingImage(imageName, overridingImages, options));
}
}
return Promise.all(promises);
} }
function loadOverridingImage(imageName, overridingImages, options) { function getDefaultWriter(outputDirectory) {
var imagePath = overridingImages[imageName]; if (defined(outputDirectory)) {
var imageOptions; return function(file, data) {
if (imageName === 'baseColorTexture') { var outputFile = path.join(outputDirectory, file);
imageOptions = { return fsExtra.outputFile(outputFile, data);
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.');
});
} }
/** /**
@ -167,79 +112,60 @@ function loadOverridingImage(imageName, overridingImages, options) {
*/ */
obj2gltf.defaults = { 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 * @type Boolean
* @default false * @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. * shader files, and textures instead of embedding them in the glTF.
* @type Boolean * @type Boolean
* @default false * @default false
*/ */
separate: false, separate : false,
/** /**
* Gets or sets whether to write out separate textures only. * Gets or sets whether to write out separate textures only.
* @type Boolean * @type Boolean
* @default false * @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. * 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 * @type Boolean
* @default false * @default false
*/ */
checkTransparency: false, checkTransparency : false,
/** /**
* Gets or sets whether the source model can reference paths outside of its directory. * Gets or sets whether the source model can reference paths outside of its directory.
* @type Boolean * @type Boolean
* @default false * @default false
*/ */
secure: 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',
/** /**
* Gets or sets whether to pack the occlusion texture in the red channel of the metallic-roughness texture. * Gets or sets whether to pack the occlusion texture in the red channel of the metallic-roughness texture.
* @type Boolean * @type Boolean
* @default false * @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 * @type Boolean
* @default false * @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 * @type Boolean
* @default false * @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 * @type Boolean
* @default false * @default false
*/ */
materialsCommon: false, materialsCommon : false
/**
* @private
*/
logger: function(message) {
console.log(message);
}
}; };
/** /**
@ -248,3 +174,12 @@ obj2gltf.defaults = {
* *
* @param {String} message The message to log. * @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.
*/

15
lib/outsideDirectory.js Normal file
View File

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

View File

@ -15,13 +15,13 @@ module.exports = readLines;
* @private * @private
*/ */
function readLines(path, callback) { function readLines(path, callback) {
return new Promise(function (resolve, reject) { return new Promise(function(resolve, reject) {
var stream = fsExtra.createReadStream(path); var stream = fsExtra.createReadStream(path);
stream.on('error', reject); stream.on('error', reject);
stream.on('end', resolve); stream.on('end', resolve);
var lineReader = readline.createInterface({ var lineReader = readline.createInterface({
input: stream input : stream
}); });
lineReader.on('line', callback); lineReader.on('line', callback);
}); });

View File

@ -1,90 +1,73 @@
'use strict'; 'use strict';
var Cesium = require('cesium'); var Cesium = require('cesium');
var fsExtra = require('fs-extra');
var mime = require('mime'); var mime = require('mime');
var path = require('path');
var PNG = require('pngjs').PNG; var PNG = require('pngjs').PNG;
var Promise = require('bluebird'); var Promise = require('bluebird');
var getBufferPadded = require('./getBufferPadded'); var getBufferPadded = require('./getBufferPadded');
var gltfToGlb = require('./gltfToGlb');
var defined = Cesium.defined; var defined = Cesium.defined;
var RuntimeError = Cesium.RuntimeError; var RuntimeError = Cesium.RuntimeError;
module.exports = writeUris; module.exports = writeGltf;
/** /**
* Write glTF resources as embedded data uris or external files. * Write glTF resources as embedded data uris or external files.
* *
* @param {Object} gltf The glTF asset. * @param {Object} gltf The glTF asset.
* @param {String} gltfPath Path where the glTF will be saved. * @param {Object} options The options object passed along from lib/obj2gltf.js
* @param {Object} options An object with the following properties: * @returns {Promise} A promise that resolves to the glTF JSON or glb buffer.
* @param {Boolean} options.separate Writes out separate buffers.
* @param {Boolean} options.separateTextures Write out separate textures only.
* @returns {Promise} A promise that resolves to the glTF asset.
* *
* @private * @private
*/ */
function writeUris(gltf, gltfPath, options) { function writeGltf(gltf, options) {
return encodeImages(gltf) return encodeTextures(gltf)
.then(function() { .then(function() {
var binary = options.binary;
var separate = options.separate; var separate = options.separate;
var separateTextures = options.separateTextures; 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 = []; var promises = [];
if (separateTextures) { if (separateTextures) {
promises.push(writeSeparateTextures(gltf, gltfPath)); promises.push(writeSeparateTextures(gltf, options));
} else { } else {
writeEmbeddedTextures(gltf); writeEmbeddedTextures(gltf);
} }
if (separate) { if (separate) {
promises.push(writeSeparateBuffer(gltf, gltfPath, name)); promises.push(writeSeparateBuffer(gltf, options));
} else { } else if (!binary) {
writeEmbeddedBuffer(gltf); writeEmbeddedBuffer(gltf);
} }
var binaryBuffer = gltf.buffers[0].extras._obj2gltf.source;
return Promise.all(promises) return Promise.all(promises)
.then(function() { .then(function() {
deleteExtras(gltf); deleteExtras(gltf);
cleanup(gltf); removeEmpty(gltf);
if (binary) {
return gltfToGlb(gltf, binaryBuffer);
}
return gltf; return gltf;
}); });
}); });
} }
function encodePng(image) { function encodePng(texture) {
// Constants defined by pngjs // Constants defined by pngjs
var rgbColorType = 2; var rgbColorType = 2;
var rgbaColorType = 6; var rgbaColorType = 6;
var png = new PNG({ var png = new PNG({
width : image.width, width : texture.width,
height : image.height, height : texture.height,
colorType : image.transparent ? rgbaColorType : rgbColorType, colorType : texture.transparent ? rgbaColorType : rgbColorType,
inputColorType : rgbaColorType, inputColorType : rgbaColorType,
inputHasAlpha : true inputHasAlpha : true
}); });
png.data = image.decoded; png.data = texture.pixels;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var chunks = []; var chunks = [];
@ -99,24 +82,22 @@ function encodePng(image) {
}); });
} }
function encodeImage(image) { function encodeTexture(texture) {
var imageExtras = image.extras._obj2gltf; if (!defined(texture.source) && defined(texture.pixels) && texture.extension === '.png') {
if (!defined(imageExtras.source) && defined(imageExtras.decoded) && imageExtras.extension === '.png') { return encodePng(texture)
return encodePng(imageExtras)
.then(function(encoded) { .then(function(encoded) {
imageExtras.source = encoded; texture.source = encoded;
}); });
} }
} }
function encodeImages(gltf) { function encodeTextures(gltf) {
// Dynamically generated metallicRoughnessOcclusion and specularGlossiness // Dynamically generated PBR textures need to be encoded to png prior to being saved
// textures need to be encoded to png's prior to being saved.
var encodePromises = []; var encodePromises = [];
var images = gltf.images; var images = gltf.images;
var length = images.length; var length = images.length;
for (var i = 0; i < length; ++i) { for (var i = 0; i < length; ++i) {
encodePromises.push(encodeImage(images[i])); encodePromises.push(encodeTexture(images[i].extras._obj2gltf));
} }
return Promise.all(encodePromises); return Promise.all(encodePromises);
} }
@ -130,12 +111,6 @@ function deleteExtras(gltf) {
for (var i = 0; i < imagesLength; ++i) { for (var i = 0; i < imagesLength; ++i) {
delete images[i].extras; 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) { function removeEmpty(json) {
@ -148,33 +123,33 @@ function removeEmpty(json) {
}); });
} }
function cleanup(gltf) { function writeSeparateBuffer(gltf, options) {
removeEmpty(gltf);
}
function writeSeparateBuffer(gltf, gltfPath, name) {
var buffer = gltf.buffers[0]; var buffer = gltf.buffers[0];
var source = buffer.extras._obj2gltf.source; var source = buffer.extras._obj2gltf.source;
var bufferUri = name + '.bin'; var bufferUri = buffer.name + '.bin';
buffer.uri = bufferUri; buffer.uri = bufferUri;
var bufferPath = path.join(path.dirname(gltfPath), bufferUri); return options.writer(bufferUri, source);
return fsExtra.outputFile(bufferPath, source);
} }
function writeSeparateTextures(gltf, gltfPath) { function writeSeparateTextures(gltf, options) {
var images = gltf.images; var images = gltf.images;
return Promise.map(images, function(image) { return Promise.map(images, function(image) {
var extras = image.extras._obj2gltf; var texture = image.extras._obj2gltf;
var imageUri = image.name + extras.extension; var imageUri = image.name + texture.extension;
image.uri = imageUri; image.uri = imageUri;
var imagePath = path.join(path.dirname(gltfPath), imageUri); return options.writer(imageUri, texture.source);
return fsExtra.outputFile(imagePath, extras.source);
}, {concurrency : 10}); }, {concurrency : 10});
} }
function writeEmbeddedBuffer(gltf) { function writeEmbeddedBuffer(gltf) {
var buffer = gltf.buffers[0]; var buffer = gltf.buffers[0];
var source = buffer.extras._obj2gltf.source; 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'); buffer.uri = 'data:application/octet-stream;base64,' + source.toString('base64');
} }
@ -189,19 +164,19 @@ function writeEmbeddedTextures(gltf) {
for (var i = 0; i < imagesLength; ++i) { for (var i = 0; i < imagesLength; ++i) {
var image = images[i]; var image = images[i];
var extras = image.extras._obj2gltf; var texture = image.extras._obj2gltf;
var imageSource = extras.source; var textureSource = texture.source;
var imageByteLength = imageSource.length; var textureByteLength = textureSource.length;
image.mimeType = mime.lookup(extras.extension); image.mimeType = mime.lookup(texture.extension);
image.bufferView = gltf.bufferViews.length; image.bufferView = gltf.bufferViews.length;
gltf.bufferViews.push({ gltf.bufferViews.push({
buffer : 0, buffer : 0,
byteOffset : byteOffset, byteOffset : byteOffset,
byteLength : imageByteLength byteLength : textureByteLength
}); });
byteOffset += imageByteLength; byteOffset += textureByteLength;
sources.push(imageSource); sources.push(textureSource);
} }
var source = getBufferPadded(Buffer.concat(sources)); var source = getBufferPadded(Buffer.concat(sources));

View File

@ -27,25 +27,25 @@
}, },
"dependencies": { "dependencies": {
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"cesium": "^1.35.2", "cesium": "^1.36.0",
"fs-extra": "^4.0.0", "fs-extra": "^4.0.1",
"jpeg-js": "^0.3.3", "jpeg-js": "^0.3.3",
"mime": "^1.3.6", "mime": "^1.3.6",
"pngjs": "^3.2.0", "pngjs": "^3.3.0",
"uuid": "^3.1.0", "uuid": "^3.1.0",
"yargs": "^8.0.2" "yargs": "^8.0.2"
}, },
"devDependencies": { "devDependencies": {
"coveralls": "^2.13.1", "coveralls": "^2.13.1",
"eslint": "^4.2.0", "eslint": "^4.4.1",
"eslint-config-cesium": "^2.0.1", "eslint-config-cesium": "^2.0.1",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"jasmine": "^2.6.0", "jasmine": "^2.7.0",
"jasmine-spec-reporter": "^4.1.1", "jasmine-spec-reporter": "^4.2.0",
"jsdoc": "^3.5.3", "jsdoc": "^3.5.4",
"nyc": "^11.0.3", "nyc": "^11.1.0",
"open": "^0.0.5", "open": "^0.0.5",
"requirejs": "^2.3.3" "requirejs": "^2.3.4"
}, },
"scripts": { "scripts": {
"jsdoc": "jsdoc ./lib -R ./README.md -d doc", "jsdoc": "jsdoc ./lib -R ./README.md -d doc",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -3,10 +3,10 @@
newmtl Material newmtl Material
Ns 96.078431 Ns 96.078431
Ka 0.000000 0.000000 0.000000 Ka 0.100000 0.000000 0.000000
Kd 0.640000 0.640000 0.640000 Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000 Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000 Ke 0.000000 0.000000 0.100000
Ni 1.000000 Ni 1.000000
d 1.000000 d 1.000000
illum 2 illum 2

View File

@ -3,93 +3,51 @@ var Cesium = require('cesium');
var Promise = require('bluebird'); var Promise = require('bluebird');
var obj2gltf = require('../../lib/obj2gltf'); var obj2gltf = require('../../lib/obj2gltf');
var createGltf = require('../../lib/createGltf'); var createGltf = require('../../lib/createGltf');
var loadImage = require('../../lib/loadImage');
var loadObj = require('../../lib/loadObj'); var loadObj = require('../../lib/loadObj');
var Material = require('../../lib/Material');
var clone = Cesium.clone; var clone = Cesium.clone;
var WebGLConstants = Cesium.WebGLConstants; var WebGLConstants = Cesium.WebGLConstants;
var boxObjUrl = 'specs/data/box/box.obj'; var boxObjPath = 'specs/data/box/box.obj';
var groupObjUrl = 'specs/data/box-objects-groups-materials/box-objects-groups-materials.obj'; var groupObjPath = 'specs/data/box-objects-groups-materials/box-objects-groups-materials.obj';
var diffuseTextureUrl = 'specs/data/box-textured/cesium.png'; var complexObjPath = 'specs/data/box-complex-material/box-complex-material.obj';
var transparentDiffuseTextureUrl = 'specs/data/box-complex-material/diffuse.png'; var noMaterialsObjPath = 'specs/data/box-no-materials/box-no-materials.obj';
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 = clone(obj2gltf.defaults); var options;
defaultOptions.overridingImages = {};
var checkTransparencyOptions = clone(defaultOptions);
checkTransparencyOptions.checkTransparency = true;
var decodeOptions = clone(defaultOptions);
decodeOptions.decode = 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() { describe('createGltf', function() {
var boxObjData; var boxObjData;
var groupObjData; var groupObjData;
var diffuseTexture; var complexObjData;
var transparentDiffuseTexture; var noMaterialsObjData;
var ambientTexture;
var normalTexture;
var emissiveTexture;
var metallicTexture;
var roughnessTexture;
beforeEach(function(done) { beforeEach(function(done) {
spyOn(console, 'log'); options = clone(obj2gltf.defaults);
options.overridingTextures = {};
options.logger = function() {};
return Promise.all([ return Promise.all([
loadObj(boxObjUrl, decodeOptions) loadObj(boxObjPath, options)
.then(function(data) { .then(function(data) {
boxObjData = data; boxObjData = data;
}), }),
loadObj(groupObjUrl, decodeOptions) loadObj(groupObjPath, options)
.then(function(data) { .then(function(data) {
groupObjData = data; groupObjData = data;
}), }),
loadImage(diffuseTextureUrl, decodeOptions) loadObj(complexObjPath, options)
.then(function(image) { .then(function(data) {
diffuseTexture = image; complexObjData = data;
}), }),
loadImage(transparentDiffuseTextureUrl, checkTransparencyOptions) loadObj(noMaterialsObjPath, options)
.then(function(image) { .then(function(data) {
transparentDiffuseTexture = image; noMaterialsObjData = data;
}),
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); ]).then(done);
}); });
it('simple gltf', function() { it('simple gltf', function() {
var gltf = createGltf(boxObjData, defaultOptions); var gltf = createGltf(boxObjData, options);
expect(gltf.materials.length).toBe(1); expect(gltf.materials.length).toBe(1);
expect(gltf.scene).toBe(0); expect(gltf.scene).toBe(0);
@ -113,7 +71,7 @@ describe('createGltf', function() {
}); });
it('multiple nodes, meshes, and primitives', function() { it('multiple nodes, meshes, and primitives', function() {
var gltf = createGltf(groupObjData, defaultOptions); var gltf = createGltf(groupObjData, options);
expect(gltf.materials.length).toBe(3); expect(gltf.materials.length).toBe(3);
expect(gltf.scene).toBe(0); expect(gltf.scene).toBe(0);
@ -131,10 +89,53 @@ describe('createGltf', function() {
} }
}); });
it('multiple textures', function() {
var gltf = createGltf(complexObjData, options);
var material = gltf.materials[0];
var pbr = material.pbrMetallicRoughness;
var textures = [pbr.metallicRoughnessTexture, pbr.baseColorTexture, material.emissiveTexture, material.normalTexture, material.occlusionTexture];
expect(textures.map(function(texture) {
return texture.index;
}).sort()).toEqual([0, 1, 2, 3, 4]);
expect(gltf.samplers[0]).toBeDefined();
});
it('creates default material', function() {
var gltf = createGltf(noMaterialsObjData, options);
var material = gltf.materials[0];
var pbr = material.pbrMetallicRoughness;
expect(material.name).toBe('default');
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('adds KHR_materials_pbrSpecularGlossiness extension when specularGlossiness is set', function() {
options.specularGlossiness = true;
var gltf = createGltf(noMaterialsObjData, options);
expect(gltf.extensionsUsed).toEqual(['KHR_materials_pbrSpecularGlossiness']);
expect(gltf.extensionsRequired).toEqual(['KHR_materials_pbrSpecularGlossiness']);
});
it('adds KHR_materials_common extension when materialsCommon is set', function() {
options.materialsCommon = true;
var gltf = createGltf(noMaterialsObjData, options);
expect(gltf.extensionsUsed).toEqual(['KHR_materials_common']);
expect(gltf.extensionsRequired).toEqual(['KHR_materials_common']);
});
it('runs without normals', function() { it('runs without normals', function() {
boxObjData.nodes[0].meshes[0].normals.length = 0; boxObjData.nodes[0].meshes[0].normals.length = 0;
var gltf = createGltf(boxObjData, defaultOptions); var gltf = createGltf(boxObjData, options);
var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes; var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes;
expect(attributes.POSITION).toBeDefined(); expect(attributes.POSITION).toBeDefined();
expect(attributes.NORMAL).toBeUndefined(); expect(attributes.NORMAL).toBeUndefined();
@ -144,7 +145,7 @@ describe('createGltf', function() {
it('runs without uvs', function() { it('runs without uvs', function() {
boxObjData.nodes[0].meshes[0].uvs.length = 0; boxObjData.nodes[0].meshes[0].uvs.length = 0;
var gltf = createGltf(boxObjData, defaultOptions); var gltf = createGltf(boxObjData, options);
var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes; var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes;
expect(attributes.POSITION).toBeDefined(); expect(attributes.POSITION).toBeDefined();
expect(attributes.NORMAL).toBeDefined(); expect(attributes.NORMAL).toBeDefined();
@ -155,7 +156,7 @@ describe('createGltf', function() {
boxObjData.nodes[0].meshes[0].normals.length = 0; boxObjData.nodes[0].meshes[0].normals.length = 0;
boxObjData.nodes[0].meshes[0].uvs.length = 0; boxObjData.nodes[0].meshes[0].uvs.length = 0;
var gltf = createGltf(boxObjData, defaultOptions); var gltf = createGltf(boxObjData, options);
var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes; var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes;
expect(attributes.POSITION).toBeDefined(); expect(attributes.POSITION).toBeDefined();
expect(attributes.NORMAL).toBeUndefined(); expect(attributes.NORMAL).toBeUndefined();
@ -195,7 +196,7 @@ describe('createGltf', function() {
var indicesLength = mesh.primitives[0].indices.length; var indicesLength = mesh.primitives[0].indices.length;
var vertexCount = mesh.positions.length / 3; var vertexCount = mesh.positions.length / 3;
var gltf = createGltf(boxObjData, defaultOptions); var gltf = createGltf(boxObjData, options);
var primitive = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0]; var primitive = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0];
var indicesAccessor = gltf.accessors[primitive.indices]; var indicesAccessor = gltf.accessors[primitive.indices];
expect(indicesAccessor.count).toBe(indicesLength); expect(indicesAccessor.count).toBe(indicesLength);
@ -205,396 +206,4 @@ describe('createGltf', function() {
var positionAccessor = gltf.accessors[primitive.attributes.POSITION]; var positionAccessor = gltf.accessors[primitive.attributes.POSITION];
expect(positionAccessor.count).toBe(vertexCount); 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('specularGlossiness', 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);
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;
expect(kmc.technique).toBe('LAMBERT');
expect(values.ambient).toEqual([0.0, 0.0, 0.0, 1]);
expect(values.diffuse).toEqual([0.5, 0.5, 0.5, 1]);
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 options = clone(defaultOptions);
options.materialsCommon = true;
var defaultMaterial = setDefaultMaterial(boxObjData);
defaultMaterial.diffuseTexture = diffuseTextureUrl;
boxObjData.images.push(diffuseTexture);
var gltf = createGltf(boxObjData, options);
var kmc = gltf.materials[0].extensions.KHR_materials_common;
var texture = gltf.textures[0];
var image = gltf.images[0];
expect(kmc.technique).toBe('LAMBERT');
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({
name : 'cesium_texture',
sampler : 0,
source : 0
});
expect(image).toBeDefined();
expect(image.name).toBe('cesium');
expect(image.extras._obj2gltf.source).toBeDefined();
expect(image.extras._obj2gltf.extension).toBe('.png');
expect(gltf.samplers[0]).toEqual({
magFilter : WebGLConstants.LINEAR,
minFilter : WebGLConstants.NEAREST_MIPMAP_LINEAR,
wrapS : WebGLConstants.REPEAT,
wrapT : WebGLConstants.REPEAT
});
});
it('sets material for alpha less than 1', function() {
var options = clone(defaultOptions);
options.materialsCommon = true;
var defaultMaterial = setDefaultMaterial(boxObjData);
defaultMaterial.alpha = 0.4;
var gltf = createGltf(boxObjData, options);
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);
expect(kmc.values.transparent).toBe(true);
expect(kmc.values.doubleSided).toBe(true);
});
it('sets material for diffuse texture and alpha less than 1', function() {
var options = clone(defaultOptions);
options.materialsCommon = true;
var defaultMaterial = setDefaultMaterial(boxObjData);
defaultMaterial.diffuseTexture = diffuseTextureUrl;
defaultMaterial.alpha = 0.4;
boxObjData.images.push(diffuseTexture);
var gltf = createGltf(boxObjData, options);
var kmc = gltf.materials[0].extensions.KHR_materials_common;
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);
});
it('sets material for transparent diffuse texture', function() {
var options = clone(defaultOptions);
options.materialsCommon = true;
var defaultMaterial = setDefaultMaterial(boxObjData);
defaultMaterial.diffuseTexture = transparentDiffuseTextureUrl;
boxObjData.images.push(transparentDiffuseTexture);
var gltf = createGltf(boxObjData, options);
var kmc = gltf.materials[0].extensions.KHR_materials_common;
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);
});
it('sets material for specular', function() {
var options = clone(defaultOptions);
options.materialsCommon = true;
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[0].extensions.KHR_materials_common;
expect(kmc.technique).toBe('PHONG');
expect(kmc.values.specular).toEqual([0.1, 0.1, 0.2, 1]);
expect(kmc.values.shininess).toEqual(0.1);
});
it('sets constant material when there are no normals', function() {
var options = clone(defaultOptions);
options.materialsCommon = true;
boxObjData.nodes[0].meshes[0].normals.length = 0;
var defaultMaterial = setDefaultMaterial(boxObjData);
defaultMaterial.diffuseTexture = diffuseTextureUrl;
boxObjData.images.push(diffuseTexture);
var gltf = createGltf(boxObjData, options);
var kmc = gltf.materials[0].extensions.KHR_materials_common;
expect(kmc.technique).toBe('CONSTANT');
expect(kmc.values.emission).toEqual({index : 0});
});
it('sets default material when texture is missing', function() {
var options = clone(defaultOptions);
options.materialsCommon = true;
var defaultMaterial = setDefaultMaterial(boxObjData);
defaultMaterial.diffuseTexture = diffuseTextureUrl;
var gltf = createGltf(boxObjData, options);
var kmc = gltf.materials[0].extensions.KHR_materials_common;
expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]);
});
it('uses default material (1)', function() {
var options = clone(defaultOptions);
options.materialsCommon = true;
boxObjData.nodes[0].meshes[0].primitives[0].material = undefined;
// Creates a material called "default"
var gltf = createGltf(boxObjData, options);
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]);
});
it('uses default material (2)', function() {
var options = clone(defaultOptions);
options.materialsCommon = true;
boxObjData.materials = {};
// Uses the original name of the material
var gltf = createGltf(boxObjData, options);
var kmc = gltf.materials[0].extensions.KHR_materials_common;
expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]);
});
it('ambient of [1, 1, 1] is treated as [0, 0, 0]', function() {
var options = clone(defaultOptions);
options.materialsCommon = true;
boxObjData.materials[0].ambientColor = [1.0, 1.0, 1.0, 1.0];
var gltf = createGltf(boxObjData, options);
var ambient = gltf.materials[0].extensions.KHR_materials_common.values.ambient;
expect(ambient).toEqual([0.0, 0.0, 0.0, 1.0]);
});
});
}); });

View File

@ -1,113 +0,0 @@
'use strict';
var loadImage = require('../../lib/loadImage');
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';
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';
describe('loadImage', function() {
it('loads png image', function(done) {
expect(loadImage(pngImage)
.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(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(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(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(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(image) {
expect(image.transparent).toBe(false);
}), done).toResolve();
});
it('loads image with checkTransparency flag', function(done) {
var options = {
checkTransparency : true
};
expect(loadImage(transparentImage, options)
.then(function(image) {
expect(image.transparent).toBe(true);
}), done).toResolve();
});
it('loads and decodes png', function(done) {
var options = {
decode : true
};
expect(loadImage(pngImage, options)
.then(function(image) {
expect(image.decoded).toBeDefined();
expect(image.width).toBe(211);
expect(image.height).toBe(211);
}), done).toResolve();
});
it('loads and decodes jpeg', function(done) {
var options = {
decode : true
};
expect(loadImage(jpegImage, options)
.then(function(image) {
expect(image.decoded).toBeDefined();
expect(image.width).toBe(211);
expect(image.height).toBe(211);
}), done).toResolve();
});
});

View File

@ -1,49 +1,459 @@
'use strict'; 'use strict';
var path = require('path'); var Cesium = require('cesium');
var Promise = require('bluebird');
var fsExtra = require('fs-extra');
var loadMtl = require('../../lib/loadMtl'); var loadMtl = require('../../lib/loadMtl');
var loadTexture = require('../../lib/loadTexture');
var obj2gltf = require('../../lib/obj2gltf'); var obj2gltf = require('../../lib/obj2gltf');
var Texture = require('../../lib/Texture');
var complexMaterialUrl = 'specs/data/box-complex-material/box-complex-material.mtl'; var clone = Cesium.clone;
var multipleMaterialsUrl = 'specs/data/box-multiple-materials/box-multiple-materials.mtl';
function getImagePath(objPath, relativePath) { var coloredMaterialPath = 'specs/data/box/box.mtl';
return path.resolve(path.dirname(objPath), relativePath); var texturedMaterialPath = 'specs/data/box-complex-material/box-complex-material.mtl';
} var multipleMaterialsPath = 'specs/data/box-multiple-materials/box-multiple-materials.mtl';
var externalMaterialPath = 'specs/data/box-external-resources/box-external-resources.mtl';
var defaultOptions = obj2gltf.defaults; var diffuseTexturePath = 'specs/data/box-textured/cesium.png';
var transparentDiffuseTexturePath = 'specs/data/box-complex-material/diffuse.png';
var alphaTexturePath = 'specs/data/box-complex-material/alpha.png';
var ambientTexturePath = 'specs/data/box-complex-material/ambient.gif';
var normalTexturePath = 'specs/data/box-complex-material/bump.png';
var emissiveTexturePath = 'specs/data/box-complex-material/emission.jpg';
var specularTexturePath = 'specs/data/box-complex-material/specular.jpeg';
var specularShininessTexturePath = 'specs/data/box-complex-material/shininess.png';
var diffuseTexture;
var transparentDiffuseTexture;
var alphaTexture;
var ambientTexture;
var normalTexture;
var emissiveTexture;
var specularTexture;
var specularShininessTexture;
var checkTransparencyOptions = {
checkTransparency : true
};
var decodeOptions = {
decode : true
};
var options;
describe('loadMtl', function() { describe('loadMtl', function() {
it('loads complex material', function(done) { beforeAll(function(done) {
expect(loadMtl(complexMaterialUrl, defaultOptions) return Promise.all([
loadTexture(diffuseTexturePath)
.then(function(texture) {
diffuseTexture = texture;
}),
loadTexture(transparentDiffuseTexturePath, checkTransparencyOptions)
.then(function(texture) {
transparentDiffuseTexture = texture;
}),
loadTexture(alphaTexturePath, decodeOptions)
.then(function(texture) {
alphaTexture = texture;
}),
loadTexture(ambientTexturePath)
.then(function(texture) {
ambientTexture = texture;
}),
loadTexture(normalTexturePath)
.then(function(texture) {
normalTexture = texture;
}),
loadTexture(emissiveTexturePath)
.then(function(texture) {
emissiveTexture = texture;
}),
loadTexture(specularTexturePath, decodeOptions)
.then(function(texture) {
specularTexture = texture;
}),
loadTexture(specularShininessTexturePath, decodeOptions)
.then(function(texture) {
specularShininessTexture = texture;
})
]).then(done);
});
beforeEach(function() {
options = clone(obj2gltf.defaults);
options.overridingTextures = {};
options.logger = function() {};
});
it('loads mtl', function(done) {
options.metallicRoughness = true;
expect(loadMtl(coloredMaterialPath, options)
.then(function(materials) { .then(function(materials) {
expect(materials.length).toBe(1);
var material = materials[0]; var material = materials[0];
var pbr = material.pbrMetallicRoughness;
expect(pbr.baseColorTexture).toBeUndefined();
expect(pbr.metallicRoughnessTexture).toBeUndefined();
expect(pbr.baseColorFactor).toEqual([0.64, 0.64, 0.64, 1.0]);
expect(pbr.metallicFactor).toBe(0.5);
expect(pbr.roughnessFactor).toBe(96.078431);
expect(material.name).toBe('Material'); expect(material.name).toBe('Material');
expect(material.ambientColor).toEqual([0.2, 0.2, 0.2, 1.0]); expect(material.emissiveTexture).toBeUndefined();
expect(material.emissiveColor).toEqual([0.1, 0.1, 0.1, 1.0]); expect(material.normalTexture).toBeUndefined();
expect(material.diffuseColor).toEqual([0.64, 0.64, 0.64, 1.0]); expect(material.ambientTexture).toBeUndefined();
expect(material.specularColor).toEqual([0.5, 0.5, 0.5, 1.0]); expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.1]);
expect(material.specularShininess).toEqual(96.078431); expect(material.alphaMode).toBe('OPAQUE');
expect(material.alpha).toEqual(0.9); expect(material.doubleSided).toBe(false);
expect(material.ambientTexture).toEqual(getImagePath(complexMaterialUrl, 'ambient.gif')); }), done).toResolve();
expect(material.emissiveTexture).toEqual(getImagePath(complexMaterialUrl, 'emission.jpg')); });
expect(material.diffuseTexture).toEqual(getImagePath(complexMaterialUrl, 'diffuse.png'));
expect(material.specularTexture).toEqual(getImagePath(complexMaterialUrl, 'specular.jpeg')); it('loads mtl with textures', function(done) {
expect(material.specularShininessTexture).toEqual(getImagePath(complexMaterialUrl, 'shininess.png')); options.metallicRoughness = true;
expect(material.normalTexture).toEqual(getImagePath(complexMaterialUrl, 'bump.png')); expect(loadMtl(texturedMaterialPath, options)
expect(material.alphaTexture).toEqual(getImagePath(complexMaterialUrl, 'alpha.png')); .then(function(materials) {
expect(materials.length).toBe(1);
var material = materials[0];
var pbr = material.pbrMetallicRoughness;
expect(pbr.baseColorTexture).toBeDefined();
expect(pbr.metallicRoughnessTexture).toBeDefined();
expect(pbr.baseColorFactor).toEqual([1.0, 1.0, 1.0, 0.9]);
expect(pbr.metallicFactor).toBe(1.0);
expect(pbr.roughnessFactor).toBe(1.0);
expect(material.name).toBe('Material');
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('BLEND');
expect(material.doubleSided).toBe(true);
}), done).toResolve(); }), done).toResolve();
}); });
it('loads mtl with multiple materials', function(done) { it('loads mtl with multiple materials', function(done) {
expect(loadMtl(multipleMaterialsUrl, defaultOptions) options.metallicRoughness = true;
expect(loadMtl(multipleMaterialsPath, options)
.then(function(materials) { .then(function(materials) {
expect(materials.length).toBe(3); expect(materials.length).toBe(3);
expect(materials[0].name).toBe('Blue'); 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].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].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(); }), done).toResolve();
}); });
it('sets overriding textures', function(done) {
spyOn(fsExtra, 'readFile').and.callThrough();
options.overridingTextures = {
metallicRoughnessOcclusionTexture : alphaTexturePath,
baseColorTexture : alphaTexturePath,
emissiveTexture : emissiveTexturePath
};
expect(loadMtl(texturedMaterialPath, options)
.then(function(materials) {
var material = materials[0];
var pbr = material.pbrMetallicRoughness;
expect(pbr.baseColorTexture.name).toBe('alpha');
expect(pbr.metallicRoughnessTexture.name).toBe('alpha');
expect(material.emissiveTexture.name).toBe('emission');
expect(material.normalTexture.name).toBe('bump');
expect(fsExtra.readFile.calls.count()).toBe(3);
}), done).toResolve();
});
it('loads texture outside of the mtl directory', function(done) {
expect(loadMtl(externalMaterialPath, options)
.then(function(materials) {
var material = materials[0];
var baseColorTexture = material.pbrMetallicRoughness.baseColorTexture;
expect(baseColorTexture.source).toBeDefined();
expect(baseColorTexture.name).toBe('cesium');
}), done).toResolve();
});
it('does not load texture outside of the mtl directory when secure is true', function(done) {
var spy = jasmine.createSpy('logger');
options.logger = spy;
options.secure = true;
expect(loadMtl(externalMaterialPath, options)
.then(function(materials) {
var material = materials[0];
var baseColorTexture = material.pbrMetallicRoughness.baseColorTexture;
expect(baseColorTexture).toBeUndefined();
expect(spy.calls.argsFor(0)[0].indexOf('Could not read texture file') >= 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]);
});
});
}); });

View File

@ -1,37 +1,33 @@
'use strict'; 'use strict';
var Cesium = require('cesium'); var Cesium = require('cesium');
var path = require('path');
var Promise = require('bluebird'); var Promise = require('bluebird');
var loadObj = require('../../lib/loadObj'); var loadObj = require('../../lib/loadObj');
var obj2gltf = require('../../lib/obj2gltf'); var obj2gltf = require('../../lib/obj2gltf');
var Cartesian3 = Cesium.Cartesian3;
var clone = Cesium.clone; var clone = Cesium.clone;
var RuntimeError = Cesium.RuntimeError; var RuntimeError = Cesium.RuntimeError;
var objUrl = 'specs/data/box/box.obj'; var objPath = 'specs/data/box/box.obj';
var objRotatedUrl = 'specs/data/box-rotated/box-rotated.obj'; var objNormalsPath = 'specs/data/box-normals/box-normals.obj';
var objNormalsUrl = 'specs/data/box-normals/box-normals.obj'; var objUvsPath = 'specs/data/box-uvs/box-uvs.obj';
var objUvsUrl = 'specs/data/box-uvs/box-uvs.obj'; var objPositionsOnlyPath = 'specs/data/box-positions-only/box-positions-only.obj';
var objPositionsOnlyUrl = 'specs/data/box-positions-only/box-positions-only.obj'; var objNegativeIndicesPath = 'specs/data/box-negative-indices/box-negative-indices.obj';
var objNegativeIndicesUrl = 'specs/data/box-negative-indices/box-negative-indices.obj'; var objTrianglesPath = 'specs/data/box-triangles/box-triangles.obj';
var objTrianglesUrl = 'specs/data/box-triangles/box-triangles.obj'; var objObjectsPath = 'specs/data/box-objects/box-objects.obj';
var objObjectsUrl = 'specs/data/box-objects/box-objects.obj'; var objGroupsPath = 'specs/data/box-groups/box-groups.obj';
var objGroupsUrl = 'specs/data/box-groups/box-groups.obj'; var objObjectsGroupsPath = 'specs/data/box-objects-groups/box-objects-groups.obj';
var objObjectsGroupsUrl = 'specs/data/box-objects-groups/box-objects-groups.obj'; var objUsemtlPath = 'specs/data/box-usemtl/box-usemtl.obj';
var objUsemtlUrl = 'specs/data/box-usemtl/box-usemtl.obj'; var objNoMaterialsPath = 'specs/data/box-no-materials/box-no-materials.obj';
var objNoMaterialsUrl = 'specs/data/box-no-materials/box-no-materials.obj'; var objMultipleMaterialsPath = 'specs/data/box-multiple-materials/box-multiple-materials.obj';
var objMultipleMaterialsUrl = 'specs/data/box-multiple-materials/box-multiple-materials.obj'; var objUncleanedPath = 'specs/data/box-uncleaned/box-uncleaned.obj';
var objUncleanedUrl = 'specs/data/box-uncleaned/box-uncleaned.obj'; var objMtllibPath = 'specs/data/box-mtllib/box-mtllib.obj';
var objMtllibUrl = 'specs/data/box-mtllib/box-mtllib.obj'; var objMissingMtllibPath = 'specs/data/box-missing-mtllib/box-missing-mtllib.obj';
var objMissingMtllibUrl = 'specs/data/box-missing-mtllib/box-missing-mtllib.obj'; var objExternalResourcesPath = 'specs/data/box-external-resources/box-external-resources.obj';
var objExternalResourcesUrl = 'specs/data/box-external-resources/box-external-resources.obj'; var objTexturedPath = 'specs/data/box-textured/box-textured.obj';
var objTexturedUrl = 'specs/data/box-textured/box-textured.obj'; var objMissingTexturePath = 'specs/data/box-missing-texture/box-missing-texture.obj';
var objMissingTextureUrl = 'specs/data/box-missing-texture/box-missing-texture.obj'; var objSubdirectoriesPath = 'specs/data/box-subdirectories/box-textured.obj';
var objSubdirectoriesUrl = 'specs/data/box-subdirectories/box-textured.obj'; var objInvalidContentsPath = 'specs/data/box/box.mtl';
var objComplexMaterialUrl = 'specs/data/box-complex-material/box-complex-material.obj'; var objInvalidPath = 'invalid.obj';
var objInvalidContentsUrl = 'specs/data/box/box.mtl';
var objInvalidUrl = 'invalid.obj';
function getMeshes(data) { function getMeshes(data) {
var meshes = []; var meshes = [];
@ -57,27 +53,25 @@ function getPrimitives(data) {
return primitives; return primitives;
} }
function getImagePath(objPath, relativePath) { var options;
return path.resolve(path.dirname(objPath), relativePath);
}
var defaultOptions = obj2gltf.defaults;
describe('loadObj', function() { describe('loadObj', function() {
beforeEach(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) { it('loads obj with positions, normals, and uvs', function(done) {
expect(loadObj(objUrl, defaultOptions) expect(loadObj(objPath, options)
.then(function(data) { .then(function(data) {
var images = data.images;
var materials = data.materials; var materials = data.materials;
var nodes = data.nodes; var nodes = data.nodes;
var name = data.name;
var meshes = getMeshes(data); var meshes = getMeshes(data);
var primitives = getPrimitives(data); var primitives = getPrimitives(data);
expect(images.length).toBe(0); expect(name).toBe('box');
expect(materials.length).toBe(1); expect(materials.length).toBe(1);
expect(nodes.length).toBe(1); expect(nodes.length).toBe(1);
expect(meshes.length).toBe(1); expect(meshes.length).toBe(1);
@ -98,7 +92,7 @@ describe('loadObj', function() {
}); });
it('loads obj with normals', function(done) { it('loads obj with normals', function(done) {
expect(loadObj(objNormalsUrl, defaultOptions) expect(loadObj(objNormalsPath, options)
.then(function(data) { .then(function(data) {
var mesh = getMeshes(data)[0]; var mesh = getMeshes(data)[0];
expect(mesh.positions.length / 3).toBe(24); expect(mesh.positions.length / 3).toBe(24);
@ -108,7 +102,7 @@ describe('loadObj', function() {
}); });
it('loads obj with uvs', function(done) { it('loads obj with uvs', function(done) {
expect(loadObj(objUvsUrl, defaultOptions) expect(loadObj(objUvsPath, options)
.then(function(data) { .then(function(data) {
var mesh = getMeshes(data)[0]; var mesh = getMeshes(data)[0];
expect(mesh.positions.length / 3).toBe(20); expect(mesh.positions.length / 3).toBe(20);
@ -119,8 +113,8 @@ describe('loadObj', function() {
it('loads obj with negative indices', function(done) { it('loads obj with negative indices', function(done) {
expect(Promise.all([ expect(Promise.all([
loadObj(objPositionsOnlyUrl, defaultOptions), loadObj(objPositionsOnlyPath, options),
loadObj(objNegativeIndicesUrl, defaultOptions) loadObj(objNegativeIndicesPath, options)
]) ])
.then(function(results) { .then(function(results) {
var positionsReference = getMeshes(results[0])[0].positions.toFloatBuffer(); var positionsReference = getMeshes(results[0])[0].positions.toFloatBuffer();
@ -130,7 +124,7 @@ describe('loadObj', function() {
}); });
it('loads obj with triangle faces', function(done) { it('loads obj with triangle faces', function(done) {
expect(loadObj(objTrianglesUrl, defaultOptions) expect(loadObj(objTrianglesPath, options)
.then(function(data) { .then(function(data) {
var mesh = getMeshes(data)[0]; var mesh = getMeshes(data)[0];
var primitive = getPrimitives(data)[0]; var primitive = getPrimitives(data)[0];
@ -140,7 +134,7 @@ describe('loadObj', function() {
}); });
it('loads obj with objects', function(done) { it('loads obj with objects', function(done) {
expect(loadObj(objObjectsUrl, defaultOptions) expect(loadObj(objObjectsPath, options)
.then(function(data) { .then(function(data) {
var nodes = data.nodes; var nodes = data.nodes;
expect(nodes.length).toBe(3); expect(nodes.length).toBe(3);
@ -157,7 +151,7 @@ describe('loadObj', function() {
}); });
it('loads obj with groups', function(done) { it('loads obj with groups', function(done) {
expect(loadObj(objGroupsUrl, defaultOptions) expect(loadObj(objGroupsPath, options)
.then(function(data) { .then(function(data) {
var nodes = data.nodes; var nodes = data.nodes;
expect(nodes.length).toBe(3); expect(nodes.length).toBe(3);
@ -174,7 +168,7 @@ describe('loadObj', function() {
}); });
it('loads obj with objects and groups', function(done) { it('loads obj with objects and groups', function(done) {
expect(loadObj(objObjectsGroupsUrl, defaultOptions) expect(loadObj(objObjectsGroupsPath, options)
.then(function(data) { .then(function(data) {
var nodes = data.nodes; var nodes = data.nodes;
expect(nodes.length).toBe(3); expect(nodes.length).toBe(3);
@ -197,7 +191,7 @@ describe('loadObj', function() {
}); });
it('loads obj with usemtl only', function(done) { it('loads obj with usemtl only', function(done) {
expect(loadObj(objUsemtlUrl, defaultOptions) expect(loadObj(objUsemtlPath, options)
.then(function(data) { .then(function(data) {
var nodes = data.nodes; var nodes = data.nodes;
expect(nodes.length).toBe(1); expect(nodes.length).toBe(1);
@ -216,7 +210,7 @@ describe('loadObj', function() {
}); });
it('loads obj with no materials', function(done) { it('loads obj with no materials', function(done) {
expect(loadObj(objNoMaterialsUrl, defaultOptions) expect(loadObj(objNoMaterialsPath, options)
.then(function(data) { .then(function(data) {
var nodes = data.nodes; var nodes = data.nodes;
expect(nodes.length).toBe(1); expect(nodes.length).toBe(1);
@ -229,7 +223,7 @@ describe('loadObj', function() {
it('loads obj with multiple materials', function(done) { it('loads obj with multiple materials', function(done) {
// The usemtl markers are interleaved, but should condense to just three primitives // The usemtl markers are interleaved, but should condense to just three primitives
expect(loadObj(objMultipleMaterialsUrl, defaultOptions) expect(loadObj(objMultipleMaterialsPath, options)
.then(function(data) { .then(function(data) {
var nodes = data.nodes; var nodes = data.nodes;
expect(nodes.length).toBe(1); expect(nodes.length).toBe(1);
@ -249,7 +243,7 @@ describe('loadObj', function() {
it('loads obj uncleaned', function(done) { it('loads obj uncleaned', function(done) {
// Obj with extraneous o, g, and usemtl lines // Obj with extraneous o, g, and usemtl lines
// Also tests handling of o and g lines with the same names // Also tests handling of o and g lines with the same names
expect(loadObj(objUncleanedUrl, defaultOptions) expect(loadObj(objUncleanedPath, options)
.then(function(data) { .then(function(data) {
var nodes = data.nodes; var nodes = data.nodes;
var meshes = getMeshes(data); var meshes = getMeshes(data);
@ -265,7 +259,7 @@ describe('loadObj', function() {
}); });
it('loads obj with multiple mtllibs', function(done) { it('loads obj with multiple mtllibs', function(done) {
expect(loadObj(objMtllibUrl, defaultOptions) expect(loadObj(objMtllibPath, options)
.then(function(data) { .then(function(data) {
var materials = data.materials; var materials = data.materials;
expect(materials.length).toBe(3); expect(materials.length).toBe(3);
@ -276,138 +270,87 @@ describe('loadObj', function() {
}); });
expect(materials[0].name).toBe('Blue'); 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].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].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(); }), done).toResolve();
}); });
it('loads obj with missing mtllib', function(done) { 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) { .then(function(data) {
expect(data.materials.length).toBe(0); 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(); }), done).toResolve();
}); });
it('loads resources outside of the obj directory', function(done) { it('loads .mtl outside of the obj directory', function(done) {
expect(loadObj(objExternalResourcesUrl, defaultOptions) expect(loadObj(objExternalResourcesPath, options)
.then(function(data) { .then(function(data) {
var imagePath = getImagePath(objTexturedUrl, 'cesium.png');
expect(data.images[0].path).toBe(imagePath);
var materials = data.materials; var materials = data.materials;
expect(materials.length).toBe(2); expect(materials.length).toBe(2);
// .mtl files are loaded in an arbitrary order, so find the "MaterialTextured" material // .mtl files are loaded in an arbitrary order, so find the "MaterialTextured" material
var materialTextured = materials[0].name === 'MaterialTextured' ? materials[0] : materials[1]; 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(); }), done).toResolve();
}); });
it('does not load resources outside of the obj directory when secure is true', function(done) { it('does not load .mtl outside of the obj directory when secure is true', function(done) {
var options = clone(defaultOptions); var spy = jasmine.createSpy('logger');
options.logger = spy;
options.secure = true; options.secure = true;
expect(loadObj(objExternalResourcesUrl, options) expect(loadObj(objExternalResourcesPath, options)
.then(function(data) { .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(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(spy.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(1)[0].indexOf('Could not read texture file') >= 0).toBe(true);
}), done).toResolve(); }), done).toResolve();
}); });
it('loads obj with texture', function(done) { it('loads obj with texture', function(done) {
expect(loadObj(objTexturedUrl, defaultOptions) expect(loadObj(objTexturedPath, options)
.then(function(data) { .then(function(data) {
var imagePath = getImagePath(objTexturedUrl, 'cesium.png'); var baseColorTexture = data.materials[0].pbrMetallicRoughness.baseColorTexture;
expect(data.images[0].path).toBe(imagePath); expect(baseColorTexture.name).toBe('cesium');
expect(data.materials[0].diffuseTexture).toEqual(imagePath); expect(baseColorTexture.source).toBeDefined();
}), done).toResolve(); }), done).toResolve();
}); });
it('loads obj with missing texture', function(done) { it('loads obj with missing texture', function(done) {
expect(loadObj(objMissingTextureUrl, defaultOptions) var spy = jasmine.createSpy('logger');
options.logger = spy;
expect(loadObj(objMissingTexturePath, options)
.then(function(data) { .then(function(data) {
var imagePath = getImagePath(objMissingTextureUrl, 'cesium.png'); var baseColorTexture = data.materials[0].pbrMetallicRoughness.baseColorTexture;
expect(data.images.length).toBe(0); expect(baseColorTexture).toBeUndefined();
expect(data.materials[0].diffuseTexture).toEqual(imagePath); expect(spy.calls.argsFor(0)[0].indexOf('Could not read texture file') >= 0).toBe(true);
expect(console.log.calls.argsFor(0)[0].indexOf('Could not read image file') >= 0).toBe(true);
}), done).toResolve(); }), done).toResolve();
}); });
it('loads obj with subdirectories', function(done) { it('loads obj with subdirectories', function(done) {
expect(loadObj(objSubdirectoriesUrl, defaultOptions) expect(loadObj(objSubdirectoriesPath, options)
.then(function(data) { .then(function(data) {
var imagePath = getImagePath(objSubdirectoriesUrl, path.join('materials', 'images', 'cesium.png')); var baseColorTexture = data.materials[0].pbrMetallicRoughness.baseColorTexture;
expect(data.images[0].path).toBe(imagePath); expect(baseColorTexture.name).toBe('cesium');
expect(data.materials[0].diffuseTexture).toEqual(imagePath); expect(baseColorTexture.source).toBeDefined();
}), done).toResolve();
});
it('loads obj with complex material', function(done) {
expect(loadObj(objComplexMaterialUrl, defaultOptions)
.then(function(data) {
var images = data.images;
expect(images.length).toBe(6);
}), 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)
.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);
}), done).toResolve(); }), done).toResolve();
}); });
it('throws when file has invalid contents', function(done) { 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) { it('throw when reading invalid file', function(done) {
expect(loadObj(objInvalidUrl, defaultOptions), done).toRejectWith(Error); expect(loadObj(objInvalidPath, options), done).toRejectWith(Error);
}); });
}); });

View File

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

View File

@ -4,28 +4,22 @@ var path = require('path');
var Promise = require('bluebird'); var Promise = require('bluebird');
var obj2gltf = require('../../lib/obj2gltf'); var obj2gltf = require('../../lib/obj2gltf');
var objPath = 'specs/data/box-textured/box-textured.obj'; var texturedObjPath = 'specs/data/box-textured/box-textured.obj';
var gltfPath = 'specs/data/box-textured/box-textured.gltf'; var complexObjPath = 'specs/data/box-complex-material/box-complex-material.obj';
var glbPath = 'specs/data/box-textured/box-textured.glb'; var missingMtllibObjPath = 'specs/data/box-missing-mtllib/box-missing-mtllib.obj';
var objPathNonExistent = 'specs/data/non-existent.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'; var textureUrl = 'specs/data/box-textured/cesium.png';
describe('obj2gltf', function() { describe('obj2gltf', function() {
beforeEach(function() { beforeEach(function() {
spyOn(fsExtra, 'outputJson').and.returnValue(Promise.resolve());
spyOn(fsExtra, 'outputFile').and.returnValue(Promise.resolve()); spyOn(fsExtra, 'outputFile').and.returnValue(Promise.resolve());
}); });
it('converts obj to gltf', function(done) { it('converts obj to gltf', function(done) {
expect(obj2gltf(objPath, gltfPath) expect(obj2gltf(texturedObjPath)
.then(function() { .then(function(gltf) {
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).toBeDefined();
expect(gltf.images.length).toBe(1); expect(gltf.images.length).toBe(1);
}), done).toResolve(); }), done).toResolve();
@ -35,52 +29,61 @@ describe('obj2gltf', function() {
var options = { var options = {
binary : true binary : true
}; };
expect(obj2gltf(objPath, gltfPath, options) expect(obj2gltf(texturedObjPath, options)
.then(function() { .then(function(glb) {
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); var magic = glb.toString('utf8', 0, 4);
expect(magic).toBe('glTF'); expect(magic).toBe('glTF');
}), done).toResolve(); }), done).toResolve();
}); });
it('converts obj to glb when gltfPath has a .glb extension', function(done) { it('convert obj to gltf with separate resources', function(done) {
expect(obj2gltf(objPath, glbPath)
.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');
}), done).toResolve();
});
it('writes out separate resources', function(done) {
var options = { var options = {
separate : true, separate : true,
separateTextures : true separateTextures : true,
outputDirectory : outputDirectory
}; };
expect(obj2gltf(objPath, gltfPath, options) expect(obj2gltf(texturedObjPath, options)
.then(function() { .then(function() {
expect(fsExtra.outputFile.calls.count()).toBe(2); // Saves out .png and .bin expect(fsExtra.outputFile.calls.count()).toBe(2); // Saves out .png and .bin
expect(fsExtra.outputJson.calls.count()).toBe(1); // Saves out .gltf
}), done).toResolve(); }), done).toResolve();
}); });
it('sets overriding images', function(done) { it('converts obj to glb with separate resources', function(done) {
var options = { var options = {
overridingImages : { separate : true,
separateTextures : true,
outputDirectory : outputDirectory,
binary : true
};
expect(obj2gltf(texturedObjPath, options)
.then(function() {
expect(fsExtra.outputFile.calls.count()).toBe(2); // Saves out .png and .bin
}), done).toResolve();
});
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 = {
overridingTextures : {
metallicRoughnessOcclusionTexture : textureUrl, metallicRoughnessOcclusionTexture : textureUrl,
normalTexture : textureUrl, normalTexture : textureUrl,
baseColorTexture : textureUrl, baseColorTexture : textureUrl,
emissiveTexture : textureUrl emissiveTexture : textureUrl
}, },
separateTextures : true separateTextures : true,
outputDirectory : outputDirectory
}; };
expect(obj2gltf(complexMaterialObjPath, complexMaterialGltfPath, options) expect(obj2gltf(complexObjPath, options)
.then(function() { .then(function() {
var args = fsExtra.outputFile.calls.allArgs(); var args = fsExtra.outputFile.calls.allArgs();
var length = args.length; var length = args.length;
@ -90,19 +93,71 @@ describe('obj2gltf', function() {
}), done).toResolve(); }), done).toResolve();
}); });
it('rejects if obj path does not exist', function(done) { it('sets overriding textures (2)', function(done) {
expect(obj2gltf(objPathNonExistent, gltfPath), done).toRejectWith(Error); var options = {
overridingTextures : {
specularGlossinessTexture : textureUrl,
occlusionTexture : textureUrl,
normalTexture : textureUrl,
baseColorTexture : textureUrl,
emissiveTexture : textureUrl
},
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('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() { it('throws if objPath is undefined', function() {
expect(function() { expect(function() {
obj2gltf(undefined, gltfPath); obj2gltf(undefined);
}).toThrowDeveloperError(); }).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() { expect(function() {
obj2gltf(objPath, undefined); obj2gltf(texturedObjPath, options);
}).toThrowDeveloperError(); }).toThrowDeveloperError();
}); });
@ -112,17 +167,19 @@ describe('obj2gltf', function() {
specularGlossiness : true specularGlossiness : true
}; };
expect(function() { expect(function() {
obj2gltf(objPath, gltfPath, options); obj2gltf(texturedObjPath, options);
}).toThrowDeveloperError(); }).toThrowDeveloperError();
}); });
it('throws if metallicRoughnessOcclusionTexture and specularGlossinessTexture are both defined', function() { it('throws if metallicRoughnessOcclusionTexture and specularGlossinessTexture are both defined', function() {
var options = { var options = {
metallicRoughnessOcclusionTexture : 'path/to/metallic-roughness-occlusion/texture', overridingTextures : {
specularGlossinessTexture : 'path/to/specular-glossiness/texture' metallicRoughnessOcclusionTexture : textureUrl,
specularGlossinessTexture : textureUrl
}
}; };
expect(function() { expect(function() {
obj2gltf(objPath, gltfPath, options); obj2gltf(texturedObjPath, options);
}).toThrowDeveloperError(); }).toThrowDeveloperError();
}); });
}); });