This commit is contained in:
Sean Lilley 2017-01-04 12:10:44 -05:00
parent d8408e3f2c
commit 0628bd7207
11 changed files with 917 additions and 781 deletions

10
.gitignore vendored
View File

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

View File

@ -2,6 +2,7 @@
/doc
/specs
/test
/output
/typings
/coverage
.jshintrc

View File

@ -11,54 +11,6 @@ Third-Party Code
obj2gltf includes the following third-party code.
### async
https://www.npmjs.com/package/async
> Copyright (c) 2010-2016 Caolan McMahon
>
> 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.
### 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.
### Cesium
http://cesiumjs.org/
@ -82,12 +34,12 @@ 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,
@ -105,6 +57,22 @@ https://www.npmjs.com/package/gltf-pipeline
>
> Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
### linebyline
https://www.npmjs.com/package/linebyline
> Copyright 2017 Craig Brookes
>
> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
>
> 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
>
> 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
>
> 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
### yargs
https://www.npmjs.com/package/yargs

View File

@ -1,53 +1,99 @@
#!/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 defined = Cesium.defined;
var defaultValue = Cesium.defaultValue;
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.',
normalize: true,
type: 'string'
},
'output': {
alias: 'o',
describe: 'Path of the converted gltf file.',
normalize: true,
type: 'string'
},
'binary': {
alias: 'b',
describe: 'Write binary glTF file using KHR_binary_glTF extension.',
type: 'boolean'
},
'separate': {
alias: 's',
describe: 'Write separate geometry/animation data files, shader files, and textures instead of embedding them in the glTF.',
type: 'boolean'
},
'separateImage': {
alias: 't',
describe: 'Write out separate textures, but embeds geometry/animation data files and shader files in the glTF.',
type: 'boolean'
},
'compress': {
alias: 'c',
describe: 'Quantize positions, compress texture coordinates, and oct-encode normals.',
type: 'boolean'
},
'cesium': {
describe: 'Optimize the glTF for Cesium by using the sun as a default light source.',
type: 'boolean'
},
'ao': {
describe: 'Apply ambient occlusion to the converted model',
type: 'boolean'
},
'generateNormals': {
alias: 'n',
describe: 'Generate normals if they are missing',
type: 'boolean'
}
}).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,
embed : !argv.s,
embedImage : !argv.t,
compress : argv.c,
ao : argv.ao,
generateNormals : argv.generateNormals,
optimizeForCesium : argv.cesium
};
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);
.catch(function(error) {
console.log(error);
});

View File

@ -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/**'];
gulp.task('jsHint', function () {
var stream = gulp.src(jsHintFiles)
@ -71,7 +71,7 @@ gulp.task('coverage', function () {
' cover' +
' --include-all-sources' +
' --dir coverage' +
' -x "specs/** coverage/** index.js gulpfile.js"' +
' -x "specs/** coverage/** doc/** index.js gulpfile.js"' +
' node_modules/jasmine/bin/jasmine.js' +
' JASMINE_CONFIG_PATH=specs/jasmine.json', {
stdio: [process.stdin, process.stdout, process.stderr]

View File

@ -1,60 +1,65 @@
"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 GltfPipeline = require('gltf-pipeline').Pipeline;
var path = require('path');
var createGltf = require('./gltf');
var loadObj = require('./obj');
var defined = Cesium.defined;
var defaultValue = Cesium.defaultValue;
var DeveloperError = Cesium.DeveloperError;
module.exports = convert;
function convert(objFile, outputPath, options) {
options = defaultValue(options, {});
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 compress = defaultValue(options.compress, false);
var quantize = defaultValue(options.quantize, false);
var ao = defaultValue(options.ao, false);
var generateNormals = defaultValue(options.generateNormals, false);
var optimizeForCesium = defaultValue(options.optimizeForCesium, 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 = {
binary : binary,
embed : embed,
embedImage : embedImage,
encodeNormals : quantize,
quantize : quantize,
compressTextureCoordinates : quantize,
aoOptions : aoOptions,
smoothNormals : generateNormals,
optimizeForCesium : optimizeForCesium,
createDirectory : false,
basePath : basePath,
preserve : true
};
return loadObj(objPath)
.then(function(objData) {
return createGltf(gltfPath, 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);
require('fs-extra').outputJsonSync('C:/Users/Sean/Desktop/test.gltf', gltf);
return GltfPipeline.processJSONToDisk(gltf, gltfPath, pipelineOptions);
});
}

View File

@ -1,148 +1,33 @@
"use strict";
'use strict';
var Cesium = require('cesium');
var Promise = require('bluebird');
var fs = require('fs-extra');
var fsExtra = require('fs-extra');
var path = require('path');
var fxExtraOutputFile = Promise.promisify(fsExtra.outputFile);
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 sizeOfFloat32 = 4;
var sizeOfUint32 = 4;
var sizeOfUint16 = 2;
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));
}
function createGltf(gltfPath, objData) {
var nodes = objData.nodes;
var materials = objData.materials;
var images = objData.images;
var sceneId = 'scene';
var gltf = {
accessors : {},
asset : {},
buffers : {},
bufferViews : {},
extensionsUsed : ['KHR_materials_common'],
images : {},
materials : {},
meshes : {},
@ -154,174 +39,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
var samplerId = 'sampler';
gltf.samplers[samplerId] = {
magFilter : WebGLConstants.LINEAR,
minFilter : WebGLConstants.LINEAR,
wrapS : WebGLConstants.REPEAT,
wrapT : WebGLConstants.REPEAT
};
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.1, 0.1, 0.1, 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 {
name : materialId,
extensions : {
KHR_materials_common : {
technique : technique,
values : {
ambient : ambient,
diffuse : diffuse,
emission : emission,
specular : specular,
shininess : shininess,
transparency : transparency,
transparent : transparent,
doubleSided : doubleSided
}
}
}
};
}
for (var imagePath in images) {
if (images.hasOwnProperty(imagePath) && defined(images[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 +138,253 @@ function createGltf(data, inputPath, modelName) {
}
}
if (bufferSeparate) {
var bufferPath = path.join(inputPath, modelName + '.bin');
return fsWriteFile(bufferPath, buffer);
var bufferId = 'buffer';
var vertexBufferViewId = 'bufferView_vertex';
var vertexBuffers = [];
var vertexByteOffset = 0;
var indexUInt16BufferViewId = 'bufferView_index_uint16';
var indexUInt16Buffers = [];
var indexUint16ByteOffset = 0;
var indexUInt32BufferViewId = 'bufferView_index_uint32';
var indexUInt32Buffers = [];
var indexUint32ByteOffset = 0;
var accessorCount = 0;
function addVertexAttribute(array, components) {
var length = array.length;
var min = new Array(components).fill(Number.POSITIVE_INFINITY);
var max = new Array(components).fill(Number.NEGATIVE_INFINITY);
var buffer = Buffer.alloc(length * sizeOfFloat32);
var count = length / components;
for (var i = 0; i < count; ++i) {
for (var j = 0; j < components; ++j) {
var index = i * components + j;
var value = array[index];
min[j] = Math.min(min[j], value);
max[j] = Math.max(max[j], value);
buffer.writeFloatLE(value, index * sizeOfFloat32);
}
}
var type = (components === 3 ? 'VEC3' : 'VEC2');
var accessor = {
bufferView : vertexBufferViewId,
byteOffset : vertexByteOffset,
byteStride : 0,
componentType : WebGLConstants.FLOAT,
count : count,
min : min,
max : max,
type : type
};
vertexByteOffset += buffer.length;
vertexBuffers.push(buffer);
var accessorId = 'accessor_' + accessorCount++;
gltf.accessors[accessorId] = accessor;
return accessorId;
}
function addIndexUInt16Array(array, min, max) {
var length = array.length;
var paddedLength = length + ((length % 2 === 0) ? 0 : 1); // Round to next multiple of 2
buffer = Buffer.alloc(paddedLength * sizeOfUint16);
for (var i = 0; i < length; ++i) {
buffer.writeUInt16LE(array[i], i * sizeOfUint16);
}
var accessor = {
bufferView : indexUInt16BufferViewId,
byteOffset : indexUint16ByteOffset,
byteStride : 0,
componentType : WebGLConstants.UNSIGNED_SHORT,
count : length,
min : [min],
max : [max],
type : 'SCALAR'
};
indexUint16ByteOffset += buffer.length;
indexUInt16Buffers.push(buffer);
return accessor;
}
function addIndexUInt32Array(array, min, max) {
var length = array.length;
buffer = Buffer.alloc(length * sizeOfUint32);
for (var i = 0; i < length; ++i) {
buffer.writeUInt32LE(array[i], i * sizeOfUint32);
}
var accessor = {
bufferView : indexUInt32BufferViewId,
byteOffset : indexUint32ByteOffset,
byteStride : 0,
componentType : WebGLConstants.UNSIGNED_INT,
count : length,
min : [min],
max : [max],
type : 'SCALAR'
};
indexUint32ByteOffset += buffer.length;
indexUInt32Buffers.push(buffer);
return accessor;
}
function addIndexArray(array) {
var length = array.length;
var min = Number.POSITIVE_INFINITY;
var max = Number.NEGATIVE_INFINITY;
for (var i = 0; i < length; ++i) {
var value = array[i];
min = Math.min(min, value);
max = Math.max(max, value);
}
// Reserve the 65535 index for primitive restart
var accessor = (max < 65535) ? addIndexUInt16Array(array, min, max) : addIndexUInt32Array(array, min, max);
var accessorId = 'accessor_' + accessorCount++;
gltf.accessors[accessorId] = accessor;
return accessorId;
}
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);
}
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);
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 indexUInt16Buffer = Buffer.concat(indexUInt16Buffers);
var indexUInt32Buffer = Buffer.concat(indexUInt32Buffers);
var buffer = Buffer.concat([vertexBuffer, indexUInt16Buffer, indexUInt32Buffer]);
// Buffers larger than ~192MB cannot be base64 encoded due to a NodeJS limitation. Instead save the buffer to a .bin file. Source: https://github.com/nodejs/node/issues/4266
var bufferUri;
var bufferPath;
if (buffer.length > 201326580) {
var bufferName = path.basename(gltfPath, path.extname(gltfPath));
bufferUri = bufferName + '.bin';
bufferPath = path.join(path.dirname(gltfPath), bufferUri);
} else {
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
};
if (indexUInt16Buffer.length > 0) {
gltf.bufferViews[indexUInt16BufferViewId] = {
buffer : bufferId,
byteLength : indexUInt16Buffer.length,
byteOffset : vertexBuffer.length,
target : WebGLConstants.ELEMENT_ARRAY_BUFFER
};
}
if (indexUInt32Buffer.length > 0) {
gltf.bufferViews[indexUInt32BufferViewId] = {
buffer : bufferId,
byteLength : indexUInt32Buffer.length,
byteOffset : vertexBuffer.length + indexUInt16Buffer.length,
target : WebGLConstants.ELEMENT_ARRAY_BUFFER
};
}
if (defined(bufferPath)) {
return fxExtraOutputFile(bufferPath, buffer)
.then(function() {
return gltf;
});
}
return gltf;
}

View File

@ -1,10 +1,13 @@
"use strict";
'use strict';
var Cesium = require('cesium');
var Promise = require('bluebird');
var fs = require('fs-extra');
var path = require('path');
var fsReadFile = Promise.promisify(fs.readFile);
var WebGLConstants = Cesium.WebGLConstants;
module.exports = loadImage;
function getChannels(colorType) {
@ -37,6 +40,19 @@ function getUriType(extension) {
}
}
function getFormat(channels) {
switch (channels) {
case 1:
return WebGLConstants.ALPHA;
case 2:
return WebGLConstants.LUMINANCE_ALPHA;
case 3:
return WebGLConstants.RGB;
case 4:
return WebGLConstants.RGBA;
}
}
function loadImage(imagePath) {
return fsReadFile(imagePath)
.then(function(data) {
@ -45,19 +61,25 @@ function loadImage(imagePath) {
var uri = uriType + ';base64,' + data.toString('base64');
var info = {
transparent: false,
channels: 3,
data: data,
uri: uri
transparent : false,
channels : 3,
data : data,
uri : uri,
format : getFormat(3)
};
if (path.extname(imagePath) === 'png') {
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;
});
}

View File

@ -1,118 +1,104 @@
"use strict";
'use strict';
var Promise = require('bluebird');
var fs = require('fs-extra');
var defined = require('cesium').defined;
var readline = require('linebyline');
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;
}
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();
}
return new Promise(function(resolve) {
var stream = readline(mtlPath);
stream.on('line', function (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 = 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;
}
return materials;
})
.catch(function() {
console.log('Could not read material file at ' + mtlPath + '. Using default material instead.');
return {};
});
stream.on('end', function () {
resolve(materials);
});
stream.on('error', function() {
console.log('Could not read material file at ' + mtlPath + '. Using default material instead.');
resolve({});
});
});
}

View File

@ -1,125 +1,149 @@
"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 readline = require('linebyline');
var loadImage = require('./image');
var Material = require('./mtl');
var loadMtl = require('./mtl');
var Cartesian3 = Cesium.Cartesian3;
var combine = Cesium.combine;
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 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;
function Mesh() {
this.name = undefined;
this.primitives = [];
this.positions = [];
this.normals = [];
this.uvs = [];
}
var vertexArray = [];
function Primitive() {
this.material = undefined;
this.indices = [];
}
// 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 ...
var scratchCartesian = new Cartesian3();
function loadObj(objPath) {
return new Promise(function(resolve, reject) {
// Global store of vertex attributes listed in the obj file
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];
// The current node, mesh, and primitive
var node;
var mesh;
var primitive;
var hasNormals = info.hasNormals;
var hasUVs = info.hasUVs;
var nodes = [];
var materialGroups = {}; // Map material to index array
var currentIndexArray;
// Used to build the indices. The vertex cache is unique to each mesh.
var vertexCache = {};
var vertexCount = 0;
// 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;
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 = {};
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];
if (primitive.material === material) {
return;
}
}
// Add a new primitive with this material
addPrimitive();
primitive.material = getName(name);
}
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) {
function getOffset(a, attributeData, components) {
var i = parseInt(a);
if (i < 0) {
// Negative vertex indexes reference the vertices immediately above it
return (data.length / components + i) * components;
return (attributeData.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);
if (defined(p)) {
var pi = getOffset(p, positions, 3);
var px = positions[pi + 0];
var py = positions[pi + 1];
var pz = positions[pi + 2];
mesh.positions.push(px, py, pz);
}
// Normals
if (hasNormals) {
if (defined(n)) {
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);
mesh.normals.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);
}
if (defined(u)) {
var ui = getOffset(u, uvs, 2);
var ux = uvs[ui + 0];
var uy = uvs[ui + 1];
mesh.uvs.push(ux, uy);
}
}
@ -130,7 +154,6 @@ function processObj(objFile, info, materials, images) {
vertexCache[v] = index;
createVertex(p, u, n);
}
return index;
}
@ -139,63 +162,57 @@ function processObj(objFile, info, materials, images) {
var index2 = addVertex(v2, p2, u2, n2);
var index3 = addVertex(v3, p3, u3, n3);
currentIndexArray.push(index1);
currentIndexArray.push(index2);
currentIndexArray.push(index3);
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);
currentIndexArray.push(index1);
currentIndexArray.push(index3);
currentIndexArray.push(index4);
primitive.indices.push(index1);
primitive.indices.push(index3);
primitive.indices.push(index4);
}
}
// v float float float
var vertexPattern = /v( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/;
// Create a default node in case there are no o/g/usemtl lines in the obj
addNode();
// 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) {
var stream = readline(objPath);
stream.on('line', function (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]),
parseFloat(result[2]),
parseFloat(result[3])
);
var px = parseFloat(result[1]);
var py = parseFloat(result[2]);
var pz = parseFloat(result[3]);
positions.push(px, py, pz);
} 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());
var normal = Cartesian3.fromElements(nx, ny, nz, scratchCartesian);
normals.push(normal.x, normal.y, normal.z);
} else if ((result = uvPattern.exec(line)) !== null) {
uvs.push(
parseFloat(result[1]),
parseFloat(result[2])
);
var u = parseFloat(result[1]);
var v = parseFloat(result[2]);
v = 1.0 - v; // Flip y so 0.0 is the bottom of the image
uvs.push(u, v);
} else if ((result = facePattern1.exec(line)) !== null) {
addFace(
result[1], result[1], undefined, undefined,
@ -224,142 +241,180 @@ function processObj(objFile, info, materials, images) {
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);
}
});
stream.on('end', function () {
resolve({
vertexCount: vertexCount,
vertexArray: vertexArray,
positionMin: positionMin,
positionMax: positionMax,
hasUVs: hasUVs,
hasNormals: hasNormals,
materialGroups: materialGroups,
materials: materials,
images: images
});
finishLoading(nodes, mtlPaths, objPath).then(resolve).catch(reject);
});
stream.on('error', reject);
});
}
function getImages(inputPath, materials) {
// Collect all the image files from the materials
var images = [];
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
};
});
});
}
function loadMaterials(mtlPaths, objPath) {
return loadResources(objPath, mtlPaths, loadMtl)
.then(function(materialsByPath) {
var materials = {};
for (var path in materialsByPath) {
if (materialsByPath.hasOwnProperty(path)) {
materials = combine(materials, materialsByPath[path]);
}
}
return materials;
});
}
function loadImages(imagePaths, objPath) {
return loadResources(objPath, imagePaths, loadImage);
}
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 loadResources(objPath, resourcePaths, loadFunction) {
var resources = {};
return Promise.map(resourcePaths, function(resourcePath) {
var absolutePath = resourcePath;
if (!path.isAbsolute(absolutePath)) {
absolutePath = path.join(path.dirname(objPath), resourcePath);
}
return loadFunction(absolutePath)
.then(function(resource) {
resources[resourcePath] = resource;
});
}).then(function() {
return resources;
});
}
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) {
setDefaultNames(nodes[i].meshes, 'mesh', usedNames);
}
}
function cleanNodes(nodes) {
nodes = removeEmptyNodes(nodes);
setDefaults(nodes);
return nodes;
}

View File

@ -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.30.0",
"fs-extra": "^2.0.0",
"gltf-pipeline": "^0.1.0-alpha10",
"linebyline": "^1.3.0",
"yargs": "^6.6.0"
},
"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.2",
"typings": "^2.1.0"
},
"scripts": {
"prepublish": "typings install",