diff --git a/.gitignore b/.gitignore
index ad27f7b..b04cad6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,14 +2,16 @@
node_modules
npm-debug.log
+# TypeScript definitions
+typings
+
# WebStorm user-specific
.idea/workspace.xml
.idea/tasks.xml
-# TypeScript definitions
-typings
-
# Generate data
-test
coverage
+doc
+output
+test
*.tgz
diff --git a/.idea/modules.xml b/.idea/modules.xml
index eb8cfab..662b990 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/OBJ2GLTF.iml b/.idea/obj2gltf.iml
similarity index 100%
rename from .idea/OBJ2GLTF.iml
rename to .idea/obj2gltf.iml
diff --git a/.npmignore b/.npmignore
index 1fe05bd..6e6218a 100644
--- a/.npmignore
+++ b/.npmignore
@@ -2,6 +2,7 @@
/doc
/specs
/test
+/output
/typings
/coverage
.jshintrc
diff --git a/.travis.yml b/.travis.yml
index a73b51f..72dada3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,3 +4,8 @@ node_js:
script:
- npm run jsHint -- --failTaskOnError
- npm run test -- --failTaskOnError --suppressPassed
+
+after_success:
+ ## We only need to run coveralls for one node version (doesn't matter which one).
+ ## We also ignore publishing failures, since restarting an existing travis build would otherwise break.
+ - if [$(node --version) == v6*]; then npm run coverage && npm run coveralls; fi
diff --git a/LICENSE.md b/LICENSE.md
index 8938747..82de349 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -11,53 +11,29 @@ Third-Party Code
obj2gltf includes the following third-party code.
-### async
+### bluebird
-https://www.npmjs.com/package/async
-
-> Copyright (c) 2010-2016 Caolan McMahon
+> The MIT License (MIT)
+>
+> Copyright (c) 2013-2015 Petka Antonov
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+> of this software and associated documentation files (the "Software"), to deal
+> in the Software without restriction, including without limitation the rights
+> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+> copies of the Software, and to permit persons to whom the Software is
+> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-### byline
-
-https://www.npmjs.com/package/byline
-
-> node-byline (C) 2011-2015 John Hewson
->
-> Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to
-deal in the Software without restriction, including without limitation the
-rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-sell copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
->
-> The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
->
-> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-IN THE SOFTWARE.
+> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+> THE SOFTWARE.
### Cesium
@@ -73,6 +49,35 @@ http://cesiumjs.org/
See https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md
+### event-stream
+
+https://www.npmjs.com/package/event-stream
+
+> The MIT License (MIT)
+>
+> Copyright (c) 2011 Dominic Tarr
+>
+> Permission is hereby granted, free of charge,
+> to any person obtaining a copy of this software and
+> associated documentation files (the "Software"), to
+> deal in the Software without restriction, including
+> without limitation the rights to use, copy, modify,
+> merge, publish, distribute, sublicense, and/or sell
+> copies of the Software, and to permit persons to whom
+> the Software is furnished to do so,
+> subject to the following conditions:
+>
+> The above copyright notice and this permission notice
+> shall be included in all copies or substantial portions of the Software.
+>
+> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+> OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+> ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
### fs-extra
https://www.npmjs.com/package/fs-extra
@@ -82,16 +87,16 @@ https://www.npmjs.com/package/fs-extra
> Copyright (c) 2011-2016 JP Richardson
>
> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
-(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
+> (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
> merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
+>
> THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
-ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+> WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+> OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+> ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### gltf-pipeline
@@ -112,24 +117,24 @@ https://www.npmjs.com/package/yargs
The command-line tool uses yargs.
> Copyright 2010 James Halliday (mail@substack.net)
-Modified work Copyright 2014 Contributors (ben@npmjs.com)
+> Modified work Copyright 2014 Contributors (ben@npmjs.com)
>
> This project is free software released under the MIT/X11 license:
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+> of this software and associated documentation files (the "Software"), to deal
+> in the Software without restriction, including without limitation the rights
+> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+> copies of the Software, and to permit persons to whom the Software is
+> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+> THE SOFTWARE.
diff --git a/README.md b/README.md
index f1e7784..d9818fc 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Using obj2gltf as a library:
var obj2gltf = require('obj2gltf');
var convert = obj2gltf.convert;
var options = {
- embedImage : false // Don't embed image in the converted glTF
+ separateTextures : true // Don't embed textures in the converted glTF
}
convert('model.obj', 'model.gltf', options)
.then(function() {
@@ -30,20 +30,24 @@ Using obj2gltf as a command-line tool:
`node bin/obj2gltf.js -i model.obj -o model.gltf -s`
-
## Usage
###Command line flags:
|Flag|Description|Required|
|----|-----------|--------|
-|`-i`|Path to the input OBJ file.| :white_check_mark: Yes|
-|`-o`|Directory or filename for the exported glTF file.|No|
-|`-b`|Output binary glTF.|No, default `false`|
+|`-h`|Display help.|No|
+|`-i`|Path to the obj file.| :white_check_mark: Yes|
+|`-o`|Path of the converted glTF file.|No|
+|`-b`|Save as binary glTF.|No, default `false`|
|`-s`|Writes out separate geometry/animation data files, shader files, and textures instead of embedding them in the glTF file.|No, default `false`|
|`-t`|Write out separate textures only.|No, default `false`|
+|`-c`|Quantize positions, compress texture coordinates, and oct-encode normals.|No, default `false`|
+|`-z`|Use the optimization stages in the glTF pipeline.|No, default `false`|
+|`-n`|Generate normals if they are missing.|No, default `false`|
+|`--cesium`|Optimize the glTF for Cesium by using the sun as a default light source.|No, default `false`|
|`--ao`|Apply ambient occlusion to the converted model.|No, default `false`|
-|`-h`|Display help|No|
+|`--bypassPipeline`|Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above and will save the glTF with the KHR_materials_common extension.|No, default `false`|
## Contributions
@@ -53,5 +57,5 @@ Pull requests are appreciated. Please use the same [Contributor License Agreeme
Developed by the Cesium team.
-
+
diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js
index caeb576..6cfaf40 100644
--- a/bin/obj2gltf.js
+++ b/bin/obj2gltf.js
@@ -1,53 +1,116 @@
#!/usr/bin/env node
-"use strict";
-var argv = require('yargs').argv;
+'use strict';
var Cesium = require('cesium');
-var defined = Cesium.defined;
-var defaultValue = Cesium.defaultValue;
+var path = require('path');
+var yargs = require('yargs');
var convert = require('../lib/convert');
-if (process.argv.length < 3 || defined(argv.h) || defined(argv.help)) {
- console.log('Usage: ./bin/obj2gltf.js [INPUT] [OPTIONS]');
- console.log(' -i, --input Path to obj file');
- console.log(' -o, --output Directory or filename for the exported glTF file');
- console.log(' -b, --binary Output binary glTF');
- console.log(' -s --separate Writes out separate geometry/animation data files, shader files, and textures instead of embedding them in the glTF file.');
- console.log(' -t --separateImage Write out separate textures only.');
- console.log(' -c --compress Quantize positions, compress texture coordinates, and oct-encode normals.');
- console.log(' -h, --help Display this help');
- console.log(' --ao Apply ambient occlusion to the converted model');
- console.log(' --cesium Optimize the glTF for Cesium by using the sun as a default light source.');
- process.exit(0);
+var defaultValue = Cesium.defaultValue;
+var defined = Cesium.defined;
+
+var args = process.argv;
+args = args.slice(2, args.length);
+
+var argv = yargs
+ .usage('Usage: node $0 -i inputPath -o outputPath')
+ .example('node $0 -i ./specs/data/box/box.obj -o box.gltf')
+ .help('h')
+ .alias('h', 'help')
+ .options({
+ 'input': {
+ alias: 'i',
+ describe: 'Path to the obj file.',
+ type: 'string',
+ normalize: true
+ },
+ 'output': {
+ alias: 'o',
+ describe: 'Path of the converted glTF file.',
+ type: 'string',
+ normalize: true
+ },
+ 'binary': {
+ alias: 'b',
+ describe: 'Save as binary glTF.',
+ type: 'boolean',
+ default: false
+ },
+ 'separate': {
+ alias: 's',
+ describe: 'Write separate geometry/animation data files, shader files, and textures instead of embedding them in the glTF.',
+ type: 'boolean',
+ default: false
+ },
+ 'separateTexture': {
+ alias: 't',
+ describe: 'Write out separate textures only.',
+ type: 'boolean',
+ default: false
+ },
+ 'compress': {
+ alias: 'c',
+ describe: 'Quantize positions, compress texture coordinates, and oct-encode normals.',
+ type: 'boolean',
+ default: false
+ },
+ 'optimize': {
+ alias: 'z',
+ describe: 'Use the optimization stages in the glTF pipeline.',
+ type: 'boolean',
+ default: false
+ },
+ 'cesium': {
+ describe: 'Optimize the glTF for Cesium by using the sun as a default light source.',
+ type: 'boolean',
+ default: false
+ },
+ 'generateNormals': {
+ alias: 'n',
+ describe: 'Generate normals if they are missing.',
+ type: 'boolean',
+ default: false
+ },
+ 'ao': {
+ describe: 'Apply ambient occlusion to the converted model.',
+ type: 'boolean',
+ default: false
+ },
+ 'bypassPipeline': {
+ describe: 'Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above and will save the glTF with the KHR_materials_common extension.',
+ type: 'boolean',
+ default: false
+ }
+ }).parse(args);
+
+var objPath = defaultValue(argv.i, argv._[0]);
+var gltfPath = defaultValue(argv.o, argv._[1]);
+
+if (!defined(objPath)) {
+ yargs.showHelp();
+ return;
}
-var objFile = defaultValue(argv._[0], defaultValue(argv.i, argv.input));
-var outputPath = defaultValue(argv._[1], defaultValue(argv.o, argv.output));
-var binary = defaultValue(defaultValue(argv.b, argv.binary), false);
-var separate = defaultValue(defaultValue(argv.s, argv.separate), false);
-var separateImage = defaultValue(defaultValue(argv.t, argv.separateImage), false);
-var compress = defaultValue(defaultValue(argv.c, argv.compress), false);
-var ao = defaultValue(argv.ao, false);
-var optimizeForCesium = defaultValue(argv.cesium, false);
-
-if (!defined(objFile)) {
- throw new Error('-i or --input argument is required. See --help for details.');
+if (!defined(gltfPath)) {
+ var extension = argv.b ? '.glb' : '.gltf';
+ var modelName = path.basename(objPath, path.extname(objPath));
+ gltfPath = path.join(path.dirname(objPath), modelName + extension);
}
+var options = {
+ binary : argv.b,
+ separate : argv.s,
+ separateTextures : argv.t,
+ compress : argv.c,
+ optimize : argv.z,
+ generateNormals : argv.n,
+ ao : argv.ao,
+ optimizeForCesium : argv.cesium,
+ bypassPipeline : argv.bypassPipeline
+};
+
console.time('Total');
-var options = {
- binary : binary,
- embed : !separate,
- embedImage : !separateImage,
- compress : compress,
- ao : ao,
- optimizeForCesium : optimizeForCesium
-};
-
-convert(objFile, outputPath, options)
+convert(objPath, gltfPath, options)
.then(function() {
console.timeEnd('Total');
- })
- .catch(function(err) {
- console.log(err);
});
diff --git a/gulpfile.js b/gulpfile.js
index f479227..8d12058 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -6,7 +6,7 @@ var fsExtra = require('fs-extra');
var gulp = require('gulp');
var gulpJshint = require('gulp-jshint');
var Jasmine = require('jasmine');
-var JasmineSpecReporter = require('jasmine-spec-reporter');
+var JasmineSpecReporter = require('jasmine-spec-reporter').SpecReporter;
var open = require('open');
var path = require('path');
var yargs = require('yargs');
@@ -20,8 +20,8 @@ var environmentSeparator = process.platform === 'win32' ? ';' : ':';
var nodeBinaries = path.join(__dirname, 'node_modules', '.bin');
process.env.PATH += environmentSeparator + nodeBinaries;
-var jsHintFiles = ['**/*.js', '!node_modules/**', '!coverage/**'];
-var specFiles = ['**/*.js', '!node_modules/**', '!coverage/**'];
+var jsHintFiles = ['**/*.js', '!node_modules/**', '!coverage/**', '!doc/**'];
+var specFiles = ['**/*.js', '!node_modules/**', '!coverage/**', '!doc/**', '!bin/**'];
gulp.task('jsHint', function () {
var stream = gulp.src(jsHintFiles)
@@ -53,8 +53,8 @@ gulp.task('test', function (done) {
gulp.task('test-watch', function () {
gulp.watch(specFiles).on('change', function () {
- //We can't simply depend on the test task because Jasmine
- //does not like being run multiple times in the same process.
+ // We can't simply depend on the test task because Jasmine
+ // does not like being run multiple times in the same process.
try {
child_process.execSync('jasmine JASMINE_CONFIG_PATH=specs/jasmine.json', {
stdio: [process.stdin, process.stdout, process.stderr]
@@ -71,7 +71,7 @@ gulp.task('coverage', function () {
' cover' +
' --include-all-sources' +
' --dir coverage' +
- ' -x "specs/** coverage/** index.js gulpfile.js"' +
+ ' -x "specs/**" -x "coverage/**" -x "doc/**" -x "bin/**" -x "index.js" -x "gulpfile.js"' +
' node_modules/jasmine/bin/jasmine.js' +
' JASMINE_CONFIG_PATH=specs/jasmine.json', {
stdio: [process.stdin, process.stdout, process.stderr]
diff --git a/lib/ArrayStorage.js b/lib/ArrayStorage.js
new file mode 100644
index 0000000..c97fd43
--- /dev/null
+++ b/lib/ArrayStorage.js
@@ -0,0 +1,107 @@
+'use strict';
+var Cesium = require('cesium');
+
+var ComponentDatatype = Cesium.ComponentDatatype;
+
+module.exports = ArrayStorage;
+
+var initialLength = 1024; // 2^10
+var doublingThreshold = 33554432; // 2^25 (~134 MB for a Float32Array)
+var fixedExpansionLength = 33554432; // 2^25 (~134 MB for a Float32Array)
+
+/**
+ * Provides expandable typed array storage for geometry data. This is preferable to JS arrays which are
+ * stored with double precision. The resizing mechanism is similar to std::vector.
+ *
+ * @param {ComponentDatatype} componentDatatype The data type.
+ * @constructor
+ *
+ * @private
+ */
+function ArrayStorage(componentDatatype) {
+ this.componentDatatype = componentDatatype;
+ this.typedArray = ComponentDatatype.createTypedArray(componentDatatype, 0);
+ this.length = 0;
+}
+
+function resize(storage, length) {
+ var typedArray = ComponentDatatype.createTypedArray(storage.componentDatatype, length);
+ typedArray.set(storage.typedArray);
+ storage.typedArray = typedArray;
+}
+
+ArrayStorage.prototype.push = function(value) {
+ var length = this.length;
+ var typedArrayLength = this.typedArray.length;
+
+ if (length === 0) {
+ resize(this, initialLength);
+ } else if (length === typedArrayLength) {
+ if (length < doublingThreshold) {
+ resize(this, typedArrayLength * 2);
+ } else {
+ resize(this, typedArrayLength + fixedExpansionLength);
+ }
+ }
+
+ this.typedArray[this.length++] = value;
+};
+
+ArrayStorage.prototype.get = function(index) {
+ return this.typedArray[index];
+};
+
+var sizeOfUint16 = 2;
+var sizeOfUint32 = 4;
+var sizeOfFloat = 4;
+
+ArrayStorage.prototype.toUint16Buffer = function() {
+ var length = this.length;
+ var typedArray = this.typedArray;
+ var paddedLength = length + ((length % 2 === 0) ? 0 : 1); // Round to next multiple of 2
+ var buffer = Buffer.alloc(paddedLength * sizeOfUint16);
+ for (var i = 0; i < length; ++i) {
+ buffer.writeUInt16LE(typedArray[i], i * sizeOfUint16);
+ }
+ return buffer;
+};
+
+ArrayStorage.prototype.toUint32Buffer = function() {
+ var length = this.length;
+ var typedArray = this.typedArray;
+ var buffer = Buffer.alloc(length * sizeOfUint32);
+ for (var i = 0; i < length; ++i) {
+ buffer.writeUInt32LE(typedArray[i], i * sizeOfUint32);
+ }
+ return buffer;
+};
+
+ArrayStorage.prototype.toFloatBuffer = function() {
+ var length = this.length;
+ var typedArray = this.typedArray;
+ var buffer = Buffer.alloc(length * sizeOfFloat);
+ for (var i = 0; i < length; ++i) {
+ buffer.writeFloatLE(typedArray[i], i * sizeOfFloat);
+ }
+ return buffer;
+};
+
+ArrayStorage.prototype.getMinMax = function(components) {
+ var length = this.length;
+ var typedArray = this.typedArray;
+ var count = length / components;
+ var min = new Array(components).fill(Number.POSITIVE_INFINITY);
+ var max = new Array(components).fill(Number.NEGATIVE_INFINITY);
+ for (var i = 0; i < count; ++i) {
+ for (var j = 0; j < components; ++j) {
+ var index = i * components + j;
+ var value = typedArray[index];
+ min[j] = Math.min(min[j], value);
+ max[j] = Math.max(max[j], value);
+ }
+ }
+ return {
+ min : min,
+ max : max
+ };
+};
diff --git a/lib/clone.js b/lib/clone.js
new file mode 100644
index 0000000..d93e059
--- /dev/null
+++ b/lib/clone.js
@@ -0,0 +1,54 @@
+'use strict';
+var Cesium = require('cesium');
+var ArrayStorage = require('./ArrayStorage');
+
+var defaultValue = Cesium.defaultValue;
+
+module.exports = clone;
+
+/**
+ * Clones an object, returning a new object containing the same properties.
+ * Modified from Cesium.clone to support typed arrays, buffers, and the ArrayStorage class.
+ *
+ * @param {Object} object The object to clone.
+ * @param {Boolean} [deep=false] If true, all properties will be deep cloned recursively.
+ * @returns {Object} The cloned object.
+ *
+ * @private
+ */
+function clone(object, deep) {
+ if (object === null || typeof object !== 'object') {
+ return object;
+ }
+
+ deep = defaultValue(deep, false);
+
+ var isBuffer = Buffer.isBuffer(object);
+ var isTypedArray = Object.prototype.toString.call(object.buffer) === '[object ArrayBuffer]';
+ var isArrayStorage = object instanceof ArrayStorage;
+
+ var result;
+ if (isBuffer) {
+ result = Buffer.from(object);
+ return result;
+ } else if (isTypedArray) {
+ result = object.slice();
+ return result;
+ } else if (isArrayStorage) {
+ result = new ArrayStorage(object.componentDatatype);
+ } else {
+ result = new object.constructor();
+ }
+
+ for (var propertyName in object) {
+ if (object.hasOwnProperty(propertyName)) {
+ var value = object[propertyName];
+ if (deep) {
+ value = clone(value, deep);
+ }
+ result[propertyName] = value;
+ }
+ }
+
+ return result;
+}
diff --git a/lib/convert.js b/lib/convert.js
index 8a135d8..6088198 100644
--- a/lib/convert.js
+++ b/lib/convert.js
@@ -1,60 +1,121 @@
-"use strict";
-var path = require('path');
-var GltfPipeline = require('gltf-pipeline').Pipeline;
-var parseObj = require('./obj');
-var createGltf = require('./gltf');
+'use strict';
var Cesium = require('cesium');
-var defined = Cesium.defined;
+var fsExtra = require('fs-extra');
+var GltfPipeline = require('gltf-pipeline').Pipeline;
+var path = require('path');
+var Promise = require('bluebird');
+var createGltf = require('./gltf');
+var loadObj = require('./obj');
+
+var fxExtraOutputFile = Promise.promisify(fsExtra.outputFile);
+var fsExtraOutputJson = Promise.promisify(fsExtra.outputJson);
+
var defaultValue = Cesium.defaultValue;
+var defined = Cesium.defined;
+var DeveloperError = Cesium.DeveloperError;
module.exports = convert;
-function convert(objFile, outputPath, options) {
- options = defaultValue(options, {});
+/**
+ * Converts an obj file to a glTF 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 {Boolean} [options.binary=false] Save as binary glTF.
+ * @param {Boolean} [options.separate=false] Writes out separate geometry/animation data files, shader files, and textures instead of embedding them in the glTF.
+ * @param {Boolean} [options.separateTextures=false] Write out separate textures only.
+ * @param {Boolean} [options.compress=false] Quantize positions, compress texture coordinates, and oct-encode normals.
+ * @param {Boolean} [options.optimize=false] Use the optimization stages in the glTF pipeline.
+ * @param {Boolean} [options.optimizeForCesium=false] Optimize the glTF for Cesium by using the sun as a default light source.
+ * @param {Boolean} [options.generateNormals=false] Generate normals if they are missing.
+ * @param {Boolean} [options.ao=false] Apply ambient occlusion to the converted model.
+ * @param {Boolean} [options.textureCompressionOptions] Options sent to the compressTextures stage of gltf-pipeline.
+ * @param {Boolean} [options.bypassPipeline=false] Bypass the gltf-pipeline for debugging purposes. This option overrides many of the options above and will save the glTF with the KHR_materials_common extension.
+ */
+
+function convert(objPath, gltfPath, options) {
+ options = defaultValue(options, defaultValue.EMPTY_OBJECT);
var binary = defaultValue(options.binary, false);
- var embed = defaultValue(options.embed, true);
- var embedImage = defaultValue(options.embedImage, true);
+ var separate = defaultValue(options.separate, false);
+ var separateTextures = defaultValue(options.separateTextures, false);
var compress = defaultValue(options.compress, false);
- var ao = defaultValue(options.ao, false);
+ var optimize = defaultValue(options.optimize, false);
var optimizeForCesium = defaultValue(options.optimizeForCesium, false);
+ var generateNormals = defaultValue(options.generateNormals, false);
+ var ao = defaultValue(options.ao, false);
+ var textureCompressionOptions = options.textureCompressionOptions;
+ var bypassPipeline = defaultValue(options.bypassPipeline, false);
- if (!defined(objFile)) {
- throw new Error('objFile is required');
+ if (!defined(objPath)) {
+ throw new DeveloperError('objPath is required');
}
- if (!defined(outputPath)) {
- outputPath = path.dirname(objFile);
+ if (!defined(gltfPath)) {
+ throw new DeveloperError('gltfPath is required');
}
- var inputPath = path.dirname(objFile);
- var modelName = path.basename(objFile, '.obj');
-
- var extension = path.extname(outputPath);
- if (extension !== '') {
- modelName = path.basename(outputPath, extension);
- outputPath = path.dirname(outputPath);
+ var basePath = path.dirname(objPath);
+ var modelName = path.basename(objPath, path.extname(objPath));
+ var extension = path.extname(gltfPath);
+ if (extension === '.glb') {
+ binary = true;
}
+ gltfPath = path.join(path.dirname(gltfPath), modelName + extension);
- extension = binary ? '.glb' : '.gltf';
- var gltfFile = path.join(outputPath, modelName + extension);
+ var aoOptions = ao ? {} : undefined;
- return parseObj(objFile, inputPath)
- .then(function(data) {
- return createGltf(data, inputPath, modelName);
+ var pipelineOptions = {
+ createDirectory : false,
+ basePath : basePath,
+ binary : binary,
+ embed : !separate,
+ embedImage : !separate && !separateTextures,
+ quantize : compress,
+ compressTextureCoordinates : compress,
+ encodeNormals : compress,
+ preserve : !optimize,
+ optimizeForCesium : optimizeForCesium,
+ smoothNormals : generateNormals,
+ aoOptions : aoOptions,
+ textureCompressionOptions : textureCompressionOptions
+ };
+
+ return loadObj(objPath)
+ .then(function(objData) {
+ return createGltf(objData);
})
.then(function(gltf) {
- var aoOptions = ao ? {} : undefined;
- var options = {
- binary: binary,
- embed: embed,
- embedImage: embedImage,
- encodeNormals: compress,
- quantize: compress,
- aoOptions: aoOptions,
- optimizeForCesium : optimizeForCesium,
- createDirectory: false,
- basePath: inputPath
- };
- return GltfPipeline.processJSONToDisk(gltf, gltfFile, options);
+ return saveExternalBuffer(gltf, gltfPath);
+ })
+ .then(function(gltf) {
+ if (bypassPipeline) {
+ return convert._outputJson(gltfPath, gltf);
+ } else {
+ return GltfPipeline.processJSONToDisk(gltf, gltfPath, pipelineOptions);
+ }
+ });
+}
+
+/**
+ * Exposed for testing
+ *
+ * @private
+ */
+convert._outputJson = fsExtraOutputJson;
+
+function saveExternalBuffer(gltf, gltfPath) {
+ var buffer = gltf.buffers[Object.keys(gltf.buffers)[0]];
+ if (defined(buffer.uri)) {
+ return Promise.resolve(gltf);
+ }
+
+ var bufferName = path.basename(gltfPath, path.extname(gltfPath));
+ var bufferUri = bufferName + '.bin';
+ buffer.uri = bufferUri;
+ var bufferPath = path.join(path.dirname(gltfPath), bufferUri);
+ return fxExtraOutputFile(bufferPath, buffer)
+ .then(function() {
+ return gltf;
});
}
diff --git a/lib/gltf.js b/lib/gltf.js
index 4a79303..c0b0189 100644
--- a/lib/gltf.js
+++ b/lib/gltf.js
@@ -1,148 +1,37 @@
-"use strict";
+'use strict';
var Cesium = require('cesium');
-var Promise = require('bluebird');
-var fs = require('fs-extra');
var path = require('path');
var defined = Cesium.defined;
var defaultValue = Cesium.defaultValue;
var WebGLConstants = Cesium.WebGLConstants;
-var fsWriteFile = Promise.promisify(fs.writeFile);
-
module.exports = createGltf;
-function createGltf(data, inputPath, modelName) {
- var vertexCount = data.vertexCount;
- var vertexArray = data.vertexArray;
- var positionMin = data.positionMin;
- var positionMax = data.positionMax;
- var hasUVs = data.hasUVs;
- var hasNormals = data.hasNormals;
- var materialGroups = data.materialGroups;
- var materials = data.materials;
- var images = data.images;
-
- var i, j, name;
-
- var sizeOfFloat32 = 4;
- var sizeOfUint32 = 4;
- var sizeOfUint16 = 2;
-
- var indexComponentType;
- var indexComponentSize;
-
- // Reserve the 65535 index for primitive restart
- if (vertexCount < 65535) {
- indexComponentType = WebGLConstants.UNSIGNED_SHORT;
- indexComponentSize = sizeOfUint16;
- } else {
- indexComponentType = WebGLConstants.UNSIGNED_INT;
- indexComponentSize = sizeOfUint32;
- }
-
- // Create primitives
- var primitives = [];
- var indexArrayLength = 0;
- var indexArray;
- var indexCount;
- for (name in materialGroups) {
- if (materialGroups.hasOwnProperty(name)) {
- indexArray = materialGroups[name];
- indexCount = indexArray.length;
- primitives.push({
- indexArray : indexArray,
- indexOffset : indexArrayLength,
- indexCount : indexCount,
- material : name
- });
- indexArrayLength += indexCount;
- }
- }
-
- // Create buffer to store vertex and index data
- var indexArrayByteLength = indexArrayLength * indexComponentSize;
- var vertexArrayLength = vertexArray.length; // In floats
- var vertexArrayByteLength = vertexArrayLength * sizeOfFloat32;
- var bufferByteLength = vertexArrayByteLength + indexArrayByteLength;
- var buffer = new Buffer(bufferByteLength);
-
- // Write vertex data
- var byteOffset = 0;
- for (i = 0; i < vertexArrayLength; ++i) {
- buffer.writeFloatLE(vertexArray[i], byteOffset);
- byteOffset += sizeOfFloat32;
- }
-
- // Write index data
- var primitivesLength = primitives.length;
- for (i = 0; i < primitivesLength; ++i) {
- indexArray = primitives[i].indexArray;
- indexCount = indexArray.length;
- for (j = 0; j < indexCount; ++j) {
- if (indexComponentSize === sizeOfUint16) {
- buffer.writeUInt16LE(indexArray[j], byteOffset);
- } else {
- buffer.writeUInt32LE(indexArray[j], byteOffset);
- }
- byteOffset += indexComponentSize;
- }
- }
-
- var positionByteOffset = 0;
- var normalByteOffset = 0;
- var uvByteOffset = 0;
- var vertexByteStride = 0;
-
- if (hasNormals && hasUVs) {
- normalByteOffset = sizeOfFloat32 * 3;
- uvByteOffset = sizeOfFloat32 * 6;
- vertexByteStride = sizeOfFloat32 * 8;
- } else if (hasNormals && !hasUVs) {
- normalByteOffset = sizeOfFloat32 * 3;
- vertexByteStride = sizeOfFloat32 * 6;
- } else if (!hasNormals && hasUVs) {
- uvByteOffset = sizeOfFloat32 * 3;
- vertexByteStride = sizeOfFloat32 * 5;
- } else if (!hasNormals && !hasUVs) {
- vertexByteStride = sizeOfFloat32 * 3;
- }
-
- var bufferId = modelName + '_buffer';
- var bufferViewVertexId = 'bufferView_vertex';
- var bufferViewIndexId = 'bufferView_index';
- var accessorPositionId = 'accessor_position';
- var accessorUVId = 'accessor_uv';
- var accessorNormalId = 'accessor_normal';
- var meshId = 'mesh_' + modelName;
- var sceneId = 'scene_' + modelName;
- var nodeId = 'node_' + modelName;
- var samplerId = 'sampler_0';
-
- function getAccessorIndexId(i) {
- return 'accessor_index_' + i;
- }
-
- function getMaterialId(material) {
- return 'material_' + material;
- }
-
- function getTextureId(image) {
- if (!defined(image)) {
- return undefined;
- }
- return 'texture_' + path.basename(image).substr(0, image.lastIndexOf('.'));
- }
-
- function getImageId(image) {
- return path.basename(image, path.extname(image));
- }
+/**
+ * Create a glTF from obj data.
+ *
+ * @param {Object} objData Output of obj.js, containing an array of nodes containing geometry information, materials, and images.
+ * @returns {Object} A glTF asset with the KHR_materials_common extension.
+ *
+ * @private
+ */
+function createGltf(objData) {
+ var nodes = objData.nodes;
+ var materials = objData.materials;
+ var images = objData.images;
+ var sceneId = 'scene';
+ var samplerId = 'sampler';
+ var bufferId = 'buffer';
+ var vertexBufferViewId = 'bufferView_vertex';
+ var indexBufferViewId = 'bufferView_index';
var gltf = {
accessors : {},
asset : {},
buffers : {},
bufferViews : {},
+ extensionsUsed : ['KHR_materials_common'],
images : {},
materials : {},
meshes : {},
@@ -154,174 +43,97 @@ function createGltf(data, inputPath, modelName) {
};
gltf.asset = {
- "generator": "OBJ2GLTF",
- "premultipliedAlpha": true,
- "profile": {
- "api": "WebGL",
- "version": "1.0"
+ generator : 'obj2gltf',
+ profile : {
+ api : 'WebGL',
+ version : '1.0'
},
- "version": 1
+ version: '1.0'
};
gltf.scenes[sceneId] = {
- nodes : [nodeId]
+ nodes : []
};
- gltf.nodes[nodeId] = {
- children : [],
- matrix : [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
- meshes : [meshId],
- name : modelName
- };
-
- gltf.samplers[samplerId] = {}; // Use default values
-
- var bufferSeparate = false;
- var bufferUri;
- if (buffer.length > 201326580) {
- // toString fails for buffers larger than ~192MB. Instead save the buffer to a .bin file.
- // Source: https://github.com/nodejs/node/issues/4266
- bufferSeparate = true;
- bufferUri = modelName + '.bin';
- } else {
- bufferUri = 'data:application/octet-stream;base64,' + buffer.toString('base64');
+ function getImageId(imagePath) {
+ return path.basename(imagePath, path.extname(imagePath));
}
- gltf.buffers[bufferId] = {
- byteLength : bufferByteLength,
- type : 'arraybuffer',
- uri : bufferUri
- };
-
- gltf.bufferViews[bufferViewVertexId] = {
- buffer : bufferId,
- byteLength : vertexArrayByteLength,
- byteOffset : 0,
- target : WebGLConstants.ARRAY_BUFFER
- };
- gltf.bufferViews[bufferViewIndexId] = {
- buffer : bufferId,
- byteLength : indexArrayByteLength,
- byteOffset : vertexArrayByteLength,
- target : WebGLConstants.ELEMENT_ARRAY_BUFFER
- };
-
- for (i = 0; i < primitivesLength; ++i) {
- var primitive = primitives[i];
- gltf.accessors[getAccessorIndexId(i)] = {
- bufferView : bufferViewIndexId,
- byteOffset : primitive.indexOffset * indexComponentSize,
- byteStride : 0,
- componentType : indexComponentType,
- count : primitive.indexCount,
- type : 'SCALAR'
- };
- }
-
- gltf.accessors[accessorPositionId] = {
- bufferView : bufferViewVertexId,
- byteOffset : positionByteOffset,
- byteStride : vertexByteStride,
- componentType : WebGLConstants.FLOAT,
- count : vertexCount,
- min : positionMin,
- max : positionMax,
- type : 'VEC3'
- };
-
- if (hasNormals) {
- gltf.accessors[accessorNormalId] = {
- bufferView : bufferViewVertexId,
- byteOffset : normalByteOffset,
- byteStride : vertexByteStride,
- componentType : WebGLConstants.FLOAT,
- count : vertexCount,
- type : 'VEC3'
- };
- }
-
- if (hasUVs) {
- gltf.accessors[accessorUVId] = {
- bufferView : bufferViewVertexId,
- byteOffset : uvByteOffset,
- byteStride : vertexByteStride,
- componentType : WebGLConstants.FLOAT,
- count : vertexCount,
- type : 'VEC2'
- };
- }
-
- var gltfPrimitives = [];
- gltf.meshes[meshId] = {
- name : modelName,
- primitives : gltfPrimitives
- };
-
- var gltfAttributes = {};
- gltfAttributes.POSITION = accessorPositionId;
- if (hasNormals) {
- gltfAttributes.NORMAL = accessorNormalId;
- }
- if (hasUVs) {
- gltfAttributes.TEXCOORD_0 = accessorUVId;
- }
-
- for (i = 0; i < primitivesLength; ++i) {
- gltfPrimitives.push({
- attributes : gltfAttributes,
- indices : getAccessorIndexId(i),
- material : getMaterialId(primitives[i].material),
- mode : WebGLConstants.TRIANGLES
- });
- }
-
- for (name in materials) {
- if (materials.hasOwnProperty(name)) {
- var material = materials[name];
- var materialId = getMaterialId(name);
- var values = {
- ambient : defaultValue(defaultValue(getTextureId(material.ambientColorMap), material.ambientColor), [0, 0, 0, 1]),
- diffuse : defaultValue(defaultValue(getTextureId(material.diffuseColorMap), material.diffuseColor), [0, 0, 0, 1]),
- emission : defaultValue(defaultValue(getTextureId(material.emissionColorMap), material.emissionColor), [0, 0, 0, 1]),
- specular : defaultValue(defaultValue(getTextureId(material.specularColorMap), material.specularColor), [0, 0, 0, 1]),
- shininess : defaultValue(material.specularShininess, 0.0)
- };
-
- gltf.materials[materialId] = {
- name: name,
- values: values
- };
+ function getTextureId(imagePath) {
+ if (!defined(imagePath) || !defined(images[imagePath])) {
+ return undefined;
}
+ return 'texture_' + getImageId(imagePath);
}
- for (name in images) {
- if (images.hasOwnProperty(name)) {
- var image = images[name];
- var imageId = getImageId(name);
- var textureId = getTextureId(name);
- var format;
- var channels = image.channels;
- switch (channels) {
- case 1:
- format = WebGLConstants.ALPHA;
- break;
- case 2:
- format = WebGLConstants.LUMINANCE_ALPHA;
- break;
- case 3:
- format = WebGLConstants.RGB;
- break;
- case 4:
- format = WebGLConstants.RGBA;
- break;
+ function createMaterial(material, hasNormals) {
+ var ambient = defaultValue(defaultValue(getTextureId(material.ambientColorMap), material.ambientColor), [0, 0, 0, 1]);
+ var diffuse = defaultValue(defaultValue(getTextureId(material.diffuseColorMap), material.diffuseColor), [0.5, 0.5, 0.5, 1]);
+ var emission = defaultValue(defaultValue(getTextureId(material.emissionColorMap), material.emissionColor), [0, 0, 0, 1]);
+ var specular = defaultValue(defaultValue(getTextureId(material.specularColorMap), material.specularColor), [0, 0, 0, 1]);
+ var alpha = defaultValue(defaultValue(material.alpha), 1.0);
+ var shininess = defaultValue(material.specularShininess, 0.0);
+ var hasSpecular = (shininess > 0.0) && (specular[0] > 0.0 || specular[1] > 0.0 || specular[2] > 0.0);
+
+ var transparent;
+ var transparency = 1.0;
+ if (typeof diffuse === 'string') {
+ transparency = alpha;
+ transparent = images[material.diffuseColorMap].transparent || (transparency < 1.0);
+ } else {
+ diffuse[3] = alpha;
+ transparent = diffuse[3] < 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';
+ return {
+ extensions : {
+ KHR_materials_common : {
+ technique : technique,
+ values : {
+ ambient : ambient,
+ diffuse : diffuse,
+ emission : emission,
+ specular : specular,
+ shininess : shininess,
+ transparency : transparency,
+ transparent : transparent,
+ doubleSided : doubleSided
+ }
+ }
}
+ };
+ }
+
+ if (Object.keys(images).length > 0) {
+ gltf.samplers[samplerId] = {
+ magFilter : WebGLConstants.LINEAR,
+ minFilter : WebGLConstants.LINEAR,
+ wrapS : WebGLConstants.REPEAT,
+ wrapT : WebGLConstants.REPEAT
+ };
+ }
+
+ for (var imagePath in images) {
+ if (images.hasOwnProperty(imagePath)) {
+ var image = images[imagePath];
+ var imageId = getImageId(imagePath);
+ var textureId = getTextureId(imagePath);
gltf.images[imageId] = {
+ name : imageId,
uri : image.uri
};
gltf.textures[textureId] = {
- format : format,
- internalFormat : format,
+ format : image.format,
+ internalFormat : image.format,
sampler : samplerId,
source : imageId,
target : WebGLConstants.TEXTURE_2D,
@@ -330,9 +142,191 @@ function createGltf(data, inputPath, modelName) {
}
}
- if (bufferSeparate) {
- var bufferPath = path.join(inputPath, modelName + '.bin');
- return fsWriteFile(bufferPath, buffer);
+ var vertexBuffers = [];
+ var vertexByteOffset = 0;
+ var indexBuffers = [];
+ var indexBuffersByteOffset = 0;
+ var accessorCount = 0;
+
+ function addVertexAttribute(array, components) {
+ var count = array.length / components;
+ var buffer = array.toFloatBuffer();
+ var minMax = array.getMinMax(components);
+
+ var type = (components === 3 ? 'VEC3' : 'VEC2');
+ var accessor = {
+ bufferView : vertexBufferViewId,
+ byteOffset : vertexByteOffset,
+ byteStride : 0,
+ componentType : WebGLConstants.FLOAT,
+ count : count,
+ min : minMax.min,
+ max : minMax.max,
+ type : type
+ };
+
+ vertexByteOffset += buffer.length;
+ vertexBuffers.push(buffer);
+ var accessorId = 'accessor_' + accessorCount++;
+ gltf.accessors[accessorId] = accessor;
+ return accessorId;
}
+
+ function addIndexArray(array, uint32Indices) {
+ var buffer = uint32Indices ? array.toUint32Buffer() : array.toUint16Buffer();
+ var componentType = uint32Indices ? WebGLConstants.UNSIGNED_INT : WebGLConstants.UNSIGNED_SHORT;
+ var length = array.length;
+ var minMax = array.getMinMax(1);
+ var accessor = {
+ bufferView : indexBufferViewId,
+ byteOffset : indexBuffersByteOffset,
+ byteStride : 0,
+ componentType : componentType,
+ count : length,
+ min : minMax.min,
+ max : minMax.max,
+ type : 'SCALAR'
+ };
+
+ indexBuffersByteOffset += buffer.length;
+ indexBuffers.push(buffer);
+
+ var accessorId = 'accessor_' + accessorCount++;
+ gltf.accessors[accessorId] = accessor;
+ return accessorId;
+ }
+
+ function requiresUint32Indices(nodes) {
+ var nodesLength = nodes.length;
+ for (var i = 0; i < nodesLength; ++i) {
+ var meshes = nodes[i].meshes;
+ var meshesLength = meshes.length;
+ for (var j = 0; j < meshesLength; ++j) {
+ // Reserve the 65535 index for primitive restart
+ var vertexCount = meshes[j].positions.length / 3;
+ if (vertexCount > 65534) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ var uint32Indices = requiresUint32Indices(nodes);
+ var gltfSceneNodes = gltf.scenes[sceneId].nodes;
+ var nodesLength = nodes.length;
+ for (var i = 0; i < nodesLength; ++i) {
+ // Add node
+ var node = nodes[i];
+ var nodeId = node.name;
+ gltfSceneNodes.push(nodeId);
+ var gltfNodeMeshes = [];
+ gltf.nodes[nodeId] = {
+ name : nodeId,
+ meshes : gltfNodeMeshes
+ };
+
+ // Add meshes to node
+ var meshes = node.meshes;
+ var meshesLength = meshes.length;
+ for (var j = 0; j < meshesLength; ++j) {
+ var mesh = meshes[j];
+ var meshId = mesh.name;
+ gltfNodeMeshes.push(meshId);
+
+ var hasPositions = mesh.positions.length > 0;
+ var hasNormals = mesh.normals.length > 0;
+ var hasUVs = mesh.uvs.length > 0;
+
+ var attributes = {};
+ if (hasPositions) {
+ attributes.POSITION = addVertexAttribute(mesh.positions, 3);
+ }
+ if (hasNormals) {
+ attributes.NORMAL = addVertexAttribute(mesh.normals, 3);
+ }
+ if (hasUVs) {
+ attributes.TEXCOORD_0 = addVertexAttribute(mesh.uvs, 2);
+ }
+
+ // Unload resources
+ mesh.positions = undefined;
+ mesh.normals = undefined;
+ mesh.uvs = undefined;
+
+ var gltfMeshPrimitives = [];
+ gltf.meshes[meshId] = {
+ name : meshId,
+ primitives : gltfMeshPrimitives
+ };
+
+ // Add primitives to mesh
+ var primitives = mesh.primitives;
+ var primitivesLength = primitives.length;
+ for (var k = 0; k < primitivesLength; ++k) {
+ var primitive = primitives[k];
+ var indexAccessorId = addIndexArray(primitive.indices, uint32Indices);
+ primitive.indices = undefined; // Unload resources
+ var materialId = primitive.material;
+
+ if (!defined(materialId)) {
+ // Create a default material if the primitive does not specify one
+ materialId = 'default';
+ }
+
+ var material = defaultValue(materials[materialId], {});
+ var gltfMaterial = gltf.materials[materialId];
+ if (defined(gltfMaterial)) {
+ // Check if this material has already been added but with incompatible shading
+ var normalShading = (gltfMaterial.extensions.KHR_materials_common.technique !== 'CONSTANT');
+ if (hasNormals !== normalShading) {
+ materialId += (hasNormals ? '_shaded' : '_constant');
+ gltfMaterial = gltf.materials[materialId];
+ }
+ }
+
+ if (!defined(gltfMaterial)) {
+ gltf.materials[materialId] = createMaterial(material, hasNormals);
+ }
+
+ gltfMeshPrimitives.push({
+ attributes : attributes,
+ indices : indexAccessorId,
+ material : materialId,
+ mode : WebGLConstants.TRIANGLES
+ });
+ }
+ }
+ }
+
+ var vertexBuffer = Buffer.concat(vertexBuffers);
+ var indexBuffer = Buffer.concat(indexBuffers);
+ var buffer = Buffer.concat([vertexBuffer, indexBuffer]);
+
+ // Buffers larger than ~192MB cannot be base64 encoded due to a NodeJS limitation. Instead the buffer will be saved to a .bin file. Source: https://github.com/nodejs/node/issues/4266
+ var bufferUri;
+ if (buffer.length <= 201326580) {
+ bufferUri = 'data:application/octet-stream;base64,' + buffer.toString('base64');
+ }
+
+ gltf.buffers[bufferId] = {
+ byteLength : buffer.byteLength,
+ uri : bufferUri
+ };
+
+ gltf.bufferViews[vertexBufferViewId] = {
+ buffer : bufferId,
+ byteLength : vertexBuffer.length,
+ byteOffset : 0,
+ target : WebGLConstants.ARRAY_BUFFER
+ };
+
+ gltf.bufferViews[indexBufferViewId] = {
+ buffer : bufferId,
+ byteLength : indexBuffer.length,
+ byteOffset : vertexBuffer.length,
+ target : WebGLConstants.ELEMENT_ARRAY_BUFFER
+ };
+
return gltf;
}
diff --git a/lib/image.js b/lib/image.js
index 0be148b..9d2ad74 100644
--- a/lib/image.js
+++ b/lib/image.js
@@ -1,12 +1,55 @@
-"use strict";
-var Promise = require('bluebird');
+'use strict';
+var Cesium = require('cesium');
var fs = require('fs-extra');
var path = require('path');
+var Promise = require('bluebird');
var fsReadFile = Promise.promisify(fs.readFile);
+var WebGLConstants = Cesium.WebGLConstants;
+
module.exports = loadImage;
+/**
+ * Load an image file and get information about it.
+ *
+ * @param {String} imagePath Path to the image file.
+ * @returns {Promise} A promise resolving to the image information, or undefined if the file doesn't exist.
+ *
+ * @private
+ */
+function loadImage(imagePath) {
+ return fsReadFile(imagePath)
+ .then(function(data) {
+ var extension = path.extname(imagePath);
+ var uriType = getUriType(extension);
+ var uri = uriType + ';base64,' + data.toString('base64');
+
+ var info = {
+ transparent : false,
+ channels : 3,
+ data : data,
+ uri : uri,
+ format : getFormat(3)
+ };
+
+ if (extension === '.png') {
+ // Color type is encoded in the 25th bit of the png
+ var colorType = data[25];
+ var channels = getChannels(colorType);
+ info.channels = channels;
+ info.transparent = (channels === 4);
+ info.format = getFormat(channels);
+ }
+
+ return info;
+ })
+ .catch(function() {
+ console.log('Could not read image file at ' + imagePath + '. Material will ignore this image.');
+ return undefined;
+ });
+}
+
function getChannels(colorType) {
switch (colorType) {
case 0: // greyscale
@@ -24,40 +67,27 @@ function getChannels(colorType) {
function getUriType(extension) {
switch (extension) {
- case 'png':
+ case '.png':
return 'data:image/png';
- case 'jpg':
+ case '.jpg':
+ case '.jpeg':
return 'data:image/jpeg';
- case 'jpeg':
- return 'data:image/jpeg';
- case 'gif':
+ case '.gif':
return 'data:image/gif';
default:
- return 'data:image/' + extension;
+ return 'data:image/' + extension.slice(1);
}
}
-function loadImage(imagePath) {
- return fsReadFile(imagePath)
- .then(function(data) {
- var extension = path.extname(imagePath).slice(1);
- var uriType = getUriType(extension);
- var uri = uriType + ';base64,' + data.toString('base64');
-
- var info = {
- transparent: false,
- channels: 3,
- data: data,
- uri: uri
- };
-
- if (path.extname(imagePath) === 'png') {
- // Color type is encoded in the 25th bit of the png
- var colorType = data[25];
- var channels = getChannels(colorType);
- info.channels = channels;
- info.transparent = (channels === 4);
- }
- return info;
- });
+function getFormat(channels) {
+ switch (channels) {
+ case 1:
+ return WebGLConstants.ALPHA;
+ case 2:
+ return WebGLConstants.LUMINANCE_ALPHA;
+ case 3:
+ return WebGLConstants.RGB;
+ case 4:
+ return WebGLConstants.RGBA;
+ }
}
diff --git a/lib/mtl.js b/lib/mtl.js
index 941be7f..50a93fe 100644
--- a/lib/mtl.js
+++ b/lib/mtl.js
@@ -1,114 +1,105 @@
-"use strict";
-var Promise = require('bluebird');
-var fs = require('fs-extra');
-var defined = require('cesium').defined;
+'use strict';
+var path = require('path');
+var readLines = require('./readLines');
-var fsReadFile = Promise.promisify(fs.readFile);
+module.exports = loadMtl;
-module.exports = {
- getDefault : getDefault,
- parse : parse
-};
-
-function createMaterial() {
- return {
- ambientColor : undefined, // Ka
- emissionColor : undefined, // Ke
- diffuseColor : undefined, // Kd
- specularColor : undefined, // Ks
- specularShininess : undefined, // Ns
- alpha : undefined, // d / Tr
- ambientColorMap : undefined, // map_Ka
- emissionColorMap : undefined, // map_Ke
- diffuseColorMap : undefined, // map_Kd
- specularColorMap : undefined, // map_Ks
- specularShininessMap : undefined, // map_Ns
- normalMap : undefined, // map_Bump
- alphaMap : undefined // map_d
- };
+function Material() {
+ this.ambientColor = undefined; // Ka
+ this.emissionColor = undefined; // Ke
+ this.diffuseColor = undefined; // Kd
+ this.specularColor = undefined; // Ks
+ this.specularShininess = undefined; // Ns
+ this.alpha = undefined; // d / Tr
+ this.ambientColorMap = undefined; // map_Ka
+ this.emissionColorMap = undefined; // map_Ke
+ this.diffuseColorMap = undefined; // map_Kd
+ this.specularColorMap = undefined; // map_Ks
+ this.specularShininessMap = undefined; // map_Ns
+ this.normalMap = undefined; // map_Bump
+ this.alphaMap = undefined; // map_d
}
-function getDefault() {
- var material = createMaterial();
- material.diffuseColor = [0.5, 0.5, 0.5, 1.0];
- return material;
-}
+/**
+ * Parse an mtl file.
+ *
+ * @param {String} mtlPath Path to the mtl file.
+ * @returns {Promise} A promise resolving to the materials, or an empty object if the mtl file doesn't exist.
+ *
+ * @private
+ */
+function loadMtl(mtlPath) {
+ var material;
+ var values;
+ var value;
+ var materials = {};
-function parse(mtlPath) {
- return fsReadFile(mtlPath, 'utf8')
- .then(function (contents) {
- var materials = {};
- var material;
- var values;
- var value;
- var lines = contents.split('\n');
- var length = lines.length;
- for (var i = 0; i < length; ++i) {
- var line = lines[i].trim();
- if (/^newmtl /i.test(line)) {
- var name = line.substring(7).trim();
- material = createMaterial();
- materials[name] = material;
- } else if (/^Ka /i.test(line)) {
- values = line.substring(3).trim().split(' ');
- material.ambientColor = [
- parseFloat(values[0]),
- parseFloat(values[1]),
- parseFloat(values[2]),
- 1.0
- ];
- } else if (/^Ke /i.test(line)) {
- values = line.substring(3).trim().split(' ');
- material.emissionColor = [
- parseFloat(values[0]),
- parseFloat(values[1]),
- parseFloat(values[2]),
- 1.0
- ];
- } else if (/^Kd /i.test(line)) {
- values = line.substring(3).trim().split(' ');
- material.diffuseColor = [
- parseFloat(values[0]),
- parseFloat(values[1]),
- parseFloat(values[2]),
- 1.0
- ];
- } else if (/^Ks /i.test(line)) {
- values = line.substring(3).trim().split(' ');
- material.specularColor = [
- parseFloat(values[0]),
- parseFloat(values[1]),
- parseFloat(values[2]),
- 1.0
- ];
- } else if (/^Ns /i.test(line)) {
- value = line.substring(3).trim();
- material.specularShininess = parseFloat(value);
- } else if (/^d /i.test(line)) {
- value = line.substring(2).trim();
- material.alpha = parseFloat(value);
- } else if (/^Tr /i.test(line)) {
- value = line.substring(3).trim();
- material.alpha = parseFloat(value);
- } else if (/^map_Ka /i.test(line)) {
- material.ambientColorMap = line.substring(7).trim();
- } else if (/^map_Ke /i.test(line)) {
- material.emissionColorMap = line.substring(7).trim();
- } else if (/^map_Kd /i.test(line)) {
- material.diffuseColorMap = line.substring(7).trim();
- } else if (/^map_Ks /i.test(line)) {
- material.specularColorMap = line.substring(7).trim();
- } else if (/^map_Ns /i.test(line)) {
- material.specularShininessMap = line.substring(7).trim();
- } else if (/^map_Bump /i.test(line)) {
- material.normalMap = line.substring(9).trim();
- } else if (/^map_d /i.test(line)) {
- material.alphaMap = line.substring(6).trim();
- }
- }
- if (defined(material.alpha)) {
- material.diffuseColor[3] = material.alpha;
- }
+ function parseLine(line) {
+ line = line.trim();
+ if (/^newmtl /i.test(line)) {
+ var name = line.substring(7).trim();
+ material = new Material();
+ materials[name] = material;
+ } else if (/^Ka /i.test(line)) {
+ values = line.substring(3).trim().split(' ');
+ material.ambientColor = [
+ parseFloat(values[0]),
+ parseFloat(values[1]),
+ parseFloat(values[2]),
+ 1.0
+ ];
+ } else if (/^Ke /i.test(line)) {
+ values = line.substring(3).trim().split(' ');
+ material.emissionColor = [
+ parseFloat(values[0]),
+ parseFloat(values[1]),
+ parseFloat(values[2]),
+ 1.0
+ ];
+ } else if (/^Kd /i.test(line)) {
+ values = line.substring(3).trim().split(' ');
+ material.diffuseColor = [
+ parseFloat(values[0]),
+ parseFloat(values[1]),
+ parseFloat(values[2]),
+ 1.0
+ ];
+ } else if (/^Ks /i.test(line)) {
+ values = line.substring(3).trim().split(' ');
+ material.specularColor = [
+ parseFloat(values[0]),
+ parseFloat(values[1]),
+ parseFloat(values[2]),
+ 1.0
+ ];
+ } else if (/^Ns /i.test(line)) {
+ value = line.substring(3).trim();
+ material.specularShininess = parseFloat(value);
+ } else if (/^d /i.test(line)) {
+ value = line.substring(2).trim();
+ material.alpha = parseFloat(value);
+ } else if (/^Tr /i.test(line)) {
+ value = line.substring(3).trim();
+ material.alpha = parseFloat(value);
+ } else if (/^map_Ka /i.test(line)) {
+ material.ambientColorMap = getAbsolutePath(line.substring(7).trim(), mtlPath);
+ } else if (/^map_Ke /i.test(line)) {
+ material.emissionColorMap = getAbsolutePath(line.substring(7).trim(), mtlPath);
+ } else if (/^map_Kd /i.test(line)) {
+ material.diffuseColorMap = getAbsolutePath(line.substring(7).trim(), mtlPath);
+ } else if (/^map_Ks /i.test(line)) {
+ material.specularColorMap = getAbsolutePath(line.substring(7).trim(), mtlPath);
+ } else if (/^map_Ns /i.test(line)) {
+ material.specularShininessMap = getAbsolutePath(line.substring(7).trim(), mtlPath);
+ } else if (/^map_Bump /i.test(line)) {
+ material.normalMap = getAbsolutePath(line.substring(9).trim(), mtlPath);
+ } else if (/^map_d /i.test(line)) {
+ material.alphaMap = getAbsolutePath(line.substring(6).trim(), mtlPath);
+ }
+ }
+
+ return readLines(mtlPath, parseLine)
+ .then(function() {
return materials;
})
.catch(function() {
@@ -116,3 +107,10 @@ function parse(mtlPath) {
return {};
});
}
+
+function getAbsolutePath(imagePath, mtlPath) {
+ if (!path.isAbsolute(imagePath)) {
+ imagePath = path.join(path.dirname(mtlPath), imagePath);
+ }
+ return imagePath;
+}
diff --git a/lib/obj.js b/lib/obj.js
index b60e569..abe024d 100644
--- a/lib/obj.js
+++ b/lib/obj.js
@@ -1,365 +1,447 @@
-"use strict";
-
+'use strict';
var Cesium = require('cesium');
-var Promise = require('bluebird');
-var byline = require('byline');
-var fs = require('fs-extra');
var path = require('path');
+var Promise = require('bluebird');
+var ArrayStorage = require('./ArrayStorage');
var loadImage = require('./image');
-var Material = require('./mtl');
+var loadMtl = require('./mtl');
+var readLines = require('./readLines');
-var Cartesian3 = Cesium.Cartesian3;
+var combine = Cesium.combine;
+var ComponentDatatype = Cesium.ComponentDatatype;
+var defaultValue = Cesium.defaultValue;
var defined = Cesium.defined;
+var RuntimeError = Cesium.RuntimeError;
-module.exports = parseObj;
+module.exports = loadObj;
-// OBJ regex patterns are from ThreeJS (https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/OBJLoader.js)
+// Object name (o) -> node
+// Group name (g) -> mesh
+// Material name (usemtl) -> primitive
-function parseObj(objFile, inputPath) {
- return getObjInfo(objFile, inputPath)
- .then(function(result) {
- var info = result.info;
- var materials = result.materials;
- var images = result.images;
- return processObj(objFile, info, materials, images);
+function Node() {
+ this.name = undefined;
+ this.meshes = [];
+}
+
+function Mesh() {
+ this.name = undefined;
+ this.primitives = [];
+ this.positions = new ArrayStorage(ComponentDatatype.FLOAT);
+ this.normals = new ArrayStorage(ComponentDatatype.FLOAT);
+ this.uvs = new ArrayStorage(ComponentDatatype.FLOAT);
+}
+
+function Primitive() {
+ this.material = undefined;
+ this.indices = new ArrayStorage(ComponentDatatype.UNSIGNED_INT);
+}
+
+// OBJ regex patterns are modified from ThreeJS (https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/OBJLoader.js)
+var vertexPattern = /v( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // v float float float
+var normalPattern = /vn( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // vn float float float
+var uvPattern = /vt( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // vt float float
+var facePattern1 = /f( +-?\d+)\/?( +-?\d+)\/?( +-?\d+)\/?( +-?\d+)?\/?/; // f vertex vertex vertex ...
+var facePattern2 = /f( +(-?\d+)\/(-?\d+)\/?)( +(-?\d+)\/(-?\d+)\/?)( +(-?\d+)\/(-?\d+)\/?)( +(-?\d+)\/(-?\d+)\/?)?/; // f vertex/uv vertex/uv vertex/uv ...
+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 ...
+
+/**
+ * Parse an obj file.
+ *
+ * @param {String} objPath Path to the obj file.
+ * @returns {Promise} A promise resolving to the obj data.
+ * @exception {RuntimeError} The file does not have any geometry information in it.
+ *
+ * @private
+ */
+function loadObj(objPath) {
+ // Global store of vertex attributes listed in the obj file
+ var positions = new ArrayStorage(ComponentDatatype.FLOAT);
+ var normals = new ArrayStorage(ComponentDatatype.FLOAT);
+ var uvs = new ArrayStorage(ComponentDatatype.FLOAT);
+
+ // The current node, mesh, and primitive
+ var node;
+ var mesh;
+ var primitive;
+
+ // All nodes seen in the obj
+ var nodes = [];
+
+ // Used to build the indices. The vertex cache is unique to each mesh.
+ var vertexCache = {};
+ var vertexCacheLimit = 1000000;
+ var vertexCacheCount = 0;
+ var vertexCount = 0;
+
+ // All mtl paths seen in the obj
+ var mtlPaths = [];
+
+ function getName(name) {
+ return (name === '' ? undefined : name);
+ }
+
+ function addNode(name) {
+ node = new Node();
+ node.name = getName(name);
+ nodes.push(node);
+ addMesh();
+ }
+
+ function addMesh(name) {
+ mesh = new Mesh();
+ mesh.name = getName(name);
+ node.meshes.push(mesh);
+ addPrimitive();
+
+ // Clear the vertex cache for each new mesh
+ vertexCache = {};
+ vertexCacheCount = 0;
+ vertexCount = 0;
+ }
+
+ function addPrimitive() {
+ primitive = new Primitive();
+ mesh.primitives.push(primitive);
+ }
+
+ function useMaterial(name) {
+ // Look to see if this material has already been used by a primitive in the mesh
+ var material = getName(name);
+ var primitives = mesh.primitives;
+ var primitivesLength = primitives.length;
+ for (var i = 0; i < primitivesLength; ++i) {
+ primitive = primitives[i]; // Sets the active primitive in case of returning early
+ if (primitive.material === material) {
+ return;
+ }
+ }
+ // Add a new primitive with this material
+ addPrimitive();
+ primitive.material = getName(name);
+ }
+
+ function getOffset(a, attributeData, components) {
+ var i = parseInt(a);
+ if (i < 0) {
+ // Negative vertex indexes reference the vertices immediately above it
+ return (attributeData.length / components + i) * components;
+ }
+ return (i - 1) * components;
+ }
+
+ function createVertex(p, u, n) {
+ // Positions
+ if (defined(p)) {
+ var pi = getOffset(p, positions, 3);
+ var px = positions.get(pi + 0);
+ var py = positions.get(pi + 1);
+ var pz = positions.get(pi + 2);
+ mesh.positions.push(px);
+ mesh.positions.push(py);
+ mesh.positions.push(pz);
+ }
+
+ // Normals
+ if (defined(n)) {
+ var ni = getOffset(n, normals, 3);
+ var nx = normals.get(ni + 0);
+ var ny = normals.get(ni + 1);
+ var nz = normals.get(ni + 2);
+ mesh.normals.push(nx);
+ mesh.normals.push(ny);
+ mesh.normals.push(nz);
+ }
+
+ // UVs
+ if (defined(u)) {
+ var ui = getOffset(u, uvs, 2);
+ var ux = uvs.get(ui + 0);
+ var uy = uvs.get(ui + 1);
+ mesh.uvs.push(ux);
+ mesh.uvs.push(uy);
+ }
+ }
+
+ function addVertex(v, p, u, n) {
+ var index = vertexCache[v];
+ if (!defined(index)) {
+ index = vertexCount++;
+ vertexCache[v] = index;
+ createVertex(p, u, n);
+
+ // Prevent the vertex cache from growing too large. As a result of clearing the cache there
+ // may be some duplicate vertices.
+ vertexCacheCount++;
+ if (vertexCacheCount > vertexCacheLimit) {
+ vertexCacheCount = 0;
+ vertexCache = {};
+ }
+ }
+ return index;
+ }
+
+ function addFace(v1, p1, u1, n1, v2, p2, u2, n2, v3, p3, u3, n3, v4, p4, u4, n4) {
+ var index1 = addVertex(v1, p1, u1, n1);
+ var index2 = addVertex(v2, p2, u2, n2);
+ var index3 = addVertex(v3, p3, u3, n3);
+
+ primitive.indices.push(index1);
+ primitive.indices.push(index2);
+ primitive.indices.push(index3);
+
+ // Triangulate if the face is a quad
+ if (defined(v4)) {
+ var index4 = addVertex(v4, p4, u4, n4);
+ primitive.indices.push(index1);
+ primitive.indices.push(index3);
+ primitive.indices.push(index4);
+ }
+ }
+
+ function parseLine(line) {
+ line = line.trim();
+ var result;
+
+ if ((line.length === 0) || (line.charAt(0) === '#')) {
+ // Don't process empty lines or comments
+ } else if (/^o\s/i.test(line)) {
+ var objectName = line.substring(2).trim();
+ addNode(objectName);
+ } else if (/^g\s/i.test(line)) {
+ var groupName = line.substring(2).trim();
+ addMesh(groupName);
+ } else if (/^usemtl\s/i.test(line)) {
+ var materialName = line.substring(7).trim();
+ useMaterial(materialName);
+ } else if (/^mtllib/i.test(line)) {
+ var paths = line.substring(7).trim().split(' ');
+ mtlPaths = mtlPaths.concat(paths);
+ } else if ((result = vertexPattern.exec(line)) !== null) {
+ positions.push(parseFloat(result[1]));
+ positions.push(parseFloat(result[2]));
+ positions.push(parseFloat(result[3]));
+ } else if ((result = normalPattern.exec(line) ) !== null) {
+ normals.push(parseFloat(result[1]));
+ normals.push(parseFloat(result[2]));
+ normals.push(parseFloat(result[3]));
+ } else if ((result = uvPattern.exec(line)) !== null) {
+ uvs.push(parseFloat(result[1]));
+ uvs.push(1.0 - parseFloat(result[2])); // Flip y so 0.0 is the bottom of the image
+ } else if ((result = facePattern1.exec(line)) !== null) {
+ addFace(
+ result[1], result[1], undefined, undefined,
+ result[2], result[2], undefined, undefined,
+ result[3], result[3], undefined, undefined,
+ result[4], result[4], undefined, undefined
+ );
+ } else if ((result = facePattern2.exec(line)) !== null) {
+ addFace(
+ result[1], result[2], result[3], undefined,
+ result[4], result[5], result[6], undefined,
+ result[7], result[8], result[9], undefined,
+ result[10], result[11], result[12], undefined
+ );
+ } else if ((result = facePattern3.exec(line)) !== null) {
+ addFace(
+ result[1], result[2], result[3], result[4],
+ result[5], result[6], result[7], result[8],
+ result[9], result[10], result[11], result[12],
+ result[13], result[14], result[15], result[16]
+ );
+ } else if ((result = facePattern4.exec(line)) !== null) {
+ addFace(
+ result[1], result[2], undefined, result[3],
+ result[4], result[5], undefined, result[6],
+ result[7], result[8], undefined, result[9],
+ result[10], result[11], undefined, result[12]
+ );
+ }
+ }
+
+ // Create a default node in case there are no o/g/usemtl lines in the obj
+ addNode();
+
+ // Parse the obj file
+ return readLines(objPath, parseLine)
+ .then(function() {
+ // Unload resources
+ positions = undefined;
+ normals = undefined;
+ uvs = undefined;
+
+ // Load materials and images
+ return finishLoading(nodes, mtlPaths, objPath);
});
}
-function processObj(objFile, info, materials, images) {
- return new Promise(function(resolve) {
- // A vertex is specified by indexes into each of the attribute arrays,
- // but these indexes may be different. This maps the separate indexes to a single index.
- var vertexCache = {};
- var vertexCount = 0;
-
- var vertexArray = [];
-
- var positions = [];
- var normals = [];
- var uvs = [];
-
- var positionMin = [Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE];
- var positionMax = [-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE];
-
- var hasNormals = info.hasNormals;
- var hasUVs = info.hasUVs;
-
- var materialGroups = {}; // Map material to index array
- var currentIndexArray;
-
- // Switch to the material-specific index array, or create it if it doesn't exist
- function useMaterial(material) {
- if (!defined(materials[material])) {
- useDefaultMaterial();
- } else {
- currentIndexArray = materialGroups[material];
- if (!defined(currentIndexArray)) {
- currentIndexArray = [];
- materialGroups[material] = currentIndexArray;
- }
- }
- }
-
- function useDefaultMaterial() {
- var defaultMaterial = 'czmDefaultMat';
- if (!defined(materials[defaultMaterial])) {
- materials[defaultMaterial] = Material.getDefault();
- }
- useMaterial(defaultMaterial);
- }
-
- var materialsLength = Object.keys(materials).length;
- if (materialsLength === 0) {
- useDefaultMaterial();
- }
-
- function getOffset(a, data, components) {
- var i = parseInt(a);
- if (i < 0) {
- // Negative vertex indexes reference the vertices immediately above it
- return (data.length / components + i) * components;
- }
- return (i - 1) * components;
- }
-
- function createVertex(p, u, n) {
- // Positions
- var pi = getOffset(p, positions, 3);
- var px = positions[pi + 0];
- var py = positions[pi + 1];
- var pz = positions[pi + 2];
-
- positionMin[0] = Math.min(px, positionMin[0]);
- positionMin[1] = Math.min(py, positionMin[1]);
- positionMin[2] = Math.min(pz, positionMin[2]);
- positionMax[0] = Math.max(px, positionMax[0]);
- positionMax[1] = Math.max(py, positionMax[1]);
- positionMax[2] = Math.max(pz, positionMax[2]);
- vertexArray.push(px, py, pz);
-
- // Normals
- if (hasNormals) {
- var ni = getOffset(n, normals, 3);
- var nx = normals[ni + 0];
- var ny = normals[ni + 1];
- var nz = normals[ni + 2];
- vertexArray.push(nx, ny, nz);
- }
-
- // UVs
- if (hasUVs) {
- if (defined(u)) {
- var ui = getOffset(u, uvs, 2);
- var ux = uvs[ui + 0];
- var uy = uvs[ui + 1];
- // Flip y so 0.0 is the bottom of the image
- uy = 1.0 - uy;
- vertexArray.push(ux, uy);
- } else {
- // Some objects in the model may not have uvs, fill with 0's for consistency
- vertexArray.push(0.0, 0.0);
- }
- }
- }
-
- function addVertex(v, p, u, n) {
- var index = vertexCache[v];
- if (!defined(index)) {
- index = vertexCount++;
- vertexCache[v] = index;
- createVertex(p, u, n);
- }
-
- return index;
- }
-
- function addFace(v1, p1, u1, n1, v2, p2, u2, n2, v3, p3, u3, n3, v4, p4, u4, n4) {
- var index1 = addVertex(v1, p1, u1, n1);
- var index2 = addVertex(v2, p2, u2, n2);
- var index3 = addVertex(v3, p3, u3, n3);
-
- currentIndexArray.push(index1);
- currentIndexArray.push(index2);
- currentIndexArray.push(index3);
-
- // Triangulate if the face is a quad
- if (defined(v4)) {
- var index4 = addVertex(v4, p4, u4, n4);
- currentIndexArray.push(index1);
- currentIndexArray.push(index3);
- currentIndexArray.push(index4);
- }
- }
-
- // v float float float
- var vertexPattern = /v( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/;
-
- // vn float float float
- var normalPattern = /vn( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/;
-
- // vt float float
- var uvPattern = /vt( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/;
-
- // f vertex vertex vertex ...
- var facePattern1 = /f( +-?\d+)\/?( +-?\d+)\/?( +-?\d+)\/?( +-?\d+)?\/?/;
-
- // f vertex/uv vertex/uv vertex/uv ...
- var facePattern2 = /f( +(-?\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//normal vertex//normal vertex//normal ...
- var facePattern4 = /f( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))?/;
-
- var stream = byline(fs.createReadStream(objFile, {encoding: 'utf8'}));
- stream.on('data', function (line) {
- line = line.trim();
- var result;
- if ((line.length === 0) || (line.charAt(0) === '#')) {
- // Don't process empty lines or comments
- } else if ((result = vertexPattern.exec(line)) !== null) {
- positions.push(
- parseFloat(result[1]),
- parseFloat(result[2]),
- parseFloat(result[3])
- );
- } else if ((result = normalPattern.exec(line) ) !== null) {
- var nx = parseFloat(result[1]);
- var ny = parseFloat(result[2]);
- var nz = parseFloat(result[3]);
- var normal = Cartesian3.normalize(new Cartesian3(nx, ny, nz), new Cartesian3());
- normals.push(normal.x, normal.y, normal.z);
- } else if ((result = uvPattern.exec(line)) !== null) {
- uvs.push(
- parseFloat(result[1]),
- parseFloat(result[2])
- );
- } else if ((result = facePattern1.exec(line)) !== null) {
- addFace(
- result[1], result[1], undefined, undefined,
- result[2], result[2], undefined, undefined,
- result[3], result[3], undefined, undefined,
- result[4], result[4], undefined, undefined
- );
- } else if ((result = facePattern2.exec(line)) !== null) {
- addFace(
- result[1], result[2], result[3], undefined,
- result[4], result[5], result[6], undefined,
- result[7], result[8], result[9], undefined,
- result[10], result[11], result[12], undefined
- );
- } else if ((result = facePattern3.exec(line)) !== null) {
- addFace(
- result[1], result[2], result[3], result[4],
- result[5], result[6], result[7], result[8],
- result[9], result[10], result[11], result[12],
- result[13], result[14], result[15], result[16]
- );
- } else if ((result = facePattern4.exec(line)) !== null) {
- addFace(
- result[1], result[2], undefined, result[3],
- result[4], result[5], undefined, result[6],
- result[7], result[8], undefined, result[9],
- result[10], result[11], undefined, result[12]
- );
- } else if (/^usemtl /.test(line)) {
- var materialName = line.substring(7).trim();
- useMaterial(materialName);
- }
+function finishLoading(nodes, mtlPaths, objPath) {
+ nodes = cleanNodes(nodes);
+ if (nodes.length === 0) {
+ throw new RuntimeError(objPath + ' does not have any geometry data');
+ }
+ return loadMaterials(mtlPaths, objPath)
+ .then(function(materials) {
+ var imagePaths = getImagePaths(materials);
+ return loadImages(imagePaths, objPath)
+ .then(function(images) {
+ return {
+ nodes : nodes,
+ materials : materials,
+ images : images
+ };
+ });
});
+}
- stream.on('end', function () {
- resolve({
- vertexCount: vertexCount,
- vertexArray: vertexArray,
- positionMin: positionMin,
- positionMax: positionMax,
- hasUVs: hasUVs,
- hasNormals: hasNormals,
- materialGroups: materialGroups,
- materials: materials,
- images: images
+function getAbsolutePath(mtlPath, objPath) {
+ if (!path.isAbsolute(mtlPath)) {
+ mtlPath = path.join(path.dirname(objPath), mtlPath);
+ }
+ return mtlPath;
+}
+
+function loadMaterials(mtlPaths, objPath) {
+ var materials = {};
+ return Promise.map(mtlPaths, function(mtlPath) {
+ mtlPath = getAbsolutePath(mtlPath, objPath);
+ return loadMtl(mtlPath)
+ .then(function(materialsInMtl) {
+ materials = combine(materials, materialsInMtl);
});
- });
+ }).then(function() {
+ return materials;
});
}
-function getImages(inputPath, materials) {
- // Collect all the image files from the materials
- var images = [];
+function loadImages(imagePaths) {
+ var images = {};
+ return Promise.map(imagePaths, function(imagePath) {
+ return loadImage(imagePath)
+ .then(function(image) {
+ if (defined(image)) {
+ images[imagePath] = image;
+ }
+ });
+ }).then(function() {
+ return images;
+ });
+}
+
+function getImagePaths(materials) {
+ var imagePaths = [];
for (var name in materials) {
if (materials.hasOwnProperty(name)) {
var material = materials[name];
- if (defined(material.ambientColorMap) && (images.indexOf(material.ambientColorMap) === -1)) {
- images.push(material.ambientColorMap);
+ if (defined(material.ambientColorMap) && imagePaths.indexOf(material.ambientColorMap) === -1) {
+ imagePaths.push(material.ambientColorMap);
}
- if (defined(material.diffuseColorMap) && (images.indexOf(material.diffuseColorMap) === -1)) {
- images.push(material.diffuseColorMap);
+ if (defined(material.diffuseColorMap) && imagePaths.indexOf(material.diffuseColorMap) === -1) {
+ imagePaths.push(material.diffuseColorMap);
}
- if (defined(material.emissionColorMap) && (images.indexOf(material.emissionColorMap) === -1)) {
- images.push(material.emissionColorMap);
+ if (defined(material.emissionColorMap) && imagePaths.indexOf(material.emissionColorMap) === -1) {
+ imagePaths.push(material.emissionColorMap);
}
- if (defined(material.specularColorMap) && (images.indexOf(material.specularColorMap) === -1)) {
- images.push(material.specularColorMap);
+ if (defined(material.specularColorMap) && imagePaths.indexOf(material.specularColorMap) === -1) {
+ imagePaths.push(material.specularColorMap);
}
}
}
+ return imagePaths;
+}
- // Load the image files
- var promises = [];
- var imagesInfo = {};
- var imagesLength = images.length;
- for (var i = 0; i < imagesLength; i++) {
- var imagePath = images[i];
- if (!path.isAbsolute(imagePath)) {
- imagePath = path.join(inputPath, imagePath);
+function removeEmptyPrimitives(primitives) {
+ var final = [];
+ var primitivesLength = primitives.length;
+ for (var i = 0; i < primitivesLength; ++i) {
+ var primitive = primitives[i];
+ if (primitive.indices.length > 0) {
+ final.push(primitive);
}
- promises.push(loadImage(imagePath));
}
- return Promise.all(promises)
- .then(function(imageInfoArray) {
- var imageInfoArrayLength = imageInfoArray.length;
- for (var j = 0; j < imageInfoArrayLength; j++) {
- var image = images[j];
- var imageInfo = imageInfoArray[j];
- imagesInfo[image] = imageInfo;
- }
- return imagesInfo;
- });
+ return final;
}
-function getMaterials(mtlPath, hasMaterialGroups) {
- if (hasMaterialGroups && defined(mtlPath)) {
- return Material.parse(mtlPath);
+function removeEmptyMeshes(meshes) {
+ var final = [];
+ var meshesLength = meshes.length;
+ for (var i = 0; i < meshesLength; ++i) {
+ var mesh = meshes[i];
+ mesh.primitives = removeEmptyPrimitives(mesh.primitives);
+ if ((mesh.primitives.length > 0) && (mesh.positions.length > 0)) {
+ final.push(mesh);
+ }
}
-
- return {};
+ return final;
}
-function getObjInfo(objFile, inputPath) {
- var mtlPath;
- var materials;
- var info;
- var hasMaterialGroups = false;
- var hasPositions = false;
- var hasNormals = false;
- var hasUVs = false;
- return new Promise(function(resolve, reject) {
- var stream = byline(fs.createReadStream(objFile, {encoding: 'utf8'}));
- stream.on('data', function (line) {
- if (!defined(mtlPath)) {
- var mtllibMatches = line.match(/^mtllib.*/gm);
- if (mtllibMatches !== null) {
- var mtlFile = mtllibMatches[0].substring(7).trim();
- mtlPath = mtlFile;
- if (!path.isAbsolute(mtlPath)) {
- mtlPath = path.join(inputPath, mtlFile);
- }
- }
- }
- if (!hasMaterialGroups) {
- hasMaterialGroups = /^usemtl/gm.test(line);
- }
- if (!hasPositions) {
- hasPositions = /^v\s/gm.test(line);
- }
- if (!hasNormals) {
- hasNormals = /^vn/gm.test(line);
- }
- if (!hasUVs) {
- hasUVs = /^vt/gm.test(line);
- }
- });
-
- stream.on('error', function(err) {
- reject(err);
- });
-
- stream.on('end', function () {
- if (!hasPositions) {
- reject(new Error('Could not process OBJ file, no positions.'));
- }
- info = {
- hasNormals: hasNormals,
- hasUVs: hasUVs
- };
- resolve();
- });
- })
- .then(function() {
- return getMaterials(mtlPath, hasMaterialGroups);
- })
- .then(function(returnedMaterials) {
- materials = returnedMaterials;
- return getImages(inputPath, materials);
- })
- .then(function(images) {
- return {
- info : info,
- materials : materials,
- images : images
- };
- });
+function meshesHaveNames(meshes) {
+ var meshesLength = meshes.length;
+ for (var i = 0; i < meshesLength; ++i) {
+ if (defined(meshes[i].name)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function removeEmptyNodes(nodes) {
+ var final = [];
+ var nodesLength = nodes.length;
+ for (var i = 0; i < nodesLength; ++i) {
+ var node = nodes[i];
+ var meshes = removeEmptyMeshes(node.meshes);
+ if (meshes.length === 0) {
+ continue;
+ }
+ node.meshes = meshes;
+ if (!defined(node.name) && meshesHaveNames(meshes)) {
+ // If the obj has groups (g) but not object groups (o) then convert meshes to nodes
+ var meshesLength = meshes.length;
+ for (var j = 0; j < meshesLength; ++j) {
+ var mesh = meshes[j];
+ var convertedNode = new Node();
+ convertedNode.name = mesh.name;
+ convertedNode.meshes = [mesh];
+ final.push(convertedNode);
+ }
+ } else {
+ final.push(node);
+ }
+ }
+ return final;
+}
+
+function setDefaultNames(items, defaultName, usedNames) {
+ var itemsLength = items.length;
+ for (var i = 0; i < itemsLength; ++i) {
+ var item = items[i];
+ var name = defaultValue(item.name, defaultName);
+ var occurrences = usedNames[name];
+ if (defined(occurrences)) {
+ usedNames[name]++;
+ name = name + '_' + occurrences;
+ } else {
+ usedNames[name] = 1;
+ }
+ item.name = name;
+ }
+}
+
+function setDefaults(nodes) {
+ var usedNames = {};
+ setDefaultNames(nodes, 'Node', usedNames);
+ var nodesLength = nodes.length;
+ for (var i = 0; i < nodesLength; ++i) {
+ var node = nodes[i];
+ setDefaultNames(node.meshes, node.name + '-Mesh', usedNames);
+ }
+}
+
+function cleanNodes(nodes) {
+ nodes = removeEmptyNodes(nodes);
+ setDefaults(nodes);
+ return nodes;
}
diff --git a/lib/readLines.js b/lib/readLines.js
new file mode 100644
index 0000000..4178147
--- /dev/null
+++ b/lib/readLines.js
@@ -0,0 +1,27 @@
+'use strict';
+var eventStream = require('event-stream');
+var fsExtra = require('fs-extra');
+var Promise = require('bluebird');
+
+module.exports = readLines;
+
+/**
+ * Read a file line-by-line.
+ *
+ * @param {String} path Path to the file.
+ * @param {Function} callback Function to call when reading each line.
+ * @returns {Promise} A promise when the reader is finished.
+ *
+ * @private
+ */
+function readLines(path, callback) {
+ return new Promise(function(resolve, reject) {
+ fsExtra.createReadStream(path)
+ .on('error', reject)
+ .on('end', resolve)
+ .pipe(eventStream.split())
+ .pipe(eventStream.mapSync(function (line) {
+ callback(line);
+ }));
+ });
+}
diff --git a/package.json b/package.json
index 75a2b70..67cd831 100644
--- a/package.json
+++ b/package.json
@@ -6,45 +6,44 @@
"contributors": [
{
"name": "Analytical Graphics, Inc., and Contributors",
- "url": "https://github.com/AnalyticalGraphicsInc/OBJ2GLTF/graphs/contributors"
+ "url": "https://github.com/AnalyticalGraphicsInc/obj2gltf/graphs/contributors"
}
],
"keywords": [
"obj",
"gltf"
],
- "homepage": "https://github.com/AnalyticalGraphicsInc/OBJ2GLTF",
+ "homepage": "https://github.com/AnalyticalGraphicsInc/obj2gltf",
"repository": {
"type": "git",
- "url": "git@github.com:AnalyticalGraphicsInc/OBJ2GLTF.git"
+ "url": "git@github.com:AnalyticalGraphicsInc/obj2gltf.git"
},
"bugs": {
- "url": "https://github.com/AnalyticalGraphicsInc/OBJ2GLTF/issues"
+ "url": "https://github.com/AnalyticalGraphicsInc/obj2gltf/issues"
},
"main": "index.js",
"engines": {
"node": ">=4.0.0"
},
"dependencies": {
- "async": "2.1.2",
- "bluebird": "3.4.6",
- "byline": "5.0.0",
- "cesium": "1.26.0",
- "fs-extra": "0.30.0",
- "gltf-pipeline": "0.1.0-alpha8",
- "yargs": "6.3.0"
+ "bluebird": "^3.4.7",
+ "cesium": "^1.31.0",
+ "event-stream": "^3.3.4",
+ "fs-extra": "^2.0.0",
+ "gltf-pipeline": "^0.1.0-alpha11",
+ "yargs": "^7.0.1"
},
"devDependencies": {
- "gulp": "3.9.1",
- "gulp-jshint": "2.0.2",
- "istanbul": "0.4.5",
- "jasmine": "2.5.2",
- "jasmine-spec-reporter": "2.7.0",
- "jshint": "2.9.4",
- "jshint-stylish": "2.2.1",
- "open": "0.0.5",
- "requirejs": "2.3.2",
- "typings": "1.4.0"
+ "gulp": "^3.9.1",
+ "gulp-jshint": "^2.0.4",
+ "istanbul": "^0.4.5",
+ "jasmine": "^2.5.3",
+ "jasmine-spec-reporter": "^3.2.0",
+ "jshint": "^2.9.4",
+ "jshint-stylish": "^2.2.1",
+ "open": "^0.0.5",
+ "requirejs": "^2.3.3",
+ "typings": "^2.1.0"
},
"scripts": {
"prepublish": "typings install",
diff --git a/specs/data/BoxTextured/CesiumLogoFlat.png b/specs/data/BoxTextured/CesiumLogoFlat.png
deleted file mode 100644
index 88bada3..0000000
Binary files a/specs/data/BoxTextured/CesiumLogoFlat.png and /dev/null differ
diff --git a/specs/data/box-complex-material/alpha.png b/specs/data/box-complex-material/alpha.png
new file mode 100644
index 0000000..8437c62
Binary files /dev/null and b/specs/data/box-complex-material/alpha.png differ
diff --git a/specs/data/box-complex-material/ambient.gif b/specs/data/box-complex-material/ambient.gif
new file mode 100644
index 0000000..1276823
Binary files /dev/null and b/specs/data/box-complex-material/ambient.gif differ
diff --git a/specs/data/box-complex-material/box-complex-material.mtl b/specs/data/box-complex-material/box-complex-material.mtl
new file mode 100644
index 0000000..3f69a9e
--- /dev/null
+++ b/specs/data/box-complex-material/box-complex-material.mtl
@@ -0,0 +1,20 @@
+# Blender MTL File: 'None'
+# Material Count: 1
+
+newmtl Material
+Ns 96.078431
+Ka 0.200000 0.200000 0.200000
+Kd 0.640000 0.640000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.100000 0.100000 0.100000
+Ni 1.000000
+d 0.900000
+Tr 0.900000
+map_Ka ambient.gif
+map_Ke emission.jpg
+map_Kd diffuse.png
+map_Ks specular.jpeg
+map_Ns shininess.png
+map_Bump bump.png
+map_d alpha.png
+illum 2
diff --git a/specs/data/BoxTextured/BoxTextured.obj b/specs/data/box-complex-material/box-complex-material.obj
similarity index 73%
rename from specs/data/BoxTextured/BoxTextured.obj
rename to specs/data/box-complex-material/box-complex-material.obj
index 64da02d..0ca30b0 100644
--- a/specs/data/BoxTextured/BoxTextured.obj
+++ b/specs/data/box-complex-material/box-complex-material.obj
@@ -1,7 +1,7 @@
-# Blender v2.77 (sub 0) OBJ File: 'BoxTextured.blend'
+# Blender v2.78 (sub 0) OBJ File: ''
# www.blender.org
-mtllib BoxTextured.mtl
-o Cube_Cube.001
+mtllib box-complex-material.mtl
+o Cube
v -1.000000 -1.000000 1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 -1.000000
@@ -26,9 +26,9 @@ vt 0.0000 0.0000
vt 1.0000 0.0000
vt 1.0000 1.0000
vt 0.0000 1.0000
-vt 0.0000 0.0000
vt 1.0000 0.0000
vt 1.0000 1.0000
+vt 0.0000 0.0000
vt 0.0000 1.0000
vn -1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
@@ -36,11 +36,11 @@ vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 1.0000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
-usemtl Textured
+usemtl Material
s off
-f 2/1/1 4/2/1 3/3/1 1/4/1
-f 4/5/2 8/6/2 7/7/2 3/8/2
-f 8/9/3 6/10/3 5/11/3 7/12/3
-f 6/13/4 2/14/4 1/15/4 5/16/4
-f 1/17/5 3/18/5 7/7/5 5/16/5
-f 6/13/6 8/6/6 4/19/6 2/20/6
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
diff --git a/specs/data/box-complex-material/bump.png b/specs/data/box-complex-material/bump.png
new file mode 100644
index 0000000..fcc5ba8
Binary files /dev/null and b/specs/data/box-complex-material/bump.png differ
diff --git a/specs/data/box-complex-material/diffuse.png b/specs/data/box-complex-material/diffuse.png
new file mode 100644
index 0000000..8704966
Binary files /dev/null and b/specs/data/box-complex-material/diffuse.png differ
diff --git a/specs/data/box-complex-material/emission.jpg b/specs/data/box-complex-material/emission.jpg
new file mode 100644
index 0000000..e5c85a9
Binary files /dev/null and b/specs/data/box-complex-material/emission.jpg differ
diff --git a/specs/data/box-complex-material/shininess.png b/specs/data/box-complex-material/shininess.png
new file mode 100644
index 0000000..bfcf1a2
Binary files /dev/null and b/specs/data/box-complex-material/shininess.png differ
diff --git a/specs/data/box-complex-material/specular.jpeg b/specs/data/box-complex-material/specular.jpeg
new file mode 100644
index 0000000..661bf98
Binary files /dev/null and b/specs/data/box-complex-material/specular.jpeg differ
diff --git a/specs/data/box-groups/box-groups.mtl b/specs/data/box-groups/box-groups.mtl
new file mode 100644
index 0000000..2f9b11a
--- /dev/null
+++ b/specs/data/box-groups/box-groups.mtl
@@ -0,0 +1,32 @@
+# Blender MTL File: 'box-objects.blend'
+# Material Count: 3
+
+newmtl Blue
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.000000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Green
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.640000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Red
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.000000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-groups/box-groups.obj b/specs/data/box-groups/box-groups.obj
new file mode 100644
index 0000000..5ac69f0
--- /dev/null
+++ b/specs/data/box-groups/box-groups.obj
@@ -0,0 +1,132 @@
+# Blender v2.78 (sub 0) OBJ File: 'box-objects.blend'
+# www.blender.org
+mtllib box-groups.mtl
+g CubeBlue
+v -1.000000 -1.000000 -4.000000
+v -1.000000 1.000000 -4.000000
+v -1.000000 -1.000000 -6.000000
+v -1.000000 1.000000 -6.000000
+v 1.000000 -1.000000 -4.000000
+v 1.000000 1.000000 -4.000000
+v 1.000000 -1.000000 -6.000000
+v 1.000000 1.000000 -6.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Blue
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
+g CubeGreen
+v 4.000000 -1.000000 1.000000
+v 4.000000 1.000000 1.000000
+v 4.000000 -1.000000 -1.000000
+v 4.000000 1.000000 -1.000000
+v 6.000000 -1.000000 1.000000
+v 6.000000 1.000000 1.000000
+v 6.000000 -1.000000 -1.000000
+v 6.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Green
+s off
+f 9/21/7 10/22/7 12/23/7 11/24/7
+f 11/25/8 12/26/8 16/27/8 15/28/8
+f 15/29/9 16/30/9 14/31/9 13/32/9
+f 13/33/10 14/34/10 10/35/10 9/36/10
+f 11/25/11 15/37/11 13/38/11 9/36/11
+f 16/39/12 12/26/12 10/35/12 14/40/12
+g CubeRed
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Red
+s off
+f 17/41/13 18/42/13 20/43/13 19/44/13
+f 19/45/14 20/46/14 24/47/14 23/48/14
+f 23/49/15 24/50/15 22/51/15 21/52/15
+f 21/53/16 22/54/16 18/55/16 17/56/16
+f 19/45/17 23/57/17 21/58/17 17/56/17
+f 24/59/18 20/46/18 18/55/18 22/60/18
diff --git a/specs/data/box-missing-mtllib/box-missing-mtllib.obj b/specs/data/box-missing-mtllib/box-missing-mtllib.obj
new file mode 100644
index 0000000..5246261
--- /dev/null
+++ b/specs/data/box-missing-mtllib/box-missing-mtllib.obj
@@ -0,0 +1,46 @@
+# Blender v2.78 (sub 0) OBJ File: ''
+# www.blender.org
+mtllib box.mtl
+o Cube
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Material
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
diff --git a/specs/data/BoxTextured/BoxTextured.mtl b/specs/data/box-missing-texture/box-missing-texture.mtl
similarity index 57%
rename from specs/data/BoxTextured/BoxTextured.mtl
rename to specs/data/box-missing-texture/box-missing-texture.mtl
index eeff817..c5c879f 100644
--- a/specs/data/BoxTextured/BoxTextured.mtl
+++ b/specs/data/box-missing-texture/box-missing-texture.mtl
@@ -1,13 +1,13 @@
-# Blender MTL File: 'BoxTextured.blend'
+# Blender MTL File: 'box.blend'
# Material Count: 1
-newmtl Textured
+newmtl Material
Ns 96.078431
-Ka 1.000000 1.000000 1.000000
+Ka 0.000000 0.000000 0.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
-map_Kd CesiumLogoFlat.png
+map_Kd cesium.png
diff --git a/specs/data/box-missing-texture/box-missing-texture.obj b/specs/data/box-missing-texture/box-missing-texture.obj
new file mode 100644
index 0000000..ced632f
--- /dev/null
+++ b/specs/data/box-missing-texture/box-missing-texture.obj
@@ -0,0 +1,46 @@
+# Blender v2.78 (sub 0) OBJ File: 'box.blend'
+# www.blender.org
+mtllib box-missing-texture.mtl
+o Cube
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Material
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
diff --git a/specs/data/box-mtllib/box-mtllib-blue.mtl b/specs/data/box-mtllib/box-mtllib-blue.mtl
new file mode 100644
index 0000000..d3fe863
--- /dev/null
+++ b/specs/data/box-mtllib/box-mtllib-blue.mtl
@@ -0,0 +1,12 @@
+# Blender MTL File: 'box-multiple-materials.blend'
+# Material Count: 1
+
+newmtl Blue
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.000000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-mtllib/box-mtllib-green.mtl b/specs/data/box-mtllib/box-mtllib-green.mtl
new file mode 100644
index 0000000..89abba9
--- /dev/null
+++ b/specs/data/box-mtllib/box-mtllib-green.mtl
@@ -0,0 +1,12 @@
+# Blender MTL File: 'box-multiple-materials.blend'
+# Material Count: 1
+
+newmtl Green
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.640000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-mtllib/box-mtllib-red.mtl b/specs/data/box-mtllib/box-mtllib-red.mtl
new file mode 100644
index 0000000..3721d86
--- /dev/null
+++ b/specs/data/box-mtllib/box-mtllib-red.mtl
@@ -0,0 +1,12 @@
+# Blender MTL File: 'box-multiple-materials.blend'
+# Material Count: 1
+
+newmtl Red
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.000000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-mtllib/box-mtllib.obj b/specs/data/box-mtllib/box-mtllib.obj
new file mode 100644
index 0000000..cced063
--- /dev/null
+++ b/specs/data/box-mtllib/box-mtllib.obj
@@ -0,0 +1,50 @@
+# Blender v2.78 (sub 0) OBJ File: 'box-multiple-materials.blend'
+# www.blender.org
+mtllib box-mtllib-red.mtl
+mtllib box-mtllib-green.mtl box-mtllib-blue.mtl
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+vn -1.0000 0.0000 0.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 0.0000 0.0000 1.0000
+usemtl Red
+f 3/1/1 7/2/1 5/3/1 1/4/1
+usemtl Green
+f 1/9/3 2/10/3 4/11/3 3/12/3
+usemtl Blue
+f 3/1/5 4/6/5 8/17/5 7/18/5
+usemtl Red
+f 8/5/2 4/6/2 2/7/2 6/8/2
+usemtl Green
+f 7/13/4 8/14/4 6/15/4 5/16/4
+usemtl Blue
+f 5/19/6 6/20/6 2/7/6 1/4/6
diff --git a/specs/data/box-multiple-materials/box-multiple-materials.mtl b/specs/data/box-multiple-materials/box-multiple-materials.mtl
new file mode 100644
index 0000000..7fb6cdb
--- /dev/null
+++ b/specs/data/box-multiple-materials/box-multiple-materials.mtl
@@ -0,0 +1,32 @@
+# Blender MTL File: 'box-multiple-materials.blend'
+# Material Count: 3
+
+newmtl Blue
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.000000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Green
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.640000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Red
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.000000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-multiple-materials/box-multiple-materials.obj b/specs/data/box-multiple-materials/box-multiple-materials.obj
new file mode 100644
index 0000000..5c4848d
--- /dev/null
+++ b/specs/data/box-multiple-materials/box-multiple-materials.obj
@@ -0,0 +1,49 @@
+# Blender v2.78 (sub 0) OBJ File: 'box-multiple-materials.blend'
+# www.blender.org
+mtllib box-multiple-materials.mtl
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+vn -1.0000 0.0000 0.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 0.0000 0.0000 1.0000
+usemtl Red
+f 3/1/1 7/2/1 5/3/1 1/4/1
+usemtl Green
+f 1/9/3 2/10/3 4/11/3 3/12/3
+usemtl Blue
+f 3/1/5 4/6/5 8/17/5 7/18/5
+usemtl Red
+f 8/5/2 4/6/2 2/7/2 6/8/2
+usemtl Green
+f 7/13/4 8/14/4 6/15/4 5/16/4
+usemtl Blue
+f 5/19/6 6/20/6 2/7/6 1/4/6
diff --git a/specs/data/box-negative-indices/box-negative-indices.mtl b/specs/data/box-negative-indices/box-negative-indices.mtl
new file mode 100644
index 0000000..abbc294
--- /dev/null
+++ b/specs/data/box-negative-indices/box-negative-indices.mtl
@@ -0,0 +1,12 @@
+# Blender MTL File: 'box.blend'
+# Material Count: 1
+
+newmtl Material
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.640000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-negative-indices/box-negative-indices.obj b/specs/data/box-negative-indices/box-negative-indices.obj
new file mode 100644
index 0000000..e3b2aa6
--- /dev/null
+++ b/specs/data/box-negative-indices/box-negative-indices.obj
@@ -0,0 +1,20 @@
+# Blender v2.78 (sub 0) OBJ File: 'box.blend'
+# www.blender.org
+mtllib box-negative-indices.mtl
+o Cube
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+usemtl Material
+s off
+f -8 -7 -5 -6
+f -6 -5 -1 -2
+f -2 -1 -3 -4
+f -4 -3 -7 -8
+f -6 -2 -4 -8
+f -1 -5 -7 -3
diff --git a/specs/data/box-no-materials/box-no-materials.obj b/specs/data/box-no-materials/box-no-materials.obj
new file mode 100644
index 0000000..a1f2147
--- /dev/null
+++ b/specs/data/box-no-materials/box-no-materials.obj
@@ -0,0 +1,125 @@
+# Blender v2.78 (sub 0) OBJ File: 'box-objects.blend'
+# www.blender.org
+v -1.000000 -1.000000 -4.000000
+v -1.000000 1.000000 -4.000000
+v -1.000000 -1.000000 -6.000000
+v -1.000000 1.000000 -6.000000
+v 1.000000 -1.000000 -4.000000
+v 1.000000 1.000000 -4.000000
+v 1.000000 -1.000000 -6.000000
+v 1.000000 1.000000 -6.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
+v 4.000000 -1.000000 1.000000
+v 4.000000 1.000000 1.000000
+v 4.000000 -1.000000 -1.000000
+v 4.000000 1.000000 -1.000000
+v 6.000000 -1.000000 1.000000
+v 6.000000 1.000000 1.000000
+v 6.000000 -1.000000 -1.000000
+v 6.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+s off
+f 9/21/7 10/22/7 12/23/7 11/24/7
+f 11/25/8 12/26/8 16/27/8 15/28/8
+f 15/29/9 16/30/9 14/31/9 13/32/9
+f 13/33/10 14/34/10 10/35/10 9/36/10
+f 11/25/11 15/37/11 13/38/11 9/36/11
+f 16/39/12 12/26/12 10/35/12 14/40/12
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+s off
+f 17/41/13 18/42/13 20/43/13 19/44/13
+f 19/45/14 20/46/14 24/47/14 23/48/14
+f 23/49/15 24/50/15 22/51/15 21/52/15
+f 21/53/16 22/54/16 18/55/16 17/56/16
+f 19/45/17 23/57/17 21/58/17 17/56/17
+f 24/59/18 20/46/18 18/55/18 22/60/18
diff --git a/specs/data/box-normals/box-normals.mtl b/specs/data/box-normals/box-normals.mtl
new file mode 100644
index 0000000..abbc294
--- /dev/null
+++ b/specs/data/box-normals/box-normals.mtl
@@ -0,0 +1,12 @@
+# Blender MTL File: 'box.blend'
+# Material Count: 1
+
+newmtl Material
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.640000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-normals/box-normals.obj b/specs/data/box-normals/box-normals.obj
new file mode 100644
index 0000000..5d727cc
--- /dev/null
+++ b/specs/data/box-normals/box-normals.obj
@@ -0,0 +1,26 @@
+# Blender v2.78 (sub 0) OBJ File: 'box.blend'
+# www.blender.org
+mtllib box-normals.mtl
+o Cube
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Material
+s off
+f 1//1 2//1 4//1 3//1
+f 3//2 4//2 8//2 7//2
+f 7//3 8//3 6//3 5//3
+f 5//4 6//4 2//4 1//4
+f 3//5 7//5 5//5 1//5
+f 8//6 4//6 2//6 6//6
diff --git a/specs/data/box-objects-groups-materials/box-objects-groups-materials.gltf b/specs/data/box-objects-groups-materials/box-objects-groups-materials.gltf
new file mode 100644
index 0000000..0220c2a
--- /dev/null
+++ b/specs/data/box-objects-groups-materials/box-objects-groups-materials.gltf
@@ -0,0 +1,486 @@
+{
+ "accessors": {
+ "accessor_0": {
+ "bufferView": "bufferView_vertex",
+ "byteOffset": 0,
+ "byteStride": 0,
+ "componentType": 5126,
+ "count": 24,
+ "min": [
+ -1,
+ -1,
+ -6
+ ],
+ "max": [
+ 1,
+ 1,
+ -4
+ ],
+ "type": "VEC3"
+ },
+ "accessor_1": {
+ "bufferView": "bufferView_vertex",
+ "byteOffset": 288,
+ "byteStride": 0,
+ "componentType": 5126,
+ "count": 24,
+ "min": [
+ -1,
+ -1,
+ -1
+ ],
+ "max": [
+ 1,
+ 1,
+ 1
+ ],
+ "type": "VEC3"
+ },
+ "accessor_2": {
+ "bufferView": "bufferView_vertex",
+ "byteOffset": 576,
+ "byteStride": 0,
+ "componentType": 5126,
+ "count": 24,
+ "min": [
+ 0,
+ 0
+ ],
+ "max": [
+ 1,
+ 1
+ ],
+ "type": "VEC2"
+ },
+ "accessor_3": {
+ "bufferView": "bufferView_index",
+ "byteOffset": 0,
+ "byteStride": 0,
+ "componentType": 5123,
+ "count": 18,
+ "min": [
+ 0
+ ],
+ "max": [
+ 11
+ ],
+ "type": "SCALAR"
+ },
+ "accessor_4": {
+ "bufferView": "bufferView_index",
+ "byteOffset": 36,
+ "byteStride": 0,
+ "componentType": 5123,
+ "count": 18,
+ "min": [
+ 12
+ ],
+ "max": [
+ 23
+ ],
+ "type": "SCALAR"
+ },
+ "accessor_5": {
+ "bufferView": "bufferView_vertex",
+ "byteOffset": 768,
+ "byteStride": 0,
+ "componentType": 5126,
+ "count": 24,
+ "min": [
+ 4,
+ -1,
+ -1
+ ],
+ "max": [
+ 6,
+ 1,
+ 1
+ ],
+ "type": "VEC3"
+ },
+ "accessor_6": {
+ "bufferView": "bufferView_vertex",
+ "byteOffset": 1056,
+ "byteStride": 0,
+ "componentType": 5126,
+ "count": 24,
+ "min": [
+ -1,
+ -1,
+ -1
+ ],
+ "max": [
+ 1,
+ 1,
+ 1
+ ],
+ "type": "VEC3"
+ },
+ "accessor_7": {
+ "bufferView": "bufferView_vertex",
+ "byteOffset": 1344,
+ "byteStride": 0,
+ "componentType": 5126,
+ "count": 24,
+ "min": [
+ 0,
+ 0
+ ],
+ "max": [
+ 1,
+ 1
+ ],
+ "type": "VEC2"
+ },
+ "accessor_8": {
+ "bufferView": "bufferView_index",
+ "byteOffset": 72,
+ "byteStride": 0,
+ "componentType": 5123,
+ "count": 18,
+ "min": [
+ 0
+ ],
+ "max": [
+ 11
+ ],
+ "type": "SCALAR"
+ },
+ "accessor_9": {
+ "bufferView": "bufferView_index",
+ "byteOffset": 108,
+ "byteStride": 0,
+ "componentType": 5123,
+ "count": 18,
+ "min": [
+ 12
+ ],
+ "max": [
+ 23
+ ],
+ "type": "SCALAR"
+ },
+ "accessor_10": {
+ "bufferView": "bufferView_vertex",
+ "byteOffset": 1536,
+ "byteStride": 0,
+ "componentType": 5126,
+ "count": 24,
+ "min": [
+ -1,
+ -1,
+ -1
+ ],
+ "max": [
+ 1,
+ 1,
+ 1
+ ],
+ "type": "VEC3"
+ },
+ "accessor_11": {
+ "bufferView": "bufferView_vertex",
+ "byteOffset": 1824,
+ "byteStride": 0,
+ "componentType": 5126,
+ "count": 24,
+ "min": [
+ -1,
+ -1,
+ -1
+ ],
+ "max": [
+ 1,
+ 1,
+ 1
+ ],
+ "type": "VEC3"
+ },
+ "accessor_12": {
+ "bufferView": "bufferView_vertex",
+ "byteOffset": 2112,
+ "byteStride": 0,
+ "componentType": 5126,
+ "count": 24,
+ "min": [
+ 0,
+ 0
+ ],
+ "max": [
+ 1,
+ 1
+ ],
+ "type": "VEC2"
+ },
+ "accessor_13": {
+ "bufferView": "bufferView_index",
+ "byteOffset": 144,
+ "byteStride": 0,
+ "componentType": 5123,
+ "count": 18,
+ "min": [
+ 0
+ ],
+ "max": [
+ 11
+ ],
+ "type": "SCALAR"
+ },
+ "accessor_14": {
+ "bufferView": "bufferView_index",
+ "byteOffset": 180,
+ "byteStride": 0,
+ "componentType": 5123,
+ "count": 18,
+ "min": [
+ 12
+ ],
+ "max": [
+ 23
+ ],
+ "type": "SCALAR"
+ }
+ },
+ "asset": {
+ "generator": "obj2gltf",
+ "profile": {
+ "api": "WebGL",
+ "version": "1.0"
+ },
+ "version": "1.0"
+ },
+ "buffers": {
+ "buffer": {
+ "byteLength": 2520,
+ "uri": "data:application/octet-stream;base64,AACAvwAAgL8AAIDAAACAvwAAgD8AAIDAAACAvwAAgD8AAMDAAACAvwAAgL8AAMDAAACAvwAAgL8AAMDAAACAvwAAgD8AAMDAAACAPwAAgD8AAMDAAACAPwAAgL8AAMDAAACAPwAAgL8AAMDAAACAPwAAgD8AAMDAAACAPwAAgD8AAIDAAACAPwAAgL8AAIDAAACAPwAAgL8AAIDAAACAPwAAgD8AAIDAAACAvwAAgD8AAIDAAACAvwAAgL8AAIDAAACAvwAAgL8AAMDAAACAPwAAgL8AAMDAAACAPwAAgL8AAIDAAACAvwAAgL8AAIDAAACAPwAAgD8AAMDAAACAvwAAgD8AAMDAAACAvwAAgD8AAIDAAACAPwAAgD8AAIDAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAACAQAAAgL8AAIA/AACAQAAAgD8AAIA/AACAQAAAgD8AAIC/AACAQAAAgL8AAIC/AACAQAAAgL8AAIC/AACAQAAAgD8AAIC/AADAQAAAgD8AAIC/AADAQAAAgL8AAIC/AADAQAAAgL8AAIC/AADAQAAAgD8AAIC/AADAQAAAgD8AAIA/AADAQAAAgL8AAIA/AADAQAAAgL8AAIA/AADAQAAAgD8AAIA/AACAQAAAgD8AAIA/AACAQAAAgL8AAIA/AACAQAAAgL8AAIC/AADAQAAAgL8AAIC/AADAQAAAgL8AAIA/AACAQAAAgL8AAIA/AADAQAAAgD8AAIC/AACAQAAAgD8AAIC/AACAQAAAgD8AAIA/AADAQAAAgD8AAIA/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIA/AACAvwAAgL8AAIA/AACAPwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIA/AACAPwAAgD8AAIA/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcA"
+ }
+ },
+ "bufferViews": {
+ "bufferView_vertex": {
+ "buffer": "buffer",
+ "byteLength": 2304,
+ "byteOffset": 0,
+ "target": 34962
+ },
+ "bufferView_index": {
+ "buffer": "buffer",
+ "byteLength": 216,
+ "byteOffset": 2304,
+ "target": 34963
+ }
+ },
+ "extensionsUsed": [
+ "KHR_materials_common"
+ ],
+ "images": {},
+ "materials": {
+ "Blue": {
+ "extensions": {
+ "KHR_materials_common": {
+ "technique": "PHONG",
+ "values": {
+ "ambient": [
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ "diffuse": [
+ 0,
+ 0,
+ 0.64,
+ 1
+ ],
+ "emission": [
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ "specular": [
+ 0.5,
+ 0.5,
+ 0.5,
+ 1
+ ],
+ "shininess": 96.078431,
+ "transparency": 1,
+ "transparent": false,
+ "doubleSided": false
+ }
+ }
+ }
+ },
+ "Green": {
+ "extensions": {
+ "KHR_materials_common": {
+ "technique": "PHONG",
+ "values": {
+ "ambient": [
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ "diffuse": [
+ 0,
+ 0.64,
+ 0,
+ 1
+ ],
+ "emission": [
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ "specular": [
+ 0.5,
+ 0.5,
+ 0.5,
+ 1
+ ],
+ "shininess": 96.078431,
+ "transparency": 1,
+ "transparent": false,
+ "doubleSided": false
+ }
+ }
+ }
+ },
+ "Red": {
+ "extensions": {
+ "KHR_materials_common": {
+ "technique": "PHONG",
+ "values": {
+ "ambient": [
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ "diffuse": [
+ 0.64,
+ 0,
+ 0,
+ 1
+ ],
+ "emission": [
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ "specular": [
+ 0.5,
+ 0.5,
+ 0.5,
+ 1
+ ],
+ "shininess": 96.078431,
+ "transparency": 1,
+ "transparent": false,
+ "doubleSided": false
+ }
+ }
+ }
+ }
+ },
+ "meshes": {
+ "CubeBlue_CubeBlue_Blue": {
+ "name": "CubeBlue_CubeBlue_Blue",
+ "primitives": [
+ {
+ "attributes": {
+ "POSITION": "accessor_0",
+ "NORMAL": "accessor_1",
+ "TEXCOORD_0": "accessor_2"
+ },
+ "indices": "accessor_3",
+ "material": "Blue",
+ "mode": 4
+ },
+ {
+ "attributes": {
+ "POSITION": "accessor_0",
+ "NORMAL": "accessor_1",
+ "TEXCOORD_0": "accessor_2"
+ },
+ "indices": "accessor_4",
+ "material": "Green",
+ "mode": 4
+ }
+ ]
+ },
+ "CubeGreen_CubeGreen_Green": {
+ "name": "CubeGreen_CubeGreen_Green",
+ "primitives": [
+ {
+ "attributes": {
+ "POSITION": "accessor_5",
+ "NORMAL": "accessor_6",
+ "TEXCOORD_0": "accessor_7"
+ },
+ "indices": "accessor_8",
+ "material": "Green",
+ "mode": 4
+ },
+ {
+ "attributes": {
+ "POSITION": "accessor_5",
+ "NORMAL": "accessor_6",
+ "TEXCOORD_0": "accessor_7"
+ },
+ "indices": "accessor_9",
+ "material": "Red",
+ "mode": 4
+ }
+ ]
+ },
+ "CubeRed_CubeRed_Red": {
+ "name": "CubeRed_CubeRed_Red",
+ "primitives": [
+ {
+ "attributes": {
+ "POSITION": "accessor_10",
+ "NORMAL": "accessor_11",
+ "TEXCOORD_0": "accessor_12"
+ },
+ "indices": "accessor_13",
+ "material": "Red",
+ "mode": 4
+ },
+ {
+ "attributes": {
+ "POSITION": "accessor_10",
+ "NORMAL": "accessor_11",
+ "TEXCOORD_0": "accessor_12"
+ },
+ "indices": "accessor_14",
+ "material": "Blue",
+ "mode": 4
+ }
+ ]
+ }
+ },
+ "nodes": {
+ "Cube": {
+ "name": "Cube",
+ "meshes": [
+ "CubeBlue_CubeBlue_Blue",
+ "CubeGreen_CubeGreen_Green",
+ "CubeRed_CubeRed_Red"
+ ]
+ }
+ },
+ "samplers": {},
+ "scene": "scene",
+ "scenes": {
+ "scene": {
+ "nodes": [
+ "Cube"
+ ]
+ }
+ },
+ "textures": {}
+}
diff --git a/specs/data/box-objects-groups-materials/box-objects-groups-materials.mtl b/specs/data/box-objects-groups-materials/box-objects-groups-materials.mtl
new file mode 100644
index 0000000..2f9b11a
--- /dev/null
+++ b/specs/data/box-objects-groups-materials/box-objects-groups-materials.mtl
@@ -0,0 +1,32 @@
+# Blender MTL File: 'box-objects.blend'
+# Material Count: 3
+
+newmtl Blue
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.000000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Green
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.640000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Red
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.000000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-objects-groups-materials/box-objects-groups-materials.obj b/specs/data/box-objects-groups-materials/box-objects-groups-materials.obj
new file mode 100644
index 0000000..1bb7698
--- /dev/null
+++ b/specs/data/box-objects-groups-materials/box-objects-groups-materials.obj
@@ -0,0 +1,133 @@
+# Blender v2.78 (sub 0) OBJ File: 'box-objects.blend'
+# www.blender.org
+mtllib box-objects-groups-materials.mtl
+o Cube
+v -1.000000 -1.000000 -4.000000
+v -1.000000 1.000000 -4.000000
+v -1.000000 -1.000000 -6.000000
+v -1.000000 1.000000 -6.000000
+v 1.000000 -1.000000 -4.000000
+v 1.000000 1.000000 -4.000000
+v 1.000000 -1.000000 -6.000000
+v 1.000000 1.000000 -6.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+g CubeBlue_CubeBlue_Blue
+usemtl Blue
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+usemtl Green
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
+v 4.000000 -1.000000 1.000000
+v 4.000000 1.000000 1.000000
+v 4.000000 -1.000000 -1.000000
+v 4.000000 1.000000 -1.000000
+v 6.000000 -1.000000 1.000000
+v 6.000000 1.000000 1.000000
+v 6.000000 -1.000000 -1.000000
+v 6.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+g CubeGreen_CubeGreen_Green
+usemtl Green
+f 9/21/7 10/22/7 12/23/7 11/24/7
+f 11/25/8 12/26/8 16/27/8 15/28/8
+f 15/29/9 16/30/9 14/31/9 13/32/9
+usemtl Red
+f 13/33/10 14/34/10 10/35/10 9/36/10
+f 11/25/11 15/37/11 13/38/11 9/36/11
+f 16/39/12 12/26/12 10/35/12 14/40/12
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+g CubeRed_CubeRed_Red
+usemtl Red
+f 17/41/13 18/42/13 20/43/13 19/44/13
+f 19/45/14 20/46/14 24/47/14 23/48/14
+f 23/49/15 24/50/15 22/51/15 21/52/15
+usemtl Blue
+f 21/53/16 22/54/16 18/55/16 17/56/16
+f 19/45/17 23/57/17 21/58/17 17/56/17
+f 24/59/18 20/46/18 18/55/18 22/60/18
diff --git a/specs/data/box-objects-groups/box-objects-groups.mtl b/specs/data/box-objects-groups/box-objects-groups.mtl
new file mode 100644
index 0000000..2f9b11a
--- /dev/null
+++ b/specs/data/box-objects-groups/box-objects-groups.mtl
@@ -0,0 +1,32 @@
+# Blender MTL File: 'box-objects.blend'
+# Material Count: 3
+
+newmtl Blue
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.000000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Green
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.640000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Red
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.000000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-objects-groups/box-objects-groups.obj b/specs/data/box-objects-groups/box-objects-groups.obj
new file mode 100644
index 0000000..2672d19
--- /dev/null
+++ b/specs/data/box-objects-groups/box-objects-groups.obj
@@ -0,0 +1,135 @@
+# Blender v2.78 (sub 0) OBJ File: 'box-objects.blend'
+# www.blender.org
+mtllib box-objects-groups.mtl
+o CubeBlue
+v -1.000000 -1.000000 -4.000000
+v -1.000000 1.000000 -4.000000
+v -1.000000 -1.000000 -6.000000
+v -1.000000 1.000000 -6.000000
+v 1.000000 -1.000000 -4.000000
+v 1.000000 1.000000 -4.000000
+v 1.000000 -1.000000 -6.000000
+v 1.000000 1.000000 -6.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+g CubeBlue_CubeBlue_Blue
+usemtl Blue
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
+o CubeGreen
+v 4.000000 -1.000000 1.000000
+v 4.000000 1.000000 1.000000
+v 4.000000 -1.000000 -1.000000
+v 4.000000 1.000000 -1.000000
+v 6.000000 -1.000000 1.000000
+v 6.000000 1.000000 1.000000
+v 6.000000 -1.000000 -1.000000
+v 6.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+g CubeGreen_CubeGreen_Green
+usemtl Green
+s off
+f 9/21/7 10/22/7 12/23/7 11/24/7
+f 11/25/8 12/26/8 16/27/8 15/28/8
+f 15/29/9 16/30/9 14/31/9 13/32/9
+f 13/33/10 14/34/10 10/35/10 9/36/10
+f 11/25/11 15/37/11 13/38/11 9/36/11
+f 16/39/12 12/26/12 10/35/12 14/40/12
+o CubeRed
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+g CubeRed_CubeRed_Red
+usemtl Red
+s off
+f 17/41/13 18/42/13 20/43/13 19/44/13
+f 19/45/14 20/46/14 24/47/14 23/48/14
+f 23/49/15 24/50/15 22/51/15 21/52/15
+f 21/53/16 22/54/16 18/55/16 17/56/16
+f 19/45/17 23/57/17 21/58/17 17/56/17
+f 24/59/18 20/46/18 18/55/18 22/60/18
diff --git a/specs/data/box-objects/box-objects.mtl b/specs/data/box-objects/box-objects.mtl
new file mode 100644
index 0000000..2f9b11a
--- /dev/null
+++ b/specs/data/box-objects/box-objects.mtl
@@ -0,0 +1,32 @@
+# Blender MTL File: 'box-objects.blend'
+# Material Count: 3
+
+newmtl Blue
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.000000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Green
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.640000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Red
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.000000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-objects/box-objects.obj b/specs/data/box-objects/box-objects.obj
new file mode 100644
index 0000000..0f86b05
--- /dev/null
+++ b/specs/data/box-objects/box-objects.obj
@@ -0,0 +1,132 @@
+# Blender v2.78 (sub 0) OBJ File: 'box-objects.blend'
+# www.blender.org
+mtllib box-objects.mtl
+o CubeBlue
+v -1.000000 -1.000000 -4.000000
+v -1.000000 1.000000 -4.000000
+v -1.000000 -1.000000 -6.000000
+v -1.000000 1.000000 -6.000000
+v 1.000000 -1.000000 -4.000000
+v 1.000000 1.000000 -4.000000
+v 1.000000 -1.000000 -6.000000
+v 1.000000 1.000000 -6.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Blue
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
+o CubeGreen
+v 4.000000 -1.000000 1.000000
+v 4.000000 1.000000 1.000000
+v 4.000000 -1.000000 -1.000000
+v 4.000000 1.000000 -1.000000
+v 6.000000 -1.000000 1.000000
+v 6.000000 1.000000 1.000000
+v 6.000000 -1.000000 -1.000000
+v 6.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Green
+s off
+f 9/21/7 10/22/7 12/23/7 11/24/7
+f 11/25/8 12/26/8 16/27/8 15/28/8
+f 15/29/9 16/30/9 14/31/9 13/32/9
+f 13/33/10 14/34/10 10/35/10 9/36/10
+f 11/25/11 15/37/11 13/38/11 9/36/11
+f 16/39/12 12/26/12 10/35/12 14/40/12
+o CubeRed
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Red
+s off
+f 17/41/13 18/42/13 20/43/13 19/44/13
+f 19/45/14 20/46/14 24/47/14 23/48/14
+f 23/49/15 24/50/15 22/51/15 21/52/15
+f 21/53/16 22/54/16 18/55/16 17/56/16
+f 19/45/17 23/57/17 21/58/17 17/56/17
+f 24/59/18 20/46/18 18/55/18 22/60/18
diff --git a/specs/data/box-positions-only/box-positions-only.mtl b/specs/data/box-positions-only/box-positions-only.mtl
new file mode 100644
index 0000000..abbc294
--- /dev/null
+++ b/specs/data/box-positions-only/box-positions-only.mtl
@@ -0,0 +1,12 @@
+# Blender MTL File: 'box.blend'
+# Material Count: 1
+
+newmtl Material
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.640000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-positions-only/box-positions-only.obj b/specs/data/box-positions-only/box-positions-only.obj
new file mode 100644
index 0000000..8d2353f
--- /dev/null
+++ b/specs/data/box-positions-only/box-positions-only.obj
@@ -0,0 +1,20 @@
+# Blender v2.78 (sub 0) OBJ File: 'box.blend'
+# www.blender.org
+mtllib box-positions-only.mtl
+o Cube
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+usemtl Material
+s off
+f 1 2 4 3
+f 3 4 8 7
+f 7 8 6 5
+f 5 6 2 1
+f 3 7 5 1
+f 8 4 2 6
diff --git a/specs/data/box-subdirectories/box-textured.obj b/specs/data/box-subdirectories/box-textured.obj
new file mode 100644
index 0000000..faf48db
--- /dev/null
+++ b/specs/data/box-subdirectories/box-textured.obj
@@ -0,0 +1,46 @@
+# Blender v2.78 (sub 0) OBJ File: 'box.blend'
+# www.blender.org
+mtllib materials/box-textured.mtl
+o Cube
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Material
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
diff --git a/specs/data/box-subdirectories/materials/box-textured.mtl b/specs/data/box-subdirectories/materials/box-textured.mtl
new file mode 100644
index 0000000..d042c1d
--- /dev/null
+++ b/specs/data/box-subdirectories/materials/box-textured.mtl
@@ -0,0 +1,13 @@
+# Blender MTL File: 'box.blend'
+# Material Count: 1
+
+newmtl Material
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.640000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+map_Kd images/cesium.png
diff --git a/specs/data/box-subdirectories/materials/images/cesium.png b/specs/data/box-subdirectories/materials/images/cesium.png
new file mode 100644
index 0000000..3b8baee
Binary files /dev/null and b/specs/data/box-subdirectories/materials/images/cesium.png differ
diff --git a/specs/data/box-textured/box-textured.mtl b/specs/data/box-textured/box-textured.mtl
new file mode 100644
index 0000000..c5c879f
--- /dev/null
+++ b/specs/data/box-textured/box-textured.mtl
@@ -0,0 +1,13 @@
+# Blender MTL File: 'box.blend'
+# Material Count: 1
+
+newmtl Material
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.640000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+map_Kd cesium.png
diff --git a/specs/data/box-textured/box-textured.obj b/specs/data/box-textured/box-textured.obj
new file mode 100644
index 0000000..4f5dc44
--- /dev/null
+++ b/specs/data/box-textured/box-textured.obj
@@ -0,0 +1,46 @@
+# Blender v2.78 (sub 0) OBJ File: 'box.blend'
+# www.blender.org
+mtllib box-textured.mtl
+o Cube
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Material
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
diff --git a/specs/data/box-textured/cesium.png b/specs/data/box-textured/cesium.png
new file mode 100644
index 0000000..3b8baee
Binary files /dev/null and b/specs/data/box-textured/cesium.png differ
diff --git a/specs/data/box-triangles/box-triangles.mtl b/specs/data/box-triangles/box-triangles.mtl
new file mode 100644
index 0000000..abbc294
--- /dev/null
+++ b/specs/data/box-triangles/box-triangles.mtl
@@ -0,0 +1,12 @@
+# Blender MTL File: 'box.blend'
+# Material Count: 1
+
+newmtl Material
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.640000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-triangles/box-triangles.obj b/specs/data/box-triangles/box-triangles.obj
new file mode 100644
index 0000000..124ab2d
--- /dev/null
+++ b/specs/data/box-triangles/box-triangles.obj
@@ -0,0 +1,46 @@
+# Blender v2.78 (sub 0) OBJ File: 'box.blend'
+# www.blender.org
+mtllib box-triangles.mtl
+o Cube
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Material
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
diff --git a/specs/data/box-uncleaned/box-uncleaned.mtl b/specs/data/box-uncleaned/box-uncleaned.mtl
new file mode 100644
index 0000000..a304b43
--- /dev/null
+++ b/specs/data/box-uncleaned/box-uncleaned.mtl
@@ -0,0 +1,12 @@
+# Blender MTL File: 'None'
+# Material Count: 1
+
+newmtl Material
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.640000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-uncleaned/box-uncleaned.obj b/specs/data/box-uncleaned/box-uncleaned.obj
new file mode 100644
index 0000000..7558d99
--- /dev/null
+++ b/specs/data/box-uncleaned/box-uncleaned.obj
@@ -0,0 +1,52 @@
+# Blender v2.78 (sub 0) OBJ File: ''
+# www.blender.org
+mtllib box-uncleaned.mtl
+o Cube
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+g Cube
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+o Cube
+g Cube
+usemtl Material
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
+g Cube
+usemtl Material
+o Cube
diff --git a/specs/data/box-usemtl/box-usemtl.mtl b/specs/data/box-usemtl/box-usemtl.mtl
new file mode 100644
index 0000000..2f9b11a
--- /dev/null
+++ b/specs/data/box-usemtl/box-usemtl.mtl
@@ -0,0 +1,32 @@
+# Blender MTL File: 'box-objects.blend'
+# Material Count: 3
+
+newmtl Blue
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.000000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Green
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.000000 0.640000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
+
+newmtl Red
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.000000 0.000000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-usemtl/box-usemtl.obj b/specs/data/box-usemtl/box-usemtl.obj
new file mode 100644
index 0000000..2350468
--- /dev/null
+++ b/specs/data/box-usemtl/box-usemtl.obj
@@ -0,0 +1,129 @@
+# Blender v2.78 (sub 0) OBJ File: 'box-objects.blend'
+# www.blender.org
+mtllib box-usemtl.mtl
+v -1.000000 -1.000000 -4.000000
+v -1.000000 1.000000 -4.000000
+v -1.000000 -1.000000 -6.000000
+v -1.000000 1.000000 -6.000000
+v 1.000000 -1.000000 -4.000000
+v 1.000000 1.000000 -4.000000
+v 1.000000 -1.000000 -6.000000
+v 1.000000 1.000000 -6.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Blue
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
+v 4.000000 -1.000000 1.000000
+v 4.000000 1.000000 1.000000
+v 4.000000 -1.000000 -1.000000
+v 4.000000 1.000000 -1.000000
+v 6.000000 -1.000000 1.000000
+v 6.000000 1.000000 1.000000
+v 6.000000 -1.000000 -1.000000
+v 6.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Green
+s off
+f 9/21/7 10/22/7 12/23/7 11/24/7
+f 11/25/8 12/26/8 16/27/8 15/28/8
+f 15/29/9 16/30/9 14/31/9 13/32/9
+f 13/33/10 14/34/10 10/35/10 9/36/10
+f 11/25/11 15/37/11 13/38/11 9/36/11
+f 16/39/12 12/26/12 10/35/12 14/40/12
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Red
+s off
+f 17/41/13 18/42/13 20/43/13 19/44/13
+f 19/45/14 20/46/14 24/47/14 23/48/14
+f 23/49/15 24/50/15 22/51/15 21/52/15
+f 21/53/16 22/54/16 18/55/16 17/56/16
+f 19/45/17 23/57/17 21/58/17 17/56/17
+f 24/59/18 20/46/18 18/55/18 22/60/18
diff --git a/specs/data/box-uvs/box-uvs.mtl b/specs/data/box-uvs/box-uvs.mtl
new file mode 100644
index 0000000..abbc294
--- /dev/null
+++ b/specs/data/box-uvs/box-uvs.mtl
@@ -0,0 +1,12 @@
+# Blender MTL File: 'box.blend'
+# Material Count: 1
+
+newmtl Material
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.640000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box-uvs/box-uvs.obj b/specs/data/box-uvs/box-uvs.obj
new file mode 100644
index 0000000..9beef82
--- /dev/null
+++ b/specs/data/box-uvs/box-uvs.obj
@@ -0,0 +1,40 @@
+# Blender v2.78 (sub 0) OBJ File: 'box.blend'
+# www.blender.org
+mtllib box-uvs.mtl
+o Cube
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+usemtl Material
+s off
+f 1/1 2/2 4/3 3/4
+f 3/5 4/6 8/7 7/8
+f 7/9 8/10 6/11 5/12
+f 5/13 6/14 2/15 1/16
+f 3/5 7/17 5/18 1/16
+f 8/19 4/6 2/15 6/20
diff --git a/specs/data/box/box.gltf b/specs/data/box/box.gltf
new file mode 100644
index 0000000..0f8ecdb
--- /dev/null
+++ b/specs/data/box/box.gltf
@@ -0,0 +1,176 @@
+{
+ "accessors": {
+ "accessor_0": {
+ "bufferView": "bufferView_vertex",
+ "byteOffset": 0,
+ "byteStride": 0,
+ "componentType": 5126,
+ "count": 24,
+ "min": [
+ -1,
+ -1,
+ -1
+ ],
+ "max": [
+ 1,
+ 1,
+ 1
+ ],
+ "type": "VEC3"
+ },
+ "accessor_1": {
+ "bufferView": "bufferView_vertex",
+ "byteOffset": 288,
+ "byteStride": 0,
+ "componentType": 5126,
+ "count": 24,
+ "min": [
+ -1,
+ -1,
+ -1
+ ],
+ "max": [
+ 1,
+ 1,
+ 1
+ ],
+ "type": "VEC3"
+ },
+ "accessor_2": {
+ "bufferView": "bufferView_vertex",
+ "byteOffset": 576,
+ "byteStride": 0,
+ "componentType": 5126,
+ "count": 24,
+ "min": [
+ 0,
+ 0
+ ],
+ "max": [
+ 1,
+ 1
+ ],
+ "type": "VEC2"
+ },
+ "accessor_3": {
+ "bufferView": "bufferView_index",
+ "byteOffset": 0,
+ "byteStride": 0,
+ "componentType": 5123,
+ "count": 36,
+ "min": [
+ 0
+ ],
+ "max": [
+ 23
+ ],
+ "type": "SCALAR"
+ }
+ },
+ "asset": {
+ "generator": "obj2gltf",
+ "profile": {
+ "api": "WebGL",
+ "version": "1.0"
+ },
+ "version": "1.0"
+ },
+ "buffers": {
+ "buffer": {
+ "byteLength": 840,
+ "uri": "data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIA/AACAvwAAgL8AAIA/AACAPwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIA/AACAPwAAgD8AAIA/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAIA/AACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcA"
+ }
+ },
+ "bufferViews": {
+ "bufferView_vertex": {
+ "buffer": "buffer",
+ "byteLength": 768,
+ "byteOffset": 0,
+ "target": 34962
+ },
+ "bufferView_index": {
+ "buffer": "buffer",
+ "byteLength": 72,
+ "byteOffset": 768,
+ "target": 34963
+ }
+ },
+ "extensionsUsed": [
+ "KHR_materials_common"
+ ],
+ "images": {},
+ "materials": {
+ "Material": {
+ "extensions": {
+ "KHR_materials_common": {
+ "technique": "PHONG",
+ "values": {
+ "ambient": [
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ "diffuse": [
+ 0.64,
+ 0.64,
+ 0.64,
+ 1
+ ],
+ "emission": [
+ 0,
+ 0,
+ 0,
+ 1
+ ],
+ "specular": [
+ 0.5,
+ 0.5,
+ 0.5,
+ 1
+ ],
+ "shininess": 96.078431,
+ "transparency": 1,
+ "transparent": false,
+ "doubleSided": false
+ }
+ }
+ }
+ }
+ },
+ "meshes": {
+ "Cube-Mesh": {
+ "name": "Cube-Mesh",
+ "primitives": [
+ {
+ "attributes": {
+ "POSITION": "accessor_0",
+ "NORMAL": "accessor_1",
+ "TEXCOORD_0": "accessor_2"
+ },
+ "indices": "accessor_3",
+ "material": "Material",
+ "mode": 4
+ }
+ ]
+ }
+ },
+ "nodes": {
+ "Cube": {
+ "name": "Cube",
+ "meshes": [
+ "Cube-Mesh"
+ ]
+ }
+ },
+ "samplers": {},
+ "scene": "scene",
+ "scenes": {
+ "scene": {
+ "nodes": [
+ "Cube"
+ ]
+ }
+ },
+ "textures": {}
+}
\ No newline at end of file
diff --git a/specs/data/box/box.mtl b/specs/data/box/box.mtl
new file mode 100644
index 0000000..a304b43
--- /dev/null
+++ b/specs/data/box/box.mtl
@@ -0,0 +1,12 @@
+# Blender MTL File: 'None'
+# Material Count: 1
+
+newmtl Material
+Ns 96.078431
+Ka 0.000000 0.000000 0.000000
+Kd 0.640000 0.640000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2
diff --git a/specs/data/box/box.obj b/specs/data/box/box.obj
new file mode 100644
index 0000000..5246261
--- /dev/null
+++ b/specs/data/box/box.obj
@@ -0,0 +1,46 @@
+# Blender v2.78 (sub 0) OBJ File: ''
+# www.blender.org
+mtllib box.mtl
+o Cube
+v -1.000000 -1.000000 1.000000
+v -1.000000 1.000000 1.000000
+v -1.000000 -1.000000 -1.000000
+v -1.000000 1.000000 -1.000000
+v 1.000000 -1.000000 1.000000
+v 1.000000 1.000000 1.000000
+v 1.000000 -1.000000 -1.000000
+v 1.000000 1.000000 -1.000000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 0.0000 0.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 1.0000
+vt 1.0000 0.0000
+vt 1.0000 1.0000
+vt 0.0000 0.0000
+vt 0.0000 1.0000
+vn -1.0000 0.0000 0.0000
+vn 0.0000 0.0000 -1.0000
+vn 1.0000 0.0000 0.0000
+vn 0.0000 0.0000 1.0000
+vn 0.0000 -1.0000 0.0000
+vn 0.0000 1.0000 0.0000
+usemtl Material
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/5/2 4/6/2 8/7/2 7/8/2
+f 7/9/3 8/10/3 6/11/3 5/12/3
+f 5/13/4 6/14/4 2/15/4 1/16/4
+f 3/5/5 7/17/5 5/18/5 1/16/5
+f 8/19/6 4/6/6 2/15/6 6/20/6
diff --git a/specs/lib/convertSpec.js b/specs/lib/convertSpec.js
index a001844..401cb83 100644
--- a/specs/lib/convertSpec.js
+++ b/specs/lib/convertSpec.js
@@ -1,23 +1,123 @@
'use strict';
-var Promise = require('bluebird');
-
+var fsExtra = require('fs-extra');
var GltfPipeline = require('gltf-pipeline').Pipeline;
var path = require('path');
var convert = require('../../lib/convert');
-var objFile = './specs/data/BoxTextured/BoxTextured.obj';
-var gltfFile = './specs/data/BoxTextured/BoxTextured.gltf';
+var objPath = 'specs/data/box-textured/box-textured.obj';
+var gltfPath = 'specs/data/box-textured/box-textured.gltf';
+var glbPath = 'specs/data/box-textured/box-textured.glb';
describe('convert', function() {
it('converts an obj to gltf', function(done) {
- var spy = spyOn(GltfPipeline, 'processJSONToDisk').and.callFake(function(gltf, outputPath, options, callback) {
- return;
- });
- expect(convert(objFile, gltfFile, {})
+ var spy = spyOn(GltfPipeline, 'processJSONToDisk');
+ expect(convert(objPath, gltfPath)
.then(function() {
var args = spy.calls.first().args;
- expect(args[0]).toBeDefined();
- expect(path.normalize(args[1])).toEqual(path.normalize(gltfFile));
+ var gltf = args[0];
+ var outputPath = args[1];
+ expect(path.normalize(outputPath)).toEqual(path.normalize(gltfPath));
+ expect(gltf).toBeDefined();
+ expect(gltf.images.cesium).toBeDefined();
}), done).toResolve();
});
+
+ it('uses default gltf-pipeline options', function(done) {
+ var spy = spyOn(GltfPipeline, 'processJSONToDisk');
+ expect(convert(objPath, gltfPath)
+ .then(function() {
+ var args = spy.calls.first().args;
+ var options = args[2];
+ expect(options).toEqual({
+ createDirectory : false,
+ basePath : path.dirname(objPath),
+ binary : false,
+ embed : true,
+ embedImage : true,
+ encodeNormals : false,
+ quantize : false,
+ compressTextureCoordinates : false,
+ aoOptions : undefined,
+ smoothNormals : false,
+ optimizeForCesium : false,
+ textureCompressionOptions : undefined,
+ preserve : true
+ });
+ }), done).toResolve();
+ });
+
+ it('sets options', function(done) {
+ var spy = spyOn(GltfPipeline, 'processJSONToDisk');
+ var textureCompressionOptions = {
+ format : 'dxt1',
+ quality : 10
+ };
+ var options = {
+ binary : true,
+ separate : true,
+ separateTextures : true,
+ compress : true,
+ optimize : true,
+ generateNormals : true,
+ ao : true,
+ optimizeForCesium : true,
+ textureCompressionOptions : textureCompressionOptions
+ };
+
+ expect(convert(objPath, gltfPath, options)
+ .then(function() {
+ var args = spy.calls.first().args;
+ var options = args[2];
+ expect(options).toEqual({
+ createDirectory : false,
+ basePath : path.dirname(objPath),
+ binary : true,
+ embed : false,
+ embedImage : false,
+ encodeNormals : true,
+ quantize : true,
+ compressTextureCoordinates : true,
+ aoOptions : {},
+ smoothNormals : true,
+ optimizeForCesium : true,
+ textureCompressionOptions : textureCompressionOptions,
+ preserve : false
+ });
+ }), done).toResolve();
+ });
+
+ it('saves as binary if gltfPath has a .glb extension', function(done) {
+ var spy = spyOn(GltfPipeline, 'processJSONToDisk');
+ expect(convert(objPath, glbPath)
+ .then(function() {
+ var args = spy.calls.first().args;
+ var options = args[2];
+ expect(options.binary).toBe(true);
+ }), done).toResolve();
+ });
+
+ it('bypassPipeline flag bypasses gltf-pipeline', function(done) {
+ var spy1 = spyOn(convert, '_outputJson');
+ var spy2 = spyOn(GltfPipeline, 'processJSONToDisk');
+ var options = {
+ bypassPipeline : true
+ };
+ expect(convert(objPath, gltfPath, options)
+ .then(function() {
+ expect(spy1.calls.count()).toBe(1);
+ expect(spy2.calls.count()).toBe(0);
+ }), done).toResolve();
+ });
+
+ it('throws if objPath is undefined', function() {
+ expect(function() {
+ convert(undefined, gltfPath);
+ }).toThrowDeveloperError();
+ });
+
+ it('throws if gltfPath is undefined', function() {
+ expect(function() {
+ convert(objPath, undefined);
+ }).toThrowDeveloperError();
+ });
});
diff --git a/specs/lib/gltfSpec.js b/specs/lib/gltfSpec.js
new file mode 100644
index 0000000..3ff7714
--- /dev/null
+++ b/specs/lib/gltfSpec.js
@@ -0,0 +1,355 @@
+'use strict';
+var Cesium = require('cesium');
+var fsExtra = require('fs-extra');
+var path = require('path');
+var Promise = require('bluebird');
+var clone = require('../../lib/clone.js');
+var createGltf = require('../../lib/gltf.js');
+var loadImage = require('../../lib/image.js');
+var loadObj = require('../../lib/obj.js');
+
+var WebGLConstants = Cesium.WebGLConstants;
+
+var fsExtraReadJson = Promise.promisify(fsExtra.readJson);
+
+var boxObjUrl = 'specs/data/box/box.obj';
+var groupObjUrl = 'specs/data/box-objects-groups-materials/box-objects-groups-materials.obj';
+var boxGltfUrl = 'specs/data/box/box.gltf';
+var groupGltfUrl = 'specs/data/box-objects-groups-materials/box-objects-groups-materials.gltf';
+var diffuseTextureUrl = 'specs/data/box-textured/cesium.png';
+var transparentDiffuseTextureUrl = 'specs/data/box-complex-material/diffuse.png';
+
+describe('gltf', function() {
+ var boxObjData;
+ var groupObjData;
+ var boxGltf;
+ var groupGltf;
+ var diffuseTexture;
+ var transparentDiffuseTexture;
+
+ beforeAll(function(done) {
+ return Promise.all([
+ loadObj(boxObjUrl)
+ .then(function(data) {
+ boxObjData = data;
+ }),
+ loadObj(groupObjUrl)
+ .then(function(data) {
+ groupObjData = data;
+ }),
+ fsExtraReadJson(boxGltfUrl)
+ .then(function(gltf) {
+ boxGltf = gltf;
+ }),
+ fsExtraReadJson(groupGltfUrl)
+ .then(function(gltf) {
+ groupGltf = gltf;
+ }),
+ loadImage(diffuseTextureUrl)
+ .then(function(image) {
+ diffuseTexture = image;
+ }),
+ loadImage(transparentDiffuseTextureUrl)
+ .then(function(image) {
+ transparentDiffuseTexture = image;
+ })
+ ]).then(done);
+ });
+
+ it('simple gltf', function() {
+ var objData = clone(boxObjData, true);
+ var gltf = createGltf(objData);
+ expect(gltf).toEqual(boxGltf);
+ });
+
+ it('multiple nodes, meshes, and primitives', function() {
+ var objData = clone(groupObjData, true);
+ var gltf = createGltf(objData);
+ expect(gltf).toEqual(groupGltf);
+
+ expect(Object.keys(gltf.materials).length).toBe(3);
+ expect(Object.keys(gltf.nodes).length).toBe(1);
+ expect(Object.keys(gltf.meshes).length).toBe(3);
+
+ // Check for two primitives in each mesh
+ for (var id in gltf.meshes) {
+ if (gltf.meshes.hasOwnProperty(id)) {
+ var mesh = gltf.meshes[id];
+ expect(mesh.primitives.length).toBe(2);
+ }
+ }
+ });
+
+ it('sets default material values', function() {
+ var objData = clone(boxObjData, true);
+ objData.materials.Material = {};
+
+ var gltf = createGltf(objData);
+ var material = gltf.materials.Material;
+ 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);
+ });
+
+ it('sets material for diffuse texture', function() {
+ var objData = clone(boxObjData, true);
+ objData.materials.Material = {
+ diffuseColorMap : diffuseTextureUrl
+ };
+ objData.images[diffuseTextureUrl] = diffuseTexture;
+
+ var gltf = createGltf(objData);
+ var kmc = gltf.materials.Material.extensions.KHR_materials_common;
+ var texture = gltf.textures.texture_cesium;
+ var image = gltf.images.cesium;
+
+ expect(kmc.technique).toBe('LAMBERT');
+ expect(kmc.values.diffuse).toEqual('texture_cesium');
+ expect(kmc.values.transparency).toBe(1.0);
+ expect(kmc.values.transparent).toBe(false);
+ expect(kmc.values.doubleSided).toBe(false);
+
+ expect(texture).toEqual({
+ format : WebGLConstants.RGB,
+ internalFormat : WebGLConstants.RGB,
+ sampler : 'sampler',
+ source : 'cesium',
+ target : WebGLConstants.TEXTURE_2D,
+ type : WebGLConstants.UNSIGNED_BYTE
+ });
+
+ expect(image).toBeDefined();
+ expect(image.name).toBe('cesium');
+ expect(image.uri.indexOf('data:image/png;base64,') >= 0).toBe(true);
+
+ expect(gltf.samplers.sampler).toEqual({
+ magFilter : WebGLConstants.LINEAR,
+ minFilter : WebGLConstants.LINEAR,
+ wrapS : WebGLConstants.REPEAT,
+ wrapT : WebGLConstants.REPEAT
+ });
+ });
+
+ it('sets material for alpha less than 1', function() {
+ var objData = clone(boxObjData, true);
+ objData.materials.Material = {
+ alpha : 0.4
+ };
+
+ var gltf = createGltf(objData);
+ var kmc = gltf.materials.Material.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 objData = clone(boxObjData, true);
+ objData.materials.Material = {
+ diffuseColorMap : diffuseTextureUrl,
+ alpha : 0.4
+ };
+ objData.images[diffuseTextureUrl] = diffuseTexture;
+
+ var gltf = createGltf(objData);
+ var kmc = gltf.materials.Material.extensions.KHR_materials_common;
+
+ expect(kmc.values.diffuse).toEqual('texture_cesium');
+ 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 objData = clone(boxObjData, true);
+ objData.materials.Material = {
+ diffuseColorMap : transparentDiffuseTextureUrl
+ };
+ objData.images[transparentDiffuseTextureUrl] = transparentDiffuseTexture;
+
+ var gltf = createGltf(objData);
+ var kmc = gltf.materials.Material.extensions.KHR_materials_common;
+
+ expect(kmc.values.diffuse).toBe('texture_diffuse');
+ 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 objData = clone(boxObjData, true);
+ objData.materials.Material = {
+ specularColor : [0.1, 0.1, 0.2, 1],
+ specularShininess : 0.1
+ };
+
+ var gltf = createGltf(objData);
+ var kmc = gltf.materials.Material.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 objData = clone(boxObjData, true);
+ objData.nodes[0].meshes[0].normals.length = 0;
+ objData.materials.Material = {
+ diffuseColorMap : diffuseTextureUrl
+ };
+ objData.images[diffuseTextureUrl] = diffuseTexture;
+
+ var gltf = createGltf(objData);
+ var kmc = gltf.materials.Material.extensions.KHR_materials_common;
+
+ expect(kmc.technique).toBe('CONSTANT');
+ expect(kmc.values.emission).toEqual('texture_cesium');
+ });
+
+ it('sets default material when texture is missing', function() {
+ var objData = clone(boxObjData, true);
+ objData.materials.Material = {
+ diffuseColorMap : diffuseTextureUrl
+ };
+
+ var gltf = createGltf(objData);
+ var kmc = gltf.materials.Material.extensions.KHR_materials_common;
+
+ expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]);
+ });
+
+ it('uses default material (1)', function() {
+ var objData = clone(boxObjData, true);
+ objData.nodes[0].meshes[0].primitives[0].material = undefined;
+
+ // Creates a material called "default"
+ var gltf = createGltf(objData);
+ expect(gltf.materials.default).toBeDefined();
+ var kmc = gltf.materials.default.extensions.KHR_materials_common;
+ expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]);
+ });
+
+ it('uses default material (2)', function() {
+ var objData = clone(boxObjData, true);
+ objData.materials = {};
+
+ // Uses the original name of the material
+ var gltf = createGltf(objData);
+ var kmc = gltf.materials.Material.extensions.KHR_materials_common;
+
+ expect(kmc.values.diffuse).toEqual([0.5, 0.5, 0.5, 1.0]);
+ });
+
+ it('handles material used with and without normals', function() {
+ // Two meshes - one with normals, and one without
+ var objData = clone(boxObjData, true);
+ objData.nodes.push(clone(objData.nodes[0], true));
+ objData.nodes[1].meshes[0].normals.length = 0;
+
+ var gltf = createGltf(objData);
+ var kmc1 = gltf.materials.Material.extensions.KHR_materials_common;
+ var kmc2 = gltf.materials.Material_constant.extensions.KHR_materials_common;
+
+ expect(kmc1.technique).toBe('PHONG');
+ expect(kmc2.technique).toBe('CONSTANT');
+
+ // Now test in a different order
+ objData = clone(boxObjData, true);
+ objData.nodes.push(clone(objData.nodes[0], true));
+ objData.nodes[0].meshes[0].normals.length = 0;
+
+ gltf = createGltf(objData);
+ kmc1 = gltf.materials.Material.extensions.KHR_materials_common;
+ kmc2 = gltf.materials.Material_shaded.extensions.KHR_materials_common;
+
+ expect(kmc1.technique).toBe('CONSTANT');
+ expect(kmc2.technique).toBe('PHONG');
+ });
+
+ it('runs without normals', function() {
+ var objData = clone(boxObjData, true);
+ objData.nodes[0].meshes[0].normals.length = 0;
+
+ var gltf = createGltf(objData);
+ var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes;
+ expect(attributes.POSITION).toBeDefined();
+ expect(attributes.NORMAL).toBeUndefined();
+ expect(attributes.TEXCOORD_0).toBeDefined();
+ });
+
+ it('runs without uvs', function() {
+ var objData = clone(boxObjData, true);
+ objData.nodes[0].meshes[0].uvs.length = 0;
+
+ var gltf = createGltf(objData);
+ var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes;
+ expect(attributes.POSITION).toBeDefined();
+ expect(attributes.NORMAL).toBeDefined();
+ expect(attributes.TEXCOORD_0).toBeUndefined();
+ });
+
+ it('runs without uvs and normals', function() {
+ var objData = clone(boxObjData, true);
+ objData.nodes[0].meshes[0].normals.length = 0;
+ objData.nodes[0].meshes[0].uvs.length = 0;
+
+ var gltf = createGltf(objData);
+ var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes;
+ expect(attributes.POSITION).toBeDefined();
+ expect(attributes.NORMAL).toBeUndefined();
+ expect(attributes.TEXCOORD_0).toBeUndefined();
+ });
+
+ function expandObjData(objData, duplicatesLength) {
+ var mesh = objData.nodes[0].meshes[0];
+ var indices = mesh.primitives[0].indices;
+ var positions = mesh.positions;
+ var normals = mesh.normals;
+ var uvs = mesh.uvs;
+
+ var indicesLength = indices.length;
+ var vertexCount = positions.length / 3;
+
+ for (var i = 1; i < duplicatesLength; ++i) {
+ for (var j = 0; j < vertexCount; ++j) {
+ positions.push(0.0);
+ positions.push(0.0);
+ positions.push(0.0);
+ normals.push(0.0);
+ normals.push(0.0);
+ normals.push(0.0);
+ uvs.push(0.0);
+ uvs.push(0.0);
+ }
+ for (var k = 0; k < indicesLength; ++k) {
+ indices.push(indices.get(k) + vertexCount * i);
+ }
+ }
+ }
+
+ it('detects need to use uint32 indices', function() {
+ var objData = clone(boxObjData, true);
+ expandObjData(objData, 2731); // Right above 65536 limit
+ var mesh = objData.nodes[0].meshes[0];
+ var indicesLength = mesh.primitives[0].indices.length;
+ var vertexCount = mesh.positions.length / 3;
+
+ var gltf = createGltf(objData);
+ var primitive = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0];
+ var indicesAccessor = gltf.accessors[primitive.indices];
+ expect(indicesAccessor.count).toBe(indicesLength);
+ expect(indicesAccessor.max[0]).toBe(vertexCount - 1);
+ expect(indicesAccessor.componentType).toBe(WebGLConstants.UNSIGNED_INT);
+
+ var positionAccessor = gltf.accessors[primitive.attributes.POSITION];
+ expect(positionAccessor.count).toBe(vertexCount);
+ });
+});
diff --git a/specs/lib/imageSpec.js b/specs/lib/imageSpec.js
new file mode 100644
index 0000000..cb5359c
--- /dev/null
+++ b/specs/lib/imageSpec.js
@@ -0,0 +1,91 @@
+'use strict';
+var Cesium = require('cesium');
+var path = require('path');
+var loadImage = require('../../lib/image.js');
+
+var WebGLConstants = Cesium.WebGLConstants;
+
+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';
+var invalidImage = 'invalid.png';
+
+describe('image', function() {
+ it('loads png image', function(done) {
+ expect(loadImage(pngImage)
+ .then(function(info) {
+ expect(info.transparent).toBe(false);
+ expect(info.channels).toBe(3);
+ expect(info.data).toBeDefined();
+ expect(info.uri.indexOf('data:image/png') === 0).toBe(true);
+ expect(info.format).toBe(WebGLConstants.RGB);
+ }), done).toResolve();
+ });
+
+ it('loads jpg image', function(done) {
+ expect(loadImage(jpgImage)
+ .then(function(info) {
+ expect(info.transparent).toBe(false);
+ expect(info.channels).toBe(3);
+ expect(info.data).toBeDefined();
+ expect(info.uri.indexOf('data:image/jpeg') === 0).toBe(true);
+ expect(info.format).toBe(WebGLConstants.RGB);
+ }), done).toResolve();
+ });
+
+ it('loads jpeg image', function(done) {
+ expect(loadImage(jpegImage)
+ .then(function(info) {
+ expect(info.transparent).toBe(false);
+ expect(info.channels).toBe(3);
+ expect(info.data).toBeDefined();
+ expect(info.uri.indexOf('data:image/jpeg') === 0).toBe(true);
+ expect(info.format).toBe(WebGLConstants.RGB);
+ }), done).toResolve();
+ });
+
+ it('loads gif image', function(done) {
+ expect(loadImage(gifImage)
+ .then(function(info) {
+ expect(info.transparent).toBe(false);
+ expect(info.channels).toBe(3);
+ expect(info.data).toBeDefined();
+ expect(info.uri.indexOf('data:image/gif') === 0).toBe(true);
+ expect(info.format).toBe(WebGLConstants.RGB);
+ }), done).toResolve();
+ });
+
+ it('loads grayscale image', function(done) {
+ expect(loadImage(grayscaleImage)
+ .then(function(info) {
+ expect(info.transparent).toBe(false);
+ expect(info.channels).toBe(1);
+ expect(info.data).toBeDefined();
+ expect(info.uri.indexOf('data:image/png') === 0).toBe(true);
+ expect(info.format).toBe(WebGLConstants.ALPHA);
+ }), done).toResolve();
+ });
+
+ it('loads transparentImage image', function(done) {
+ expect(loadImage(transparentImage)
+ .then(function(info) {
+ expect(info.transparent).toBe(true);
+ expect(info.channels).toBe(4);
+ expect(info.data).toBeDefined();
+ expect(info.uri.indexOf('data:image/png') === 0).toBe(true);
+ expect(info.format).toBe(WebGLConstants.RGBA);
+ }), done).toResolve();
+ });
+
+ it('handles invalid image file', function(done) {
+ spyOn(console, 'log');
+ expect(loadImage(invalidImage)
+ .then(function(image) {
+ expect(image).toBeUndefined();
+ expect(console.log.calls.argsFor(0)[0].indexOf('Could not read image file') >= 0).toBe(true);
+ }), done).toResolve();
+ });
+});
diff --git a/specs/lib/mtlSpec.js b/specs/lib/mtlSpec.js
new file mode 100644
index 0000000..36be197
--- /dev/null
+++ b/specs/lib/mtlSpec.js
@@ -0,0 +1,53 @@
+'use strict';
+var path = require('path');
+var loadMtl = require('../../lib/mtl.js');
+
+var complexMaterialUrl = 'specs/data/box-complex-material/box-complex-material.mtl';
+var multipleMaterialsUrl = 'specs/data/box-multiple-materials/box-multiple-materials.mtl';
+var invalidMaterialUrl = 'invalid.mtl';
+
+function getImagePath(objPath, relativePath) {
+ return path.normalize(path.join(path.dirname(objPath), relativePath));
+}
+
+describe('mtl', function() {
+ it('loads complex material', function(done) {
+ expect(loadMtl(complexMaterialUrl)
+ .then(function(materials) {
+ var material = materials.Material;
+ expect(material).toBeDefined();
+ expect(material.ambientColor).toEqual([0.2, 0.2, 0.2, 1.0]);
+ expect(material.emissionColor).toEqual([0.1, 0.1, 0.1, 1.0]);
+ expect(material.diffuseColor).toEqual([0.64, 0.64, 0.64, 1.0]);
+ expect(material.specularColor).toEqual([0.5, 0.5, 0.5, 1.0]);
+ expect(material.specularShininess).toEqual(96.078431);
+ expect(material.alpha).toEqual(0.9);
+ expect(material.ambientColorMap).toEqual(getImagePath(complexMaterialUrl, 'ambient.gif'));
+ expect(material.emissionColorMap).toEqual(getImagePath(complexMaterialUrl, 'emission.jpg'));
+ expect(material.diffuseColorMap).toEqual(getImagePath(complexMaterialUrl, 'diffuse.png'));
+ expect(material.specularColorMap).toEqual(getImagePath(complexMaterialUrl, 'specular.jpeg'));
+ expect(material.specularShininessMap).toEqual(getImagePath(complexMaterialUrl, 'shininess.png'));
+ expect(material.normalMap).toEqual(getImagePath(complexMaterialUrl, 'bump.png'));
+ expect(material.alphaMap).toEqual(getImagePath(complexMaterialUrl, 'alpha.png'));
+ }), done).toResolve();
+ });
+
+ it('loads mtl with multiple materials', function(done) {
+ expect(loadMtl(multipleMaterialsUrl)
+ .then(function(materials) {
+ expect(Object.keys(materials).length).toBe(3);
+ expect(materials.Red.diffuseColor).toEqual([0.64, 0.0, 0.0, 1.0]);
+ expect(materials.Green.diffuseColor).toEqual([0.0, 0.64, 0.0, 1.0]);
+ expect(materials.Blue.diffuseColor).toEqual([0.0, 0.0, 0.64, 1.0]);
+ }), done).toResolve();
+ });
+
+ it('handles invalid mtl file', function(done) {
+ spyOn(console, 'log');
+ expect(loadMtl(invalidMaterialUrl)
+ .then(function(materials) {
+ expect(materials).toEqual({});
+ expect(console.log.calls.argsFor(0)[0].indexOf('Could not read material file') >= 0).toBe(true);
+ }), done).toResolve();
+ });
+});
diff --git a/specs/lib/objSpec.js b/specs/lib/objSpec.js
new file mode 100644
index 0000000..89f0f91
--- /dev/null
+++ b/specs/lib/objSpec.js
@@ -0,0 +1,328 @@
+'use strict';
+var Cesium = require('cesium');
+var path = require('path');
+var Promise = require('bluebird');
+var loadObj = require('../../lib/obj.js');
+
+var RuntimeError = Cesium.RuntimeError;
+
+var objUrl = 'specs/data/box/box.obj';
+var objNormalsUrl = 'specs/data/box-normals/box-normals.obj';
+var objUvsUrl = 'specs/data/box-uvs/box-uvs.obj';
+var objPositionsOnlyUrl = 'specs/data/box-positions-only/box-positions-only.obj';
+var objNegativeIndicesUrl = 'specs/data/box-negative-indices/box-negative-indices.obj';
+var objTrianglesUrl = 'specs/data/box-triangles/box-triangles.obj';
+var objObjectsUrl = 'specs/data/box-objects/box-objects.obj';
+var objGroupsUrl = 'specs/data/box-groups/box-groups.obj';
+var objObjectsGroupsUrl = 'specs/data/box-objects-groups/box-objects-groups.obj';
+var objUsemtlUrl = 'specs/data/box-usemtl/box-usemtl.obj';
+var objNoMaterialsUrl = 'specs/data/box-no-materials/box-no-materials.obj';
+var objMultipleMaterialsUrl = 'specs/data/box-multiple-materials/box-multiple-materials.obj';
+var objUncleanedUrl = 'specs/data/box-uncleaned/box-uncleaned.obj';
+var objMtllibUrl = 'specs/data/box-mtllib/box-mtllib.obj';
+var objMissingMtllibUrl = 'specs/data/box-missing-mtllib/box-missing-mtllib.obj';
+var objTexturedUrl = 'specs/data/box-textured/box-textured.obj';
+var objMissingTextureUrl = 'specs/data/box-missing-texture/box-missing-texture.obj';
+var objSubdirectoriesUrl = 'specs/data/box-subdirectories/box-textured.obj';
+var objComplexMaterialUrl = 'specs/data/box-complex-material/box-complex-material.obj';
+var objInvalidContentsUrl = 'specs/data/box/box.mtl';
+var objInvalidUrl = 'invalid.obj';
+
+function getMeshes(data) {
+ var meshes = [];
+ var nodes = data.nodes;
+ var nodesLength = nodes.length;
+ for (var i = 0; i < nodesLength; ++i) {
+ meshes = meshes.concat(nodes[i].meshes);
+ }
+ return meshes;
+}
+
+function getPrimitives(data) {
+ var primitives = [];
+ var nodes = data.nodes;
+ var nodesLength = nodes.length;
+ for (var i = 0; i < nodesLength; ++i) {
+ var meshes = nodes[i].meshes;
+ var meshesLength = meshes.length;
+ for (var j = 0; j < meshesLength; ++j) {
+ primitives = primitives.concat(meshes[j].primitives);
+ }
+ }
+ return primitives;
+}
+
+function getImagePath(objPath, relativePath) {
+ return path.normalize(path.join(path.dirname(objPath), relativePath));
+}
+
+describe('obj', function() {
+ it('loads obj with positions, normals, and uvs', function(done) {
+ expect(loadObj(objUrl)
+ .then(function(data) {
+ var images = data.images;
+ var materials = data.materials;
+ var nodes = data.nodes;
+ var meshes = getMeshes(data);
+ var primitives = getPrimitives(data);
+
+ expect(Object.keys(images).length).toBe(0);
+ expect(materials.Material).toBeDefined();
+ expect(nodes.length).toBe(1);
+ expect(meshes.length).toBe(1);
+ expect(primitives.length).toBe(1);
+
+ var node = nodes[0];
+ var mesh = meshes[0];
+ var primitive = primitives[0];
+
+ expect(node.name).toBe('Cube');
+ expect(mesh.name).toBe('Cube-Mesh');
+ expect(mesh.positions.length / 3).toBe(24);
+ expect(mesh.normals.length / 3).toBe(24);
+ expect(mesh.uvs.length / 2).toBe(24);
+ expect(primitive.indices.length).toBe(36);
+ expect(primitive.material).toBe('Material');
+ }), done).toResolve();
+ });
+
+ it('loads obj with normals', function(done) {
+ expect(loadObj(objNormalsUrl)
+ .then(function(data) {
+ var mesh = getMeshes(data)[0];
+ expect(mesh.positions.length / 3).toBe(24);
+ expect(mesh.normals.length / 3).toBe(24);
+ expect(mesh.uvs.length / 2).toBe(0);
+ }), done).toResolve();
+ });
+
+ it('loads obj with uvs', function(done) {
+ expect(loadObj(objUvsUrl)
+ .then(function(data) {
+ var mesh = getMeshes(data)[0];
+ expect(mesh.positions.length / 3).toBe(20);
+ expect(mesh.normals.length / 3).toBe(0);
+ expect(mesh.uvs.length / 2).toBe(20);
+ }), done).toResolve();
+ });
+
+ it('loads obj with negative indices', function(done) {
+ expect(Promise.all([
+ loadObj(objPositionsOnlyUrl),
+ loadObj(objNegativeIndicesUrl)
+ ])
+ .then(function(results) {
+ var positionsReference = getMeshes(results[0])[0].positions.toFloatBuffer();
+ var positions = getMeshes(results[1])[0].positions.toFloatBuffer();
+ expect(positions).toEqual(positionsReference);
+ }), done).toResolve();
+ });
+
+ it('loads obj with triangle faces', function(done) {
+ expect(loadObj(objTrianglesUrl)
+ .then(function(data) {
+ var mesh = getMeshes(data)[0];
+ var primitive = getPrimitives(data)[0];
+ expect(mesh.positions.length / 3).toBe(24);
+ expect(primitive.indices.length).toBe(36);
+ }), done).toResolve();
+ });
+
+ it('loads obj with triangle faces', function(done) {
+ expect(loadObj(objTrianglesUrl)
+ .then(function(data) {
+ var mesh = getMeshes(data)[0];
+ var primitive = getPrimitives(data)[0];
+ expect(mesh.positions.length / 3).toBe(24);
+ expect(primitive.indices.length).toBe(36);
+ }), done).toResolve();
+ });
+
+ it('loads obj with objects', function(done) {
+ expect(loadObj(objObjectsUrl)
+ .then(function(data) {
+ var nodes = data.nodes;
+ expect(nodes.length).toBe(3);
+ expect(nodes[0].name).toBe('CubeBlue');
+ expect(nodes[1].name).toBe('CubeGreen');
+ expect(nodes[2].name).toBe('CubeRed');
+
+ var primitives = getPrimitives(data);
+ expect(primitives.length).toBe(3);
+ expect(primitives[0].material).toBe('Blue');
+ expect(primitives[1].material).toBe('Green');
+ expect(primitives[2].material).toBe('Red');
+ }), done).toResolve();
+ });
+
+ it('loads obj with groups', function(done) {
+ expect(loadObj(objGroupsUrl)
+ .then(function(data) {
+ var nodes = data.nodes;
+ expect(nodes.length).toBe(3);
+ expect(nodes[0].name).toBe('CubeBlue');
+ expect(nodes[1].name).toBe('CubeGreen');
+ expect(nodes[2].name).toBe('CubeRed');
+
+ var primitives = getPrimitives(data);
+ expect(primitives.length).toBe(3);
+ expect(primitives[0].material).toBe('Blue');
+ expect(primitives[1].material).toBe('Green');
+ expect(primitives[2].material).toBe('Red');
+ }), done).toResolve();
+ });
+
+ it('loads obj with objects and groups', function(done) {
+ expect(loadObj(objObjectsGroupsUrl)
+ .then(function(data) {
+ var nodes = data.nodes;
+ expect(nodes.length).toBe(3);
+ expect(nodes[0].name).toBe('CubeBlue');
+ expect(nodes[1].name).toBe('CubeGreen');
+ expect(nodes[2].name).toBe('CubeRed');
+
+ var meshes = getMeshes(data);
+ expect(meshes.length).toBe(3);
+ expect(meshes[0].name).toBe('CubeBlue_CubeBlue_Blue');
+ expect(meshes[1].name).toBe('CubeGreen_CubeGreen_Green');
+ expect(meshes[2].name).toBe('CubeRed_CubeRed_Red');
+
+ var primitives = getPrimitives(data);
+ expect(primitives.length).toBe(3);
+ expect(primitives[0].material).toBe('Blue');
+ expect(primitives[1].material).toBe('Green');
+ expect(primitives[2].material).toBe('Red');
+ }), done).toResolve();
+ });
+
+ it('loads obj with usemtl only', function(done) {
+ expect(loadObj(objUsemtlUrl)
+ .then(function(data) {
+ var nodes = data.nodes;
+ expect(nodes.length).toBe(1);
+ expect(nodes[0].name).toBe('Node'); // default name
+
+ var meshes = getMeshes(data);
+ expect(meshes.length).toBe(1);
+ expect(meshes[0].name).toBe('Node-Mesh');
+
+ var primitives = getPrimitives(data);
+ expect(primitives.length).toBe(3);
+ expect(primitives[0].material).toBe('Blue');
+ expect(primitives[1].material).toBe('Green');
+ expect(primitives[2].material).toBe('Red');
+ }), done).toResolve();
+ });
+
+ it('loads obj with no materials', function(done) {
+ expect(loadObj(objNoMaterialsUrl)
+ .then(function(data) {
+ var nodes = data.nodes;
+ expect(nodes.length).toBe(1);
+ expect(nodes[0].name).toBe('Node'); // default name
+
+ var primitives = getPrimitives(data);
+ expect(primitives.length).toBe(1);
+ }), done).toResolve();
+ });
+
+ it('loads obj with multiple materials', function(done) {
+ // The usemtl markers are interleaved, but should condense to just three primitives
+ expect(loadObj(objMultipleMaterialsUrl)
+ .then(function(data) {
+ var nodes = data.nodes;
+ expect(nodes.length).toBe(1);
+
+ var primitives = getPrimitives(data);
+ expect(primitives.length).toBe(3);
+
+ expect(primitives[0].indices.length).toBe(12);
+ expect(primitives[1].indices.length).toBe(12);
+ expect(primitives[2].indices.length).toBe(12);
+ expect(primitives[0].material).toBe('Red');
+ expect(primitives[1].material).toBe('Green');
+ expect(primitives[2].material).toBe('Blue');
+ }), done).toResolve();
+ });
+
+ it('loads obj uncleaned', function(done) {
+ // Obj with extraneous o, g, and usemtl lines
+ // Also tests handling of o and g lines with the same names
+ expect(loadObj(objUncleanedUrl)
+ .then(function(data) {
+ var nodes = data.nodes;
+ var meshes = getMeshes(data);
+ var primitives = getPrimitives(data);
+
+ expect(nodes.length).toBe(1);
+ expect(meshes.length).toBe(1);
+ expect(primitives.length).toBe(1);
+
+ expect(nodes[0].name).toBe('Cube');
+ expect(meshes[0].name).toBe('Cube_1');
+ }), done).toResolve();
+ });
+
+ it('loads obj with multiple mtllibs', function(done) {
+ expect(loadObj(objMtllibUrl)
+ .then(function(data) {
+ var materials = data.materials;
+ expect(Object.keys(materials).length).toBe(3);
+ expect(materials.Red.diffuseColor).toEqual([0.64, 0.0, 0.0, 1.0]);
+ expect(materials.Green.diffuseColor).toEqual([0.0, 0.64, 0.0, 1.0]);
+ expect(materials.Blue.diffuseColor).toEqual([0.0, 0.0, 0.64, 1.0]);
+ }), done).toResolve();
+ });
+
+ it('loads obj with missing mtllib', function(done) {
+ spyOn(console, 'log');
+ expect(loadObj(objMissingMtllibUrl)
+ .then(function(data) {
+ expect(data.materials).toEqual({});
+ }), done).toResolve();
+ });
+
+ it('loads obj with texture', function(done) {
+ expect(loadObj(objTexturedUrl)
+ .then(function(data) {
+ var imagePath = getImagePath(objTexturedUrl, 'cesium.png');
+ expect(data.images[imagePath]).toBeDefined();
+ expect(data.materials.Material.diffuseColorMap).toEqual(imagePath);
+ }), done).toResolve();
+ });
+
+ it('loads obj with missing texture', function(done) {
+ spyOn(console, 'log');
+ expect(loadObj(objMissingTextureUrl)
+ .then(function(data) {
+ var imagePath = getImagePath(objMissingTextureUrl, 'cesium.png');
+ expect(data.images[imagePath]).toBeUndefined();
+ expect(data.materials.Material.diffuseColorMap).toEqual(imagePath);
+ }), done).toResolve();
+ });
+
+ it('loads obj with subdirectories', function(done) {
+ expect(loadObj(objSubdirectoriesUrl)
+ .then(function(data) {
+ var imagePath = getImagePath(objSubdirectoriesUrl, path.join('materials', 'images', 'cesium.png'));
+ expect(data.images[imagePath]).toBeDefined();
+ expect(data.materials.Material.diffuseColorMap).toEqual(imagePath);
+ }), done).toResolve();
+ });
+
+ it('loads obj with complex material', function(done) {
+ expect(loadObj(objComplexMaterialUrl)
+ .then(function(data) {
+ var images = data.images;
+ expect(Object.keys(images).length).toBe(4); // Only ambient, diffuse, emission, and specular maps are supported by the converter
+ }), done).toResolve();
+ });
+
+ it('does not process file with invalid contents', function(done) {
+ expect(loadObj(objInvalidContentsUrl), done).toRejectWith(RuntimeError);
+ });
+
+ it('throw when reading invalid file', function(done) {
+ expect(loadObj(objInvalidUrl), done).toRejectWith(Error);
+ });
+});