Merge branch 'master' into unlit

This commit is contained in:
Sean Lilley 2018-11-24 17:04:09 -05:00
commit b5da7be229
20 changed files with 724 additions and 422 deletions

View File

@ -1,3 +1,6 @@
{ {
"extends": "cesium/node" "extends": "cesium/node",
"rules": {
"no-var": "off"
}
} }

View File

@ -1,12 +1,25 @@
Change Log Change Log
========== ==========
### 2.3.0 ??? ### 2.3.2 2018-11-02
* Improved handling of primitives with different attributes using the same material. Materials are now duplicated. [#162](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/162)
* Fixed a bug where primitives without texture coordinates could use materials containing textures. Those textures are now removed. [#162](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/162)
* Improved parsing of faces with mismatching attributes. [#161](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/161)
### 2.3.1 2018-10-16
* Improved parsing models with concave or n-sided faces. [#157](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/157)
* Fixed handling of objs with interleaved materials. [#155](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/155)
### 2.3.0 2018-09-19
* Fixed handling of objs with mismatching attribute layouts. [#153](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/153)
* Fixed normalization of Windows paths when running the converter on Linux. [#150](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/150)
* Added ability to use the first material in the mtl file when the obj is missing `usemtl`. [#133](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/133) * Added ability to use the first material in the mtl file when the obj is missing `usemtl`. [#133](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/133)
* Fixed handling of unnormalized input normals. [#136](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/136) * Fixed handling of unnormalized input normals. [#136](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/136)
### 2.2.0 2017-01-29 ### 2.2.0 2018-01-29
* Fixed handling of materials where the diffuse and ambient texture are the same. [#127](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/127) * Fixed handling of materials where the diffuse and ambient texture are the same. [#127](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/127)
* Added ability to load alpha textures. [#124](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/124) * Added ability to load alpha textures. [#124](https://github.com/AnalyticalGraphicsInc/obj2gltf/pull/124)

View File

@ -1,5 +1,7 @@
# OBJ2GLTF # OBJ2GLTF
[![Greenkeeper badge](https://badges.greenkeeper.io/AnalyticalGraphicsInc/obj2gltf.svg)](https://greenkeeper.io/)
Convert OBJ assets to [glTF](https://www.khronos.org/gltf) 2.0. Convert OBJ assets to [glTF](https://www.khronos.org/gltf) 2.0.
## Getting Started ## Getting Started

View File

@ -22,7 +22,14 @@ process.env.PATH += environmentSeparator + nodeBinaries;
var specFiles = ['**/*.js', '!node_modules/**', '!coverage/**', '!doc/**', '!bin/**']; var specFiles = ['**/*.js', '!node_modules/**', '!coverage/**', '!doc/**', '!bin/**'];
gulp.task('test', function (done) { module.exports = {
test: test,
'test-watch': testWatch,
coverage: coverage,
cloc: cloc
};
function test(done) {
var jasmine = new Jasmine(); var jasmine = new Jasmine();
jasmine.loadConfigFile('specs/jasmine.json'); jasmine.loadConfigFile('specs/jasmine.json');
jasmine.addReporter(new JasmineSpecReporter({ jasmine.addReporter(new JasmineSpecReporter({
@ -32,10 +39,10 @@ gulp.task('test', function (done) {
jasmine.onComplete(function (passed) { jasmine.onComplete(function (passed) {
done(argv.failTaskOnError && !passed ? 1 : 0); done(argv.failTaskOnError && !passed ? 1 : 0);
}); });
}); }
gulp.task('test-watch', function () { function testWatch() {
gulp.watch(specFiles).on('change', function () { return gulp.watch(specFiles).on('change', function () {
// We can't simply depend on the test task because Jasmine // We can't simply depend on the test task because Jasmine
// does not like being run multiple times in the same process. // does not like being run multiple times in the same process.
try { try {
@ -46,9 +53,9 @@ gulp.task('test-watch', function () {
console.log('Tests failed to execute.'); console.log('Tests failed to execute.');
} }
}); });
}); }
gulp.task('coverage', function () { async function coverage() {
fsExtra.removeSync('coverage/server'); fsExtra.removeSync('coverage/server');
child_process.execSync('nyc' + child_process.execSync('nyc' +
' --all' + ' --all' +
@ -60,10 +67,9 @@ gulp.task('coverage', function () {
stdio: [process.stdin, process.stdout, process.stderr] stdio: [process.stdin, process.stdout, process.stderr]
}); });
open('coverage/lcov-report/index.html'); open('coverage/lcov-report/index.html');
}); }
function cloc() {
gulp.task('cloc', function() {
var cmdLine; var cmdLine;
var clocPath = path.join('node_modules', 'cloc', 'lib', 'cloc'); var clocPath = path.join('node_modules', 'cloc', 'lib', 'cloc');
@ -99,4 +105,4 @@ gulp.task('cloc', function() {
}); });
}); });
}); });
}); }

View File

@ -4,6 +4,7 @@ var getBufferPadded = require('./getBufferPadded');
var getDefaultMaterial = require('./loadMtl').getDefaultMaterial; var getDefaultMaterial = require('./loadMtl').getDefaultMaterial;
var Texture = require('./Texture'); var Texture = require('./Texture');
var defaultValue = Cesium.defaultValue;
var defined = Cesium.defined; var defined = Cesium.defined;
var WebGLConstants = Cesium.WebGLConstants; var WebGLConstants = Cesium.WebGLConstants;
@ -23,6 +24,9 @@ function createGltf(objData, options) {
var materials = objData.materials; var materials = objData.materials;
var name = objData.name; var name = objData.name;
// Split materials used by primitives with different types of attributes
materials = splitIncompatibleMaterials(nodes, materials, options);
var gltf = { var gltf = {
accessors : [], accessors : [],
asset : {}, asset : {},
@ -133,10 +137,8 @@ function addBufferView(gltf, buffers, accessors, byteStride, target) {
} }
function addBuffers(gltf, bufferState, name) { function addBuffers(gltf, bufferState, name) {
// Positions and normals share the same byte stride so they can share the same bufferView addBufferView(gltf, bufferState.positionBuffers, bufferState.positionAccessors, 12, WebGLConstants.ARRAY_BUFFER);
var positionsAndNormalsAccessors = bufferState.positionAccessors.concat(bufferState.normalAccessors); addBufferView(gltf, bufferState.normalBuffers, bufferState.normalAccessors, 12, WebGLConstants.ARRAY_BUFFER);
var positionsAndNormalsBuffers = bufferState.positionBuffers.concat(bufferState.normalBuffers);
addBufferView(gltf, positionsAndNormalsBuffers, positionsAndNormalsAccessors, 12, WebGLConstants.ARRAY_BUFFER);
addBufferView(gltf, bufferState.uvBuffers, bufferState.uvAccessors, 8, WebGLConstants.ARRAY_BUFFER); addBufferView(gltf, bufferState.uvBuffers, bufferState.uvAccessors, 8, WebGLConstants.ARRAY_BUFFER);
addBufferView(gltf, bufferState.indexBuffers, bufferState.indexAccessors, undefined, WebGLConstants.ELEMENT_ARRAY_BUFFER); addBufferView(gltf, bufferState.indexBuffers, bufferState.indexAccessors, undefined, WebGLConstants.ELEMENT_ARRAY_BUFFER);
@ -198,6 +200,31 @@ function getTexture(gltf, texture) {
}; };
} }
function cloneMaterial(material, removeTextures) {
if (typeof material !== 'object') {
return material;
} else if (material instanceof Texture) {
if (removeTextures) {
return undefined;
}
return material;
} else if (Array.isArray(material)) {
var length = material.length;
var clonedArray = new Array(length);
for (var i = 0; i < length; ++i) {
clonedArray[i] = cloneMaterial(material[i], removeTextures);
}
return clonedArray;
}
var clonedObject = {};
for (var name in material) {
if (material.hasOwnProperty(name)) {
clonedObject[name] = cloneMaterial(material[name], removeTextures);
}
}
return clonedObject;
}
function resolveTextures(gltf, material) { function resolveTextures(gltf, material) {
for (var name in material) { for (var name in material) {
if (material.hasOwnProperty(name)) { if (material.hasOwnProperty(name)) {
@ -211,7 +238,7 @@ function resolveTextures(gltf, material) {
} }
} }
function addMaterial(gltf, material, options) { function addGltfMaterial(gltf, material, options) {
resolveTextures(gltf, material); resolveTextures(gltf, material);
var materialIndex = gltf.materials.length; var materialIndex = gltf.materials.length;
if (options.unlit) { if (options.unlit) {
@ -224,43 +251,94 @@ function addMaterial(gltf, material, options) {
return materialIndex; return materialIndex;
} }
function getMaterial(gltf, materials, materialName, options) { function getMaterialByName(materials, materialName) {
if (!defined(materialName)) {
// Create a default material if the primitive does not specify one
materialName = 'default';
}
var i;
var material;
var materialsLength = materials.length; var materialsLength = materials.length;
for (i = 0; i < materialsLength; ++i) { for (var i = 0; i < materialsLength; ++i) {
if (materials[i].name === materialName) { if (materials[i].name === materialName) {
material = materials[i]; return materials[i];
break;
} }
} }
}
if (!defined(material)) { function getMaterialIndex(materials, materialName) {
material = getDefaultMaterial(options); var materialsLength = materials.length;
material.name = materialName; for (var i = 0; i < materialsLength; ++i) {
if (materials[i].name === materialName) {
return i;
} }
}
}
var materialIndex; function getOrCreateGltfMaterial(gltf, materials, materialName, options) {
materialsLength = gltf.materials.length; var material = getMaterialByName(materials, materialName);
for (i = 0; i < materialsLength; ++i) { var materialIndex = getMaterialIndex(gltf.materials, materialName);
if (gltf.materials[i].name === materialName) {
materialIndex = i;
break;
}
}
if (!defined(materialIndex)) { if (!defined(materialIndex)) {
materialIndex = addMaterial(gltf, material, options); materialIndex = addGltfMaterial(gltf, material, options);
} }
return materialIndex; return materialIndex;
} }
function primitiveInfoMatch(a, b) {
return a.hasUvs === b.hasUvs &&
a.hasNormals === b.hasNormals;
}
function getSplitMaterialName(originalMaterialName, primitiveInfo, primitiveInfoByMaterial) {
var splitMaterialName = originalMaterialName;
var suffix = 2;
while (defined(primitiveInfoByMaterial[splitMaterialName])) {
if (primitiveInfoMatch(primitiveInfo, primitiveInfoByMaterial[splitMaterialName])) {
break;
}
splitMaterialName = originalMaterialName + '-' + suffix++;
}
return splitMaterialName;
}
function splitIncompatibleMaterials(nodes, materials, options) {
var splitMaterials = [];
var primitiveInfoByMaterial = {};
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) {
var primitives = meshes[j].primitives;
var primitivesLength = primitives.length;
for (var k = 0; k < primitivesLength; ++k) {
var primitive = primitives[k];
var hasUvs = primitive.uvs.length > 0;
var hasNormals = primitive.normals.length > 0;
var primitiveInfo = {
hasUvs : hasUvs,
hasNormals : hasNormals
};
var originalMaterialName = defaultValue(primitive.material, 'default');
var splitMaterialName = getSplitMaterialName(originalMaterialName, primitiveInfo, primitiveInfoByMaterial);
primitive.material = splitMaterialName;
primitiveInfoByMaterial[splitMaterialName] = primitiveInfo;
var splitMaterial = getMaterialByName(splitMaterials, splitMaterialName);
if (defined(splitMaterial)) {
continue;
}
var originalMaterial = getMaterialByName(materials, originalMaterialName);
if (defined(originalMaterial)) {
splitMaterial = cloneMaterial(originalMaterial, !hasUvs);
} else {
splitMaterial = getDefaultMaterial(options);
}
splitMaterial.name = splitMaterialName;
splitMaterials.push(splitMaterial);
}
}
}
return splitMaterials;
}
function addVertexAttribute(gltf, array, components, name) { function addVertexAttribute(gltf, array, components, name) {
var count = array.length / components; var count = array.length / components;
var minMax = array.getMinMax(components); var minMax = array.getMinMax(components);
@ -305,68 +383,73 @@ function requiresUint32Indices(nodes) {
var meshes = nodes[i].meshes; var meshes = nodes[i].meshes;
var meshesLength = meshes.length; var meshesLength = meshes.length;
for (var j = 0; j < meshesLength; ++j) { for (var j = 0; j < meshesLength; ++j) {
var primitives = meshes[j].primitives;
var primitivesLength = primitives.length;
for (var k = 0; k < primitivesLength; ++k) {
// Reserve the 65535 index for primitive restart // Reserve the 65535 index for primitive restart
var vertexCount = meshes[j].positions.length / 3; var vertexCount = primitives[k].positions.length / 3;
if (vertexCount > 65534) { if (vertexCount > 65534) {
return true; return true;
} }
} }
} }
}
return false; return false;
} }
function addMesh(gltf, materials, bufferState, uint32Indices, mesh, options) { function addPrimitive(gltf, materials, bufferState, uint32Indices, mesh, primitive, index, options) {
var hasPositions = mesh.positions.length > 0; var hasPositions = primitive.positions.length > 0;
var hasNormals = mesh.normals.length > 0; var hasNormals = primitive.normals.length > 0;
var hasUVs = mesh.uvs.length > 0; var hasUVs = primitive.uvs.length > 0;
// Vertex attributes are shared by all primitives in the mesh
var accessorIndex; var accessorIndex;
var attributes = {}; var attributes = {};
if (hasPositions) { if (hasPositions) {
accessorIndex = addVertexAttribute(gltf, mesh.positions, 3, mesh.name + '_positions'); accessorIndex = addVertexAttribute(gltf, primitive.positions, 3, mesh.name + '_' + index + '_positions');
attributes.POSITION = accessorIndex; attributes.POSITION = accessorIndex;
bufferState.positionBuffers.push(mesh.positions.toFloatBuffer()); bufferState.positionBuffers.push(primitive.positions.toFloatBuffer());
bufferState.positionAccessors.push(accessorIndex); bufferState.positionAccessors.push(accessorIndex);
} }
if (hasNormals) { if (hasNormals) {
accessorIndex = addVertexAttribute(gltf, mesh.normals, 3, mesh.name + '_normals'); accessorIndex = addVertexAttribute(gltf, primitive.normals, 3, mesh.name + '_' + index + '_normals');
attributes.NORMAL = accessorIndex; attributes.NORMAL = accessorIndex;
bufferState.normalBuffers.push(mesh.normals.toFloatBuffer()); bufferState.normalBuffers.push(primitive.normals.toFloatBuffer());
bufferState.normalAccessors.push(accessorIndex); bufferState.normalAccessors.push(accessorIndex);
} }
if (hasUVs) { if (hasUVs) {
accessorIndex = addVertexAttribute(gltf, mesh.uvs, 2, mesh.name + '_texcoords'); accessorIndex = addVertexAttribute(gltf, primitive.uvs, 2, mesh.name + '_' + index + '_texcoords');
attributes.TEXCOORD_0 = accessorIndex; attributes.TEXCOORD_0 = accessorIndex;
bufferState.uvBuffers.push(mesh.uvs.toFloatBuffer()); bufferState.uvBuffers.push(primitive.uvs.toFloatBuffer());
bufferState.uvAccessors.push(accessorIndex); bufferState.uvAccessors.push(accessorIndex);
} }
// Unload resources var indexAccessorIndex = addIndexArray(gltf, primitive.indices, uint32Indices, mesh.name + '_' + index + '_indices');
mesh.positions = undefined;
mesh.normals = undefined;
mesh.uvs = undefined;
var gltfPrimitives = [];
var primitives = mesh.primitives;
var primitivesLength = primitives.length;
for (var i = 0; i < primitivesLength; ++i) {
var primitive = primitives[i];
var indexAccessorIndex = addIndexArray(gltf, primitive.indices, uint32Indices, mesh.name + '_' + i + '_indices');
var indexBuffer = uint32Indices ? primitive.indices.toUint32Buffer() : primitive.indices.toUint16Buffer(); var indexBuffer = uint32Indices ? primitive.indices.toUint32Buffer() : primitive.indices.toUint16Buffer();
bufferState.indexBuffers.push(indexBuffer); bufferState.indexBuffers.push(indexBuffer);
bufferState.indexAccessors.push(indexAccessorIndex); bufferState.indexAccessors.push(indexAccessorIndex);
primitive.indices = undefined; // Unload resources // Unload resources
primitive.positions = undefined;
primitive.normals = undefined;
primitive.uvs = undefined;
primitive.indices = undefined;
var materialIndex = getMaterial(gltf, materials, primitive.material, options); var materialIndex = getOrCreateGltfMaterial(gltf, materials, primitive.material, options);
gltfPrimitives.push({ return {
attributes : attributes, attributes : attributes,
indices : indexAccessorIndex, indices : indexAccessorIndex,
material : materialIndex, material : materialIndex,
mode : WebGLConstants.TRIANGLES mode : WebGLConstants.TRIANGLES
}); };
}
function addMesh(gltf, materials, bufferState, uint32Indices, mesh, options) {
var gltfPrimitives = [];
var primitives = mesh.primitives;
var primitivesLength = primitives.length;
for (var i = 0; i < primitivesLength; ++i) {
gltfPrimitives.push(addPrimitive(gltf, materials, bufferState, uint32Indices, mesh, primitives[i], i, options));
} }
var gltfMesh = { var gltfMesh = {

View File

@ -81,19 +81,15 @@ function loadMtl(mtlPath, options) {
materials.push(material); materials.push(material);
} }
/** function normalizeTexturePath(texturePath, mtlDirectory) {
* Removes texture options from texture name // Removes texture options from texture name
* NOTE: assumes no spaces in texture name // Assumes no spaces in texture name
*
* @param {String} name
* @returns {String} The clean texture name
*/
function cleanTextureName (name) {
var re = /-(bm|t|s|o|blendu|blendv|boost|mm|texres|clamp|imfchan|type)/; var re = /-(bm|t|s|o|blendu|blendv|boost|mm|texres|clamp|imfchan|type)/;
if (re.test(name)) { if (re.test(texturePath)) {
return name.split(/\s+/).pop(); texturePath = texturePath.split(/\s+/).pop();
} }
return name; texturePath = texturePath.replace(/\\/g, '/');
return path.normalize(path.join(mtlDirectory, texturePath));
} }
function parseLine(line) { function parseLine(line) {
@ -144,31 +140,31 @@ function loadMtl(mtlPath, options) {
material.alpha = correctAlpha(1.0 - parseFloat(value)); material.alpha = correctAlpha(1.0 - parseFloat(value));
} else if (/^map_Ka /i.test(line)) { } else if (/^map_Ka /i.test(line)) {
if (!defined(overridingAmbientTexture)) { if (!defined(overridingAmbientTexture)) {
material.ambientTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim())); material.ambientTexture = normalizeTexturePath(line.substring(7).trim(), mtlDirectory);
} }
} else if (/^map_Ke /i.test(line)) { } else if (/^map_Ke /i.test(line)) {
if (!defined(overridingEmissiveTexture)) { if (!defined(overridingEmissiveTexture)) {
material.emissiveTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim())); material.emissiveTexture = normalizeTexturePath(line.substring(7).trim(), mtlDirectory);
} }
} else if (/^map_Kd /i.test(line)) { } else if (/^map_Kd /i.test(line)) {
if (!defined(overridingDiffuseTexture)) { if (!defined(overridingDiffuseTexture)) {
material.diffuseTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim())); material.diffuseTexture = normalizeTexturePath(line.substring(7).trim(), mtlDirectory);
} }
} else if (/^map_Ks /i.test(line)) { } else if (/^map_Ks /i.test(line)) {
if (!defined(overridingSpecularTexture)) { if (!defined(overridingSpecularTexture)) {
material.specularTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim())); material.specularTexture = normalizeTexturePath(line.substring(7).trim(), mtlDirectory);
} }
} else if (/^map_Ns /i.test(line)) { } else if (/^map_Ns /i.test(line)) {
if (!defined(overridingSpecularShininessTexture)) { if (!defined(overridingSpecularShininessTexture)) {
material.specularShininessTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim())); material.specularShininessTexture = normalizeTexturePath(line.substring(7).trim(), mtlDirectory);
} }
} else if (/^map_Bump /i.test(line)) { } else if (/^map_Bump /i.test(line)) {
if (!defined(overridingNormalTexture)) { if (!defined(overridingNormalTexture)) {
material.normalTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(9).trim())); material.normalTexture = normalizeTexturePath(line.substring(9).trim(), mtlDirectory);
} }
} else if (/^map_d /i.test(line)) { } else if (/^map_d /i.test(line)) {
if (!defined(overridingAlphaTexture)) { if (!defined(overridingAlphaTexture)) {
material.alphaTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(6).trim())); material.alphaTexture = normalizeTexturePath(line.substring(6).trim(), mtlDirectory);
} }
} }
} }
@ -244,7 +240,7 @@ function loadMaterialTexture(material, name, textureOptions, mtlDirectory, textu
var texturePromise = texturePromiseMap[texturePath]; var texturePromise = texturePromiseMap[texturePath];
if (!defined(texturePromise)) { if (!defined(texturePromise)) {
var shallowPath = path.resolve(path.join(mtlDirectory, path.basename(texturePath))); var shallowPath = path.join(mtlDirectory, path.basename(texturePath));
if (options.secure && outsideDirectory(texturePath, mtlDirectory)) { if (options.secure && outsideDirectory(texturePath, mtlDirectory)) {
// Try looking for the texture in the same directory as the obj // Try looking for the texture in the same directory as the obj
options.logger('Texture file is outside of the mtl directory and the secure flag is true. Attempting to read the texture file from within the obj directory instead.'); options.logger('Texture file is outside of the mtl directory and the secure flag is true. Attempting to read the texture file from within the obj directory instead.');

View File

@ -8,17 +8,12 @@ var loadMtl = require('./loadMtl');
var outsideDirectory = require('./outsideDirectory'); var outsideDirectory = require('./outsideDirectory');
var readLines = require('./readLines'); var readLines = require('./readLines');
var Cartesian2 = Cesium.Cartesian2;
var Cartesian3 = Cesium.Cartesian3; var Cartesian3 = Cesium.Cartesian3;
var ComponentDatatype = Cesium.ComponentDatatype; var ComponentDatatype = Cesium.ComponentDatatype;
var CoplanarPolygonGeometryLibrary = Cesium.CoplanarPolygonGeometryLibrary;
var defaultValue = Cesium.defaultValue; var defaultValue = Cesium.defaultValue;
var defined = Cesium.defined; var defined = Cesium.defined;
var IntersectionTests = Cesium.IntersectionTests;
var Matrix3 = Cesium.Matrix3;
var OrientedBoundingBox = Cesium.OrientedBoundingBox;
var Plane = Cesium.Plane;
var PolygonPipeline = Cesium.PolygonPipeline; var PolygonPipeline = Cesium.PolygonPipeline;
var Ray = Cesium.Ray;
var RuntimeError = Cesium.RuntimeError; var RuntimeError = Cesium.RuntimeError;
var WindingOrder = Cesium.WindingOrder; var WindingOrder = Cesium.WindingOrder;
@ -36,14 +31,14 @@ function Node() {
function Mesh() { function Mesh() {
this.name = undefined; this.name = undefined;
this.primitives = []; this.primitives = [];
this.positions = new ArrayStorage(ComponentDatatype.FLOAT);
this.normals = new ArrayStorage(ComponentDatatype.FLOAT);
this.uvs = new ArrayStorage(ComponentDatatype.FLOAT);
} }
function Primitive() { function Primitive() {
this.material = undefined; this.material = undefined;
this.indices = new ArrayStorage(ComponentDatatype.UNSIGNED_INT); this.indices = new ArrayStorage(ComponentDatatype.UNSIGNED_INT);
this.positions = new ArrayStorage(ComponentDatatype.FLOAT);
this.normals = new ArrayStorage(ComponentDatatype.FLOAT);
this.uvs = new ArrayStorage(ComponentDatatype.FLOAT);
} }
// OBJ regex patterns are modified from ThreeJS (https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/OBJLoader.js) // OBJ regex patterns are modified from ThreeJS (https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/OBJLoader.js)
@ -76,7 +71,7 @@ function loadObj(objPath, options) {
// All nodes seen in the obj // All nodes seen in the obj
var nodes = []; var nodes = [];
// Used to build the indices. The vertex cache is unique to each mesh. // Used to build the indices. The vertex cache is unique to each primitive.
var vertexCache = {}; var vertexCache = {};
var vertexCacheLimit = 1000000; var vertexCacheLimit = 1000000;
var vertexCacheCount = 0; var vertexCacheCount = 0;
@ -94,7 +89,10 @@ function loadObj(objPath, options) {
var faceUvs = []; var faceUvs = [];
var faceNormals = []; var faceNormals = [];
var vertexIndices = []; function clearVertexCache() {
vertexCache = {};
vertexCacheCount = 0;
}
function getName(name) { function getName(name) {
return (name === '' ? undefined : name); return (name === '' ? undefined : name);
@ -112,36 +110,56 @@ function loadObj(objPath, options) {
mesh.name = getName(name); mesh.name = getName(name);
node.meshes.push(mesh); node.meshes.push(mesh);
addPrimitive(); addPrimitive();
// Clear the vertex cache for each new mesh
vertexCache = {};
vertexCacheCount = 0;
vertexCount = 0;
} }
function addPrimitive() { function addPrimitive() {
primitive = new Primitive(); primitive = new Primitive();
primitive.material = activeMaterial; primitive.material = activeMaterial;
mesh.primitives.push(primitive); mesh.primitives.push(primitive);
// Clear the vertex cache for each new primitive
clearVertexCache();
vertexCount = 0;
} }
function useMaterial(name) { function reusePrimitive(callback) {
var material = getName(name);
activeMaterial = material;
// Look to see if this material has already been used by a primitive in the mesh
var primitives = mesh.primitives; var primitives = mesh.primitives;
var primitivesLength = primitives.length; var primitivesLength = primitives.length;
for (var i = 0; i < primitivesLength; ++i) { for (var i = 0; i < primitivesLength; ++i) {
if (primitives[i].material === material) { if (primitives[i].material === activeMaterial) {
if (!defined(callback) || callback(primitives[i])) {
primitive = primitives[i]; primitive = primitives[i];
clearVertexCache();
vertexCount = primitive.positions.length / 3;
return; return;
} }
} }
// Add a new primitive with this material }
addPrimitive(); addPrimitive();
} }
function useMaterial(name) {
activeMaterial = getName(name);
reusePrimitive();
}
function faceAndPrimitiveMatch(uvs, normals, primitive) {
var faceHasUvs = uvs[0].length > 0;
var faceHasNormals = normals[0].length > 0;
var primitiveHasUvs = primitive.uvs.length > 0;
var primitiveHasNormals = primitive.normals.length > 0;
return primitiveHasUvs === faceHasUvs && primitiveHasNormals === faceHasNormals;
}
function checkPrimitive(uvs, normals) {
var firstFace = primitive.indices.length === 0;
if (!firstFace && !faceAndPrimitiveMatch(uvs, normals, primitive)) {
reusePrimitive(function(primitive) {
return faceAndPrimitiveMatch(uvs, normals, primitive);
});
}
}
function getOffset(a, attributeData, components) { function getOffset(a, attributeData, components) {
var i = parseInt(a); var i = parseInt(a);
if (i < 0) { if (i < 0) {
@ -158,9 +176,9 @@ function loadObj(objPath, options) {
var px = positions.get(pi + 0); var px = positions.get(pi + 0);
var py = positions.get(pi + 1); var py = positions.get(pi + 1);
var pz = positions.get(pi + 2); var pz = positions.get(pi + 2);
mesh.positions.push(px); primitive.positions.push(px);
mesh.positions.push(py); primitive.positions.push(py);
mesh.positions.push(pz); primitive.positions.push(pz);
} }
// Normals // Normals
@ -169,9 +187,9 @@ function loadObj(objPath, options) {
var nx = normals.get(ni + 0); var nx = normals.get(ni + 0);
var ny = normals.get(ni + 1); var ny = normals.get(ni + 1);
var nz = normals.get(ni + 2); var nz = normals.get(ni + 2);
mesh.normals.push(nx); primitive.normals.push(nx);
mesh.normals.push(ny); primitive.normals.push(ny);
mesh.normals.push(nz); primitive.normals.push(nz);
} }
// UVs // UVs
@ -179,8 +197,8 @@ function loadObj(objPath, options) {
var ui = getOffset(u, uvs, 2); var ui = getOffset(u, uvs, 2);
var ux = uvs.get(ui + 0); var ux = uvs.get(ui + 0);
var uy = uvs.get(ui + 1); var uy = uvs.get(ui + 1);
mesh.uvs.push(ux); primitive.uvs.push(ux);
mesh.uvs.push(uy); primitive.uvs.push(uy);
} }
} }
@ -195,114 +213,13 @@ function loadObj(objPath, options) {
// may be some duplicate vertices. // may be some duplicate vertices.
vertexCacheCount++; vertexCacheCount++;
if (vertexCacheCount > vertexCacheLimit) { if (vertexCacheCount > vertexCacheLimit) {
vertexCacheCount = 0; clearVertexCache();
vertexCache = {};
} }
} }
return index; return index;
} }
// Given a set of 3D points, project them onto whichever axis will produce the least distortion. function getPosition(index, result) {
var scratchIntersectionPoint = new Cartesian3();
var scratchXAxis = new Cartesian3();
var scratchYAxis = new Cartesian3();
var scratchZAxis = new Cartesian3();
var scratchOrigin = new Cartesian3();
var scratchNormal = new Cartesian3();
var scratchRay = new Ray();
var scratchPlane = new Plane(Cesium.Cartesian3.UNIT_X, 0);
var scratchPositions2D = [new Cartesian2(), new Cartesian2(), new Cartesian2()];
function projectTo2D(positions) {
var positions2D = [];
var obb = OrientedBoundingBox.fromPoints(positions);
var halfAxes = obb.halfAxes;
Matrix3.getColumn(halfAxes, 0, scratchXAxis);
Matrix3.getColumn(halfAxes, 1, scratchYAxis);
Matrix3.getColumn(halfAxes, 2, scratchZAxis);
var xMag = Cartesian3.magnitude(scratchXAxis);
var yMag = Cartesian3.magnitude(scratchYAxis);
var zMag = Cartesian3.magnitude(scratchZAxis);
var min = Math.min(xMag, yMag, zMag);
var i;
// If all the points are on a line, just remove one of the zero dimensions
if (xMag === 0 && (yMag === 0 || zMag === 0)) {
for (i = 0; i < positions.length; i++) {
if (i === scratchPositions2D.length) {
scratchPositions2D.push(new Cartesian2());
}
positions2D[i] = new Cartesian2.fromElements(positions[i].y, positions[i].z, scratchPositions2D[i]);
}
return positions2D;
} else if (yMag === 0 && zMag === 0) {
for (i = 0; i < positions.length; i++) {
if (i === scratchPositions2D.length) {
scratchPositions2D.push(new Cartesian2());
}
positions2D[i] = new Cartesian2.fromElements(positions[i].x, positions[i].y, scratchPositions2D[i]);
}
return positions2D;
}
var center = obb.center;
var planeXAxis;
var planeYAxis;
if (min === xMag) {
if (!scratchXAxis.equals(Cartesian3.ZERO)) {
Cartesian3.add(center, scratchXAxis, scratchOrigin);
Cartesian3.normalize(scratchXAxis, scratchNormal);
}
planeXAxis = Cartesian3.normalize(scratchYAxis, scratchYAxis);
planeYAxis = Cartesian3.normalize(scratchZAxis, scratchZAxis);
} else if (min === yMag) {
if (!scratchYAxis.equals(Cartesian3.ZERO)) {
Cartesian3.add(center, scratchYAxis, scratchOrigin);
Cartesian3.normalize(scratchYAxis, scratchNormal);
}
planeXAxis = Cartesian3.normalize(scratchXAxis, scratchXAxis);
planeYAxis = Cartesian3.normalize(scratchZAxis, scratchZAxis);
} else {
if (!scratchZAxis.equals(Cartesian3.ZERO)) {
Cartesian3.add(center, scratchZAxis, scratchOrigin);
Cartesian3.normalize(scratchZAxis, scratchNormal);
}
planeXAxis = Cartesian3.normalize(scratchXAxis, scratchXAxis);
planeYAxis = Cartesian3.normalize(scratchYAxis, scratchYAxis);
}
if (min === 0) {
scratchNormal = Cartesian3.cross(planeXAxis, planeYAxis, scratchNormal);
scratchNormal = Cartesian3.normalize(scratchNormal, scratchNormal);
}
Plane.fromPointNormal(scratchOrigin, scratchNormal, scratchPlane);
scratchRay.direction = scratchNormal;
for (i = 0; i < positions.length; i++) {
scratchRay.origin = positions[i];
var intersectionPoint = IntersectionTests.rayPlane(scratchRay, scratchPlane, scratchIntersectionPoint);
if (!defined(intersectionPoint)) {
Cartesian3.negate(scratchRay.direction, scratchRay.direction);
intersectionPoint = IntersectionTests.rayPlane(scratchRay, scratchPlane, scratchIntersectionPoint);
}
var v = Cartesian3.subtract(intersectionPoint, scratchOrigin, intersectionPoint);
var x = Cartesian3.dot(planeXAxis, v);
var y = Cartesian3.dot(planeYAxis, v);
if (i === scratchPositions2D.length) {
scratchPositions2D.push(new Cartesian2());
}
positions2D[i] = new Cartesian2.fromElements(x, y, scratchPositions2D[i]);
}
return positions2D;
}
function get3DPoint(index, result) {
var pi = getOffset(index, positions, 3); var pi = getOffset(index, positions, 3);
var px = positions.get(pi + 0); var px = positions.get(pi + 0);
var py = positions.get(pi + 1); var py = positions.get(pi + 1);
@ -310,7 +227,7 @@ function loadObj(objPath, options) {
return Cartesian3.fromElements(px, py, pz, result); return Cartesian3.fromElements(px, py, pz, result);
} }
function get3DNormal(index, result) { function getNormal(index, result) {
var ni = getOffset(index, normals, 3); var ni = getOffset(index, normals, 3);
var nx = normals.get(ni + 0); var nx = normals.get(ni + 0);
var ny = normals.get(ni + 1); var ny = normals.get(ni + 1);
@ -318,36 +235,28 @@ function loadObj(objPath, options) {
return Cartesian3.fromElements(nx, ny, nz, result); return Cartesian3.fromElements(nx, ny, nz, result);
} }
// Given a sequence of three points A B C, determine whether vector BC
// "turns" clockwise (positive) or counter-clockwise (negative) from vector AB
var scratch1 = new Cartesian3(); var scratch1 = new Cartesian3();
var scratch2 = new Cartesian3(); var scratch2 = new Cartesian3();
function getTurnDirection(pointA, pointB, pointC) {
var vector1 = Cartesian2.subtract(pointA, pointB, scratch1);
var vector2 = Cartesian2.subtract(pointC, pointB, scratch2);
return vector1.x * vector2.y - vector1.y * vector2.x;
}
// Given the cartesian 2 vertices of a polygon, determine if convex
function isConvex(positions2D) {
var turnDirection = getTurnDirection(positions2D[0], positions2D[1], positions2D[2]);
for (var i=1; i < positions2D.length-2; ++i) {
var currentTurnDirection = getTurnDirection(positions2D[i], positions2D[i+1], positions2D[i+2]);
if (turnDirection * currentTurnDirection < 0) {
return false;
}
}
return true;
}
var scratch3 = new Cartesian3(); var scratch3 = new Cartesian3();
var scratch4 = new Cartesian3(); var scratch4 = new Cartesian3();
var scratch5 = new Cartesian3(); var scratch5 = new Cartesian3();
// Checks if winding order matches the given normal. var scratchCenter = new Cartesian3();
function checkWindingCorrect(positionIndex1, positionIndex2, positionIndex3, normal) { var scratchAxis1 = new Cartesian3();
var A = get3DPoint(positionIndex1, scratch1); var scratchAxis2 = new Cartesian3();
var B = get3DPoint(positionIndex2, scratch2); var scratchNormal = new Cartesian3();
var C = get3DPoint(positionIndex3, scratch3); var scratchPositions = [new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3()];
var scratchVertexIndices = [];
var scratchPoints = [];
function checkWindingCorrect(positionIndex1, positionIndex2, positionIndex3, normalIndex) {
if (normalIndex.length === 0) {
// If no face normal, we have to assume the winding is correct.
return true;
}
var normal = getNormal(normalIndex, scratchNormal);
var A = getPosition(positionIndex1, scratch1);
var B = getPosition(positionIndex2, scratch2);
var C = getPosition(positionIndex3, scratch3);
var BA = Cartesian3.subtract(B, A, scratch4); var BA = Cartesian3.subtract(B, A, scratch4);
var CA = Cartesian3.subtract(C, A, scratch5); var CA = Cartesian3.subtract(C, A, scratch5);
@ -368,58 +277,45 @@ function loadObj(objPath, options) {
} }
} }
var scratchPositions3D = [new Cartesian3(), new Cartesian3(), new Cartesian3()];
function addFace(vertices, positions, uvs, normals) { function addFace(vertices, positions, uvs, normals) {
var isWindingCorrect = true; var i;
var faceNormal; var isWindingCorrect;
// If normals are defined, find a face normal to use in winding order sanitization. checkPrimitive(uvs, normals);
// If no face normal, we have to assume the winding is correct.
if (normals[0].length > 0) {
faceNormal = get3DNormal(normals[0], scratchNormal);
isWindingCorrect = checkWindingCorrect(positions[0], positions[1], positions[2], faceNormal);
}
if (vertices.length === 3) { if (vertices.length === 3) {
isWindingCorrect = checkWindingCorrect(positions[0], positions[1], positions[2], normals[0]);
var index1 = addVertex(vertices[0], positions[0], uvs[0], normals[0]); var index1 = addVertex(vertices[0], positions[0], uvs[0], normals[0]);
var index2 = addVertex(vertices[1], positions[1], uvs[1], normals[1]); var index2 = addVertex(vertices[1], positions[1], uvs[1], normals[1]);
var index3 = addVertex(vertices[2], positions[2], uvs[2], normals[2]); var index3 = addVertex(vertices[2], positions[2], uvs[2], normals[2]);
addTriangle(index1, index2, index3, isWindingCorrect); addTriangle(index1, index2, index3, isWindingCorrect);
} else { // Triangulate if the face is not a triangle } else { // Triangulate if the face is not a triangle
var positions3D = []; var points = scratchPoints;
var vertexIndices = scratchVertexIndices;
points.length = 0;
vertexIndices.length = 0; vertexIndices.length = 0;
var i;
for (i = 0; i < vertices.length; ++i) { for (i = 0; i < vertices.length; ++i) {
var index = addVertex(vertices[i], positions[i], uvs[i], normals[i]); var index = addVertex(vertices[i], positions[i], uvs[i], normals[i]);
vertexIndices.push(index); vertexIndices.push(index);
if (i === scratchPositions.length) {
// Collect the vertex positions as 3D points scratchPositions.push(new Cartesian3());
if (i === scratchPositions3D.length) {
scratchPositions3D.push(new Cartesian3());
} }
positions3D.push(get3DPoint(positions[i], scratchPositions3D[i])); points.push(getPosition(positions[i], scratchPositions[i]));
} }
var positions2D = projectTo2D(positions3D); var validGeometry = CoplanarPolygonGeometryLibrary.computeProjectTo2DArguments(points, scratchCenter, scratchAxis1, scratchAxis2);
if (!validGeometry) {
return;
}
var projectPoints = CoplanarPolygonGeometryLibrary.createProjectPointsTo2DFunction(scratchCenter, scratchAxis1, scratchAxis2);
var points2D = projectPoints(points);
var indices = PolygonPipeline.triangulate(points2D);
isWindingCorrect = PolygonPipeline.computeWindingOrder2D(points2D) !== WindingOrder.CLOCKWISE;
if (isConvex(positions2D)) { for (i = 0; i < indices.length - 2; i += 3) {
for (i=1; i < vertices.length-1; ++i) { addTriangle(vertexIndices[indices[i]], vertexIndices[indices[i+1]], vertexIndices[indices[i+2]], isWindingCorrect);
addTriangle(vertexIndices[0], vertexIndices[i], vertexIndices[i+1], isWindingCorrect);
}
} else {
// Since the projection doesn't preserve winding order, reverse the order of
// the vertices before triangulating to enforce counter clockwise.
var projectedWindingOrder = PolygonPipeline.computeWindingOrder2D(positions2D);
if (projectedWindingOrder === WindingOrder.CLOCKWISE) {
positions2D.reverse();
}
// Use an ear-clipping algorithm to triangulate
var positionIndices = PolygonPipeline.triangulate(positions2D);
for (i = 0; i < positionIndices.length-2; i += 3) {
addTriangle(vertexIndices[positionIndices[i]], vertexIndices[positionIndices[i+1]], vertexIndices[positionIndices[i+2]], isWindingCorrect);
}
} }
} }
} }
@ -502,7 +398,7 @@ function loadObj(objPath, options) {
uvs = undefined; uvs = undefined;
// Load materials and textures // Load materials and textures
return finishLoading(nodes, mtlPaths, objPath, options); return finishLoading(nodes, mtlPaths, objPath, defined(activeMaterial), options);
}); });
} }
@ -523,7 +419,7 @@ function getMtlPaths(mtllibLine) {
return mtlPaths; return mtlPaths;
} }
function finishLoading(nodes, mtlPaths, objPath, options) { function finishLoading(nodes, mtlPaths, objPath, usesMaterials, options) {
nodes = cleanNodes(nodes); nodes = cleanNodes(nodes);
if (nodes.length === 0) { if (nodes.length === 0) {
throw new RuntimeError(objPath + ' does not have any geometry data'); throw new RuntimeError(objPath + ' does not have any geometry data');
@ -531,7 +427,9 @@ function finishLoading(nodes, mtlPaths, objPath, options) {
var name = path.basename(objPath, path.extname(objPath)); var name = path.basename(objPath, path.extname(objPath));
return loadMtls(mtlPaths, objPath, options) return loadMtls(mtlPaths, objPath, options)
.then(function(materials) { .then(function(materials) {
assignDefaultMaterial(nodes, materials); if (materials.length > 0 && !usesMaterials) {
assignDefaultMaterial(nodes, materials, usesMaterials);
}
return { return {
nodes : nodes, nodes : nodes,
materials : materials, materials : materials,
@ -540,6 +438,11 @@ function finishLoading(nodes, mtlPaths, objPath, options) {
}); });
} }
function normalizeMtlPath(mtlPath, objDirectory) {
mtlPath = mtlPath.replace(/\\/g, '/');
return path.normalize(path.join(objDirectory, mtlPath));
}
function loadMtls(mtlPaths, objPath, options) { function loadMtls(mtlPaths, objPath, options) {
var objDirectory = path.dirname(objPath); var objDirectory = path.dirname(objPath);
var materials = []; var materials = [];
@ -550,8 +453,8 @@ function loadMtls(mtlPaths, objPath, options) {
}); });
return Promise.map(mtlPaths, function(mtlPath) { return Promise.map(mtlPaths, function(mtlPath) {
mtlPath = path.resolve(objDirectory, mtlPath); mtlPath = normalizeMtlPath(mtlPath, objDirectory);
var shallowPath = path.resolve(path.join(objDirectory, path.basename(mtlPath))); var shallowPath = path.join(objDirectory, path.basename(mtlPath));
if (options.secure && outsideDirectory(mtlPath, objDirectory)) { if (options.secure && outsideDirectory(mtlPath, objDirectory)) {
// Try looking for the .mtl in the same directory as the obj // Try looking for the .mtl in the same directory as the obj
options.logger('The material file is outside of the obj directory and the secure flag is true. Attempting to read the material file from within the obj directory instead.'); options.logger('The material file is outside of the obj directory and the secure flag is true. Attempting to read the material file from within the obj directory instead.');
@ -586,9 +489,6 @@ function loadMtls(mtlPaths, objPath, options) {
} }
function assignDefaultMaterial(nodes, materials) { function assignDefaultMaterial(nodes, materials) {
if (materials.length === 0) {
return;
}
var defaultMaterial = materials[0].name; var defaultMaterial = materials[0].name;
var nodesLength = nodes.length; var nodesLength = nodes.length;
for (var i = 0; i < nodesLength; ++i) { for (var i = 0; i < nodesLength; ++i) {
@ -609,10 +509,10 @@ function removeEmptyMeshes(meshes) {
return meshes.filter(function(mesh) { return meshes.filter(function(mesh) {
// Remove empty primitives // Remove empty primitives
mesh.primitives = mesh.primitives.filter(function(primitive) { mesh.primitives = mesh.primitives.filter(function(primitive) {
return primitive.indices.length > 0; return primitive.indices.length > 0 && primitive.positions.length > 0;
}); });
// Valid meshes must have at least one primitive and contain positions // Valid meshes must have at least one primitive
return (mesh.primitives.length > 0) && (mesh.positions.length > 0); return (mesh.primitives.length > 0);
}); });
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "obj2gltf", "name": "obj2gltf",
"version": "2.2.0", "version": "2.3.2",
"description": "Convert OBJ model format to glTF", "description": "Convert OBJ model format to glTF",
"license": "Apache-2.0", "license": "Apache-2.0",
"contributors": [ "contributors": [
@ -26,26 +26,26 @@
"node": ">=4.0.0" "node": ">=4.0.0"
}, },
"dependencies": { "dependencies": {
"bluebird": "^3.5.1", "bluebird": "^3.5.2",
"cesium": "^1.39.0", "cesium": "^1.50.0",
"fs-extra": "^4.0.2", "fs-extra": "^7.0.0",
"jpeg-js": "^0.3.3", "jpeg-js": "^0.3.4",
"mime": "^2.0.3", "mime": "^2.3.1",
"pngjs": "^3.3.0", "pngjs": "^3.3.3",
"yargs": "^10.0.3" "yargs": "^12.0.2"
}, },
"devDependencies": { "devDependencies": {
"cloc": "^2.3.3", "cloc": "^2.3.4",
"coveralls": "^3.0.0", "coveralls": "^3.0.2",
"eslint": "^4.4.1", "eslint": "^5.6.1",
"eslint-config-cesium": "^2.0.1", "eslint-config-cesium": "^6.0.0",
"gulp": "^3.9.1", "gulp": "^4.0.0",
"jasmine": "^2.7.0", "jasmine": "^3.2.0",
"jasmine-spec-reporter": "^4.2.0", "jasmine-spec-reporter": "^4.2.1",
"jsdoc": "^3.5.4", "jsdoc": "^3.5.5",
"nyc": "^11.1.0", "nyc": "^13.0.1",
"open": "^0.0.5", "open": "^0.0.5",
"requirejs": "^2.3.4" "requirejs": "^2.3.6"
}, },
"scripts": { "scripts": {
"jsdoc": "jsdoc ./lib -R ./README.md -d doc", "jsdoc": "jsdoc ./lib -R ./README.md -d doc",

View File

@ -0,0 +1,13 @@
# Blender MTL File: 'None'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 0.100000 0.000000 0.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.100000
Ni 1.000000
d 1.000000
illum 2
map_Kd cesium.png

View File

@ -0,0 +1,67 @@
# Blender v2.78 (sub 0) OBJ File: ''
# www.blender.org
mtllib box-mixed-attributes-2.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
# Using default material
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 8/10 6/11 5/12
f 5/13 6/14 2/15 1/16
f 3//5 7//5 5//5 1//5
f 8//6 4//6 2//6 6//6
usemtl Material
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 8/10 6/11 5/12
f 5/13 6/14 2/15 1/16
f 3//5 7//5 5//5 1//5
f 8//6 4//6 2//6 6//6
usemtl Missing
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 8/10 6/11 5/12
f 5/13 6/14 2/15 1/16
f 3//5 7//5 5//5 1//5
f 8//6 4//6 2//6 6//6
o CubeCopy
usemtl Material
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 8/10 6/11 5/12
f 5/13 6/14 2/15 1/16
f 3//5 7//5 5//5 1//5
f 8//6 4//6 2//6 6//6

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -0,0 +1,12 @@
# Blender MTL File: 'None'
# Material Count: 1
newmtl Material
Ns 96.078431
Ka 0.100000 0.000000 0.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.100000
Ni 1.000000
d 1.000000
illum 2

View File

@ -0,0 +1,46 @@
# Blender v2.78 (sub 0) OBJ File: ''
# www.blender.org
mtllib box-mixed-attributes.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 4 8 7
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 7//5 5//5 1//5
f 8/19 4/6 2/15 6/20

View File

@ -0,0 +1,46 @@
# Blender v2.78 (sub 0) OBJ File: 'box.blend'
# www.blender.org
mtllib materials\\box-windows-paths.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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -4,14 +4,17 @@ var Promise = require('bluebird');
var obj2gltf = require('../../lib/obj2gltf'); var obj2gltf = require('../../lib/obj2gltf');
var createGltf = require('../../lib/createGltf'); var createGltf = require('../../lib/createGltf');
var loadObj = require('../../lib/loadObj'); var loadObj = require('../../lib/loadObj');
var getDefaultMaterial = require('../../lib/loadMtl').getDefaultMaterial;
var clone = Cesium.clone; var clone = Cesium.clone;
var defined = Cesium.defined;
var WebGLConstants = Cesium.WebGLConstants; var WebGLConstants = Cesium.WebGLConstants;
var boxObjPath = 'specs/data/box/box.obj'; var boxObjPath = 'specs/data/box/box.obj';
var groupObjPath = 'specs/data/box-objects-groups-materials/box-objects-groups-materials.obj'; var groupObjPath = 'specs/data/box-objects-groups-materials/box-objects-groups-materials.obj';
var complexObjPath = 'specs/data/box-complex-material/box-complex-material.obj'; var complexObjPath = 'specs/data/box-complex-material/box-complex-material.obj';
var noMaterialsObjPath = 'specs/data/box-no-materials/box-no-materials.obj'; var noMaterialsObjPath = 'specs/data/box-no-materials/box-no-materials.obj';
var mixedAttributesObjPath = 'specs/data/box-mixed-attributes-2/box-mixed-attributes-2.obj';
var options; var options;
@ -20,6 +23,7 @@ describe('createGltf', function() {
var groupObjData; var groupObjData;
var complexObjData; var complexObjData;
var noMaterialsObjData; var noMaterialsObjData;
var mixedAttributesObjData;
beforeEach(function(done) { beforeEach(function(done) {
options = clone(obj2gltf.defaults); options = clone(obj2gltf.defaults);
@ -42,6 +46,10 @@ describe('createGltf', function() {
loadObj(noMaterialsObjPath, options) loadObj(noMaterialsObjPath, options)
.then(function(data) { .then(function(data) {
noMaterialsObjData = data; noMaterialsObjData = data;
}),
loadObj(mixedAttributesObjPath, options)
.then(function(data) {
mixedAttributesObjData = data;
}) })
]).then(done); ]).then(done);
}); });
@ -133,42 +141,108 @@ describe('createGltf', function() {
}); });
it('runs without normals', function() { it('runs without normals', function() {
boxObjData.nodes[0].meshes[0].normals.length = 0; boxObjData.nodes[0].meshes[0].primitives[0].normals.length = 0;
var gltf = createGltf(boxObjData, options); var gltf = createGltf(boxObjData, options);
var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes; var attributes = gltf.meshes[0].primitives[0].attributes;
expect(attributes.POSITION).toBeDefined(); expect(attributes.POSITION).toBeDefined();
expect(attributes.NORMAL).toBeUndefined(); expect(attributes.NORMAL).toBeUndefined();
expect(attributes.TEXCOORD_0).toBeDefined(); expect(attributes.TEXCOORD_0).toBeDefined();
}); });
it('runs without uvs', function() { it('runs without uvs', function() {
boxObjData.nodes[0].meshes[0].uvs.length = 0; boxObjData.nodes[0].meshes[0].primitives[0].uvs.length = 0;
var gltf = createGltf(boxObjData, options); var gltf = createGltf(boxObjData, options);
var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes; var attributes = gltf.meshes[0].primitives[0].attributes;
expect(attributes.POSITION).toBeDefined(); expect(attributes.POSITION).toBeDefined();
expect(attributes.NORMAL).toBeDefined(); expect(attributes.NORMAL).toBeDefined();
expect(attributes.TEXCOORD_0).toBeUndefined(); expect(attributes.TEXCOORD_0).toBeUndefined();
}); });
it('runs without uvs and normals', function() { it('runs without uvs and normals', function() {
boxObjData.nodes[0].meshes[0].normals.length = 0; boxObjData.nodes[0].meshes[0].primitives[0].normals.length = 0;
boxObjData.nodes[0].meshes[0].uvs.length = 0; boxObjData.nodes[0].meshes[0].primitives[0].uvs.length = 0;
var gltf = createGltf(boxObjData, options); var gltf = createGltf(boxObjData, options);
var attributes = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0].attributes; var attributes = gltf.meshes[0].primitives[0].attributes;
expect(attributes.POSITION).toBeDefined(); expect(attributes.POSITION).toBeDefined();
expect(attributes.NORMAL).toBeUndefined(); expect(attributes.NORMAL).toBeUndefined();
expect(attributes.TEXCOORD_0).toBeUndefined(); expect(attributes.TEXCOORD_0).toBeUndefined();
}); });
it('splits incompatible materials', function() {
var gltf = createGltf(mixedAttributesObjData, options);
var materials = gltf.materials;
var meshes = gltf.meshes;
var referenceMaterial = mixedAttributesObjData.materials[0];
delete referenceMaterial.name;
referenceMaterial.pbrMetallicRoughness.baseColorTexture = {
index : 0
};
var referenceMaterialNoTextures = clone(referenceMaterial, true);
referenceMaterialNoTextures.pbrMetallicRoughness.baseColorTexture = undefined;
var defaultMaterial = getDefaultMaterial(options);
delete defaultMaterial.name;
var materialNames = materials.map(function(material) {
var name = material.name;
delete material.name;
return name;
});
// Expect three copies of each material for
// * positions/normals/uvs
// * positions/normals
// * positions/uvs
expect(materialNames).toEqual([
'default',
'default-2',
'default-3',
'Material',
'Material-2',
'Material-3',
'Missing',
'Missing-2',
'Missing-3'
]);
expect(materials.length).toBe(9);
expect(materials[0]).toEqual(defaultMaterial);
expect(materials[1]).toEqual(defaultMaterial);
expect(materials[2]).toEqual(defaultMaterial);
expect(materials[3]).toEqual(referenceMaterial);
expect(materials[4]).toEqual(referenceMaterial);
expect(materials[5]).toEqual(referenceMaterialNoTextures);
expect(materials[6]).toEqual(defaultMaterial);
expect(materials[7]).toEqual(defaultMaterial);
expect(materials[8]).toEqual(defaultMaterial);
// Test that primitives without uvs reference materials without textures
var meshesLength = meshes.length;
for (var i = 0; i < meshesLength; ++i) {
var mesh = meshes[i];
var primitives = mesh.primitives;
var primitivesLength = primitives.length;
for (var j = 0; j < primitivesLength; ++j) {
var primitive = primitives[j];
var material = materials[primitive.material];
if (!defined(primitive.attributes.TEXCOORD_0)) {
expect(material.pbrMetallicRoughness.baseColorTexture).toBeUndefined();
}
}
}
});
function expandObjData(objData, duplicatesLength) { function expandObjData(objData, duplicatesLength) {
var mesh = objData.nodes[0].meshes[0]; var primitive = objData.nodes[0].meshes[0].primitives[0];
var indices = mesh.primitives[0].indices; var indices = primitive.indices;
var positions = mesh.positions; var positions = primitive.positions;
var normals = mesh.normals; var normals = primitive.normals;
var uvs = mesh.uvs; var uvs = primitive.uvs;
var indicesLength = indices.length; var indicesLength = indices.length;
var vertexCount = positions.length / 3; var vertexCount = positions.length / 3;
@ -192,12 +266,12 @@ describe('createGltf', function() {
it('detects need to use uint32 indices', function() { it('detects need to use uint32 indices', function() {
expandObjData(boxObjData, 2731); // Right above 65536 limit expandObjData(boxObjData, 2731); // Right above 65536 limit
var mesh = boxObjData.nodes[0].meshes[0]; var primitive = boxObjData.nodes[0].meshes[0].primitives[0];
var indicesLength = mesh.primitives[0].indices.length; var indicesLength = primitive.indices.length;
var vertexCount = mesh.positions.length / 3; var vertexCount = primitive.positions.length / 3;
var gltf = createGltf(boxObjData, options); var gltf = createGltf(boxObjData, options);
var primitive = gltf.meshes[Object.keys(gltf.meshes)[0]].primitives[0]; primitive = gltf.meshes[0].primitives[0];
var indicesAccessor = gltf.accessors[primitive.indices]; var indicesAccessor = gltf.accessors[primitive.indices];
expect(indicesAccessor.count).toBe(indicesLength); expect(indicesAccessor.count).toBe(indicesLength);
expect(indicesAccessor.max[0]).toBe(vertexCount - 1); expect(indicesAccessor.max[0]).toBe(vertexCount - 1);

View File

@ -34,9 +34,11 @@ var objExternalResourcesInRootPath = 'specs/data/box-external-resources-in-root/
var objTexturedPath = 'specs/data/box-textured/box-textured.obj'; var objTexturedPath = 'specs/data/box-textured/box-textured.obj';
var objMissingTexturePath = 'specs/data/box-missing-texture/box-missing-texture.obj'; var objMissingTexturePath = 'specs/data/box-missing-texture/box-missing-texture.obj';
var objSubdirectoriesPath = 'specs/data/box-subdirectories/box-textured.obj'; var objSubdirectoriesPath = 'specs/data/box-subdirectories/box-textured.obj';
var objWindowsPaths = 'specs/data/box-windows-paths/box-windows-paths.obj';
var objInvalidContentsPath = 'specs/data/box/box.mtl'; var objInvalidContentsPath = 'specs/data/box/box.mtl';
var objConcavePath = 'specs/data/concave/concave.obj'; var objConcavePath = 'specs/data/concave/concave.obj';
var objUnnormalizedPath = 'specs/data/box-unnormalized/box-unnormalized.obj'; var objUnnormalizedPath = 'specs/data/box-unnormalized/box-unnormalized.obj';
var objMixedAttributesPath = 'specs/data/box-mixed-attributes/box-mixed-attributes.obj';
var objInvalidPath = 'invalid.obj'; var objInvalidPath = 'invalid.obj';
function getMeshes(data) { function getMeshes(data) {
@ -93,9 +95,9 @@ describe('loadObj', function() {
expect(node.name).toBe('Cube'); expect(node.name).toBe('Cube');
expect(mesh.name).toBe('Cube-Mesh'); expect(mesh.name).toBe('Cube-Mesh');
expect(mesh.positions.length / 3).toBe(24); expect(primitive.positions.length / 3).toBe(24);
expect(mesh.normals.length / 3).toBe(24); expect(primitive.normals.length / 3).toBe(24);
expect(mesh.uvs.length / 2).toBe(24); expect(primitive.uvs.length / 2).toBe(24);
expect(primitive.indices.length).toBe(36); expect(primitive.indices.length).toBe(36);
expect(primitive.material).toBe('Material'); expect(primitive.material).toBe('Material');
}), done).toResolve(); }), done).toResolve();
@ -104,10 +106,10 @@ describe('loadObj', function() {
it('loads obj with normals', function(done) { it('loads obj with normals', function(done) {
expect(loadObj(objNormalsPath, options) expect(loadObj(objNormalsPath, options)
.then(function(data) { .then(function(data) {
var mesh = getMeshes(data)[0]; var primitive = getPrimitives(data)[0];
expect(mesh.positions.length / 3).toBe(24); expect(primitive.positions.length / 3).toBe(24);
expect(mesh.normals.length / 3).toBe(24); expect(primitive.normals.length / 3).toBe(24);
expect(mesh.uvs.length / 2).toBe(0); expect(primitive.uvs.length / 2).toBe(0);
}), done).toResolve(); }), done).toResolve();
}); });
@ -115,8 +117,8 @@ describe('loadObj', function() {
expect(loadObj(objUnnormalizedPath, options) expect(loadObj(objUnnormalizedPath, options)
.then(function(data) { .then(function(data) {
var scratchNormal = new Cesium.Cartesian3(); var scratchNormal = new Cesium.Cartesian3();
var mesh = getMeshes(data)[0]; var primitive = getPrimitives(data)[0];
var normals = mesh.normals; var normals = primitive.normals;
var normalsLength = normals.length / 3; var normalsLength = normals.length / 3;
for (var i = 0; i < normalsLength; ++i) { for (var i = 0; i < normalsLength; ++i) {
var normalX = normals.get(i * 3); var normalX = normals.get(i * 3);
@ -131,10 +133,10 @@ describe('loadObj', function() {
it('loads obj with uvs', function(done) { it('loads obj with uvs', function(done) {
expect(loadObj(objUvsPath, options) expect(loadObj(objUvsPath, options)
.then(function(data) { .then(function(data) {
var mesh = getMeshes(data)[0]; var primitive = getPrimitives(data)[0];
expect(mesh.positions.length / 3).toBe(20); expect(primitive.positions.length / 3).toBe(20);
expect(mesh.normals.length / 3).toBe(0); expect(primitive.normals.length / 3).toBe(0);
expect(mesh.uvs.length / 2).toBe(20); expect(primitive.uvs.length / 2).toBe(20);
}), done).toResolve(); }), done).toResolve();
}); });
@ -144,8 +146,8 @@ describe('loadObj', function() {
loadObj(objNegativeIndicesPath, options) loadObj(objNegativeIndicesPath, options)
]) ])
.then(function(results) { .then(function(results) {
var positionsReference = getMeshes(results[0])[0].positions.toFloatBuffer(); var positionsReference = getPrimitives(results[0])[0].positions.toFloatBuffer();
var positions = getMeshes(results[1])[0].positions.toFloatBuffer(); var positions = getPrimitives(results[1])[0].positions.toFloatBuffer();
expect(positions).toEqual(positionsReference); expect(positions).toEqual(positionsReference);
}), done).toResolve(); }), done).toResolve();
}); });
@ -153,9 +155,8 @@ describe('loadObj', function() {
it('loads obj with triangle faces', function(done) { it('loads obj with triangle faces', function(done) {
expect(loadObj(objTrianglesPath, options) expect(loadObj(objTrianglesPath, options)
.then(function(data) { .then(function(data) {
var mesh = getMeshes(data)[0];
var primitive = getPrimitives(data)[0]; var primitive = getPrimitives(data)[0];
expect(mesh.positions.length / 3).toBe(24); expect(primitive.positions.length / 3).toBe(24);
expect(primitive.indices.length).toBe(36); expect(primitive.indices.length).toBe(36);
}), done).toResolve(); }), done).toResolve();
}); });
@ -255,9 +256,8 @@ describe('loadObj', function() {
it('loads obj with concave face containing 5 vertices', function(done) { it('loads obj with concave face containing 5 vertices', function(done) {
expect(loadObj(objConcavePath, options) expect(loadObj(objConcavePath, options)
.then(function(data) { .then(function(data) {
var mesh = getMeshes(data)[0];
var primitive = getPrimitives(data)[0]; var primitive = getPrimitives(data)[0];
expect(mesh.positions.length / 3).toBe(30); expect(primitive.positions.length / 3).toBe(30);
expect(primitive.indices.length).toBe(48); expect(primitive.indices.length).toBe(48);
}), done).toResolve(); }), done).toResolve();
}); });
@ -309,6 +309,13 @@ describe('loadObj', function() {
expect(primitives[0].material).toBe('Red'); expect(primitives[0].material).toBe('Red');
expect(primitives[1].material).toBe('Green'); expect(primitives[1].material).toBe('Green');
expect(primitives[2].material).toBe('Blue'); expect(primitives[2].material).toBe('Blue');
for (var i = 0; i < 3; ++i) {
var indices = primitives[i].indices;
for (var j = 0; j < indices.length; ++j) {
expect(indices.get(j)).toBeLessThan(8);
}
}
}), done).toResolve(); }), done).toResolve();
}); });
@ -478,6 +485,27 @@ describe('loadObj', function() {
}), done).toResolve(); }), done).toResolve();
}); });
it('loads obj with windows paths', function(done) {
expect(loadObj(objWindowsPaths, options)
.then(function(data) {
var baseColorTexture = data.materials[0].pbrMetallicRoughness.baseColorTexture;
expect(baseColorTexture.name).toBe('cesium');
expect(baseColorTexture.source).toBeDefined();
}), done).toResolve();
});
it('separates faces that don\'t use the same attributes as other faces in the primitive', function(done) {
expect(loadObj(objMixedAttributesPath, options)
.then(function(data) {
var primitives = getPrimitives(data);
expect(primitives.length).toBe(4);
expect(primitives[0].indices.length).toBe(18); // 6 faces
expect(primitives[1].indices.length).toBe(6); // 2 faces
expect(primitives[2].indices.length).toBe(6); // 2 faces
expect(primitives[3].indices.length).toBe(6); // 2 faces
}), done).toResolve();
});
it('throws when file has invalid contents', function(done) { it('throws when file has invalid contents', function(done) {
expect(loadObj(objInvalidContentsPath, options), done).toRejectWith(RuntimeError); expect(loadObj(objInvalidContentsPath, options), done).toRejectWith(RuntimeError);
}); });