From b8b118bca0e66578fa11007e0cedee2ba5b2f7b6 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 13 Mar 2017 15:28:51 -0400 Subject: [PATCH] Refactor and tests --- .gitignore | 10 +- .idea/modules.xml | 2 +- .idea/{OBJ2GLTF.iml => obj2gltf.iml} | 0 .npmignore | 1 + .travis.yml | 5 + LICENSE.md | 121 +-- README.md | 18 +- bin/obj2gltf.js | 143 +++- gulpfile.js | 12 +- lib/ArrayStorage.js | 107 +++ lib/clone.js | 54 ++ lib/convert.js | 141 +++- lib/gltf.js | 562 +++++++------ lib/image.js | 92 ++- lib/mtl.js | 210 +++-- lib/obj.js | 740 ++++++++++-------- lib/readLines.js | 27 + package.json | 41 +- specs/data/BoxTextured/CesiumLogoFlat.png | Bin 22051 -> 0 bytes specs/data/box-complex-material/alpha.png | Bin 0 -> 1843 bytes specs/data/box-complex-material/ambient.gif | Bin 0 -> 4040 bytes .../box-complex-material.mtl | 20 + .../box-complex-material.obj} | 22 +- specs/data/box-complex-material/bump.png | Bin 0 -> 4720 bytes specs/data/box-complex-material/diffuse.png | Bin 0 -> 8889 bytes specs/data/box-complex-material/emission.jpg | Bin 0 -> 7092 bytes specs/data/box-complex-material/shininess.png | Bin 0 -> 4094 bytes specs/data/box-complex-material/specular.jpeg | Bin 0 -> 5618 bytes specs/data/box-groups/box-groups.mtl | 32 + specs/data/box-groups/box-groups.obj | 132 ++++ .../box-missing-mtllib/box-missing-mtllib.obj | 46 ++ .../box-missing-texture.mtl} | 8 +- .../box-missing-texture.obj | 46 ++ specs/data/box-mtllib/box-mtllib-blue.mtl | 12 + specs/data/box-mtllib/box-mtllib-green.mtl | 12 + specs/data/box-mtllib/box-mtllib-red.mtl | 12 + specs/data/box-mtllib/box-mtllib.obj | 50 ++ .../box-multiple-materials.mtl | 32 + .../box-multiple-materials.obj | 49 ++ .../box-negative-indices.mtl | 12 + .../box-negative-indices.obj | 20 + .../box-no-materials/box-no-materials.obj | 125 +++ specs/data/box-normals/box-normals.mtl | 12 + specs/data/box-normals/box-normals.obj | 26 + .../box-objects-groups-materials.gltf | 486 ++++++++++++ .../box-objects-groups-materials.mtl | 32 + .../box-objects-groups-materials.obj | 133 ++++ .../box-objects-groups/box-objects-groups.mtl | 32 + .../box-objects-groups/box-objects-groups.obj | 135 ++++ specs/data/box-objects/box-objects.mtl | 32 + specs/data/box-objects/box-objects.obj | 132 ++++ .../box-positions-only/box-positions-only.mtl | 12 + .../box-positions-only/box-positions-only.obj | 20 + .../data/box-subdirectories/box-textured.obj | 46 ++ .../materials/box-textured.mtl | 13 + .../materials/images/cesium.png | Bin 0 -> 7665 bytes specs/data/box-textured/box-textured.mtl | 13 + specs/data/box-textured/box-textured.obj | 46 ++ specs/data/box-textured/cesium.png | Bin 0 -> 7665 bytes specs/data/box-triangles/box-triangles.mtl | 12 + specs/data/box-triangles/box-triangles.obj | 46 ++ specs/data/box-uncleaned/box-uncleaned.mtl | 12 + specs/data/box-uncleaned/box-uncleaned.obj | 52 ++ specs/data/box-usemtl/box-usemtl.mtl | 32 + specs/data/box-usemtl/box-usemtl.obj | 129 +++ specs/data/box-uvs/box-uvs.mtl | 12 + specs/data/box-uvs/box-uvs.obj | 40 + specs/data/box/box.gltf | 176 +++++ specs/data/box/box.mtl | 12 + specs/data/box/box.obj | 46 ++ specs/lib/convertSpec.js | 120 ++- specs/lib/gltfSpec.js | 355 +++++++++ specs/lib/imageSpec.js | 91 +++ specs/lib/mtlSpec.js | 53 ++ specs/lib/objSpec.js | 328 ++++++++ 75 files changed, 4650 insertions(+), 952 deletions(-) rename .idea/{OBJ2GLTF.iml => obj2gltf.iml} (100%) create mode 100644 lib/ArrayStorage.js create mode 100644 lib/clone.js create mode 100644 lib/readLines.js delete mode 100644 specs/data/BoxTextured/CesiumLogoFlat.png create mode 100644 specs/data/box-complex-material/alpha.png create mode 100644 specs/data/box-complex-material/ambient.gif create mode 100644 specs/data/box-complex-material/box-complex-material.mtl rename specs/data/{BoxTextured/BoxTextured.obj => box-complex-material/box-complex-material.obj} (73%) create mode 100644 specs/data/box-complex-material/bump.png create mode 100644 specs/data/box-complex-material/diffuse.png create mode 100644 specs/data/box-complex-material/emission.jpg create mode 100644 specs/data/box-complex-material/shininess.png create mode 100644 specs/data/box-complex-material/specular.jpeg create mode 100644 specs/data/box-groups/box-groups.mtl create mode 100644 specs/data/box-groups/box-groups.obj create mode 100644 specs/data/box-missing-mtllib/box-missing-mtllib.obj rename specs/data/{BoxTextured/BoxTextured.mtl => box-missing-texture/box-missing-texture.mtl} (57%) create mode 100644 specs/data/box-missing-texture/box-missing-texture.obj create mode 100644 specs/data/box-mtllib/box-mtllib-blue.mtl create mode 100644 specs/data/box-mtllib/box-mtllib-green.mtl create mode 100644 specs/data/box-mtllib/box-mtllib-red.mtl create mode 100644 specs/data/box-mtllib/box-mtllib.obj create mode 100644 specs/data/box-multiple-materials/box-multiple-materials.mtl create mode 100644 specs/data/box-multiple-materials/box-multiple-materials.obj create mode 100644 specs/data/box-negative-indices/box-negative-indices.mtl create mode 100644 specs/data/box-negative-indices/box-negative-indices.obj create mode 100644 specs/data/box-no-materials/box-no-materials.obj create mode 100644 specs/data/box-normals/box-normals.mtl create mode 100644 specs/data/box-normals/box-normals.obj create mode 100644 specs/data/box-objects-groups-materials/box-objects-groups-materials.gltf create mode 100644 specs/data/box-objects-groups-materials/box-objects-groups-materials.mtl create mode 100644 specs/data/box-objects-groups-materials/box-objects-groups-materials.obj create mode 100644 specs/data/box-objects-groups/box-objects-groups.mtl create mode 100644 specs/data/box-objects-groups/box-objects-groups.obj create mode 100644 specs/data/box-objects/box-objects.mtl create mode 100644 specs/data/box-objects/box-objects.obj create mode 100644 specs/data/box-positions-only/box-positions-only.mtl create mode 100644 specs/data/box-positions-only/box-positions-only.obj create mode 100644 specs/data/box-subdirectories/box-textured.obj create mode 100644 specs/data/box-subdirectories/materials/box-textured.mtl create mode 100644 specs/data/box-subdirectories/materials/images/cesium.png create mode 100644 specs/data/box-textured/box-textured.mtl create mode 100644 specs/data/box-textured/box-textured.obj create mode 100644 specs/data/box-textured/cesium.png create mode 100644 specs/data/box-triangles/box-triangles.mtl create mode 100644 specs/data/box-triangles/box-triangles.obj create mode 100644 specs/data/box-uncleaned/box-uncleaned.mtl create mode 100644 specs/data/box-uncleaned/box-uncleaned.obj create mode 100644 specs/data/box-usemtl/box-usemtl.mtl create mode 100644 specs/data/box-usemtl/box-usemtl.obj create mode 100644 specs/data/box-uvs/box-uvs.mtl create mode 100644 specs/data/box-uvs/box-uvs.obj create mode 100644 specs/data/box/box.gltf create mode 100644 specs/data/box/box.mtl create mode 100644 specs/data/box/box.obj create mode 100644 specs/lib/gltfSpec.js create mode 100644 specs/lib/imageSpec.js create mode 100644 specs/lib/mtlSpec.js create mode 100644 specs/lib/objSpec.js 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 88bada374836fcecc80ef666ab093f61a42e7e0e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22051 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 BWBULA diff --git a/specs/data/box-complex-material/alpha.png b/specs/data/box-complex-material/alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..8437c62e20eaafd489a2e1b2fda6784706d37591 GIT binary patch literal 1843 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fcc5ba8ca8f1cee6a086ab43efa054578e6a670a GIT binary patch literal 4720 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 Hu0mjfJUwN5 literal 0 HcmV?d00001 diff --git a/specs/data/box-complex-material/emission.jpg b/specs/data/box-complex-material/emission.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e5c85a943f6c5c95ca46b1bfd5fc9e8d7f1c4b77 GIT binary patch literal 7092 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 literal 0 HcmV?d00001 diff --git a/specs/data/box-complex-material/shininess.png b/specs/data/box-complex-material/shininess.png new file mode 100644 index 0000000000000000000000000000000000000000..bfcf1a2c625c38a8da19b7c7390102dde2b3f90a GIT binary patch literal 4094 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 literal 0 HcmV?d00001 diff --git a/specs/data/box-complex-material/specular.jpeg b/specs/data/box-complex-material/specular.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..661bf98060289f196f149b5c0b6b05ff737f9ff8 GIT binary patch literal 5618 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 IJS9*353{Fhng9R* literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3b8baee1bce0079bd7afeb38d5e0b1bde9732ba5 GIT binary patch literal 7665 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3b8baee1bce0079bd7afeb38d5e0b1bde9732ba5 GIT binary patch literal 7665 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 literal 0 HcmV?d00001 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); + }); +});